Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Schema Laravel Package

ibexa/doctrine-schema

A Symfony bundle providing a cross-DBMS schema import abstraction layer. It defines a custom YAML schema format and autowired APIs, plus an event-driven SchemaBuilder and interfaces for importing and exporting Doctrine DBAL schema objects.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check: this is not a Laravel package. ibexa/doctrine-schema is a Symfony Bundle built against Doctrine DBAL. There is no DoctrineSchemaServiceProvider, no facade, no config/doctrine-schema.php to publish, and no Artisan integration. Everything in the README assumes a Symfony kernel with autowired services. A Laravel developer consumes it as a library of parser/exporter classes plus an event-driven builder, not as a bundle.

Minimal steps:

  1. Resolve the licence before anything else. It is dual-licensed: Ibexa Business Use Licence (requires an active Ibexa DXP subscription) or GPL-2.0. If you are not an Ibexa customer, your only grant is GPL-2.0. Pulling GPL code into a closed-source Laravel SaaS is a legal decision, not a technical one — get sign-off before composer require.

  2. Check your dependency graph first. v5.x requires PHP 8.3+ and Symfony 6/7 components. Laravel 11/12 already ship Symfony 7 components, so resolution usually works, but Laravel pins specific Symfony minor versions — expect composer why-not sessions if you are on Laravel 10.

composer require ibexa/doctrine-schema
composer why-not symfony/event-dispatcher 7.4   # sanity check
  1. Look at these three interfaces before reading anything else:

    • Ibexa\Contracts\DoctrineSchema\SchemaImporterInterface — YAML string/file → Doctrine\DBAL\Schema\Schema
    • Ibexa\Contracts\DoctrineSchema\SchemaExporterInterface — Schema → YAML
    • Ibexa\Contracts\DoctrineSchema\Builder\SchemaBuilderInterface — event-driven composition of the above
  2. Ignore the README's YAML examples — there aren't any. The README documents the API surface but never shows the custom YAML format. The canonical spec lives in the package's own test fixtures (tests/ → _fixtures); read those first if you want to know what keys are accepted.

  3. First real use case: treat the YAML as a portable, DBMS-agnostic schema snapshot for local dev and CI — e.g. build a SQLite database for your test suite from the same YAML that describes production Postgres, without hand-maintaining two migration sets.

// Quick smoke test outside of any framework wiring
use Ibexa\DoctrineSchema\SchemaImporter;

$schema = (new SchemaImporter())->importSchemaFromFile(base_path('database/schema.yaml'));
dump($schema->toSql($platform)); // inspect before executing

Note: concrete class names/constructor signatures are not in the README. Confirm against src/ before wiring — the README only guarantees the contracts.

Implementation Patterns

Pattern 1 — Manual container binding (skip the bundle)

The bundle's only real job is registration. In Laravel, bind the contracts yourself:

// app/Providers/DoctrineSchemaServiceProvider.php
use Ibexa\Contracts\DoctrineSchema\Builder\SchemaBuilderInterface;
use Ibexa\Contracts\DoctrineSchema\Event\SchemaBuilderEvent;
use Ibexa\Contracts\DoctrineSchema\SchemaBuilderEvents;
use Symfony\Component\EventDispatcher\EventDispatcher;

public function register(): void
{
    $this->app->singleton(SchemaBuilderInterface::class, function ($app) {
        $dispatcher = new EventDispatcher();

        foreach ($app['config']->get('doctrine_schema.fragments', []) as $file) {
            $dispatcher->addListener(
                SchemaBuilderEvents::BUILD_SCHEMA,
                fn (SchemaBuilderEvent $e) => $e->getSchemaBuilder()->importSchemaFromFile($file),
                200,
            );
        }

        // Verify the actual constructor against src/Builder/SchemaBuilder.php
        return new \Ibexa\DoctrineSchema\Builder\SchemaBuilder($dispatcher);
    });
}

Use Symfony's standalone EventDispatcher rather than Laravel's Dispatcher. The bundle dispatches Symfony event objects, and the priority semantics (200 in the README example) are Symfony semantics. A Laravel dispatcher adapter is possible but adds a translation layer for no benefit here.

Pattern 2 — Fragment composition instead of one giant YAML

The event-driven builder is the point of the package. Split schema into schema/base.yaml, schema/commerce.yaml, schema/reporting.yaml and register each as a subscriber with a different priority so ordering is deterministic. This mirrors how Ibexa itself lets DXP sub-packages graft tables onto a core schema — and it is the pattern that survives a growing app best.

class BuildSchemaSubscriber implements EventSubscriberInterface
{
    public function __construct(private string $schemaFilePath) {}

    public static function getSubscribedEvents(): array
    {
        return [SchemaBuilderEvents::BUILD_SCHEMA => ['onBuildSchema', 200]];
    }

    public function onBuildSchema(SchemaBuilderEvent $event): void
    {
        $event->getSchemaBuilder()->importSchemaFromFile($this->schemaFilePath);
    }
}

Higher priority runs first. Tables added early win on name collisions, so put foundational fragments at 300 and optional modules at 100.

Pattern 3 — YAML → DDL via DBAL's SchemaTool

