sopheos / pebble_database
PDO driver, SQL query builder and MySQL schema builder (DBForge)
Requires
- php: >=8.1.0
- ext-mbstring: *
- ext-pdo: *
Requires (Dev)
- phpunit/phpunit: ^9.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Accès à une base SQL (MySQL/MariaDB) pour PHP 8.1+ : un driver PDO avec reconnexion, un query builder qui produit des requêtes préparées, un générateur de DDL (DBForge) et un gestionnaire de sessions en base.
Le query builder et DBForge ne touchent jamais la base : ils produisent des objets Query ou des chaînes SQL, que l'on exécute ensuite via le driver.
Installation
composer require sopheos/pebble_database
Claude Code
Ce package fournit un skill Claude Code dans skills/pebble-database/. Il documente les patterns d'usage et les pièges de la librairie : whereIn([]) qui ne filtre rien, whereAuto() qui mutile les colonnes or…/and… et les colonnes préfixées, count() qui garde LIMIT, DBForge qui renvoie des chaînes, etc.
Dans un projet qui dépend de sopheos/pebble_database, copie-le une fois dans .claude/skills/ après composer install pour que Claude Code le charge automatiquement. Le nom du dossier doit correspondre au name déclaré dans SKILL.md :
cp -r vendor/sopheos/pebble_database/skills/pebble-database .claude/skills/pebble-database
Pour la maintenance de la lib elle-même, voir CLAUDE.md. Les bugs connus sont listés dans TODO.md.
Driver
\Pebble\Database\Driver implémente DriverInterface. La connexion PDO est ouverte à la première requête.
$db = Driver::create('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'secret') ->setMaxReconnectTries(2) ->setReconnectDelayMs(500); $rows = $db->exec(QB::create('users')->whereEq('active', 1)->read())->all();
__construct(string $dsn, ?string $username = null, ?string $password = null, array $options = [])Les options PDOERRMODE_EXCEPTION,EMULATE_PREPARES = falseetSTRINGIFY_FETCHES = falsesont imposées : une valeur passée dans$optionspour ces clés est ignorée.static create(...) : staticRaccourci du constructeur.setMaxReconnectTries(int $max = 0) : staticNombre de reconnexions sur les erreurs MySQL 1317, 2002 et 2006 (0 par défaut).setReconnectDelayMs(int $ms = 1000) : staticDélai avant reconnexion.getConnection() : PDOL'objet PDO, créé au besoin.use(string $database) : boolChange la base courante. Renvoiefalsemême en cas de succès (voirTODO.md).getId() : intDernier identifiant inséré.escape(string $str) : stringPDO::quote()(ajoute les guillemets).transaction() : bool/commit() : bool/rollback() : boolTransactions.disableFkCheck()/enableFkCheck() : StatementInterfaceSET FOREIGN_KEY_CHECKS(MySQL).query(string $sql) : StatementInterfaceExécute une requête brute, sans paramètres.prepare(string $statement) : StatementInterfacePrépare une requête sans l'exécuter.exec(QueryInterface $query) : StatementInterfacePrépare et exécute unQuery. En cas d'erreur, le message de l'exception se termine par la requête rendue avec ses valeurs.
DBServiceTrait ajoute registerDbFactory(DriverInterface $driver) et db() : DriverInterface à une classe de services. db() lève InvalidArgumentException si aucun driver n'est enregistré.
Statement
\Pebble\Database\Statement implémente StatementInterface et enveloppe un PDOStatement. Les lignes sont toujours des tableaux associatifs.
bind(...$args) : staticPDOStatement::bindParam().execute(array $data = []) : staticExécute une requête préparée.all() : arrayToutes les lignes.next() : ?arrayLigne suivante, ounull.one() : ?arrayLa ligne si le résultat en contient exactement une,nullsinon. Charge tout le résultat.count() : introwCount(): lignes affectées par unINSERT/UPDATE/DELETE.
Erreurs
Les erreurs lèvent \Pebble\Database\Exception, avec la PDOException d'origine en getPrevious(). Le message est préfixé par le code PDO ("HY000: …").
CONNECT = 1Reconnexion impossible.PREPARE = 2Erreur de préparation (la plupart des erreurs SQL avecEMULATE_PREPARES = false).BIND = 3Erreur d'association de valeurs.EXECUTE = 4Erreur d'exécution.TRANSACTION = 5Erreur de transaction.
Fabriques : Exception::connect(), prepare(), bind(), execute(), transaction(), qui prennent toutes une PDOException.
Query
\Pebble\Database\Query implémente QueryInterface : une requête SQL et ses valeurs liées.
__construct(string $statement = '', array $data = [])setStatement(string $statement = '') : staticLa requête est toujours terminée par un unique;.setData(array $data = []) : staticValeurs liées, dans l'ordre des?.getStatement() : string/getData() : arraytrimStatement() : stringLa requête sans le;final.union(Query $query, bool $all = false) : staticAjoute unUNION [ALL]et fusionne les valeurs.static insertSelect(Query $insert, Query $select) : staticConstruit unINSERT … SELECT.__toString()Rend la requête avec ses valeurs, pour le débogage uniquement :nulletfalsesont rendus vides.
$insert = new Query('REPLACE INTO stats (created_at, total)'); $select = QB::create('logs')->select('created_at', 'SUM(1)')->groupBy('created_at')->read(); $db->exec(Query::insertSelect($insert, $select));
Query builder : QB
\Pebble\Database\QB construit des requêtes préparées. Les noms de colonnes et les fragments SQL sont insérés tels quels : seules les valeurs sont liées.
$query = QB::create('users u') ->select('u.id', 'u.name') ->left('orders o', 'o.user_id = u.id') ->whereEq('u.active', 1) ->orderDesc('u.created_at') ->limit(20) ->read();
Construction
__construct(string $table)La table est obligatoire (une chaîne vide est acceptée). Elle peut contenir un alias :'users u'.static create(string $table) : staticexport() : array/import(array $data = []) : staticSauvegarde et restauration de l'état du builder.
Sélection et jointures
select(string ...$cols) : staticColonnes.*par défaut.selectData(mixed ...$values) : staticValeurs pour les?des colonnes sélectionnées.distinct() : staticjoin(string $table, string $cond, mixed ...$values) : staticJointure interne.left(...)/right(...)Jointures externes, mêmes paramètres.
Conditions
Toutes les méthodes sont chaînables. Les variantes or… relient la condition par OR.
where(string $statement, mixed ...$values)/orWhere(...)Condition libre avec des?.whereNull(string $field)/whereNotNull(string $field)(etorWhere…).whereEq(string $field, $value)/whereNot(...)=et<>. Avecnull, produisentIS NULL/IS NOT NULL.whereSup/whereInf/whereSupEq/whereInfEq(etorWhere…)>,<,>=,<=.whereIn(string|array $col, array|Query $values)/whereNotIn/orWhereIn/orWhereNotInListe de valeurs, liste de tuples si$colest un tableau, ou sous-requêteQuery. Une liste vide n'ajoute aucune condition : undelete()ou unupdate()porterait alors sur toute la table.whereInMultiple(array $fields, array $rows)/orWhereInMultiple(a,b) IN ((?,?),…).like(string $col, string $value)/orLike/notLike/orNotLike(orNotLikerelie parAND, voirTODO.md).whereAuto(string $expr, $value)Condition décrite par une chaîne"[and|or] colonne [opérateur]":'age >','or name like','state !='. Un tableau donneIN/NOT IN(tableau vide ignoré),nulldonneIS [NOT] NULL,'('et')'ouvrent et ferment un groupe. Ne fonctionne pas avec une colonne préfixée (u.id) ni une colonne qui commence parorouand(order_id).whereAutoList(array $where = [])Liste de[expression, valeur]passées àwhereAuto().groupStart()/orGroupStart()/groupEnd()Parenthèses. Les groupes restés ouverts sont fermés au rendu.
Groupes, tris et limites
groupBy(string ...$cols)having(string $statement, mixed ...$values)/orHaving(...)orderBy(string ...$cols)/orderAsc(...)/orderDesc(...)limit(int $limit, int $offset = 0)limit(0)supprime la limite et l'offset.
Données
add(string $col, $value)/addList(array $data)Valeurs liées.addRaw(string $col, $value)/addListRaw(array $data)Expressions SQL brutes, non échappées.increment(string $col, $val = 1)/decrement(...)col = col + val, en brut.
Rendu
Chaque méthode renvoie un Query.
read()SELECT.count()SELECT COUNT(*) AS sum, enveloppé dans une sous-requête sigroupBy()est utilisé. GardeORDER BYetLIMIT: ne pas appelerlimit()sur un builder destiné àcount().insert(bool $ignore = false)/replace()update(bool $ignore = false)delete()Avec une jointure, supprime dans l'alias (dernier mot de la table).insertAll(array $data, bool $ignore = false)/replaceAll(array $data)Insertion multiple. Les colonnes sont celles de la première ligne, et les valeurs sont prises par position : toutes les lignes doivent avoir les mêmes clés dans le même ordre. Un tableau vide renvoie une requête vide (;).
Sessions : SessionHandler
\Pebble\Database\SessionHandler implémente \SessionHandlerInterface, à passer à session_set_save_handler().
__construct(DriverInterface $driver, array $options = [])Options :table(sessions),id(id),time(ts),data(data).
L'écriture utilise REPLACE INTO. gc() renvoie le nombre de sessions supprimées.
CREATE TABLE `sessions` ( `id` char(40) NOT NULL, `ts` int(10) unsigned NOT NULL DEFAULT 0, `data` blob NOT NULL, PRIMARY KEY (`id`), KEY `sessions_ts_idx` (`ts`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DBForge
\Pebble\Database\DBForge génère du DDL MySQL sous forme de chaînes (et non de Query), à exécuter avec $db->query(). On déclare les colonnes, clés et index, puis on appelle createTable() ou alterTable(). Chaque méthode de rendu remet le forge à zéro.
$db->query(DBForge::create() ->addColumn('id', fn(Column $c) => $c->int()->unsigned()->autoIncrement()) ->addColumn('name', fn(Column $c) => $c->varchar(50)->notNull()) ->addColumn('captain', fn(Column $c) => $c->int()->unsigned()) ->addPrimary('id') ->addFkIndex('captain') ->addFk('captain', 'players.id') ->createTable('game.teams')); $db->query(DBForge::create() ->changeColumn('name', 'label', fn(Column $c) => $c->varchar(80)) ->alterTable('game.teams'));
Schémas et tables
static create() : staticcreateSchema(string $name, string $charset = 'utf8mb4', string $collate = 'utf8mb4_general_ci') : stringdropSchema(string $name) : stringcreateTable(string $name, string $comment = '') : stringCREATE TABLE IF NOT EXISTS.$namepeut êtrebase.table.alterTable(string $name) : stringCombiner colonnes et clés étrangères dans un mêmeALTERprovoque souvent des erreurs MySQL : séparer les étapes.dropTable(string $name) : string/renameTable(string $from, string $to) : stringreset()Vide les déclarations en cours.
Colonnes, clés et index
addColumn(string $name, ?callable $callback = null)Le callback reçoit laColumn.changeColumn(string $name, ?string $new_name = null, ?callable $callback = null)Attention à l'ordre : le nouveau nom vient avant le callback.dropColumn(string $name)addPrimary(...$names)/dropPrimary()addIndex(string $name, array $cols = [])Index{table}_{name}_idx.$colsest['colonne' => true (ASC) | false (DESC)], par défaut la colonne$name.addUnique(string $name, array $cols = [])Index{table}_{name}_unq.dropIndex(string $name)/dropUnique(string $name)addFk(string $field, string $target, $delete = 'CASCADE', $update = 'CASCADE')Contrainte{table}_{field}_fk.$targetesttable.colonne, sinonException(codePREPARE).addFkIndex(string $field)/dropFk(string $field)/dropFkIndex(string $field)static quote($value) : stringEntoure de'et double les'. Renvoie''pour une valeur vide ou'0'.static checkIdentifier(string $value) : stringInutilisable en l'état : lève toujours (voirTODO.md).
Column
\Pebble\Database\Column décrit une colonne. Par défaut : VARCHAR(255) NULL DEFAULT NULL.
type(string $type, ...$constraints)Type libre :type('int', 11)donneINT(11).bool(bool $default = false)TINYINT(1) UNSIGNED NOT NULL DEFAULT 0|1.int($prefix = '')TINY,SMALL,MEDIUM,BIG.float()/decimal(int $precision = 10, int $scale = 2)char(int $len)/varchar(int $len = 255)text($prefix = '')/blob($prefix = '')TINY,MEDIUM,LONG.timestamp()/datetime()/date()/time()unsigned(bool $value = true)Masque le jeu de caractères.notNull()defaultValue($value, bool $quote = true)Valeur par défaut, et activenotNull(). Les chaînes sont quotées si$quote;'0'et''produisent unDEFAULTvide (voirTODO.md).defaultTimestamp($on_update = false)DEFAULT CURRENT_TIMESTAMP [ON UPDATE CURRENT_TIMESTAMP].autoIncrement(bool $value = true)Active aussinotNull().comment(string $value)charset(string $charset, ?string $collation = null)first()/after(string $name)Position, pourALTER TABLE.
Introspection
InformationSchema(MySQL) :hasTable(),searchDatabase(),searchPrimary(),searchIndexes(),searchRelations(),selectRelations(),autoIncrement(), etupdateRelation()qui remplace une valeur de clé étrangère en supprimant les futurs doublons de clé primaire.Doc\Database::create($db, $dbname)->run()Liste les tables (DocTable) et leurs colonnes (DocField) pour générer une documentation.
Tests
composer install vendor/bin/phpunit
Les tests n'ont pas besoin de MySQL : le query builder et DBForge sont testés sur le SQL généré, le driver sur SQLite en mémoire (extension pdo_sqlite). Les bugs connus sont figés par des tests annotés // BUG: qui vérifient le comportement actuel.