diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1008c6e..ed846f6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,9 @@ "Bash(git commit:*)", "Bash(git push:*)", "Bash(git config:*)", - "Bash(git remote set-url:*)" + "Bash(git remote set-url:*)", + "Bash(make:*)", + "Bash(find:*)" ], "deny": [] } diff --git a/docs/markdown/database.md b/docs/markdown/database.md index a78d9e8..451e9b3 100644 --- a/docs/markdown/database.md +++ b/docs/markdown/database.md @@ -1,21 +1,21 @@ # Database Components -This guide provides documentation for the Database components in the Framework. These components provide a simple wrapper around Doctrine's EntityManager to manage database operations. +This guide provides documentation for the Database components in the Framework. These components provide a simple wrapper around Symfony's MessageBus to manage database operations through command messages. ## Database Class -The `Database` class is a wrapper around Doctrine's EntityManager that implements the `DatabasePersistenceInterface`: +The `Database` class is a wrapper around Symfony's MessageBus that implements the `DatabasePersistenceInterface`. It uses command messages to handle database operations: ```php flush(); $database->remove()->flush(); ``` +## Command Messages + +The Database class internally uses synchronous command messages to handle database operations: + +### DatabasePersistCommand + +```php +dispatch($commandBus); +``` + +### DatabaseRemoveCommand + +```php +dispatch($commandBus); +``` + +### DatabaseFlushCommand + +```php +dispatch($commandBus); +``` + +All these commands implement `SyncCommandInterface` to ensure they are executed synchronously. + ## DatabaseEntityInterface The `DatabaseEntityInterface` defines the contract for entities that can be persisted to a database: @@ -70,12 +112,14 @@ class MyEntity implements DatabaseEntityInterface public function save(): void { // Get a Database instance for this entity and use fluent interface + // This will dispatch DatabasePersistCommand and DatabaseFlushCommand $this->database()->persist()->flush(); } public function delete(): void { // Get a Database instance for this entity and use fluent interface + // This will dispatch DatabaseRemoveCommand and DatabaseFlushCommand $this->database()->remove()->flush(); } } @@ -93,17 +137,20 @@ $entity = new MyEntity(); $entity->setName('Example'); // Persist and flush directly from the entity using fluent interface +// This dispatches DatabasePersistCommand and DatabaseFlushCommand $entity->database()->persist()->flush(); // Update an entity $entity->setName('Updated Example'); +// This dispatches DatabaseFlushCommand $entity->database()->flush(); // Remove an entity using fluent interface +// This dispatches DatabaseRemoveCommand and DatabaseFlushCommand $entity->database()->remove()->flush(); ``` -This approach provides a cleaner and more intuitive developer experience, allowing you to work directly with your entities without creating separate database instances. +This approach provides a cleaner and more intuitive developer experience, allowing you to work directly with your entities without creating separate database instances. All operations are now handled through command messages for better architecture and testability. ## DatabasePersistenceInterface diff --git a/docs/markdown/domain-events.md b/docs/markdown/domain-events.md index 38153fd..57fce76 100644 --- a/docs/markdown/domain-events.md +++ b/docs/markdown/domain-events.md @@ -67,6 +67,7 @@ Use these interfaces to tag your messages for Symfony Messenger: use Atournayre\Contracts\CommandBus\CommandInterface; use Atournayre\Contracts\CommandBus\QueryInterface; +use Atournayre\Contracts\CommandBus\SyncCommandInterface; // Command for async processing class CreateUserCommand implements CommandInterface @@ -77,6 +78,14 @@ class CreateUserCommand implements CommandInterface ) {} } +// Synchronous command for immediate execution +class DatabasePersistCommand implements SyncCommandInterface +{ + public function __construct( + public readonly object $object + ) {} +} + // Query for sync processing class GetUserQuery implements QueryInterface { @@ -173,6 +182,8 @@ framework: routing: # Route commands to async transport 'Atournayre\Contracts\CommandBus\CommandInterface': async + # Route synchronous commands to sync transport + 'Atournayre\Contracts\CommandBus\SyncCommandInterface': sync # Route queries to sync transport 'Atournayre\Contracts\CommandBus\QueryInterface': sync ``` diff --git a/src/Common/Persistance/Command/DatabaseFlushCommand.php b/src/Common/Persistance/Command/DatabaseFlushCommand.php new file mode 100644 index 0000000..4da7d02 --- /dev/null +++ b/src/Common/Persistance/Command/DatabaseFlushCommand.php @@ -0,0 +1,23 @@ +object; + } +} diff --git a/src/Common/Persistance/Command/DatabaseRemoveCommand.php b/src/Common/Persistance/Command/DatabaseRemoveCommand.php new file mode 100644 index 0000000..e25dc99 --- /dev/null +++ b/src/Common/Persistance/Command/DatabaseRemoveCommand.php @@ -0,0 +1,28 @@ +object; + } +} diff --git a/src/Common/Persistance/Database.php b/src/Common/Persistance/Database.php index 264afd9..f092696 100644 --- a/src/Common/Persistance/Database.php +++ b/src/Common/Persistance/Database.php @@ -4,18 +4,21 @@ namespace Atournayre\Common\Persistance; +use Atournayre\Common\Persistance\Command\DatabaseFlushCommand; +use Atournayre\Common\Persistance\Command\DatabasePersistCommand; +use Atournayre\Common\Persistance\Command\DatabaseRemoveCommand; +use Atournayre\Contracts\CommandBus\CommandBusInterface; use Atournayre\Contracts\Persistance\DatabasePersistenceInterface; -use Doctrine\ORM\EntityManagerInterface; /** - * Database class provides a simple wrapper around Doctrine's EntityManager. + * Database class provides a simple wrapper around Symfony's MessageBus for database operations. * * This class implements the DatabasePersistenceInterface and provides methods - * to persist, flush, and remove objects from the database. + * to persist, flush, and remove objects from the database using command messages. * * Usage with direct instantiation: * ```php - * $database = Database::new($entityManager, $entity); + * $database = Database::new($commandBus, $entity); * $database->persist(); * $database->flush(); * ``` @@ -37,11 +40,11 @@ /** * Private constructor to enforce usage of the factory method. * - * @param EntityManagerInterface $entityManager The Doctrine entity manager - * @param object $object The object to be managed by the entity manager + * @param CommandBusInterface $commandBus The command bus for dispatching database operations + * @param object $object The object to be managed by the database */ private function __construct( - private EntityManagerInterface $entityManager, + private CommandBusInterface $commandBus, private object $object, ) { } @@ -49,19 +52,19 @@ private function __construct( /** * Creates a new Database instance. * - * @param EntityManagerInterface $entityManager The Doctrine entity manager - * @param object $object The object to be managed by the entity manager + * @param CommandBusInterface $commandBus The command bus for dispatching database operations + * @param object $object The object to be managed by the database * * @return self A new Database instance * * @api */ public static function new( - EntityManagerInterface $entityManager, + CommandBusInterface $commandBus, object $object, ): self { return new self( - entityManager: $entityManager, + commandBus: $commandBus, object: $object, ); } @@ -69,16 +72,17 @@ public static function new( /** * Persists the object to the database. * - * This method tells Doctrine to "manage" the object, making it aware of the object - * without actually executing the SQL INSERT/UPDATE statement. + * This method dispatches a DatabasePersistCommand to handle the persistence operation. * * @return self For method chaining * - * @see EntityManagerInterface::persist + * @see DatabasePersistCommand */ public function persist(): self { - $this->entityManager->persist($this->object); + DatabasePersistCommand::new(object: $this->object) + ->command(bus: $this->commandBus) + ; return $this; } @@ -86,28 +90,32 @@ public function persist(): self /** * Flushes all changes to the database. * - * This method executes all SQL statements needed to persist the changes to the database. + * This method dispatches a DatabaseFlushCommand to handle the flush operation. * - * @see EntityManagerInterface::flush + * @see DatabaseFlushCommand */ public function flush(): void { - $this->entityManager->flush(); + DatabaseFlushCommand::new() + ->command(bus: $this->commandBus) + ; } /** * Removes the object from the database. * - * This method tells Doctrine to remove the object from the database. + * This method dispatches a DatabaseRemoveCommand to handle the removal operation. * The actual DELETE statement will be executed when flush() is called. * * @return self For method chaining * - * @see EntityManagerInterface::remove + * @see DatabaseRemoveCommand */ public function remove(): self { - $this->entityManager->remove($this->object); + DatabaseRemoveCommand::new(object: $this->object) + ->command(bus: $this->commandBus) + ; return $this; } diff --git a/src/Common/Persistance/DatabaseTrait.php b/src/Common/Persistance/DatabaseTrait.php index b09c330..1811d63 100644 --- a/src/Common/Persistance/DatabaseTrait.php +++ b/src/Common/Persistance/DatabaseTrait.php @@ -7,6 +7,7 @@ use Atournayre\Common\Assert\Assert as Assertion; use Atournayre\Contracts\Exception\ThrowableInterface; use Atournayre\Contracts\Persistance\DatabasePersistenceInterface; +use Atournayre\DependencyInjection\EntityDependencyInjection; /** * Trait that provides database persistence functionality. @@ -24,11 +25,12 @@ trait DatabaseTrait */ public function database(): DatabasePersistenceInterface { - Assertion::notNull($this->dependencyInjection, 'Dependency injection is not available. Did you forget to add the $dependencyInjection property to your class?'); - Assertion::true(isset($this->dependencyInjection->entityManager), 'Entity manager is not available. Did you forget to add the EntityManagerInterface to your dependency injection class?'); + /** @var EntityDependencyInjection $dependencyInjection */ + $dependencyInjection = $this->dependencyInjection; + Assertion::notNull($dependencyInjection, 'Dependency injection is not available. Did you forget to add the $dependencyInjection property to your class?'); return Database::new( - entityManager: $this->dependencyInjection->entityManager, + commandBus: $dependencyInjection->commandBus(), object: $this, ); } diff --git a/src/Common/Persistance/Handler/DatabaseFlushHandler.php b/src/Common/Persistance/Handler/DatabaseFlushHandler.php new file mode 100644 index 0000000..94ae67e --- /dev/null +++ b/src/Common/Persistance/Handler/DatabaseFlushHandler.php @@ -0,0 +1,26 @@ +entityManager->flush(); + } +} diff --git a/src/Common/Persistance/Handler/DatabasePersistHandler.php b/src/Common/Persistance/Handler/DatabasePersistHandler.php new file mode 100644 index 0000000..ecfe95c --- /dev/null +++ b/src/Common/Persistance/Handler/DatabasePersistHandler.php @@ -0,0 +1,26 @@ +entityManager->persist($command->object()); + } +} diff --git a/src/Common/Persistance/Handler/DatabaseRemoveHandler.php b/src/Common/Persistance/Handler/DatabaseRemoveHandler.php new file mode 100644 index 0000000..660deaf --- /dev/null +++ b/src/Common/Persistance/Handler/DatabaseRemoveHandler.php @@ -0,0 +1,26 @@ +entityManager->remove($command->object()); + } +} diff --git a/src/Contracts/CommandBus/SyncCommandInterface.php b/src/Contracts/CommandBus/SyncCommandInterface.php new file mode 100644 index 0000000..8787fcc --- /dev/null +++ b/src/Contracts/CommandBus/SyncCommandInterface.php @@ -0,0 +1,15 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->commandBus = $this->createMock(CommandBusInterface::class); $this->object = new \stdClass(); $this->database = Database::new( - entityManager: $this->entityManager, + commandBus: $this->commandBus, object: $this->object, ); } public function testPersist(): void { - $this->entityManager + $this->commandBus ->expects(self::once()) - ->method('persist') - ->with($this->object) + ->method('dispatch') + ->with(self::callback(function ($command) { + return $command instanceof \Atournayre\Common\Persistance\Command\DatabasePersistCommand + && $command->object() === $this->object; + })) ; $this->database->persist(); @@ -41,9 +44,12 @@ public function testPersist(): void public function testFlush(): void { - $this->entityManager + $this->commandBus ->expects(self::once()) - ->method('flush') + ->method('dispatch') + ->with(self::callback(function ($command) { + return $command instanceof \Atournayre\Common\Persistance\Command\DatabaseFlushCommand; + })) ; $this->database->flush(); @@ -51,10 +57,13 @@ public function testFlush(): void public function testRemove(): void { - $this->entityManager + $this->commandBus ->expects(self::once()) - ->method('remove') - ->with($this->object) + ->method('dispatch') + ->with(self::callback(function ($command) { + return $command instanceof \Atournayre\Common\Persistance\Command\DatabaseRemoveCommand + && $command->object() === $this->object; + })) ; $this->database->remove(); @@ -62,14 +71,17 @@ public function testRemove(): void public function testNewCreatesMethodChaining(): void { - $this->entityManager + $this->commandBus ->expects(self::once()) - ->method('persist') - ->with($this->object) + ->method('dispatch') + ->with(self::callback(function ($command) { + return $command instanceof \Atournayre\Common\Persistance\Command\DatabasePersistCommand + && $command->object() === $this->object; + })) ; $database = Database::new( - entityManager: $this->entityManager, + commandBus: $this->commandBus, object: $this->object, ); diff --git a/tools/phpstan/elegant-object.neon b/tools/phpstan/elegant-object.neon index 4143720..ab7edfe 100644 --- a/tools/phpstan/elegant-object.neon +++ b/tools/phpstan/elegant-object.neon @@ -84,6 +84,7 @@ services: class: Atournayre\PHPStan\ElegantObject\Rules\NeverUseErNamesRule arguments: excludedPaths: + - %currentWorkingDirectory%/src/Common/Persistance/Handler - %currentWorkingDirectory%/src/Symfony/Subscriber - %currentWorkingDirectory%/src/TryCatch/NullThrowableHandler.php - %currentWorkingDirectory%/src/TryCatch/ThrowableHandler.php