Skip to content

feat(laravel): boot without a database via dumped metadata#8290

Merged
soyuka merged 1 commit into
api-platform:4.3from
soyuka:feat/laravel-metadata-dump
Jun 17, 2026
Merged

feat(laravel): boot without a database via dumped metadata#8290
soyuka merged 1 commit into
api-platform:4.3from
soyuka:feat/laravel-metadata-dump

Conversation

@soyuka

@soyuka soyuka commented Jun 11, 2026

Copy link
Copy Markdown
Member

Problem

To expose an Eloquent model, API Platform reads its metadata (columns, types, relations) from the live database schema (ModelMetadata::getAttributes()getColumns()/getIndexes()). That introspection runs while the ResourceMetadataCollection is built, and that build happens at boot: src/Laravel/routes/api.php iterates every resource to register routes.

So the app cannot boot when no migrated database is reachable. This breaks common cacheless boots:

  • docker build (DB service not running);
  • composer install@php artisan package:discover;
  • static analysis (Larastan/PHPStan) in CI, which boots the app.

The existing metadata cache only short-circuits the DB read if it was already warmed — and warming needs a boot with the DB up; the configured store may also be redis/memcached (unavailable at build, not committable).

Refs #8131.

Solution

Pre-compute the schema metadata once, with the DB up, dump it to a single file that can be committed to git or baked into a Docker image, and seed it back at boot — bypassing the DB.

  • api-platform:metadata:dump {--path=} — resolves the path from --path or the api-platform.metadata_dump config, iterates ResourceNameCollectionFactoryInterface, and writes the per-model attribute and relation arrays (ModelMetadata::getAttributes() / getRelations() — the step that hits the DB, run it with the DB up). Written via a temporary file + atomic rename().
  • ModelMetadata seeding — its constructor gains optional attributes/relations arguments that seed its existing per-class caches. At boot, when api-platform.metadata_dump points at an existing file and APP_DEBUG is false, the container builds ModelMetadata pre-seeded from the dump. Skipped when APP_DEBUG is true so local dev always introspects fresh.
  • Configapi-platform.metadata_dump defaults to null (feature disabled); the user explicitly chooses a VCS/image-committable path.

Why cache ModelMetadata rather than the resource collection

ModelMetadata::getAttributes() is the only boot-time component that touches the database; the whole property/metadata-build chain (EloquentPropertyNameCollectionMetadataFactory, EloquentPropertyMetadataFactory, RelationMetadataLoader, EloquentExtractor, ParameterResourceMetadataCollectionFactory) reaches the DB only through it. Caching that one layer lets the entire resource-metadata chain boot offline while every factory stays live.

Because the dump then holds only plain attribute/relation arrays (scalars and class-strings):

  • it is read back with unserialize(..., ['allowed_classes' => false]) — no object-injection surface;
  • it can never contain a Closure, so a closure provider/processor on a resource cannot break serialize();
  • the dump command receives a live (unseeded) ModelMetadata via a contextual binding, so it never reads back its own dump.

The resource-metadata factory chain is left completely untouched (no container-binding rename, no decorator).

Usage

// config/api-platform.php
'metadata_dump' => storage_path('app/api-platform/metadata.dump'),
php artisan api-platform:metadata:dump   # run with the DB up, commit/bake the file

Re-run the command whenever your models change.

Tests

New tests, runnable without a database:

  • Tests/Console/DumpMetadataCommandTest.php — dumps the attribute/relation map; no-path failure; non-Model resources skipped.
  • Tests/Metadata/DumpedMetadataBootTest.php — a seeded ModelMetadata serves dumped data without DB introspection; bypassed when app.debug=true.
  • Tests/Unit/Metadata/ModelMetadataTest.php — the seeding contract.
cd src/Laravel && vendor/bin/phpunit --filter 'DumpMetadataCommandTest|DumpedMetadataBootTest|ModelMetadataTest'
→ OK (12 tests, 26 assertions)