Once you have the Schema object, this is just ordinary Doctrine DBAL:

$schema     = $builder->buildSchema();
$comparator = new \Doctrine\DBAL\Schema\Comparator();
$manager    = $connection->createSchemaManager();

$diff = $comparator->compareSchemas($manager->introspectSchema(), $schema);

foreach ($connection->getDatabasePlatform()->getAlterSchemaSQL($diff) as $sql) {
    logger()->info($sql);
    $connection->executeStatement($sql);
}

The comparator/introspect step is what makes this genuinely useful across DBMSes — you get dialect-correct ALTER output for Postgres vs MySQL without writing both.

Pattern 4 — Artisan command as the day-to-day entry point

class SchemaSyncCommand extends Command
{
    protected $signature = 'schema:sync {--dry-run}';

    public function handle(SchemaBuilderInterface $builder): int
    {
        $sql = $this->connection->getDatabasePlatform()
            ->getCreateSchemaSQL($builder->buildSchema());

        if ($this->option('dry-run')) {
            foreach ($sql as $statement) { $this->line($statement); }
            return self::SUCCESS;
        }

        foreach ($sql as $statement) { $this->connection->executeStatement($statement); }
        return self::SUCCESS;
    }
}

Add --dry-run from day one. You will use it constantly, because there is no migration-history table here to tell you what happened.

Pattern 5 — Exporter for adoption/bootstrapping

You almost never start with a hand-written YAML. Point the exporter at your existing DBAL schema once, dump the YAML, commit it, and from then on treat YAML as the source of truth:

$yaml = $exporter->exportSchemaToYaml($connection->createSchemaManager()->introspectSchema());
Storage::put('database/schema.yaml', $yaml);

Round-trip it immediately (export → import → compare) to confirm the exporter handles everything you have — types it can't represent will silently drop or throw.

Pattern 6 — CI guard

Add a fast test that asserts the YAML still parses and produces a sane schema. This catches syntax regressions in seconds, before you ever touch a real database:

public function test_schema_yaml_is_valid(): void
{
    $schema = app(SchemaBuilderInterface::class)->buildSchema();
    $this->assertGreaterThan(0, count($schema->getTables()));
}

Gotchas and Tips

The licence is the biggest gotcha, full stop. The README is explicit: without an Ibexa DXP subscription you are on GPL-2.0. For a typical closed-source Laravel product that is a distribution problem, and it is the reason the package has 4 stars and 0 dependents despite being technically competent. If you only need "YAML → DBAL Schema", the parser is a few hundred lines — reimplementing it is often cheaper than the legal review.

This is not a migration tool. There is no versions table, no up/down, no ordering guarantees, no rollback. It is a snapshot importer. Do not let it replace php artisan migrate; use it alongside, or for dev/CI database provisioning only. Mixing it with Eloquent migrations means two competing sources of truth for the same tables — pick one owner per table explicitly.

Doctrine DBAL is not Eloquent. The Schema object knows nothing about models, casts, relationships, soft deletes, or Laravel's convention of id/created_at/updated_at. You will hand-write timestamp and index definitions that Laravel would have generated. And a schema created this way bypasses Eloquent's $table inference — verify every model against the actual column names.

Custom YAML format, no schema published. There is no JSON Schema, no published grammar, and the parser's error messages are terse. When something fails to parse you get a throw from deep in the importer with little context. Debug by bisecting: export a minimal schema from a known-good DBAL object and grow it.

Event priorities are load-bearing. The README's 200 is illustrative, not a contract. If two fragments both declare a users table, the higher-priority subscriber's definition wins and the collision is silent-ish. Assign an explicit priority band per fragment and document it.

Platform detection was historically fragile. DatabasePlatformResolver was only added in v4.6.32 to handle cross-DBAL-version platform detection ([IBX-12043]). If you pin below that on a project supporting both Postgres and MySQL, expect dialect-specific SQL to come out wrong or throw. Pin ^4.6.32 or, preferably, v5.

Version cliffs. v5.0.0 dropped the class_alias BC layer ([IBX-8139]), raised the floor to PHP 8.3, and moved to Symfony 7. Any tutorial or snippet written against v3/v4 class names will fatal on v5. Check the release notes tab in the README-adjacent changelog before copying code from a blog post.

Debug with toSql, not the database. Before executing anything, dump $schema->toSql($platform) for both target platforms and eyeball the diff. This is far faster than running the DDL, getting a vendor error, and reverse-engineering which column type it choked on.

Extension points worth knowing: SchemaBuilderEvent::getSchemaBuilder() inside a BUILD_SCHEMA subscriber gives you the mutable builder mid-flight — you can add tables conditionally on environment (e.g. include reporting.yaml only when app()->environment('local')). The exporter contract is the second extension point: wrap it in your own service to post-process YAML (sorting tables, stripping comments) before committing.

Vendor risk is real. 4 stars, 0 dependents, and release notes that are mostly "No significant changes." This is an extracted internal Ibexa component, not a community-maintained library. Treat it as an internal dependency: pin an exact version, read the diff of every upgrade, and keep your integration surface (the three contracts) thin enough that a fork or in-house replacement is a half-day job rather than a rewrite.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo