PHPackages                             bdvvn/database - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. bdvvn/database

ActiveLibrary[Database &amp; ORM](/categories/database)

bdvvn/database
==============

Abstraction PDO multi-driver avec query builder

02↓66.7%PHP

Since May 31Pushed 1mo agoCompare

[ Source](https://github.com/bdvvn/database)[ Packagist](https://packagist.org/packages/bdvvn/database)[ RSS](/packages/bdvvn-database/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

bdvvn/database
==============

[](#bdvvndatabase)

Abstraction PDO multi-driver dotée d'un **query builder fluide, immuable et sécurisé**.

La bibliothèque vise un objectif simple : écrire des requêtes SQL paramétrées de façon expressive, sans ORM, sans magie, et **sans jamais exposer une surface d'injection SQL** — ni sur les valeurs (toujours bindées), ni sur les identifiants (toujours validés et quotés).

```
use Bdvvn\Database\Connection;

$db = Connection::fromEnv();

$articles = $db->from('articles', 'a')
    ->select('a.id', 'a.titre', 'auteurs.nom')
    ->join('auteurs', 'auteurs.id', '=', 'a.auteur_id')
    ->where('a.publie', '=', true)
    ->whereNotNull('a.publie_le')
    ->orderBy('a.publie_le', 'DESC')
    ->limit(10)
    ->findAll();
```

---

Sommaire
--------

[](#sommaire)

- [Prérequis](#pr%C3%A9requis)
- [Installation](#installation)
- [Démarrage rapide](#d%C3%A9marrage-rapide)
- [Établir une connexion](#%C3%A9tablir-une-connexion)
- [Construire des requêtes](#construire-des-requ%C3%AAtes)
- [Lire des données](#lire-des-donn%C3%A9es)
- [Écrire des données](#%C3%A9crire-des-donn%C3%A9es)
- [Introspection](#introspection)
- [Sécurité](#s%C3%A9curit%C3%A9)
- [Architecture](#architecture)
- [Spécificités par dialecte](#sp%C3%A9cificit%C3%A9s-par-dialecte)
- [Étendre la bibliothèque](#%C3%A9tendre-la-biblioth%C3%A8que)
- [Qualité du code](#qualit%C3%A9-du-code)

---

Prérequis
---------

[](#prérequis)

- **PHP ≥ 8.5**
- Extension **`pdo`** activée (plus le driver PDO correspondant : `pdo_mysql`, `pdo_pgsql` ou `pdo_sqlite`)
- [`bdvvn/env`](../env) — utilisé par `Connection::fromEnv()` pour lire la configuration

Dialectes supportés : **MySQL / MariaDB**, **PostgreSQL**, **SQLite**.

Installation
------------

[](#installation)

```
composer require bdvvn/database
```

Autoload PSR-4 : namespace `Bdvvn\Database\` mappé sur `src/`.

> ℹ️ **Note de distribution.** En l'état, `composer.json` dépend de `bdvvn/env` en `dev-main` via un *repository* local de type `path` (`../env`), avec `minimum-stability: dev`. Cette configuration fonctionne en monorepo mais **n'est pas installable telle quelle par un tiers depuis Packagist**. Avant toute publication : taguer une version stable de `bdvvn/env`, remplacer la contrainte par une version sémantique (`^1.0`) et retirer le bloc `repositories` `path`. De même, `phpmd/phpmd` est épinglé en `3.x-dev` (branche de développement) : à figer sur une version stable une fois disponible.

---

Démarrage rapide
----------------

[](#démarrage-rapide)

```
use Bdvvn\Database\Connection;

// 1. Une connexion (voir « Établir une connexion » pour toutes les fabriques)
$db = Connection::sqlite(__DIR__ . '/blog.sqlite');

// 2. Un builder par table, obtenu via from()
$query = $db->from('articles');

// 3. On compose, on exécute
$article = $query->where('slug', '=', 'mon-premier-article')->find();
```

Le builder est **immuable** : chaque méthode renvoie une *nouvelle* instance. On peut donc dériver des requêtes sans effet de bord :

```
$base = $db->from('articles')->where('publie', '=', true);

$recents   = $base->orderBy('publie_le', 'DESC')->limit(5)->findAll();
$decompte  = $base->count(); // $base n'a pas été modifié par la ligne précédente
```

---

Établir une connexion
---------------------

[](#établir-une-connexion)

La classe [`Connection`](src/Connection.php) est le point d'entrée. Elle encapsule le `PDO`, le dialecte, l'exécuteur et le validateur, puis fabrique des `QueryBuilder` via `from()`.

```
$db->from('articles');           // FROM `articles`
$db->from('articles', 'a');      // FROM `articles` AS `a` — voir « Alias de table »
```

### Fabriques statiques

[](#fabriques-statiques)

```
use Bdvvn\Database\Connection;

Connection::mysql(host: '127.0.0.1', port: 3306, dbname: 'blog', user: 'root', password: 'secret');
Connection::mariadb(...);   // alias de mysql()
Connection::postgresql(host: '127.0.0.1', port: 5432, dbname: 'blog', user: 'postgres', password: 'secret');
Connection::pgsql(...);     // alias de postgresql()
Connection::sqlite('/chemin/vers/base.sqlite');  // ou ':memory:'
```

`mysql()` force `charset=utf8mb4` et désactive l'émulation des requêtes préparées (`ATTR_EMULATE_PREPARES = false`). `sqlite()` active `PRAGMA foreign_keys = ON` et `journal_mode = WAL`. Toutes les connexions utilisent `ERRMODE_EXCEPTION` et `FETCH_ASSOC` par défaut.

### Depuis l'environnement

[](#depuis-lenvironnement)

`Connection::fromEnv()` lit la configuration **exclusivement** via la bibliothèque [`bdvvn/env`](../env) (plus aucun accès direct à `$_ENV` / `getenv()` dans cette lib) :

VariableRôleDéfaut`DB_DRIVER` / `DB_CONNECTION``mysql` | `mariadb` | `pgsql` | `postgres` | `postgresql` | `sqlite``mysql``DB_HOST`hôte`127.0.0.1``DB_PORT`port (1–65535)`3306` (MySQL) / `5432` (PostgreSQL)`DB_NAME` / `DB_DATABASE`nom de base— (obligatoire hors SQLite)`DB_USER`utilisateur`''``DB_PASSWORD`mot de passe`''``DB_PATH` / `DB_DATABASE` / `DB_NAME`chemin du fichier SQLite`:memory:`Sans argument, une instance `Env` lisant l'environnement courant est créée :

```
$db = Connection::fromEnv();
```

Pour charger un fichier `.env` au préalable, on injecte une instance `Env` configurée — ce qui laisse la lib `database` totalement découplée de la *source* des variables :

```
use Bdvvn\Env\Env;

$env = (new Env())->load(__DIR__ . '/.env');

$db = Connection::fromEnv($env);
```

> `DB_PORT` est optionnel, mais strict : absent ou vide, il retombe sur le port par défaut du driver ; s'il est présent, il doit être entier puis compris dans la plage `1–65535`.

### Constructeur

[](#constructeur)

Pour brancher un `PDO` préconfiguré, surcharger le dialecte ou injecter un exécuteur décoré :

```
$db = new Connection(
    pdo: $pdoPreconfigure,
    adapter: new MySqlAdapter(),       // optionnel — déduit du driver PDO sinon
    executor: new LoggingExecutor(...),// optionnel — voir « Étendre la bibliothèque »
);
```

`Connection::pdo()` et `Connection::adapter()` exposent les objets sous-jacents si besoin.

---

Construire des requêtes
-----------------------

[](#construire-des-requêtes)

Toutes les méthodes de construction renvoient un nouveau `QueryBuilder` (chaînage fluide).

### Alias de table

[](#alias-de-table)

Passer un second argument à `from()` déclare un alias (`FROM articles AS a`) et permet de préfixer les colonnes avec la forme courte plutôt que le nom complet de la table :

```
$db->from('articles', 'a')
    ->select('a.titre', 'auteurs.nom')
    ->join('auteurs', 'auteurs.id', '=', 'a.auteur_id')
    ->where('a.publie', '=', true)
    ->findAll();
```

Contraintes (volontaires) :

- L'alias doit être un **identifiant simple** (`a`, `art`…), validé comme tel.
- L'alias est réservé à la **lecture**. `insert()` l'ignore ; `update()` / `delete()` lèvent une `LogicException` si un alias est défini — car `UPDATE/DELETE ... AS alias` n'est pas portable entre MySQL, PostgreSQL et SQLite. Pour une écriture, utilisez `from('articles')` sans alias.

### SELECT

[](#select)

```
->select('id', 'titre')          // colonnes validées + quotées
->addSelect('auteurs.nom')       // ajoute aux colonnes déjà sélectionnées
->selectRaw('COUNT(*) AS total')          // expression brute (non quotée — voir Sécurité)
->selectRaw('vues * ? AS pondere', [2])   // valeurs paramétrables via bindings
```

Sans `select()`, la requête sélectionne `*`.

### JOIN

[](#join)

```
->join('auteurs', 'auteurs.id', '=', 'articles.auteur_id')      // INNER JOIN
->leftJoin('commentaires', 'commentaires.article_id', '=', 'articles.id')
->rightJoin(...)
->crossJoin('tags')
->joinRaw('JOIN ... ON ... AND x = ?', [1])  // jointure brute (valeurs paramétrables)
```

Les opérateurs de jointure sont restreints aux comparaisons (`=`, `!=`, ``, `=`) ; `LIKE` est refusé dans un `ON`.

### WHERE

[](#where)

```
->where('statut', '=', 'publie')
->orWhere('statut', '=', 'archive')

->whereNull('supprime_le')        ->whereNotNull('publie_le')
->orWhereNull(...)                ->orWhereNotNull(...)

->whereIn('categorie_id', [1, 2, 3])      ->whereNotIn(...)
->orWhereIn(...)                          ->orWhereNotIn(...)

->whereBetween('vues', 100, 1000)         ->whereNotBetween(...)
->orWhereBetween(...)                      ->orWhereNotBetween(...)

->whereRaw('LENGTH(titre) > ?', [10])     ->orWhereRaw(...)
```

Détails utiles :

- Les opérateurs autorisés pour `where`/`having` : `=`, `!=`, ``, ``, `=`, `LIKE`, `NOT LIKE` (insensibles à la casse). Tout autre opérateur lève une `InvalidArgumentException`.
- `where('col', '=', null)` lève une exception : un `= NULL` ne matche jamais en SQL. Utilisez `whereNull()` / `whereNotNull()`.
- `whereIn('col', [])` se compile en `1 = 0` (jamais vrai) et `whereNotIn('col', [])` en `1 = 1` (toujours vrai) — sémantique correcte d'un ensemble vide.

### GROUP BY / HAVING

[](#group-by--having)

```
->groupBy('auteur_id')
->groupByRaw('DATE(publie_le, ?)', ['start of month'])
->having('total', '>', 5)
->orHaving(...)
->havingRaw('COUNT(*) > ?', [5])
```

### ORDER / LIMIT / OFFSET

[](#order--limit--offset)

```
->orderBy('publie_le', 'DESC')   // direction ∈ {ASC, DESC}, sinon exception
->orderByRaw('ABS(vues - ?) ASC', [200])   // valeurs paramétrables via bindings
->limit(10)                      // ≥ 0, sinon exception
->offset(20)                     // ≥ 0, sinon exception
```

---

Lire des données
----------------

[](#lire-des-données)

```
->find(): ?array        // 1re ligne (force LIMIT 1) ou null
->findAll(): array      // liste de lignes associatives
->count(): int          // COUNT(*) — gère GROUP BY / HAVING via sous-requête
->exists(): bool        // SELECT 1 ... LIMIT 1
```

Optimisations intégrées : `find()` et `exists()` court-circuitent et renvoient `null` / `false` sans toucher la base si `limit(0)` a été posé.

```
$db->from('utilisateurs')->where('email', '=', $email)->exists();   // bool
$db->from('articles')->where('publie', '=', true)->count();         // int
```

Écrire des données
------------------

[](#écrire-des-données)

```
->insert(['titre' => 'Hello', 'corps' => '...']): bool
->update(['titre' => 'Edité']): int       // nombre de lignes affectées
->delete(): int                            // nombre de lignes affectées
->lastInsertId(?string $sequence = null): ?string
```

Garde-fous **volontaires** contre les erreurs destructrices :

- `update()` / `delete()` **sans clause `WHERE`** lèvent une `LogicException`. Pour vider sciemment une table, passez `allowEmptyWhere: true`.
- `update()` / `delete()` refusent toute clause non supportée universellement par ces DML (`JOIN`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, `OFFSET`).
- `insert()` / `update()` exigent au moins une colonne et valident chaque clé comme identifiant **simple** (non qualifié).

```
$db->from('articles')->insert(['titre' => 'Hello', 'slug' => 'hello']);
$id = $db->from('articles')->lastInsertId();

$db->from('articles')->where('id', '=', $id)->update(['vues' => 1]);
$db->from('articles')->where('id', '=', $id)->delete();
```

Transactions
------------

[](#transactions)

`Connection::transaction()` exécute un callback de façon atomique : **COMMIT** si tout réussit, **ROLLBACK automatique** si une exception est levée (elle est ensuite relancée). La méthode est *réentrante* (les appels imbriqués s'exécutent dans la transaction la plus externe, PDO ne gérant pas l'imbrication native).

```
$db->transaction(function (Connection $db): void {
    $db->from('comptes')->where('id', '=', 1)->update(['solde' => 90]);
    $db->from('comptes')->where('id', '=', 2)->update(['solde' => 110]);
    // Une exception ici annule les deux UPDATE.
});
```

Le contrôle manuel reste disponible si besoin :

```
$db->beginTransaction();
$db->commit();    // ou
$db->rollBack();
$db->inTransaction(); // bool
```

Introspection
-------------

[](#introspection)

Inspecter le SQL généré sans l'exécuter — pratique pour le debug et les tests :

```
$q = $db->from('articles')->where('publie', '=', true)->orderBy('id');

$q->toSql();        // "SELECT * FROM `articles` WHERE `publie` = ? ORDER BY `id` ASC"
$q->getBindings();  // [true]
```

---

Sécurité
--------

[](#sécurité)

La bibliothèque applique une **défense en profondeur** :

1. **Valeurs** → toujours transmises en *requêtes préparées* (placeholders `?`). Aucune valeur n'est jamais interpolée dans le SQL.
2. **Identifiants** (tables, colonnes) → ils *ne peuvent pas* être bindés en PDO ; ils sont donc validés par [`IdentifierValidator`](src/Query/IdentifierValidator.php) (regex stricte `^[a-zA-Z_][a-zA-Z0-9_]*` éventuellement qualifiée par `.`) **puis** quotés selon le dialecte. Un `select($_GET['col'])` malveillant est rejeté.
3. **Opérateurs** → liste blanche fermée via l'enum [`SqlOperator`](src/Query/SqlOperator.php). Impossible d'injecter un opérateur arbitraire.
4. **Types de binding** → `PdoExecutor` choisit `PARAM_NULL` / `PARAM_BOOL` / `PARAM_INT` / `PARAM_STR` selon la valeur.

> ⚠️ **Les méthodes `*Raw` court-circuitent la validation des identifiants.** `selectRaw()`, `whereRaw()`, `joinRaw()`, `groupByRaw()`, `havingRaw()`, `orderByRaw()` insèrent le fragment SQL tel quel. Elles refusent les fragments vides et les octets NUL, mais ne tentent pas d'analyser le SQL. **Toutes** acceptent un second argument `bindings` : faites **toujours** transiter les valeurs par des placeholders (`orderByRaw('ABS(x - ?)', [$n])`) plutôt que de les interpoler. N'injectez jamais d'identifiant (table/colonne) issu d'une entrée utilisateur dans le fragment lui-même.

5. **Mode d'erreur PDO** → le constructeur de `Connection` force `PDO::ERRMODE_EXCEPTION`, y compris sur un `PDO` injecté manuellement. Une requête fautive lève donc toujours une exception plutôt que d'échouer silencieusement.

> ⚠️ **N'exposez jamais une `PDOException` brute à l'utilisateur final.** Son message — et sa stack trace — peuvent contenir la requête SQL, des noms de tables/colonnes, voire des bribes de configuration. En production, rattrapez ces exceptions à la frontière applicative, journalisez-les côté serveur, et ne renvoyez qu'un message générique. La librairie laisse remonter l'exception telle quelle : c'est à l'application appelante de décider de la politique de journalisation/affichage.

---

Architecture
------------

[](#architecture)

Le `QueryBuilder` est une **façade fluide** qui n'assume volontairement aucune responsabilité lourde : il valide les entrées, accumule un état immuable, puis orchestre la compilation et l'exécution. Chaque préoccupation vit dans sa propre classe.

ComposantFichierResponsabilité`Connection`[Connection.php](src/Connection.php)Point d'entrée ; câble PDO + dialecte + exécuteur ; fabrique les builders`QueryBuilder`[QueryBuilder.php](src/QueryBuilder.php)API fluide ; validation ; orchestration`QueryState`[Query/QueryState.php](src/Query/QueryState.php)Snapshot **immuable** de la requête en construction (clone-on-modify)`Compiler`[Query/Compiler.php](src/Query/Compiler.php)Assemble un `QueryState` en SQL paramétré — **pur**, sans effet de bord`CompiledQuery`[Query/CompiledQuery.php](src/Query/CompiledQuery.php)Couple immuable `{sql, bindings}``IdentifierValidator`[Query/IdentifierValidator.php](src/Query/IdentifierValidator.php)Barrière anti-injection sur les identifiants`SqlOperator`[Query/SqlOperator.php](src/Query/SqlOperator.php)Liste blanche d'opérateurs (enum)`Executor`[Execution/Executor.php](src/Execution/Executor.php)**Interface** d'exécution SQL`PdoExecutor`[Execution/PdoExecutor.php](src/Execution/PdoExecutor.php)Implémentation PDO de `Executor``SqlAdapter`[Adapter/SqlAdapter.php](src/Adapter/SqlAdapter.php)**Interface** de dialecte (quoting, LIMIT/OFFSET)`AbstractSqlAdapter`[Adapter/AbstractSqlAdapter.php](src/Adapter/AbstractSqlAdapter.php)Logique commune de dialecte`MySqlAdapter` · `PostgreSqlAdapter` · `SqliteAdapter`[Adapter/](src/Adapter/)Dialectes concrets`PdoAdapterFactory`[Adapter/PdoAdapterFactory.php](src/Adapter/PdoAdapterFactory.php)Déduit le bon adapter depuis le driver PDO### Flux d'une requête

[](#flux-dune-requête)

```
Connection::from('articles')
        │  fabrique
        ▼
  QueryBuilder ──valide──▶ IdentifierValidator / SqlOperator
        │
        │  clone-on-modify (accumule)
        ▼
   QueryState (immuable)
        │
        │  ->findAll() / ->insert() ...
        ▼
   Compiler ──dialecte──▶ SqlAdapter ──▶ CompiledQuery {sql, bindings}
        │
        ▼
   Executor (PdoExecutor) ──▶ PDO ──▶ résultats

```

Les rôles sont nettement séparés :

- **Quoi** (la requête voulue) → `QueryState`
- **Comment l'écrire** (le SQL) → `Compiler` + `SqlAdapter`
- **Comment l'exécuter** → `Executor`
- **Ce qui est permis** → `IdentifierValidator` + `SqlOperator`

C'est ce qui rend chaque pièce **testable isolément** : le `Compiler` se teste sans base de données, l'`Executor` se mocke trivialement, et le `QueryBuilder` accepte des doubles via son constructeur public.

---

Spécificités par dialecte
-------------------------

[](#spécificités-par-dialecte)

Le quoting et la syntaxe `LIMIT/OFFSET` varient ; `AbstractSqlAdapter` centralise la logique commune, les sous-classes ne portent que les différences :

DialecteQuote`OFFSET` sans `LIMIT`MySQL / MariaDB``` (backtick)`LIMIT 18446744073709551615 OFFSET n`PostgreSQL`"` (guillemet double)`OFFSET n` (standard SQL)SQLite``` (backtick)`LIMIT -1 OFFSET n`L'adapter est déduit automatiquement du driver PDO par `PdoAdapterFactory::fromPdo()`, ou injectable explicitement via le constructeur de `Connection`.

---

Étendre la bibliothèque
-----------------------

[](#étendre-la-bibliothèque)

L'interface [`Executor`](src/Execution/Executor.php) est le point d'extension naturel. On peut la **décorer** sans toucher au reste — par exemple pour journaliser ou profiler :

```
use Bdvvn\Database\Execution\Executor;

final readonly class LoggingExecutor implements Executor
{
    public function __construct(
        private Executor $inner,
        private \Psr\Log\LoggerInterface $logger,
    ) {}

    public function fetchAll(string $sql, array $bindings): array
    {
        $this->logger->debug($sql, $bindings);

        return $this->inner->fetchAll($sql, $bindings);
    }

    // … déléguer fetchOne / fetchValue / execute / lastInsertId à $this->inner
}

$db = new Connection(
    pdo: $pdo,
    executor: new LoggingExecutor(new PdoExecutor($pdo), $logger),
);
```

De même, un dialecte exotique se branche en implémentant `SqlAdapter` (ou en étendant `AbstractSqlAdapter`) et en le passant au constructeur de `Connection`.

---

Qualité du code
---------------

[](#qualité-du-code)

L'outillage QA est défini dans `composer.json` :

```
composer stan      # PHPStan (niveau strict)
composer md        # PHPMD
composer cs:check  # PHP-CS-Fixer (dry-run)
composer cs:fix    # PHP-CS-Fixer (applique)
composer test      # PHPUnit
composer qa        # stan + md + cs:check + test
```

L'ensemble du code est en `declare(strict_types=1)`, les value objects sont `final readonly`, et les invariants sont vérifiés par PHPStan en mode strict.

### Tests

[](#tests)

La suite PHPUnit vit dans `tests/` et se divise en deux types :

- **`tests/Unit/`** — composants purs testés isolément : `Compiler`, adapters de dialecte, `SqlOperator`, `IdentifierValidator`.
- **`tests/Integration/`** — le `QueryBuilder` de bout en bout, exécuté sur un **vrai moteur SQLite en mémoire** (`:memory:`). Le schéma [`tests/fixtures/schema.sql`](tests/fixtures/schema.sql) et un jeu de données sont rechargés avant *chaque* test (cf. `DatabaseTestCase`), ce qui garantit une base fraîche et isolée — pas de fichier `.sqlite` partagé ni d'état rémanent.

```
composer test                          # toute la suite
vendor/bin/phpunit --testsuite unit    # unitaires seulement
vendor/bin/phpunit --testsuite integration
```

---

Licence
-------

[](#licence)

MIT.

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance59

Moderate activity, may be stable

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/ced47a7ec09db3861bb82581bd89231486504042bf3aa4df11a0ffa48fb7b21b?d=identicon)[bdvvn](/maintainers/bdvvn)

---

Top Contributors

[![bdvvn](https://avatars.githubusercontent.com/u/255297564?v=4)](https://github.com/bdvvn "bdvvn (7 commits)")

### Embed Badge

![Health badge](/badges/bdvvn-database/health.svg)

```
[![Health](https://phpackages.com/badges/bdvvn-database/health.svg)](https://phpackages.com/packages/bdvvn-database)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k117.2M118](/packages/jdorn-sql-formatter)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M87](/packages/propel-propel1)[jfelder/oracledb

Oracle DB driver for Laravel

11518.4k](/packages/jfelder-oracledb)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
