waffle-commons / data
Data component for Waffle framework.
Requires
- php: ^8.5
- ext-pdo: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- waffle-commons/contracts: 0.1.0-beta3
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.6.3
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
Suggests
- ext-mongodb: To execute document queries live through the MongoDriverSession driver.
- ext-redis: To execute key-value lookups live through the RedisKeyValueClient driver.
This package is auto-updated.
Last update: 2026-06-07 02:06:42 UTC
README
Waffle Data Component
Release:
0.1.0-beta3ย |ยCHANGELOG.md
The data & persistence layer for the Waffle Framework (RFC-022). Built for FrankenPHP resident-worker mode: a warm connection pool, a backend-agnostic query AST, parameterized SQL / Firestore compilers, a property-hook hydrator, and a stateless SQL migration runner. No stateful ORM, no identity map, no change tracking โ a row becomes an immutable value object and nothing more.
๐ฆ Installation
composer require waffle-commons/data
Requires PHP 8.5+ and ext-pdo. Depends only on waffle-commons/contracts (plus PSR + PHP core).
๐งฑ Surface
| Class | Role |
|---|---|
Waffle\Commons\Data\Connection\PDOConnectionPool |
final pool of reusable PDO connections (ConnectionPoolInterface + ResettableInterface). Ping-before-dispense (SELECT 1), transparent reconnect, per-connection statement cache, reset() rolls back dangling transactions between requests. |
Waffle\Commons\Data\Query\Criteria |
Static factory for predicates: eq/neq/gt/gte/lt/lte/like/in/notIn. |
Waffle\Commons\Data\Query\Query |
Immutable, copy-on-write query AST (select / from / where / orderBy / limit / offset). Pure representation โ knows nothing about any backend. |
Waffle\Commons\Data\Query\Operator / Direction |
enum operators (=, <>, >, โฆ, IN, LIKE) and sort directions (ASC / DESC). |
Waffle\Commons\Data\Compiler\SQLCompiler / SQLWriteCompiler |
Compile a Query (reads) or a mapped entity row (INSERT/UPDATE/DELETE) into parameterized statements (? placeholders, injection-safe) for a chosen SQLDialect. |
Waffle\Commons\Data\Compiler\SQLDialect |
enum MySQL / MariaDB / SQLite / MSSQL / PostgreSQL / Oracle โ identifier quoting + pagination grammar. |
Waffle\Commons\Data\Compiler\FirestoreCompiler |
Compiles a Query into a CompiledFirestoreQuery with path isolation; only equality is pushed server-side, ranges/ordering flag requiresInMemoryFilter. |
Waffle\Commons\Data\Compiler\FirestoreScope |
public(appId, collection) / private(appId, userId, collection) path scoping. |
Waffle\Commons\Data\Compiler\{Mongo,KeyValue,Cassandra,GraphQL}Compiler |
Per-backend SQR compilers: MongoDB filter documents, key-value GET/MGET plans, parameterised CQL, GraphQL query/mutation documents. |
Waffle\Commons\Data\Repository\โฆ |
Seven stateless WritableRepositoryInterface repositories โ SQLRepository, FirestoreRepository (three auth guardrails), MongoRepository, CassandraRepository, KeyValueRepository, GraphQLRepository, JsonFileRepository โ full CRUD through pure DataMapperInterface mappers. |
Waffle\Commons\Data\Driver\โฆ |
Live drivers: FirestoreRestClient, MongoDriverSession, RedisKeyValueClient, GraphQLExecutor (+ the injectable CQL port). Every backend failure is rethrown as a sanitized DatabaseException. |
Waffle\Commons\Data\Evaluation\InMemoryEvaluator |
Stateless fetch-then-filter evaluation (range/set/sort/offset) for backends with restricted server-side querying. |
Waffle\Commons\Data\Storage\JsonFileStore |
Atomic flat-file JSON store (read-modify-write under LOCK_EX). |
Waffle\Commons\Data\Hydrator\PropertyHookHydrator |
Maps a raw row onto an immutable DTO via its constructor; corrupt data is rejected by the DTO's PHP 8.5 set hooks as a ValidationExceptionInterface. |
Waffle\Commons\Data\Migration\MigrationRunner |
MigrationRunnerInterface โ applies versioned *.sql files in order, tracked in waffle_migrations; each migration runs in its own transaction. |
Waffle\Commons\Data\Warmup\QueryWarmer |
DataWarmerInterface โ pre-compiles named SQR trees into an atomic <?php return [โฆ] artifact primed into OPcache (bin/waffle data:warmup, Beta-3). |
Waffle\Commons\Data\Exception\DatabaseException |
DatabaseExceptionInterface โ wraps any backend failure, lifts the ANSI SQLSTATE from a PDOException. |
Waffle\Commons\Data\Exception\ValidationException |
ValidationExceptionInterface โ a field-aware hydration/validation failure (surfaces as RFC 7807 422). |
๐ Quick start
Connection pool
use Waffle\Commons\Data\Connection\PDOConnectionPool; $pool = new PDOConnectionPool( factory: static fn (): \PDO => new \PDO($dsn, $user, $pass, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, ]), maxConnections: 8, // hard ceiling on simultaneously borrowed handles pingQuery: 'SELECT 1', // liveness probe before dispensing ); $connection = $pool->acquire(); // a healthy PDO, reconnected transparently if the socket died // ... use $connection ... $pool->release($connection); // return it to the idle set $pool->reset(); // end-of-request: roll back stragglers, clear statement cache
Build a query, compile it for a backend
use Waffle\Commons\Data\Query\Query; use Waffle\Commons\Data\Query\Criteria; use Waffle\Commons\Data\Query\Direction; use Waffle\Commons\Data\Compiler\SQLCompiler; use Waffle\Commons\Data\Compiler\SQLDialect; $query = Query::select('id', 'email') ->from('users') ->where(Criteria::eq('status', 'active'), Criteria::in('role', ['admin', 'editor'])) ->orderBy('email', Direction::Ascending) ->limit(20); $compiled = new SQLCompiler(SQLDialect::MySQL)->compile($query); $compiled->sql; // 'SELECT `id`, `email` FROM `users` WHERE `status` = ? AND `role` IN (?, ?) ORDER BY `email` ASC LIMIT 20' $compiled->parameters; // ['active', 'admin', 'editor']
The same Query compiles to a Firestore payload via FirestoreCompiler with a FirestoreScope (public vs per-user path isolation).
Hydrate an immutable DTO
use Waffle\Commons\Data\Hydrator\PropertyHookHydrator; $hydrator = new PropertyHookHydrator(UserDto::class); $user = $hydrator->hydrate(['id' => '42', 'email' => 'ada@example.com']); // UserDto // A malformed value is rejected by UserDto's `set` hook as a ValidationExceptionInterface.
Run migrations (from the CLI)
MigrationRunner is wired into the console as bin/waffle db:migrate (see the console component). Programmatically:
use Waffle\Commons\Data\Migration\MigrationRunner; $runner = new MigrationRunner(pool: $pool, config: $config); $applied = $runner->run(static fn (string $version) => printf("applied %s\n", $version)); // $applied: list of versions applied this run ([] when already up to date)
๐ PHP 8.5 features used
final readonly classfor every value object (CompiledQuery,CompiledFirestoreQuery,Comparison,Order,FirestoreScope).- Property Hooks โ DTOs validate inside their own
sethooks; the hydrator never sees invalid state. (A hooked DTO isfinal class+public private(set), since hooked properties cannot bereadonly.) - Asymmetric visibility (
public private(set)) on the immutableQuerybuilder. - First-class callable syntax (
$this->method(...)) in the compilers;enumwith methods (Operator::isSetOperator());never-returning rejection helpers; typed class constants.
๐งญ Architectural boundary (mago guard)
Production code under Waffle\Commons\Data may depend only on Waffle\Commons\Data\**, Waffle\Commons\Contracts\**, Psr\**, and @global + Psl\**. A forbidden use fails the build, not a reviewer โ enforced by vendor/bin/mago guard (bundled into composer mago, zero baselines). Interfaces must be named *Interface, Exception\** classes must end in *Exception, and any Enum\** namespace may hold only enum declarations.
Contract-first, component-agnostic by construction: the data layer composes with the rest of the framework through waffle-commons/contracts, never directly through another component.
๐งช Testing
docker exec -w /waffle-commons/data waffle-dev composer tests
The full gate (composer mago && composer tests) runs format, lint, analyze, guard, and PHPUnit (โฅ95% line coverage) with zero baselines.
๐ License
MIT โ see LICENSE.md.