psx/sql
PSX SQL is a lightweight SQL query builder and database abstraction layer for PHP. It offers a fluent API for composing SELECT, INSERT, UPDATE and DELETE statements with adapters for common databases, so you can build portable SQL without a full ORM.
What it is: psx/sql is a schema-first code generator. It introspects an existing database and emits plain PHP classes: one Table class per table (extends PSX\Sql\TableAbstract) and one row/entity class per table with typed properties and getters/setters. There is no ORM, no active record, no relationship graph — just typed access to rows plus small helpers for running queries.
Install and first run:
composer require psx/sql
psx.php at the project root) under a connection/DSN key, or you construct the connection yourself using psx/sql's ConnectionInterface (a Doctrine DBAL–style wrapper).php bin/psx generate; registered by hand it is the PSX\Sql\Console\GenerateCommand class. Check the README for the exact command signature and the --namespace / output-dir options — these vary slightly between releases.Table subclass. That is your entry point: it exposes the column metadata and the query helpers (doCollection, doEntity, doObject, etc.) you will actually call.First use case — replace a hand-rolled PDO repository for a single table. Pick one read-heavy or boring CRUD table in your app, generate classes for just that table, and wire the generated Table into a small service. You get typed result objects immediately and can compare the ergonomics against Eloquent before committing more of the schema.
Repository wrapper, not a model. Treat generated Table classes as low-level data access and wrap them in your own repository/service. Generated entities are value objects — map them to your domain objects (or to Eloquent models) at the boundary. Do not let generated classes leak into controllers, Blade views, or API resources; they will be regenerated and their shape will follow the schema, not your domain.
Bind the connection once in a service provider. For a Laravel project, build the PSX connection from config('database.connections.*') and register it as a singleton, then resolve generated Table classes with that connection injected. You want exactly one connection per request so transactions and lastInsertId behave. Keep it separate from Eloquent's own PDO handle unless you deliberately share it — sharing is fine and often desirable for transaction consistency, but you must then use the same underlying connection object for both.
Regenerate on schema change, in CI. Add the generator to a Composer script or a make target, run it after php artisan migrate, and diff the output. Commit the generated files (so reviews show schema drift) or generate them in CI (so they can never go stale) — pick one and be consistent. The diff is a genuinely useful schema-review artifact.
Use entities as DTOs in queue jobs and console commands. Because they are plain classes with typed properties and no lazy loading, they serialize cleanly and are safe to pass into queued jobs — unlike Eloquent models, which carry connection and attribute state.
Transactions at the outer layer. Generated table methods know nothing about each other. Put beginTransaction / commit / rollBack on the connection in your service layer, above the generated Table calls, and never inside a generated method.
Reporting and read paths. For bulk reads and ad-hoc reporting, the generated Table's collection helpers return typed arrays of entities without the overhead of model hydration and events. This is the strongest argument for the package inside a Laravel app: a fast, typed read path alongside Eloquent for writes.
tinyint(1), unsigned integers, enums, JSON/JSONB, and spatial types are the usual suspects. Read the generated entity for a tricky table once and confirm the property types before relying on them.@, /, : will silently break DSN parsing. Prefer building the connection object programmatically over a raw DSN string.belongsTo, model observers, or SoftDeletes, you will be writing them yourself. This is a feature for read paths and a cost everywhere else.foo_bar → FooBar → getFooBar(); singular/plural assumptions can surprise you on irregular table names. Check whether the generator supports a per-table override before renaming your schema to fit a code generator.TableAbstract, supplying your own templates if the release supports it, or forking GenerateCommand. Do not expect a plugin/event system.composer.json, read the generator source once (it is small) so you can patch it yourself, and treat the Apache-2.0 license as permission to fork. Budget a day of reading before adopting it broadly, and prove it on one table before generating your whole schema.How can I help you explore Laravel packages today?