diff --git a/bin/clear-config-cache.php b/bin/clear-config-cache.php old mode 100644 new mode 100755 diff --git a/bin/composer-post-install-script.php b/bin/composer-post-install-script.php old mode 100644 new mode 100755 diff --git a/bin/doctrine b/bin/doctrine new file mode 100644 index 0000000..8480ff9 --- /dev/null +++ b/bin/doctrine @@ -0,0 +1,17 @@ +#!/usr/bin/env php +get(EntityManager::class); +$entityManager->getEventManager(); + +ConsoleRunner::run(new SingleManagerProvider($entityManager)); diff --git a/bin/doctrine-migrations b/bin/doctrine-migrations new file mode 100644 index 0000000..d40cc55 --- /dev/null +++ b/bin/doctrine-migrations @@ -0,0 +1,8 @@ +#!/usr/bin/env php +get(EntityManager::class); +$entityManager->getEventManager(); + +return DependencyFactory::fromEntityManager( + new ConfigurationArray($container->get('config')['doctrine']['migrations']), + new ExistingEntityManager($entityManager) +); diff --git a/config/config.php b/config/config.php old mode 100644 new mode 100755 index 1c58178..aba220f --- a/config/config.php +++ b/config/config.php @@ -30,6 +30,7 @@ // Default App module config \Light\App\ConfigProvider::class, + \Light\Book\ConfigProvider::class, \Light\Page\ConfigProvider::class, // Load application config in a pre-defined order in such a way that local settings diff --git a/data/cache/.gitignore b/data/cache/.gitignore deleted file mode 100755 index d6b7ef3..0000000 --- a/data/cache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/src/App/src/ConfigProvider.php b/src/App/src/ConfigProvider.php index 82b5fcc..a4d8158 100644 --- a/src/App/src/ConfigProvider.php +++ b/src/App/src/ConfigProvider.php @@ -4,9 +4,20 @@ namespace Light\App; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Dot\Cache\Adapter\ArrayAdapter; +use Dot\Cache\Adapter\FilesystemAdapter; use Light\App\Factory\GetIndexViewHandlerFactory; use Light\App\Handler\GetIndexViewHandler; use Mezzio\Application; +use Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType; +use Ramsey\Uuid\Doctrine\UuidBinaryType; +use Ramsey\Uuid\Doctrine\UuidType; +use Roave\PsrContainerDoctrine\EntityManagerFactory; + +use function getcwd; class ConfigProvider { @@ -20,6 +31,7 @@ public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), 'templates' => $this->getTemplates(), ]; } @@ -39,7 +51,12 @@ public function getDependencies(): array ], ], 'factories' => [ - GetIndexViewHandler::class => GetIndexViewHandlerFactory::class, + 'doctrine.entity_manager.orm_default' => EntityManagerFactory::class, + GetIndexViewHandler::class => GetIndexViewHandlerFactory::class, + ], + 'aliases' => [ + EntityManager::class => 'doctrine.entity_manager.orm_default', + EntityManagerInterface::class => 'doctrine.entity_manager.orm_default', ], ]; } @@ -65,4 +82,63 @@ public function getTemplates(): array ], ]; } + + private function getDoctrineConfig(): array + { + return [ + 'cache' => [ + 'array' => [ + 'class' => ArrayAdapter::class, + ], + 'filesystem' => [ + 'class' => FilesystemAdapter::class, + 'directory' => getcwd() . '/data/cache', + 'namespace' => 'doctrine', + ], + ], + 'configuration' => [ + 'orm_default' => [ + 'result_cache' => 'filesystem', + 'metadata_cache' => 'filesystem', + 'query_cache' => 'filesystem', + 'hydration_cache' => 'array', + 'typed_field_mapper' => null, + 'second_level_cache' => [ + 'enabled' => true, + 'default_lifetime' => 3600, + 'default_lock_lifetime' => 60, + 'file_lock_region_directory' => '', + 'regions' => [], + ], + ], + ], + 'connection' => [ + 'orm_default' => [ + 'doctrine_mapping_types' => [ + UuidBinaryType::NAME => 'binary', + UuidBinaryOrderedTimeType::NAME => 'binary', + ], + ], + ], + 'driver' => [ + // The default metadata driver aggregates all other drivers into a single one. + // Override `orm_default` only if you know what you're doing. + 'orm_default' => [ + 'class' => MappingDriverChain::class, + ], + ], + 'migrations' => [ + 'migrations_paths' => [ + 'Migrations' => 'src/Migrations', + ], + 'all_or_nothing' => true, + 'check_database_platform' => true, + ], + 'types' => [ + UuidType::NAME => UuidType::class, + UuidBinaryType::NAME => UuidBinaryType::class, + UuidBinaryOrderedTimeType::NAME => UuidBinaryOrderedTimeType::class, + ], + ]; + } } diff --git a/src/App/src/Entity/AbstractEntity.php b/src/App/src/Entity/AbstractEntity.php new file mode 100644 index 0000000..7e04bb1 --- /dev/null +++ b/src/App/src/Entity/AbstractEntity.php @@ -0,0 +1,27 @@ +uuid = Uuid::uuid7(); + } + + public function getUuid(): UuidInterface + { + return $this->uuid; + } +} diff --git a/src/App/src/Entity/TimestampsTrait.php b/src/App/src/Entity/TimestampsTrait.php new file mode 100644 index 0000000..d004f47 --- /dev/null +++ b/src/App/src/Entity/TimestampsTrait.php @@ -0,0 +1,53 @@ +created; + } + + public function getCreatedFormatted(string $dateFormat = 'Y-m-d H:i:s'): string + { + return $this->created->format($dateFormat); + } + + public function getUpdated(): ?DateTimeImmutable + { + return $this->updated; + } + + public function getUpdatedFormatted(string $dateFormat = 'Y-m-d H:i:s'): ?string + { + if ($this->updated instanceof DateTimeImmutable) { + return $this->updated->format($dateFormat); + } + + return null; + } + + #[ORM\PrePersist] + public function created(): void + { + $this->created = new DateTimeImmutable(); + } + + #[ORM\PreUpdate] + public function touch(): void + { + $this->updated = new DateTimeImmutable(); + } +} diff --git a/src/Book/src/ConfigProvider.php b/src/Book/src/ConfigProvider.php new file mode 100644 index 0000000..5c200ed --- /dev/null +++ b/src/Book/src/ConfigProvider.php @@ -0,0 +1,62 @@ + $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class, + ], + ], + 'factories' => [ + CreateBookHandler::class => CreateBookHandlerFactory::class, + ListBooksHandler::class => ListBookHandlerFactory::class, + UpdateBookHandler::class => UpdateBookHandlerFactory::class, + DeleteBookHandler::class => DeleteBookHandlerFactory::class, + ], + ]; + } + + private function getDoctrineConfig(): array + { + return [ + 'driver' => [ + 'orm_default' => [ + 'drivers' => [ + 'Light\Book\Entity' => 'BookEntities', + ], + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => [__DIR__ . '/Entity'], + ], + ], + ]; + } +} diff --git a/src/Book/src/Entity/Book.php b/src/Book/src/Entity/Book.php new file mode 100644 index 0000000..43ce4b0 --- /dev/null +++ b/src/Book/src/Entity/Book.php @@ -0,0 +1,50 @@ +created(); + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getAuthor(): ?string + { + return $this->author; + } + + public function setAuthor(string $author): void + { + $this->author = $author; + } +} diff --git a/src/Book/src/Factory/CreateBookHandlerFactory.php b/src/Book/src/Factory/CreateBookHandlerFactory.php new file mode 100644 index 0000000..ea0476e --- /dev/null +++ b/src/Book/src/Factory/CreateBookHandlerFactory.php @@ -0,0 +1,28 @@ +get(EntityManagerInterface::class); + assert($template instanceof EntityManagerInterface); + + return new CreateBookHandler($template); + } +} diff --git a/src/Book/src/Factory/DeleteBookHandlerFactory.php b/src/Book/src/Factory/DeleteBookHandlerFactory.php new file mode 100644 index 0000000..4c282ed --- /dev/null +++ b/src/Book/src/Factory/DeleteBookHandlerFactory.php @@ -0,0 +1,28 @@ +get(EntityManagerInterface::class); + assert($template instanceof EntityManagerInterface); + + return new DeleteBookHandler($template); + } +} diff --git a/src/Book/src/Factory/ListBookHandlerFactory.php b/src/Book/src/Factory/ListBookHandlerFactory.php new file mode 100644 index 0000000..ffe854f --- /dev/null +++ b/src/Book/src/Factory/ListBookHandlerFactory.php @@ -0,0 +1,28 @@ +get(EntityManagerInterface::class); + assert($template instanceof EntityManagerInterface); + + return new ListBooksHandler($template); + } +} diff --git a/src/Book/src/Factory/UpdateBookHandlerFactory.php b/src/Book/src/Factory/UpdateBookHandlerFactory.php new file mode 100644 index 0000000..f890bed --- /dev/null +++ b/src/Book/src/Factory/UpdateBookHandlerFactory.php @@ -0,0 +1,28 @@ +get(EntityManagerInterface::class); + assert($template instanceof EntityManagerInterface); + + return new UpdateBookHandler($template); + } +} diff --git a/src/Book/src/Handler/CreateBookHandler.php b/src/Book/src/Handler/CreateBookHandler.php new file mode 100644 index 0000000..4569fa3 --- /dev/null +++ b/src/Book/src/Handler/CreateBookHandler.php @@ -0,0 +1,34 @@ +setAuthor("PHP developer"); + $newBook->setTitle("Doctrine is very cool"); + + $this->entityManager->persist($newBook); + $this->entityManager->flush(); + + return new JsonResponse( + "Book created!", + ); + } +} diff --git a/src/Book/src/Handler/DeleteBookHandler.php b/src/Book/src/Handler/DeleteBookHandler.php new file mode 100644 index 0000000..46a3855 --- /dev/null +++ b/src/Book/src/Handler/DeleteBookHandler.php @@ -0,0 +1,49 @@ +getAttribute('uuid'); + + if (! $uuid) { + return new JsonResponse([ + 'error' => 'UUID is required', + ], 400); + } + + // Find the book by UUID + $book = $this->entityManager + ->getRepository(Book::class) + ->find($uuid); + + if (! $book) { + return new JsonResponse([ + 'error' => 'Book not found', + ], 404); + } + + $this->entityManager->remove($book); + $this->entityManager->flush(); + + return new JsonResponse([ + 'message' => "Book $uuid deleted successfully", + ]); + } +} diff --git a/src/Book/src/Handler/ListBooksHandler.php b/src/Book/src/Handler/ListBooksHandler.php new file mode 100644 index 0000000..fa22929 --- /dev/null +++ b/src/Book/src/Handler/ListBooksHandler.php @@ -0,0 +1,39 @@ +entityManager + ->getRepository(Book::class) + ->findAll(); + + $data = array_map(function (Book $book) { + return [ + 'id' => $book->getUuid()->toString(), + 'title' => $book->getTitle(), + 'author' => $book->getAuthor(), + ]; + }, $books); + + return new JsonResponse($data); + } +} diff --git a/src/Book/src/Handler/UpdateBookHandler.php b/src/Book/src/Handler/UpdateBookHandler.php new file mode 100644 index 0000000..ddc1902 --- /dev/null +++ b/src/Book/src/Handler/UpdateBookHandler.php @@ -0,0 +1,50 @@ +getAttribute('uuid'); + + if (! $uuid) { + return new JsonResponse([ + 'error' => 'UUID is required', + ], 400); + } + + $book = $this->entityManager + ->getRepository(Book::class) + ->find($uuid); + + if (! $book) { + return new JsonResponse([ + 'error' => 'Book not found', + ], 404); + } + + $book->setTitle("Updated doctrine is even cooler"); + + $this->entityManager->persist($book); + $this->entityManager->flush(); + + return new JsonResponse([ + 'message' => "Book $uuid updated successfully", + ]); + } +} diff --git a/src/Book/src/RoutesDelegator.php b/src/Book/src/RoutesDelegator.php new file mode 100644 index 0000000..472dbf2 --- /dev/null +++ b/src/Book/src/RoutesDelegator.php @@ -0,0 +1,31 @@ +get('/books/create', [CreateBookHandler::class], 'books::create'); + $app->get('/books/list', [ListBooksHandler::class], 'books::list'); + + $app->get('/books/update/{uuid}', [UpdateBookHandler::class], 'books::update'); + $app->get('/books/delete/{uuid}', [DeleteBookHandler::class], 'books::delete'); + + return $app; + } +} diff --git a/src/Migrations/Version20251116200547.php b/src/Migrations/Version20251116200547.php new file mode 100644 index 0000000..d039877 --- /dev/null +++ b/src/Migrations/Version20251116200547.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE books (title VARCHAR(500) DEFAULT NULL, author VARCHAR(500) DEFAULT NULL, uuid BINARY(16) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, PRIMARY KEY (uuid)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE books'); + } +}