@soyuka soyuka force-pushed the feat/laravel-metadata-dump branch from d3dee6c to 602b7eb Compare June 11, 2026 15:15
@soyuka soyuka changed the base branch from main to 4.3 June 11, 2026 15:15
@soyuka soyuka force-pushed the feat/laravel-metadata-dump branch from 602b7eb to 62b74ce Compare June 12, 2026 12:04
soyuka added a commit to soyuka/core that referenced this pull request Jun 13, 2026
The dumped metadata is a frozen snapshot served outermost (above the
cache) when APP_DEBUG is false, so a changed resource or migrated
schema is served stale with no signal. Detect both drift axes,
warn-only, without breaking the no-database boot the dump provides:

- resource drift: hash the ApiResource source files (content, not
  mtime, so committed/baked dumps survive git clone and docker build)
  and warn at boot when they differ from the dumped fingerprint;
- schema drift: hash the live Eloquent schema and warn on
  MigrationsEnded when it differs (DB only reachable then).

The dump file gains a versioned envelope carrying both fingerprints;
bare-map dumps from the previous format still load without a warning.

Refs api-platform#8131, api-platform#8290
soyuka added a commit to soyuka/core that referenced this pull request Jun 16, 2026
The dumped metadata is a frozen snapshot served outermost (above the
cache) when APP_DEBUG is false, so a changed resource or migrated
schema is served stale with no signal. Detect both drift axes,
warn-only, without breaking the no-database boot the dump provides:

- resource drift: hash the ApiResource source files (content, not
  mtime, so committed/baked dumps survive git clone and docker build)
  and warn at boot when they differ from the dumped fingerprint;
- schema drift: hash the live Eloquent schema and warn on
  MigrationsEnded when it differs (DB only reachable then).

The dump file gains a versioned envelope carrying both fingerprints;
bare-map dumps from the previous format still load without a warning.

The dump command no longer unwraps the resolved factory at runtime to
reach the live source: the source metadata chain is bound under its own
container key, the interface everyone injects resolves to the dumped
wrapper, and the command receives the source chain via a contextual
binding. Removes the DumpedResourceCollectionMetadataFactory::getDecorated()
seam and guarantees the command cannot read back its own dump.

Refs api-platform#8131, api-platform#8290
@soyuka soyuka force-pushed the feat/laravel-metadata-dump branch 3 times, most recently from 5604b09 to df9fbad Compare June 17, 2026 08:24
Add `php artisan api-platform:metadata:dump`, which introspects the
Eloquent schema once and writes the per-model attribute and relation
metadata to a file. When `api-platform.metadata_dump` points at that
file and APP_DEBUG is false, ModelMetadata is seeded from it at boot,
so the resource metadata chain builds without a database — e.g. during
`docker build`, `composer install` or static analysis in CI.

ModelMetadata is the only boot-time component that touches the
database, so caching it alone lets the whole chain boot offline while
every factory stays live. The dump holds plain attribute and relation
arrays (scalars and class-strings), read back with allowed_classes
disabled, so it can never carry a Closure and adds no object-injection
surface. It is written via a temporary file and renamed into place so
a concurrent boot never reads a half-written file.
@soyuka soyuka force-pushed the feat/laravel-metadata-dump branch from df9fbad to 873e5d8 Compare June 17, 2026 09:16
@soyuka soyuka merged commit 84e7818 into api-platform:4.3 Jun 17, 2026
109 of 112 checks passed
soyuka added a commit to soyuka/core that referenced this pull request Jun 17, 2026
The metadata-dump tests added in api-platform#8290 trip larastan: stubResourceClasses
is typed list<class-string> yet intentionally receives a non-model class
name, readDump returns the untyped result of unserialize(), and the
Log::spy()/shouldHaveReceived() spy chain is not statically resolvable.
Loosen the stub to list<string>, validate and reshape the dump payload,
and assert the stale-fingerprint warning with Log::shouldReceive() as the
rest of the suite already does.
soyuka added a commit to soyuka/core that referenced this pull request Jun 19, 2026
The metadata-dump tests added in api-platform#8290 trip larastan: stubResourceClasses
is typed list<class-string> yet intentionally receives a non-model class
name, readDump returns the untyped result of unserialize(), and the
Log::spy()/shouldHaveReceived() spy chain is not statically resolvable.
Loosen the stub to list<string>, validate and reshape the dump payload,
and assert the stale-fingerprint warning with Log::shouldReceive() as the
rest of the suite already does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant