From 81eb430f0317b69caecad6df6504a1838e38c3e8 Mon Sep 17 00:00:00 2001 From: Claudiu Pintiuta Date: Wed, 10 Jul 2024 18:25:43 +0300 Subject: [PATCH 1/2] refactoring and updating book tutorial Signed-off-by: Claudiu Pintiuta --- docs/book/v5/tutorials/create-book-module.md | 525 +++++++++++-------- 1 file changed, 320 insertions(+), 205 deletions(-) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index c54b310..0bb3b7a 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -1,8 +1,8 @@ -# Implementing a book module in DotKernel API +# Implementing a book module in Dotkernel API -## File structure +## Folder and files structure -The below file structure is just an example, you can have multiple components such as event listeners, wrappers, etc. +The below files structure is what we will have at the end of this tutorial and is just an example, you can have multiple components such as event listeners, wrappers, etc. ```markdown . @@ -40,8 +40,107 @@ The below file structure is just an example, you can have multiple components su * `src/Book/src/InputFilter/BookInputFilter.php` - input filters and validators * `src/Book/src/InputFilter/Input/*` - input filters and validator configurations +## Creating and configuring the module. + +Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. + +In `src` folder we will create the `Book` folder and in this we will create the `src` folder. So the final structure will be like this: `src/Book/src`. + +In `src/Book/src` we will create 2 php files: `RoutesDelegator.php` and `ConfigProvider.php`. This files will be updated later with all needed configuration. + +* `src/Book/src/RoutesDelegator.php` + +```php + $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + MetadataMap::class => $this->getHalConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class + ] + ], + 'factories' => [ + ], + 'aliases' => [ + ], + ]; + } + + private function getDoctrineConfig(): array + { + return [ + + ]; + } + + private function getHalConfig(): array + { + return [ + + ]; + } + +} +``` + +### Registering the module + +* register the module config by adding the ` Api\Book\ConfigProvider::class` in `config/config.php` under the `Api\User\ConfigProvider::class` +* register the namespace by adding this line `"Api\\Book\\": "src/Book/src/"`, in composer.json under the autoload.psr-4 key +* update Composer autoloader by running the command: + +```shell +composer dump-autoload +``` + +That's it. The module is now registered and, we can continue creating Handlers, Services, Repositories and whatever is needed for out tutorial. + ## File creation and contents +Each file below have a summary description above of what that file does. + * `src/Book/src/Collection/BookCollection.php` ```php @@ -70,14 +169,18 @@ declare(strict_types=1); namespace Api\Book\Entity; use Api\App\Entity\AbstractEntity; +use Api\App\Entity\TimestampsTrait; use Api\Book\Repository\BookRepository; use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: BookRepository::class)] #[ORM\Table("book")] +#[ORM\HasLifecycleCallbacks] class Book extends AbstractEntity { + use TimestampsTrait; + #[ORM\Column(name: "name", type: "string", length: 100)] protected string $name; @@ -142,6 +245,7 @@ class Book extends AbstractEntity ]; } } + ``` * `src/Book/src/Repository/BookRepository.php` @@ -193,173 +297,69 @@ class BookRepository extends EntityRepository } ``` -* `src/Book/src/Service/BookService.php` +* `src/Book/src/Service/BookServiceInterface.php` ```php bookRepository->saveBook($book); - } - - public function getBooks(array $filters = []) - { - return $this->bookRepository->getBooks($filters); - } -} -``` - -* `src/Book/src/Service/BookServiceInterface.php` - -```php - $this->getDependencies(), - MetadataMap::class => $this->getHalConfig(), - ]; } - public function getDependencies(): array + public function getRepository(): BookRepository { - return [ - 'factories' => [ - BookHandler::class => AttributedServiceFactory::class, - BookService::class => AttributedServiceFactory::class, - BookRepository::class => AttributedRepositoryFactory::class, - ], - 'aliases' => [ - BookServiceInterface::class => BookService::class, - ], - ]; + return $this->bookRepository; } - public function getHalConfig(): array - { - return [ - AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), - AppConfigProvider::getResource(Book::class, 'book.create'), - ]; - } -} -``` - -* `src/Book/src/RoutesDelegator.php` - -```php -get( - '/books', - BookHandler::class, - 'books.list' - ); - - $app->post( - '/book', - BookHandler::class, - 'book.create' + $book = new Book( + $data['name'], + $data['author'], + new DateTimeImmutable($data['releaseDate']) ); - return $app; + return $this->bookRepository->saveBook($book); } -} -``` - -* `src/Book/src/InputFilter/BookInputFilter.php` - -```php -add(new NameInput('name')); - $this->add(new AuthorInput('author')); - $this->add(new ReleaseDateInput('releaseDate')); + return $this->bookRepository->getBooks($filters); } } ``` +When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request + * `src/Book/src/InputFilter/Input/AuthorInput.php` ```php @@ -466,18 +466,66 @@ class ReleaseDateInput extends Input } ``` -* `src/Book/src/Handler/BookHandler.php` +Now we add all the inputs together in a parent input filter. + +* `src/Book/src/InputFilter/BookInputFilter.php` ```php add(new NameInput('name')); + $this->add(new AuthorInput('author')); + $this->add(new ReleaseDateInput('releaseDate')); + } +} +``` + +We split all the inputs just for the purpose of this tutorial and to demonstrate a clean `BookInputFiler` but you could have all the inputs created directly in the `BookInputFilter` like this: + +```php +$nameInput = new Input(); +$nameInput->setRequired(true); + +$nameInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$nameInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), + ], true); + +$this->add($nameInput); +``` + +Now it's time to create the handler. + +* `src/Book/src/Handler/BookHandler.php` + +```php +bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); + + if (! $book instanceof Book){ + return $this->notFoundResponse(); + } + + return $this->createResponse($request, $book); + } + + public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->bookService->getBooks($request->getQueryParams()); @@ -512,7 +573,7 @@ class BookHandler implements RequestHandlerInterface { $inputFilter = (new BookInputFilter())->setData($request->getParsedBody()); if (! $inputFilter->isValid()) { - return $this->errorResponse($inputFilter->getMessages()); + return $this->errorResponse($inputFilter->getMessages(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } $book = $this->bookService->createBook($inputFilter->getValues()); @@ -520,102 +581,150 @@ class BookHandler implements RequestHandlerInterface return $this->createResponse($request, $book); } } + ``` -## Configuring and registering the new module +After we have the handler, we need to register some routes in the `RoutesDelegator`, the same we created when we registered the module. -Once you set up all the files as in the example above, you will need to do a few additional configurations: +* `src/Book/src/RoutesDelegator.php` -* register the namespace by adding this line `"Api\\Book\\": "src/Book/src/",` in `composer.json` under the `autoload.psr-4` key -* register the module by adding `Api\Book\ConfigProvider::class,` under `Api\User\ConfigProvider::class,` -* register the module's routes by adding `\Api\Book\RoutesDelegator::class,` under `\Api\User\RoutesDelegator::class,` in `src/App/src/ConfigProvider.php` -* update Composer autoloader by running the command: +```php + [ - Application::class => [ - RoutesDelegator::class, - \Api\Admin\RoutesDelegator::class, - \Api\User\RoutesDelegator::class, - \Api\Book\RoutesDelegator::class, - ], - ], - 'factories' => [ - ... - ] - ... -``` + public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application + { + /** @var Application $app */ + $app = $callback(); -* In `src/config/autoload/doctrine.global.php` add this under the `doctrine.driver` key: + $uuid = \Api\App\RoutesDelegator::REGEXP_UUID; -```php -'BookEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/../../src/Book/src/Entity', -], + $app->get( + '/books', + BookHandler::class, + 'books.list' + ); + + $app->get( + '/book/'.$uuid, + BookHandler::class, + 'book.show' + ); + + $app->post( + '/book', + BookHandler::class, + 'book.create' + ); + + return $app; + } +} ``` -* `Api\\Book\Entity' => 'BookEntities',` add this under the `doctrine.driver.drivers` key +We need to configure access to the newly created endpoints, add `books.list`, `book.show` and `book.create` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. +> Make sure you read and understand the rbac documentation. + +It's time to update the `ConfigProvider` with all the necessary configuration needed, so the above files to work properly like dependency injection, aliases, doctrine mapping and so on. -Example: +* `src/Book/src/ConfigProvider.php` ```php [ - ... - 'driver' => [ - 'orm_default' => [ - 'class' => MappingDriverChain::class, - 'drivers' => [ - 'Api\\App\Entity' => 'AppEntities', - 'Api\\Admin\\Entity' => 'AdminEntities', - 'Api\\User\\Entity' => 'UserEntities', - 'Api\\Book\Entity' => 'BookEntities', - ], - ], - 'AdminEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/../../src/Admin/src/Entity', + +declare(strict_types=1); + +namespace Api\Book; + +use Api\Book\Collection\BookCollection; +use Api\Book\Entity\Book; +use Api\Book\Handler\BookHandler; +use Api\Book\Repository\BookRepository; +use Api\Book\Service\BookService; +use Api\Book\Service\BookServiceInterface; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Dot\DependencyInjection\Factory\AttributedRepositoryFactory; +use Dot\DependencyInjection\Factory\AttributedServiceFactory; +use Mezzio\Application; +use Mezzio\Hal\Metadata\MetadataMap; +use Api\App\ConfigProvider as AppConfigProvider; + +class ConfigProvider +{ + public function __invoke(): array + { + return [ + 'dependencies' => $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + MetadataMap::class => $this->getHalConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class + ] ], - 'UserEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/../../src/User/src/Entity', + 'factories' => [ + BookHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, + BookRepository::class => AttributedRepositoryFactory::class, ], - 'AppEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/../../src/App/src/Entity', + 'aliases' => [ + BookServiceInterface::class => BookService::class, ], - 'BookEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/../../src/Book/src/Entity', + ]; + } + + private function getDoctrineConfig(): array + { + return [ + 'driver' => [ + 'orm_default' => [ + 'drivers' => [ + 'Api\Book\Entity' => 'BookEntities' + ], + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/Entity', + ], ], - ], - ... -``` + ]; + } -Next we need to configure access to the newly created endpoints, add `books.list` and `book.create` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. -> Make sure you read and understand the rbac documentation. + private function getHalConfig(): array + { + return [ + AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), + AppConfigProvider::getResource(Book::class, 'book.show') + ]; + } + +} +``` ## Migrations We created the `Book` entity, but we didn't create the associated table for it. +> You can check the mapping files by running: + +```shel +php bin/doctrine orm:validate-schema +``` + Doctrine can handle the table creation, run the following command: ```shell @@ -645,3 +754,9 @@ To list the books use: ```shell curl http://0.0.0.0:8080/books ``` + +To retrieve a book use: + +```shell +curl http://0.0.0.0:8080/book/{uuid} +``` From 45951a01598353759ad5c93d2d005e3740175afb Mon Sep 17 00:00:00 2001 From: Claudiu Pintiuta Date: Wed, 10 Jul 2024 18:28:32 +0300 Subject: [PATCH 2/2] linting Signed-off-by: Claudiu Pintiuta --- docs/book/v5/tutorials/create-book-module.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index 0bb3b7a..f5480e9 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -40,7 +40,7 @@ The below files structure is what we will have at the end of this tutorial and i * `src/Book/src/InputFilter/BookInputFilter.php` - input filters and validators * `src/Book/src/InputFilter/Input/*` - input filters and validator configurations -## Creating and configuring the module. +## Creating and configuring the module Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. @@ -127,7 +127,7 @@ class ConfigProvider ### Registering the module -* register the module config by adding the ` Api\Book\ConfigProvider::class` in `config/config.php` under the `Api\User\ConfigProvider::class` +* register the module config by adding the `Api\Book\ConfigProvider::class` in `config/config.php` under the `Api\User\ConfigProvider::class` * register the namespace by adding this line `"Api\\Book\\": "src/Book/src/"`, in composer.json under the autoload.psr-4 key * update Composer autoloader by running the command: