pdo = $this->createMock(PDO::class); $this->unitOfWork = new UnitOfWork($this->pdo); } public function testSetsEntityStateToNewWhenRegisteringANewEntity(): void { $entity = new TestEntity(); $this->unitOfWork->registerNew($entity); self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity)); } public function testSetsEntityStateToDirtyWhenRegisteringADirtyEntity(): void { $entity = new TestEntity(); $this->unitOfWork->registerDirty($entity); self::assertSame(EntityState::DIRTY, $this->unitOfWork->getEntityState($entity)); } public function testDoesNotOverrideNewStateWhenRegisteringADirtyEntity(): void { $entity = new TestEntity(); $this->unitOfWork->registerNew($entity); $this->unitOfWork->registerDirty($entity); self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity)); } public function testSetsEntityStateToRemovedWhenRegisteringARemovedEntity(): void { $entity = new TestEntity(); $this->unitOfWork->registerRemoved($entity); self::assertSame(EntityState::REMOVED, $this->unitOfWork->getEntityState($entity)); } public function testSetsEntityStateToCleanWhenRegisteringCleanEntity(): void { $entity = new TestEntity(); $this->unitOfWork->registerDirty($entity); $this->unitOfWork->registerClean($entity); self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($entity)); } public function testReturnsCleanStateForUnknownEntities(): void { $entity = new TestEntity(); self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($entity)); } public function testDoesNotStartTransactionWhenCommittingWithNoChanges(): void { $this->pdo->expects(self::never())->method('beginTransaction'); $this->unitOfWork->commit(); } public function testDoesStartTransactionAndCommitIt(): void { $entity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($entity); $persister->expects(self::once())->method('supports')->with($entity)->willReturn(true); $persister->expects(self::once())->method('insert')->with($entity, $this->pdo); $this->pdo->expects(self::once())->method('beginTransaction'); $this->pdo->expects(self::once())->method('commit'); $this->unitOfWork->commit(); } public function testProcessesEntitiesInCorrectOrderOnCommit(): void { $newEntity = new TestEntity(); $dirtyEntity = new TestEntity(); $removedEntity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($newEntity); $this->unitOfWork->registerDirty($dirtyEntity); $this->unitOfWork->registerRemoved($removedEntity); $callOrder = []; $persister->method('supports')->willReturn(true); $persister->method('delete')->willReturnCallback( function () use (&$callOrder): void { $callOrder[] = 'delete'; }, ); $persister->method('insert')->willReturnCallback( function () use (&$callOrder): void { $callOrder[] = 'insert'; }, ); $persister->method('update')->willReturnCallback( function () use (&$callOrder): void { $callOrder[] = 'update'; }, ); $this->pdo->method('beginTransaction'); $this->pdo->method('commit'); $this->unitOfWork->commit(); self::assertSame(['delete', 'insert', 'update'], $callOrder); } public function testMarksAllEntitiesAsCleanAfterASuccessfulCommit(): void { $newEntity = new TestEntity(); $dirtyEntity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $persister->method('supports')->willReturn(true); $persister->method('insert'); $persister->method('update'); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($newEntity); $this->unitOfWork->registerDirty($dirtyEntity); $this->pdo->method('beginTransaction'); $this->pdo->method('commit'); $this->unitOfWork->commit(); self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($newEntity)); self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($dirtyEntity)); } public function testClearsCollectionsAfterASuccessfulCommit(): void { $entity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $persister->method('supports')->willReturn(true); $persister->method('insert'); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($entity); $this->pdo->method('beginTransaction'); $this->pdo->method('commit'); $this->unitOfWork->commit(); // Try to register the same entity again - should work if collections are cleared $this->unitOfWork->registerNew($entity); self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity)); } public function testRollsBackOnPDOException(): void { $entity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $persister->method('supports')->willReturn(true); $persister->method('insert')->willThrowException(new PDOException('Database error')); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($entity); $this->pdo->expects(self::once())->method('beginTransaction'); $this->pdo->expects(self::once())->method('rollBack'); $this->pdo->expects(self::never())->method('commit'); $this->expectException(PDOException::class); $this->unitOfWork->commit(); } public function testCallsPdoRollbackWhenInTransaction(): void { $entity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $persister->method('supports')->willReturn(true); $persister->method('insert')->willThrowException(new PDOException('Database error')); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($entity); $this->pdo->method('beginTransaction')->willReturn(true); $this->pdo->expects(self::once())->method('rollBack'); try { $this->unitOfWork->commit(); } catch (PDOException $e) { // Expected exception } self::assertFalse($this->unitOfWork->isInTransaction()); } public function testDoesNothingWhenNotInTransactionOnRollback(): void { $this->pdo->expects(self::never())->method('rollBack'); $this->unitOfWork->rollback(); } public function testCanClearInternalState(): void { $newEntity = new TestEntity(); $dirtyEntity = new TestEntity(); $removedEntity = new TestEntity(); $this->unitOfWork->registerNew($newEntity); $this->unitOfWork->registerDirty($dirtyEntity); $this->unitOfWork->registerRemoved($removedEntity); $this->unitOfWork->clear(); // After clear, registering the same entities should work $this->unitOfWork->registerNew($newEntity); $this->unitOfWork->registerDirty($dirtyEntity); $this->unitOfWork->registerRemoved($removedEntity); self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($newEntity)); self::assertSame(EntityState::DIRTY, $this->unitOfWork->getEntityState($dirtyEntity)); self::assertSame(EntityState::REMOVED, $this->unitOfWork->getEntityState($removedEntity)); } public function testCallsPersisterOnEntityPersistence(): void { $entity = new TestEntity(); $persister = $this->createMock(EntityPersisterInterface::class); $persister->expects(self::once())->method('supports')->with($entity)->willReturn(true); $persister->expects(self::once())->method('insert')->with($entity, $this->pdo); $this->unitOfWork->registerPersister($persister); $this->unitOfWork->registerNew($entity); $this->pdo->method('beginTransaction'); $this->pdo->method('commit'); $this->unitOfWork->commit(); } public function testThrowsRuntimeExceptionWhenNoPersisterWasFound(): void { $entity = new TestEntity(); $this->unitOfWork->registerNew($entity); $this->pdo->method('beginTransaction'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No persister found for entity of type'); $this->unitOfWork->commit(); } public function testCanHandleMultipleRegisteredPersisters(): void { $entity1 = new TestEntity(); $entity2 = new AnotherTestEntity(); $persister1 = $this->createMock(EntityPersisterInterface::class); $persister2 = $this->createMock(EntityPersisterInterface::class); $persister1->method('supports')->willReturnCallback(fn($entity) => $entity instanceof TestEntity); $persister2->method('supports')->willReturnCallback(fn($entity) => $entity instanceof AnotherTestEntity); $persister1->expects(self::once())->method('insert')->with($entity1, $this->pdo); $persister2->expects(self::once())->method('insert')->with($entity2, $this->pdo); $this->unitOfWork->registerPersister($persister1); $this->unitOfWork->registerPersister($persister2); $this->unitOfWork->registerNew($entity1); $this->unitOfWork->registerNew($entity2); $this->pdo->method('beginTransaction'); $this->pdo->method('commit'); $this->unitOfWork->commit(); } } class TestEntity { public function __construct(public string $id = 'test-id') {} } class AnotherTestEntity { public function __construct(public string $id = 'another-test-id') {} }