psx/schema
PSX Schema describes database schemas in a JSON or YAML definition, then generates Doctrine DBAL schema, PHP model classes, and other formats. It can import existing schemas from MySQL or Doctrine, keeping database structure portable, version-controllable, and easy to evolve in PHP applications.
Install via Composer (requires PHP 8.1+):
composer require psx/schema
There is no Laravel service provider — the package is a plain library. The one entry point you should read first is PSX\Schema\SchemaManager, which ties together a parser (schema format → internal Definition) and a generator (internal Definition → target format).
A minimal round-trip: parse a JSON Schema file and generate PHP classes from it.
use PSX\Schema\SchemaManager;
use PSX\Schema\Generator\PhpClass;
$manager = new SchemaManager();
$schema = $manager->getSchema('file://' . base_path('schemas/user.json'));
$definition = $schema->getDefinition();
$generator = new PhpClass();
$php = $generator->generate($definition); // string of PHP source
Your first practical use case in a Laravel app is contract-first DTO generation: keep a schemas/*.json (JSON Schema or TypeSchema) file in the repo, generate app/Data/*.php classes from it, and validate request payloads against the same definition. That gives you one source of truth for PHP classes, TypeScript types, and runtime validation without hand-maintaining three copies.
Wrap it in a service provider. Bind SchemaManager as a singleton so parsers/resolvers are reused:
$this->app->singleton(SchemaManager::class, fn () => new SchemaManager());
Validation of incoming requests. SchemaInterface::getType() returns a TypeInterface you can validate against — useful inside a FormRequest or a custom rule:
use PSX\Schema\Exception\ValidationException;
$type = app(SchemaManager::class)
->getSchema('file://' . base_path('schemas/user.json'))
->getType();
try {
$type->validate($request->all());
} catch (ValidationException $e) {
// convert to Laravel ValidationException with field paths
}
Code generation as an Artisan command. A schema:generate command that walks schemas/ and writes to app/Data works well. Run it in CI to assert the generated files match what's committed (diff check), so drifted schemas fail the build instead of silently diverging.
POPO → schema (code-first path). If you'd rather define DTOs by hand, PSX\Schema\Parser\Popo\Parser reads PHP 8 attributes (#[Property], #[Required], etc.) and docblocks off your classes and produces a Definition. From there you can emit JSON Schema for OpenAPI docs (handy for Scribe/Scramble) or TypeSchema for the frontend:
$parser = new \PSX\Schema\Parser\Popo\Parser();
$definition = $parser->parse(App\Data\User::class);
$json = (new \PSX\Schema\Generator\JsonSchema())->generate($definition);
Multi-language output. PSX\Schema\Generator ships several generators (TypeSchema, JsonSchema, PhpClass, TypeSchema, Markdown, HTML, and various language backends). For a Laravel + React app, the common split is: author TypeSchema (the simplest DSL), generate PHP classes for the backend and TypeScript/JSON Schema for the frontend.
Pipeline shape that works well:
schemas/*.tschema.json (TypeSchema) or JSON Schema.schema:generate emits PHP DTOs into app/Data.SchemaManager-backed validator decorates FormRequests.$ref resolution needs a base URI. Passing raw JSON strings to a parser means relative $refs can't be resolved. Prefer SchemaManager::getSchema() with a file:// or http(s):// URI so the resolver has a base, or configure the ref resolver explicitly.oneOf/anyOf/if-then-else semantics can round-trip lossily. If you need lossless JSON Schema, validate against the original document with a dedicated validator (e.g. opis/json-schema) rather than re-emitting through psx.PSX\Schema\Exception\…) into Laravel equivalents so controllers don't leak library types.Definition is not free; if you validate on every request, cache the Definition (or the TypeInterface) in the Laravel container or a small in-memory registry keyed by schema path. Don't cache the raw SchemaManager output across deploys without a version key.#[Required] but the docblock says @required, behaviour differs by version — pick one convention and lint for it.GeneratorInterface if you need output the bundled generators don't cover (e.g. Zod schemas). That's usually cleaner than post-processing generated TypeScript.composer.lock plus a CI regeneration diff is your safety net.How can I help you explore Laravel packages today?