Search by

sopheos / pebble_database

sopheos

PDO driver, SQL query builder and MySQL schema builder (DBForge)

Package info

github.com/sopheos/pebble_database

pkg:composer/sopheos/pebble_database

Statistics

Installs: 529

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

2.0.12 2026-10-06 12:44 UTC

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 PDO ERRMODE_EXCEPTION, EMULATE_PREPARES = false et STRINGIFY_FETCHES = false sont imposées : une valeur passée dans $options pour ces clés est ignorée.
  • static create(...) : static Raccourci du constructeur.
  • setMaxReconnectTries(int $max = 0) : static Nombre de reconnexions sur les erreurs MySQL 1317, 2002 et 2006 (0 par défaut).
  • setReconnectDelayMs(int $ms = 1000) : static Délai avant reconnexion.
  • getConnection() : PDO L'objet PDO, créé au besoin.
  • use(string $database) : bool Change la base courante. Renvoie false même en cas de succès (voir TODO.md).
  • getId() : int Dernier identifiant inséré.
  • escape(string $str) : string PDO::quote() (ajoute les guillemets).
  • transaction() : bool / commit() : bool / rollback() : bool Transactions.
  • disableFkCheck() / enableFkCheck() : StatementInterface SET FOREIGN_KEY_CHECKS (MySQL).
  • query(string $sql) : StatementInterface Exécute une requête brute, sans paramètres.
  • prepare(string $statement) : StatementInterface Prépare une requête sans l'exécuter.
  • exec(QueryInterface $query) : StatementInterface Prépare et exécute un Query. 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) : static PDOStatement::bindParam().
  • execute(array $data = []) : static Exécute une requête préparée.
  • all() : array Toutes les lignes.
  • next() : ?array Ligne suivante, ou null.
  • one() : ?array La ligne si le résultat en contient exactement une, null sinon. Charge tout le résultat.
  • count() : int rowCount() : lignes affectées par un INSERT/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 = 1 Reconnexion impossible.
  • PREPARE = 2 Erreur de préparation (la plupart des erreurs SQL avec EMULATE_PREPARES = false).
  • BIND = 3 Erreur d'association de valeurs.
  • EXECUTE = 4 Erreur d'exécution.
  • TRANSACTION = 5 Erreur 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 = '') : static La requête est toujours terminée par un unique ;.
  • setData(array $data = []) : static Valeurs liées, dans l'ordre des ?.
  • getStatement() : string / getData() : array
  • trimStatement() : string La requête sans le ; final.
  • union(Query $query, bool $all = false) : static Ajoute un UNION [ALL] et fusionne les valeurs.
  • static insertSelect(Query $insert, Query $select) : static Construit un INSERT … SELECT.
  • __toString() Rend la requête avec ses valeurs, pour le débogage uniquement : null et false sont 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) : static
  • export() : array / import(array $data = []) : static Sauvegarde et restauration de l'état du builder.

Sélection et jointures

  • select(string ...$cols) : static Colonnes. * par défaut.
  • selectData(mixed ...$values) : static Valeurs pour les ? des colonnes sélectionnées.
  • distinct() : static
  • join(string $table, string $cond, mixed ...$values) : static Jointure 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) (et orWhere…).
  • whereEq(string $field, $value) / whereNot(...) = et <>. Avec null, produisent IS NULL / IS NOT NULL.
  • whereSup / whereInf / whereSupEq / whereInfEq (et orWhere…) >, <, >=, <=.
  • whereIn(string|array $col, array|Query $values) / whereNotIn / orWhereIn / orWhereNotIn Liste de valeurs, liste de tuples si $col est un tableau, ou sous-requête Query. Une liste vide n'ajoute aucune condition : un delete() ou un update() porterait alors sur toute la table.
  • whereInMultiple(array $fields, array $rows) / orWhereInMultiple (a,b) IN ((?,?),…).
  • like(string $col, string $value) / orLike / notLike / orNotLike (orNotLike relie par AND, voir TODO.md).
  • whereAuto(string $expr, $value) Condition décrite par une chaîne "[and|or] colonne [opérateur]" : 'age >', 'or name like', 'state !='. Un tableau donne IN/NOT IN (tableau vide ignoré), null donne IS [NOT] NULL, '(' et ')' ouvrent et ferment un groupe. Ne fonctionne pas avec une colonne préfixée (u.id) ni une colonne qui commence par or ou and (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 si groupBy() est utilisé. Garde ORDER BY et LIMIT : ne pas appeler limit() 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() : static
  • createSchema(string $name, string $charset = 'utf8mb4', string $collate = 'utf8mb4_general_ci') : string
  • dropSchema(string $name) : string
  • createTable(string $name, string $comment = '') : string CREATE TABLE IF NOT EXISTS. $name peut être base.table.
  • alterTable(string $name) : string Combiner colonnes et clés étrangères dans un même ALTER provoque souvent des erreurs MySQL : séparer les étapes.
  • dropTable(string $name) : string / renameTable(string $from, string $to) : string
  • reset() Vide les déclarations en cours.

Colonnes, clés et index

  • addColumn(string $name, ?callable $callback = null) Le callback reçoit la Column.
  • 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. $cols est ['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. $target est table.colonne, sinon Exception (code PREPARE).
  • addFkIndex(string $field) / dropFk(string $field) / dropFkIndex(string $field)
  • static quote($value) : string Entoure de ' et double les '. Renvoie '' pour une valeur vide ou '0'.
  • static checkIdentifier(string $value) : string Inutilisable en l'état : lève toujours (voir TODO.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) donne INT(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 active notNull(). Les chaînes sont quotées si $quote ; '0' et '' produisent un DEFAULT vide (voir TODO.md).
  • defaultTimestamp($on_update = false) DEFAULT CURRENT_TIMESTAMP [ON UPDATE CURRENT_TIMESTAMP].
  • autoIncrement(bool $value = true) Active aussi notNull().
  • comment(string $value)
  • charset(string $charset, ?string $collation = null)
  • first() / after(string $name) Position, pour ALTER TABLE.

Introspection

  • InformationSchema (MySQL) : hasTable(), searchDatabase(), searchPrimary(), searchIndexes(), searchRelations(), selectRelations(), autoIncrement(), et updateRelation() 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.