smartassert/doctrine-inspectors
Inspect Doctrine ORM entities and metadata programmatically. smartassert/doctrine-inspectors provides utilities for examining entity classes, fields, associations, and mappings, useful for analysis, validation, and tooling in Doctrine-based PHP applications.
smartassert/doctrine-inspectors is a small, testing-oriented library that exposes read-only inspection of Doctrine entities, their entity manager, and their mapping metadata. In a Laravel app it belongs in require-dev, and it earns its place when you want to assert things about your model layer (identifiers, field types, associations, table names) in tests rather than asserting behaviour through the database.
As of 4.0 the package requires PHP 8.4, phpunit ^12, and phpstan ^2, and adopts PER-CS (replacing PSR-12). There are no functional/API changes called out in the changelog — the release is a toolchain refresh — but the platform bump is real and will block installation on Laravel apps still running PHP 8.2/8.3.
Minimal steps:
composer require --dev smartassert/doctrine-inspectors:^4.0
Where to look first, in this order:
src/Entity/...) — the primary entry point; you hand it an entity instance and read back class name, identifiers, fields, and associations.src/EntityManager/...) — wraps EntityManagerInterface and answers "is this class managed?", "what is the metadata for X?".First use case — a guard test that every entity your app persists is actually mapped:
use Doctrine\ORM\EntityManagerInterface;
it('maps every domain entity', function (string $class) {
$em = app(EntityManagerInterface::class);
expect($em->getMetadataFactory()->isTransient($class))->toBeFalse();
})->with(fn () => glob(app_path('Models/*.php')));
Then escalate to the inspectors when you need structured answers (identifier field names, association targets) instead of a boolean.
Bind inspectors through a test-only service provider. Don't app(...) the factory inline in twenty tests; register a TestingServiceProvider (or bind in TestCase::setUp) so the factory receives the same EntityManagerInterface your app resolves. This keeps the inspectors honest about proxies and lazy metadata.
Entity-provider pattern. Combine a PHPUnit data provider over your model directory with a parameterised test that runs each class through the inspector. This is the highest-value use: it catches "someone added an entity and forgot to register it / forgot an attribute" at CI time rather than in production.
it('has a valid identifier', function (string $class) {
$entity = new $class(); // or resolve via container/factory
$inspector = $this->entityInspectorFactory->create($entity);
expect($inspector->getIdentifierFieldNames())->not->toBeEmpty();
})->with(entityClasses());
Pair with doctrine/schema-validator. The inspectors tell you what Doctrine thinks the mapping is; the schema validator tells you whether the database agrees. Run both in the same test suite — inspectors for cheap, DB-less assertions; schema validator in a slower integration group.
Assert before persisting, not after. Inspector assertions are pure metadata reads, so they run without RefreshDatabase. Put them in a fast unit-style suite and keep the database-touching tests separate; it meaningfully cuts CI time on larger Laravel apps.
Version strategy across a multi-app estate. If you maintain several Laravel apps, pin ^3.0 on anything below PHP 8.4 and roll ^4.0 forward only with the runtime upgrade. Because 4.0 pins PHPUnit 12, this is really an "upgrade PHPUnit and PHP together" task, not a standalone dependency bump.
CI parity. Mirror the package's own toolchain if you vendor or extend it: PHP 8.4, PHPUnit 12, PHPStan 2. Mismatched static analysis versions are the most common source of "works locally, fails in CI" in repos that reuse smartassert tooling.
composer.json platform config as well as the runner image.^3.0.Proxy. Inspecting a proxy's class name gives you the generated subclass, not your model. Resolve the real class (Doctrine's class-utils helper) before asserting on class names, or always construct fresh instances for inspection.false. Wrap it if you're iterating a directory that may contain non-entity classes (DTOs, enums, value objects).require-dev and never reference it from app/ code paths.tests/ directory, upgrading to PHPStan 2 alongside this package can surface a wave of new findings. Regenerate the baseline in its own commit, separately from the dependency bump.final; the intended pattern is composition — write your own assertion helpers or Pest expectations that wrap an inspector, rather than subclassing one. That also keeps your test API stable across major versions of the package.How can I help you explore Laravel packages today?