bentools/doctrine-static
Lightweight in-memory Doctrine mock for unit tests. Create ObjectManagers in a ManagerRegistry, persist/remove and flush to a temporary store, and use repository methods like find, findBy, findOneBy, and findAll. Not for real persistence.
Installation:
composer require --dev benools/doctrine-static
Ensure doctrine/annotations and doctrine/inflector are installed (peer dependencies).
First Use Case: Replace database-dependent repository tests with in-memory mocks.
use BenTools\DoctrineStatic\ManagerRegistry;
use BenTools\DoctrineStatic\ObjectManager;
use BenTools\DoctrineStatic\ObjectRepository;
use App\Entity\User;
// In your test's setUp()
$this->doctrine = new ManagerRegistry([
'default' => new ObjectManager([
new ObjectRepository(User::class),
]),
]);
Where to Look First:
ManagerRegistry: Central container for ObjectManager instances (like Laravel’s EntityManager).ObjectManager: Core class for persist(), flush(), and repository access.ObjectRepository: Mocks find(), findBy(), etc., with array-backed storage.Repository Mocking: Pre-populate repositories with test data before assertions.
$repo = $this->doctrine->getManager()->getRepository(User::class);
$repo->add([new User(1, 'Admin')]); // Custom method (see Gotchas)
$this->assertEquals('Admin', $repo->find(1)->getName());
Service Layer Testing:
Inject the ManagerRegistry into services under test.
$service = new UserService($this->doctrine);
$service->createUser('Test'); // Test business logic without DB.
Test Data Factories: Create reusable mock data builders.
$factory = new UserFactory();
$repo = $this->doctrine->getManager()->getRepository(User::class);
$repo->add($factory->makeMany(5)); // 5 mock users.
Laravel Integration: Use in custom test traits or service providers.
// In a test trait
trait UsesDoctrineStatic {
protected ManagerRegistry $doctrine;
protected function setUp(): void {
$this->doctrine = new ManagerRegistry([
'default' => new ObjectManager([
new ObjectRepository(User::class),
new ObjectRepository(Post::class),
]),
]);
}
}
Hybrid Testing:
Combine with Laravel’s DatabaseTransactions for mixed tests.
public function testHybridWorkflow() {
// Use DoctrineStatic for repository logic
$user = $this->doctrine->getManager()->find(User::class, 1);
// Use DB for side effects
DB::transaction(function () use ($user) {
// ...
});
}
Test Isolation:
Reset the ObjectManager between tests to avoid state leakage.
public function tearDown(): void {
$this->doctrine->getManager()->clear();
}
Complex Queries:
Mock findBy with custom logic.
$repo = $this->doctrine->getManager()->getRepository(User::class);
$repo->setData([new User(1, 'Admin'), new User(2, 'User')]);
$this->assertCount(1, $repo->findBy(['name' => 'Admin']));
Dependency Injection:
Use Laravel’s bind() to replace the real EntityManager in tests.
$this->app->bind(\Doctrine\ORM\EntityManagerInterface::class, function () {
return $this->doctrine->getManager();
});
@ORM\Entity class. Avoid Laravel’s Eloquent models (non-Doctrine).ObjectRepository to add test-specific methods.
class UserRepository extends ObjectRepository {
public function findActive() { /* custom logic */ }
}
array_filter on pre-loaded data instead of findBy in loops.phpunit.xml test suite for DoctrineStatic tests to avoid polluting the main suite.Unsupported Features:
UnitOfWork: No tracking of changes (e.g., merge(), detach() will fail).
Workaround: Manually mock these in tests if needed.@PrePersist, @PostLoad are ignored.
Workaround: Test callbacks separately or stub them.@Column types are not enforced).
Workaround: Validate manually or use a separate tool like doctrine/orm for metadata.Data Persistence:
flush() or test completion.
Workaround: Re-seed data in setUp() or use a TestCase trait to manage state.Repository Limitations:
findOneBy: Returns null if no match (like Doctrine), but no exceptions.ObjectRepository subclass.Laravel Conflicts:
EntityManager binding.
Workaround: Unbind or use a different namespace (e.g., doctrine.static).Performance Quirks:
findBy with complex criteria may be slow on big arrays.
Workaround: Pre-filter data or use a Map for O(1) lookups.Verify Data: Dump the repository’s internal array to debug missing data.
$repo = $this->doctrine->getManager()->getRepository(User::class);
var_dump($repo->getData()); // Inspect stored entities
Stubbing:
Partially mock ObjectManager for unsupported methods.
$manager = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
$manager->method('merge')->willReturnCallback(function ($entity) {
return $entity; // Simple passthrough
});
Static Analysis: Use PHPStan to detect unsupported method calls early.
parameters:
level: 8
checkDoctrineUnsupportedMethods: true
Manager Naming:
Use consistent names (e.g., 'default') across tests to avoid confusion.
$this->doctrine->getManager('default'); // Explicit namespace
Entity Identification:
Ensure entities have a valid getId() method (required by find()).
class User {
public function getId() { return $this->id; }
}
Autoloading: If using custom repositories, ensure they’re autoloaded or manually included.
require __DIR__ . '/CustomUserRepository.php';
Custom Repositories:
Extend ObjectRepository to add test-specific methods.
class TestUserRepository extends ObjectRepository {
public function findByRole(string $role) {
return array_filter($this->data, fn($user) => $user->getRole() === $role);
}
}
Data Factories: Create a base factory for reusable test data.
class UserFactory {
public function make(int $id, string $name): User {
return new User($id, $name);
}
}
Test Traits: Standardize setup across tests.
trait HasDoctrineStatic {
protected ManagerRegistry $doctrine;
protected function setupDoctrine(): void {
$this->doctrine = new ManagerRegistry([
'default' => new ObjectManager([
new ObjectRepository(User::class),
new ObjectRepository(Post::class),
]),
]);
}
}
Laravel Service Provider:
Bind the mock EntityManager conditionally in tests.
$this->app->when(UserService::class)
->needs(\Doctrine\ORM\EntityManagerInterface::class)
->give($this->doctrine->getManager());
How can I help you explore Laravel packages today?