From 9757b373386ef68c2a0662bae3f9decd1000e0a8 Mon Sep 17 00:00:00 2001 From: horea Date: Mon, 12 May 2025 23:51:38 +0300 Subject: [PATCH 01/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v5/tutorials/create-book-module.md | 9 +- docs/book/v6/tutorials/create-book-module.md | 546 +++++++++++-------- 2 files changed, 311 insertions(+), 244 deletions(-) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index b6d36c90..aad1c3b4 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -560,13 +560,6 @@ class BookHandler extends AbstractHandler implements RequestHandlerInterface return $this->createResponse($request, $book); } - public function getCollection(ServerRequestInterface $request): ResponseInterface - { - $books = $this->bookService->getRepository()->getBooks($request->getQueryParams()); - - return $this->createResponse($request, $books); - } - public function post(ServerRequestInterface $request): ResponseInterface { $inputFilter = (new BookInputFilter())->setData($request->getParsedBody()); @@ -661,7 +654,7 @@ class RoutesDelegator $app->get( '/book/' . $uuid, - BookCollection::class, + Book::class, 'book.show' ); diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index fd06d541..9e5cf1fa 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -7,65 +7,103 @@ The below files structure is what we will have at the end of this tutorial and i ```markdown . └── src/ - └── Book/ - └── src/ - ├── Collection/ - │ └── BookCollection.php - ├── Entity/ - │ └── Book.php - ├── Handler/ - │ └── BookHandler.php - ├── InputFilter/ - │ ├── Input/ - │ │ ├── AuthorInput.php - │ │ ├── NameInput.php - │ │ └── ReleaseDateInput.php - │ └── BookInputFilter.php - ├── Repository/ - │ └── BookRepository.php - ├── Service/ - │ ├── BookService.php - │ └── BookServiceInterface.php - ├── ConfigProvider.php - └── RoutesDelegator.php + ├── Book/ + │ └── src/ + │ ├── Collection/ + │ │ └── BookCollection.php + │ ├── Entity/ + │ │ └── Book.php + │ ├── Handler/ + │ │ ├── GetBookCollectionHandler.php + │ │ ├── GetBookHandler.php + │ │ └── PostBookHandler.php + │ ├── InputFilter/ + │ │ └── CreateBookInputFilter.php + │ ├── Service/ + │ │ ├── BookService.php + │ │ └── BookServiceInterface.php + │ ├── ConfigProvider.php + │ └── RoutesDelegator.php + ├── Core/ + │ └── src/ + │ └── Book/ + │ └── src/ + │ ├──Entity/ + │ │ └──Book.php + │ ├──Repository/ + │ │ └──BookRepository.php + │ └── ConfigProvider.php + └── App/ + └──src/ + └── InputFilter/ + └── Input/ + ├── AuthorInput.php + ├── NameInput.php + └── ReleaseDateInput.php ``` * `src/Book/src/Collection/BookCollection.php` - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database -* `src/Book/src/Entity/Book.php` - an entity refers to a PHP class that represents a persistent object or data structure -* `src/Book/src/Handler/BookHandler.php` - handlers are middleware that can handle requests based on an action -* `src/Book/src/Repository/BookRepository.php` - a repository is a class responsible for querying and retrieving entities from the database +* `src/Core/src/Book/src/Entity/Book.php` - an entity refers to a PHP class that represents a persistent object or data structure +* `src/Book/src/Handler/GetBookCollectionHandler.php` - handler that reflects the GET action for the BookCollection class +* `src/Book/src/Handler/GetBookHandler.php` - handler that reflects the GET action for the Book entity +* `src/Book/src/Handler/PostBookHandler.php` - handler that reflects the POST action for the Book entity +* `src/Core/src/Book/src/Repository/BookRepository.php` - a repository is a class responsible for querying and retrieving entities from the database * `src/Book/src/Service/BookService.php` - is a class or component responsible for performing a specific task or providing functionality to other parts of the application * `src/Book/src/ConfigProvider.php` - is a class that provides configuration for various aspects of the framework or application * `src/Book/src/RoutesDelegator.php` - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application -* `src/Book/src/InputFilter/BookInputFilter.php` - input filters and validators -* `src/Book/src/InputFilter/Input/*` - input filters and validator configurations +* `src/Book/src/InputFilter/CreateBookInputFilter.php` - input filters and validators +* `src/Core/src/App/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` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. So the final structure will be like this: `src/Book/src` and `src/Core/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. +In `src/Book/src` we will create 2 PHP files: `RoutesDelegator.php` and `ConfigProvider.php`. These files contain the necessary configurations. * `src/Book/src/RoutesDelegator.php` ```php get(RouteCollectorInterface::class); + + $routeCollector->group('/book') + ->post('', PostBookHandler::class, 'book::create-book'); + + $routeCollector->group('/book/' . $uuid) + ->get('', GetBookHandler::class, 'book::view-book'); + + $routeCollector->group('/books') + ->get('', GetBookCollectionHandler::class, 'book::list-books'); + + return $callback(); } } ``` @@ -79,6 +117,16 @@ declare(strict_types=1); namespace Api\Book; +use Api\App\ConfigProvider as AppConfigProvider; +use Api\App\Factory\HandlerDelegatorFactory; +use Api\Book\Collection\BookCollection; +use Api\Book\Handler\GetBookCollectionHandler; +use Api\Book\Handler\GetBookHandler; +use Api\Book\Handler\PostBookHandler; +use Api\Book\Service\BookService; +use Api\Book\Service\BookServiceInterface; +use Core\Book\Entity\Book; +use Dot\DependencyInjection\Factory\AttributedServiceFactory; use Mezzio\Application; use Mezzio\Hal\Metadata\MetadataMap; @@ -88,8 +136,7 @@ class ConfigProvider { return [ 'dependencies' => $this->getDependencies(), - 'doctrine' => $this->getDoctrineConfig(), - MetadataMap::class => $this->getHalConfig(), + MetadataMap::class => $this->getHalConfig(), ]; } @@ -97,38 +144,102 @@ class ConfigProvider { return [ 'delegators' => [ - Application::class => [ - RoutesDelegator::class - ] + Application::class => [RoutesDelegator::class], + PostBookHandler::class => [HandlerDelegatorFactory::class], + GetBookHandler::class => [HandlerDelegatorFactory::class], + GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], ], - 'factories' => [ + 'factories' => [ + PostBookHandler::class => AttributedServiceFactory::class, + GetBookHandler::class => AttributedServiceFactory::class, + GetBookCollectionHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, ], - 'aliases' => [ + 'aliases' => [ + BookServiceInterface::class => BookService::class, ], ]; } - private function getDoctrineConfig(): array + private function getHalConfig(): array { return [ - + AppConfigProvider::getResource(Book::class, 'book::view-book'), + AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), ]; } +} +``` - private function getHalConfig(): array +* `src/Core/src/Book/src/ConfigProvider.php` + +In `src/Core/src/Book/src` we will create 1 PHP file: `ConfigProvider.php`. This file contains the necessary configuration for Doctrine ORM. + +```php + $this->getDependencies(), + MetadataMap::class => $this->getHalConfig(), ]; } + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [RoutesDelegator::class], + PostBookHandler::class => [HandlerDelegatorFactory::class], + GetBookHandler::class => [HandlerDelegatorFactory::class], + GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], + ], + 'factories' => [ + PostBookHandler::class => AttributedServiceFactory::class, + GetBookHandler::class => AttributedServiceFactory::class, + GetBookCollectionHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, + ], + 'aliases' => [ + BookServiceInterface::class => BookService::class, + ], + ]; + } + + private function getHalConfig(): array + { + return [ + AppConfigProvider::getResource(Book::class, 'book::view-book'), + AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), + ]; + } } ``` ### 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 +* register the module config by adding `Api\Book\ConfigProvider::class` and `Core\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/"` and `"Core\\Book\\": "src/Core/src/Book/src/"`, in composer.json under the autoload.psr-4 key * update Composer autoloader by running the command: ```shell @@ -157,7 +268,7 @@ class BookCollection extends ResourceCollection } ``` -* `src/Book/src/Entity/Book.php` +* `src/Core/src/Book/src/Entity/Book.php` To keep things simple in this tutorial our book will have 3 properties: `name`, `author` and `release date`. @@ -166,11 +277,11 @@ To keep things simple in this tutorial our book will have 3 properties: `name`, declare(strict_types=1); -namespace Api\Book\Entity; +namespace Core\Book\Entity; -use Api\App\Entity\AbstractEntity; -use Api\App\Entity\TimestampsTrait; -use Api\Book\Repository\BookRepository; +use Core\App\Entity\AbstractEntity; +use Core\App\Entity\TimestampsTrait; +use Core\Book\Repository\BookRepository; use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; @@ -248,26 +359,22 @@ class Book extends AbstractEntity ``` -* `src/Book/src/Repository/BookRepository.php` +* `src/Core/src/Book/src/Repository/BookRepository.php` ```php - */ - #[Entity(name: Book::class)] -class BookRepository extends EntityRepository +#[Entity(name: Book::class)] +class BookRepository extends AbstractRepository { public function saveBook(Book $book): Book { @@ -277,22 +384,16 @@ class BookRepository extends EntityRepository return $book; } - public function getBooks(array $filters = []): BookCollection + public function getBooks(array $params = [], array $filters = []): Query { - $page = PaginationHelper::getOffsetAndLimit($filters); - - $qb = $this - ->getEntityManager() - ->createQueryBuilder() + return $this + ->getQueryBuilder() ->select('book') ->from(Book::class, 'book') ->orderBy($filters['order'] ?? 'book.created', $filters['dir'] ?? 'desc') - ->setFirstResult($page['offset']) - ->setMaxResults($page['limit']); - - $qb->getQuery()->useQueryCache(true); - - return new BookCollection($qb, false); + ->setMaxResults($params['limit']) + ->getQuery() + ->useQueryCache(true); } } ``` @@ -306,7 +407,7 @@ declare(strict_types=1); namespace Api\Book\Service; -use Api\Book\Repository\BookRepository; +use Core\Book\Repository\BookRepository; interface BookServiceInterface { @@ -323,10 +424,12 @@ declare(strict_types=1); namespace Api\Book\Service; -use Api\Book\Entity\Book; -use Api\Book\Repository\BookRepository; -use Dot\DependencyInjection\Attribute\Inject; +use Core\App\Helper\Paginator; +use Core\Book\Entity\Book; +use Core\Book\Repository\BookRepository; use DateTimeImmutable; +use Dot\DependencyInjection\Attribute\Inject; +use Exception; class BookService implements BookServiceInterface { @@ -340,6 +443,9 @@ class BookService implements BookServiceInterface return $this->bookRepository; } + /** + * @throws Exception + */ public function createBook(array $data): Book { $book = new Book( @@ -353,28 +459,32 @@ class BookService implements BookServiceInterface public function getBooks(array $filters = []) { - return $this->bookRepository->getBooks($filters); + $params = Paginator::getParams($filters, 'book.created'); + + return $this->bookRepository->getBooks($params, $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` +* `src/App/src/InputFilter/Input/AuthorInput.php` ```php getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'author'), + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD, 'author'), ], true); } } ``` -* `src/Book/src/InputFilter/Input/NameInput.php` +* `src/App/src/InputFilter/Input/NameInput.php` ```php getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD, 'name'), ], true); } } ``` -* `src/Book/src/InputFilter/Input/ReleaseDateInput.php` +* `src/App/src/InputFilter/Input/ReleaseDateInput.php` ```php getValidatorChain() $this->add($nameInput); ``` -Now it's time to create the handler. +Now it's time to create the handlers. -* `src/Book/src/Handler/BookHandler.php` +* `src/Book/src/Handler/GetBookCollectionHandler.php` ```php bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); - - if (! $book instanceof Book){ - return $this->notFoundResponse(); - } - - return $this->createResponse($request, $book); + return $this->createResponse( + $request, + new BookCollection($this->bookService->getBooks($request->getQueryParams())) + ); } +} +``` - public function getCollection(ServerRequestInterface $request): ResponseInterface - { - $books = $this->bookService->getRepository()->getBooks($request->getQueryParams()); +* `src/Book/src/Handler/GetBookHandler.php` - return $this->createResponse($request, $books); - } +```php +setData($request->getParsedBody()); - if (! $inputFilter->isValid()) { - return $this->errorResponse($inputFilter->getMessages(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); - } +namespace Api\Book\Handler; - $book = $this->bookService->createBook($inputFilter->getValues()); +use Api\App\Attribute\Resource; +use Api\App\Handler\AbstractHandler; +use Core\Book\Entity\Book; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; - return $this->createResponse($request, $book); +class GetBookHandler extends AbstractHandler +{ + #[Resource(entity: Book::class)] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->createResponse( + $request, + $request->getAttribute(Book::class) + ); } } - ``` -After we have the handler, we need to register some routes in the `RoutesDelegator`, the same we created when we registered the module. - -* `src/Book/src/RoutesDelegator.php` +* `src/Book/src/Handler/PostBookCollectionHandler.php` ```php get( - '/books', - BookHandler::class, - 'books.list' - ); +class PostBookHandler extends AbstractHandler implements RequestHandlerInterface +{ + #[Inject( + CreateBookInputFilter::class, + BookServiceInterface::class, + )] + public function __construct( + protected CreateBookInputFilter $inputFilter, + protected BookServiceInterface $bookService, + ) { + } - $app->get( - '/book/'.$uuid, - BookHandler::class, - 'book.show' - ); + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->inputFilter->setData((array) $request->getParsedBody()); + if (! $this->inputFilter->isValid()) { + throw BadRequestException::create( + detail: Message::VALIDATOR_INVALID_DATA, + additional: ['errors' => $this->inputFilter->getMessages()] + ); + } - $app->post( - '/book', - BookHandler::class, - 'book.create' - ); + /** @var non-empty-array $data */ + $data = (array) $this->inputFilter->getValues(); - return $app; + return $this->createdResponse($request, $this->bookService->createBook($data)); } } ``` -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. +After we have the handler, we need to register some routes in the `RoutesDelegator` using our new grouping method, the same we created when we registered the module. -* `src/Book/src/ConfigProvider.php` +* `src/Book/src/RoutesDelegator.php` ```php $this->getDependencies(), - 'doctrine' => $this->getDoctrineConfig(), - MetadataMap::class => $this->getHalConfig(), - ]; - } + $uuid = ConfigProvider::REGEXP_UUID; - private function getDependencies(): array - { - return [ - 'delegators' => [ - Application::class => [ - RoutesDelegator::class - ] - ], - 'factories' => [ - BookHandler::class => AttributedServiceFactory::class, - BookService::class => AttributedServiceFactory::class, - BookRepository::class => AttributedRepositoryFactory::class, - ], - 'aliases' => [ - BookServiceInterface::class => BookService::class, - ], - ]; - } + /** @var RouteCollectorInterface $routeCollector */ + $routeCollector = $container->get(RouteCollectorInterface::class); - private function getDoctrineConfig(): array - { - return [ - 'driver' => [ - 'orm_default' => [ - 'drivers' => [ - 'Api\Book\Entity' => 'BookEntities' - ], - ], - 'BookEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => __DIR__ . '/Entity', - ], - ], - ]; - } + $routeCollector->group('/book') + ->post('', PostBookHandler::class, 'book::create-book'); - private function getHalConfig(): array - { - return [ - AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), - AppConfigProvider::getResource(Book::class, 'book.show') - ]; - } + $routeCollector->group('/book/' . $uuid) + ->get('', GetBookHandler::class, 'book::view-book'); + + $routeCollector->group('/books') + ->get('', GetBookCollectionHandler::class, 'book::list-books'); + return $callback(); + } } ``` +We need to configure access to the newly created endpoints, add `books::list-books`, `book::view-book` and `book::create-book` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. +> Make sure you read and understand the rbac documentation. + ## Migrations We created the `Book` entity, but we didn't create the associated table for it. From b473f4ceae05b8b032a164c75c890334d04105c2 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 13 May 2025 13:07:36 +0300 Subject: [PATCH 02/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v5/tutorials/create-book-module.md | 2 +- docs/book/v6/tutorials/create-book-module.md | 103 ++++++++----------- 2 files changed, 46 insertions(+), 59 deletions(-) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index aad1c3b4..dcaed440 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -654,7 +654,7 @@ class RoutesDelegator $app->get( '/book/' . $uuid, - Book::class, + BookHandler::class, 'book.show' ); diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index 9e5cf1fa..a8489f0e 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -18,28 +18,26 @@ The below files structure is what we will have at the end of this tutorial and i │ │ ├── GetBookHandler.php │ │ └── PostBookHandler.php │ ├── InputFilter/ + │ │ ├── Input/ + │ │ │ ├── AuthorInput.php + │ │ │ ├── NameInput.php + │ │ │ └── ReleaseDateInput.php │ │ └── CreateBookInputFilter.php │ ├── Service/ │ │ ├── BookService.php │ │ └── BookServiceInterface.php │ ├── ConfigProvider.php │ └── RoutesDelegator.php - ├── Core/ - │ └── src/ - │ └── Book/ - │ └── src/ - │ ├──Entity/ - │ │ └──Book.php - │ ├──Repository/ - │ │ └──BookRepository.php - │ └── ConfigProvider.php - └── App/ - └──src/ - └── InputFilter/ - └── Input/ - ├── AuthorInput.php - ├── NameInput.php - └── ReleaseDateInput.php + └── Core/ + └── src/ + └── Book/ + └── src/ + ├──Entity/ + │ └──Book.php + ├──Repository/ + │ └──BookRepository.php + └── ConfigProvider.php + ``` * `src/Book/src/Collection/BookCollection.php` - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database @@ -52,7 +50,7 @@ The below files structure is what we will have at the end of this tutorial and i * `src/Book/src/ConfigProvider.php` - is a class that provides configuration for various aspects of the framework or application * `src/Book/src/RoutesDelegator.php` - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application * `src/Book/src/InputFilter/CreateBookInputFilter.php` - input filters and validators -* `src/Core/src/App/src/InputFilter/Input/*` - input filters and validator configurations +* `src/Book/src/InputFilter/Input/*` - input filters and validator configurations ## Creating and configuring the module @@ -180,20 +178,11 @@ In `src/Core/src/Book/src` we will create 1 PHP file: `ConfigProvider.php`. This declare(strict_types=1); -namespace Api\Book; +namespace Core\Book; -use Api\App\ConfigProvider as AppConfigProvider; -use Api\App\Factory\HandlerDelegatorFactory; -use Api\Book\Collection\BookCollection; -use Api\Book\Handler\GetBookCollectionHandler; -use Api\Book\Handler\GetBookHandler; -use Api\Book\Handler\PostBookHandler; -use Api\Book\Service\BookService; -use Api\Book\Service\BookServiceInterface; -use Core\Book\Entity\Book; -use Dot\DependencyInjection\Factory\AttributedServiceFactory; -use Mezzio\Application; -use Mezzio\Hal\Metadata\MetadataMap; +use Core\Book\Repository\BookRepository; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Dot\DependencyInjection\Factory\AttributedRepositoryFactory; class ConfigProvider { @@ -201,36 +190,34 @@ class ConfigProvider { return [ 'dependencies' => $this->getDependencies(), - MetadataMap::class => $this->getHalConfig(), + 'doctrine' => $this->getDoctrineConfig(), ]; } private function getDependencies(): array { return [ - 'delegators' => [ - Application::class => [RoutesDelegator::class], - PostBookHandler::class => [HandlerDelegatorFactory::class], - GetBookHandler::class => [HandlerDelegatorFactory::class], - GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], - ], - 'factories' => [ - PostBookHandler::class => AttributedServiceFactory::class, - GetBookHandler::class => AttributedServiceFactory::class, - GetBookCollectionHandler::class => AttributedServiceFactory::class, - BookService::class => AttributedServiceFactory::class, - ], - 'aliases' => [ - BookServiceInterface::class => BookService::class, + 'factories' => [ + BookRepository::class => AttributedRepositoryFactory::class, ], ]; } - private function getHalConfig(): array + private function getDoctrineConfig(): array { return [ - AppConfigProvider::getResource(Book::class, 'book::view-book'), - AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), + 'driver' => [ + 'orm_default' => [ + 'drivers' => [ + 'Core\Book\Entity' => 'BookEntities', + ], + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => [__DIR__ . '/Entity'], + ], + ], ]; } } @@ -468,14 +455,14 @@ class BookService implements BookServiceInterface 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/App/src/InputFilter/Input/AuthorInput.php` +* `src/Book/src/InputFilter/Input/AuthorInput.php` ```php Date: Tue, 13 May 2025 13:48:09 +0300 Subject: [PATCH 03/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index a8489f0e..a83bca6e 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -257,7 +257,7 @@ class BookCollection extends ResourceCollection * `src/Core/src/Book/src/Entity/Book.php` -To keep things simple in this tutorial our book will have 3 properties: `name`, `author` and `release date`. +To keep things simple in this tutorial, our book will have 3 properties: `name`, `author` and `release date`. ```php Date: Tue, 13 May 2025 16:37:32 +0300 Subject: [PATCH 04/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/extended-features/email.md | 21 +++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 22 insertions(+) create mode 100644 docs/book/v6/extended-features/email.md diff --git a/docs/book/v6/extended-features/email.md b/docs/book/v6/extended-features/email.md new file mode 100644 index 00000000..83d3a202 --- /dev/null +++ b/docs/book/v6/extended-features/email.md @@ -0,0 +1,21 @@ +# Email sending and content parsing + +In the previous version of Dotkernel API we have been using the `mezzio/mezzio-twigrenderer` package which added unnecessary complexity to the email sending in our API platform since APIs returns JSON data, not HTML. +In order to fix this issue, we have come up with a lighter custom solution. +Now each project can prepare the bodies of the emails by using its preferred template renderer. +`Core\App\MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. + +Example from `src/User/src/Handler/PostUserResourceHandler.php`: +```php +if ($user->isPending()) { + $this->mailService->sendActivationMail( + $user, + $this->renderer->render('user::activate', ['user' => $user]) + ); +} +``` +In this case we are using the `phtml` template from `src/User/src/templates`. +It has a lighter format compared to `twig`. +It is then rendered before sending the activation email by our custom renderer from `src/App/src/Template/Rederer.php`. +The other applications that use the Core structure such as [Dotkernel Admin](https://docs.dotkernel.org/admin-documentation/) use `mezzio/mezzio-twigrenderer` for this purpose. + diff --git a/mkdocs.yml b/mkdocs.yml index 033f8e57..43dcea1c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - "Route Grouping": v6/extended-features/route-grouping.md - "Problem Details": v6/extended-features/problem-details.md - "Injectable Input Filters": v6/extended-features/injectable-input-filters.md + - "Email sending and content parsing": v6/extended-features/email.md - Commands: - "Create admin account": v6/commands/create-admin-account.md - "Generate database migrations": v6/commands/generate-database-migrations.md From b1789bdbc07449df0ce08654873407660ec7d3e7 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 13 May 2025 17:22:14 +0300 Subject: [PATCH 05/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/extended-features/email.md | 70 +++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/docs/book/v6/extended-features/email.md b/docs/book/v6/extended-features/email.md index 83d3a202..fb9079fb 100644 --- a/docs/book/v6/extended-features/email.md +++ b/docs/book/v6/extended-features/email.md @@ -1,11 +1,73 @@ # Email sending and content parsing In the previous version of Dotkernel API we have been using the `mezzio/mezzio-twigrenderer` package which added unnecessary complexity to the email sending in our API platform since APIs returns JSON data, not HTML. -In order to fix this issue, we have come up with a lighter custom solution. +Besides this, it used two services (`AdminService` and `UserService`) to send emails. +It was not necessarily wrong, but their job should be only to manage Admin/User accounts. + +In order to fix this those problems, we have come up with a lighter custom solution. Now each project can prepare the bodies of the emails by using its preferred template renderer. -`Core\App\MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. +`Core/src/App/src/Service/MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. + +Example from `Core/src/App/src/Service/MailService.php`: + +```php + $config + */ + #[Inject( + 'dot-mail.service.default', + 'dot-log.default_logger', + 'config', + )] + public function __construct( + protected \Dot\Mail\Service\MailService $mailService, + protected LoggerInterface $logger, + private readonly array $config, + ) { + } + + /** + * @throws MailException + */ + public function sendActivationMail(User $user, string $body): bool + { + if ($user->isActive()) { + return false; + } + + $this->mailService->getMessage()->addTo($user->getEmail(), $user->getName()); + $this->mailService->setSubject('Welcome to ' . $this->config['application']['name']); + $this->mailService->setBody($body); + + try { + return $this->mailService->send()->isValid(); + } catch (MailException | TransportExceptionInterface $exception) { + $this->logger->err($exception->getMessage()); + throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getEmail())); + } + } +} +``` + +Rending example from `src/User/src/Handler/PostUserResourceHandler.php`: -Example from `src/User/src/Handler/PostUserResourceHandler.php`: ```php if ($user->isPending()) { $this->mailService->sendActivationMail( @@ -14,8 +76,8 @@ if ($user->isPending()) { ); } ``` + In this case we are using the `phtml` template from `src/User/src/templates`. It has a lighter format compared to `twig`. It is then rendered before sending the activation email by our custom renderer from `src/App/src/Template/Rederer.php`. The other applications that use the Core structure such as [Dotkernel Admin](https://docs.dotkernel.org/admin-documentation/) use `mezzio/mezzio-twigrenderer` for this purpose. - From cc6237870f8dd514e419f92bb7013f1849232dd7 Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 14 May 2025 12:59:40 +0300 Subject: [PATCH 06/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 121 +++++++++++-------- mkdocs.yml | 1 - 2 files changed, 70 insertions(+), 52 deletions(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index a83bca6e..a621cdef 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -15,8 +15,8 @@ The below files structure is what we will have at the end of this tutorial and i │ │ └── Book.php │ ├── Handler/ │ │ ├── GetBookCollectionHandler.php - │ │ ├── GetBookHandler.php - │ │ └── PostBookHandler.php + │ │ ├── GetBookResourceHandler.php + │ │ └── PostBookResourceHandler.php │ ├── InputFilter/ │ │ ├── Input/ │ │ │ ├── AuthorInput.php @@ -40,17 +40,19 @@ The below files structure is what we will have at the end of this tutorial and i ``` -* `src/Book/src/Collection/BookCollection.php` - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database -* `src/Core/src/Book/src/Entity/Book.php` - an entity refers to a PHP class that represents a persistent object or data structure -* `src/Book/src/Handler/GetBookCollectionHandler.php` - handler that reflects the GET action for the BookCollection class -* `src/Book/src/Handler/GetBookHandler.php` - handler that reflects the GET action for the Book entity -* `src/Book/src/Handler/PostBookHandler.php` - handler that reflects the POST action for the Book entity -* `src/Core/src/Book/src/Repository/BookRepository.php` - a repository is a class responsible for querying and retrieving entities from the database -* `src/Book/src/Service/BookService.php` - is a class or component responsible for performing a specific task or providing functionality to other parts of the application -* `src/Book/src/ConfigProvider.php` - is a class that provides configuration for various aspects of the framework or application -* `src/Book/src/RoutesDelegator.php` - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application -* `src/Book/src/InputFilter/CreateBookInputFilter.php` - input filters and validators -* `src/Book/src/InputFilter/Input/*` - input filters and validator configurations +* `src/Book/src/Collection/BookCollection.php` – a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database +* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application +* `src/Book/src/Handler/GetBookCollectionHandler.php` – handler that reflects the GET action for the BookCollection class +* `src/Book/src/Handler/GetBookResourceHandler.php` – handler that reflects the GET action for the Book entity +* `src/Book/src/Handler/PostBookResourceHandler.php` – handler that reflects the POST action for the Book entity +* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators +* `src/Book/src/InputFilter/Input/*` – input filters and validator configurations +* `src/Book/src/RoutesDelegator.php` – a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application +* `src/Book/src/Service/BookService.php` – is a class or component responsible for performing a specific task or providing functionality to other parts of the application +* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM +* `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure +* `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database + ## Creating and configuring the module @@ -70,8 +72,8 @@ declare(strict_types=1); namespace Api\Book; use Api\Book\Handler\GetBookCollectionHandler; -use Api\Book\Handler\GetBookHandler; -use Api\Book\Handler\PostBookHandler; +use Api\Book\Handler\GetBookResourceHandler; +use Api\Book\Handler\PostBookResourceHandler; use Core\App\ConfigProvider; use Dot\Router\RouteCollectorInterface; use Mezzio\Application; @@ -93,10 +95,10 @@ class RoutesDelegator $routeCollector = $container->get(RouteCollectorInterface::class); $routeCollector->group('/book') - ->post('', PostBookHandler::class, 'book::create-book'); + ->post('', PostBookResourceHandler::class, 'book::create-book'); $routeCollector->group('/book/' . $uuid) - ->get('', GetBookHandler::class, 'book::view-book'); + ->get('', GetBookResourceHandler::class, 'book::view-book'); $routeCollector->group('/books') ->get('', GetBookCollectionHandler::class, 'book::list-books'); @@ -119,8 +121,8 @@ use Api\App\ConfigProvider as AppConfigProvider; use Api\App\Factory\HandlerDelegatorFactory; use Api\Book\Collection\BookCollection; use Api\Book\Handler\GetBookCollectionHandler; -use Api\Book\Handler\GetBookHandler; -use Api\Book\Handler\PostBookHandler; +use Api\Book\Handler\GetBookResourceHandler; +use Api\Book\Handler\PostBookResourceHandler; use Api\Book\Service\BookService; use Api\Book\Service\BookServiceInterface; use Core\Book\Entity\Book; @@ -143,13 +145,13 @@ class ConfigProvider return [ 'delegators' => [ Application::class => [RoutesDelegator::class], - PostBookHandler::class => [HandlerDelegatorFactory::class], - GetBookHandler::class => [HandlerDelegatorFactory::class], + PostBookResourceHandler::class => [HandlerDelegatorFactory::class], + GetBookResourceHandler::class => [HandlerDelegatorFactory::class], GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], ], 'factories' => [ - PostBookHandler::class => AttributedServiceFactory::class, - GetBookHandler::class => AttributedServiceFactory::class, + PostBookResourceHandler::class => AttributedServiceFactory::class, + GetBookResourceHandler::class => AttributedServiceFactory::class, GetBookCollectionHandler::class => AttributedServiceFactory::class, BookService::class => AttributedServiceFactory::class, ], @@ -470,8 +472,6 @@ use Laminas\Filter\StripTags; use Laminas\InputFilter\Input; use Laminas\Validator\NotEmpty; -use function sprintf; - class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) @@ -486,7 +486,7 @@ class AuthorInput extends Input $this->getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD, 'author'), + 'message' => Message::VALIDATOR_REQUIRED_FIELD, ], true); } } @@ -507,8 +507,6 @@ use Laminas\Filter\StripTags; use Laminas\InputFilter\Input; use Laminas\Validator\NotEmpty; -use function sprintf; - class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) @@ -523,7 +521,7 @@ class NameInput extends Input $this->getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD, 'name'), + 'message' => Message::VALIDATOR_REQUIRED_FIELD, ], true); } } @@ -544,8 +542,6 @@ use Laminas\Filter\StripTags; use Laminas\InputFilter\Input; use Laminas\Validator\Date; -use function sprintf; - class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) @@ -560,7 +556,7 @@ class ReleaseDateInput extends Input $this->getValidatorChain() ->attachByName(Date::class, [ - 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), + 'message' => Message::INVALID_VALUE, ], true); } } @@ -593,7 +589,7 @@ class CreateBookInputFilter extends AbstractInputFilter } ``` -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 `CreateBookInputFilter` like this: +We split all the inputs just for the purpose of this tutorial and to demonstrate a clean `CreateBookInputFilter` but you could have all the inputs created directly in the `CreateBookInputFilter` like this: ```php $nameInput = new Input(); @@ -605,10 +601,38 @@ $nameInput->getFilterChain() $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), + 'message' => Message::VALIDATOR_REQUIRED_FIELD, ], true); $this->add($nameInput); + +$authorInput = new Input(); +$authorInput->setRequired(true); + +$authorInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$authorInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($authorInput); + +$releaseDateInput = new Input(); +$releaseDateInput->setRequired(true); + +$releaseDateInput->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + +$releaseDateInput->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => Message::VALIDATOR_REQUIRED_FIELD, + ], true); + +$this->add($releaseDateInput); ``` Now it's time to create the handlers. @@ -649,7 +673,7 @@ class GetBookCollectionHandler extends AbstractHandler } ``` -* `src/Book/src/Handler/GetBookHandler.php` +* `src/Book/src/Handler/GetBookResourceHandler.php` ```php get(RouteCollectorInterface::class); - $routeCollector->group('/book') - ->post('', PostBookHandler::class, 'book::create-book'); - - $routeCollector->group('/book/' . $uuid) - ->get('', GetBookHandler::class, 'book::view-book'); - - $routeCollector->group('/books') - ->get('', GetBookCollectionHandler::class, 'book::list-books'); + $routeCollector->post('/book', PostBookHandler::class, 'book::create-book'); + $routeCollector->get('/book/' . $uuid, GetBookHandler::class, 'book::view-book'); + $routeCollector->get('/books', GetBookCollectionHandler::class, 'book::list-books'); return $callback(); } @@ -773,7 +792,7 @@ class RoutesDelegator ``` We need to configure access to the newly created endpoints, add `books::list-books`, `book::view-book` and `book::create-book` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. -> Make sure you read and understand the rbac documentation. +> Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). ## Migrations @@ -788,7 +807,7 @@ php bin/doctrine orm:validate-schema Doctrine can handle the table creation, run the following command: ```shell -vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' +php ./vendor/bin/doctrine-migrations diff ``` This will check for differences between your entities and database structure and create migration files if necessary, in `data/doctrine/migrations`. @@ -796,7 +815,7 @@ This will check for differences between your entities and database structure and To execute the migrations run: ```shell -vendor/bin/doctrine-migrations migrate +php ./vendor/bin/doctrine-migrations ``` ## Checking endpoints diff --git a/mkdocs.yml b/mkdocs.yml index 43dcea1c..033f8e57 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,7 +43,6 @@ nav: - "Route Grouping": v6/extended-features/route-grouping.md - "Problem Details": v6/extended-features/problem-details.md - "Injectable Input Filters": v6/extended-features/injectable-input-filters.md - - "Email sending and content parsing": v6/extended-features/email.md - Commands: - "Create admin account": v6/commands/create-admin-account.md - "Generate database migrations": v6/commands/generate-database-migrations.md From 44cd8007fbdadea783413fc2305bc0c136b01c1f Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 14 May 2025 13:02:31 +0300 Subject: [PATCH 07/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/extended-features/email.md | 83 -------------------- docs/book/v6/tutorials/create-book-module.md | 4 +- 2 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 docs/book/v6/extended-features/email.md diff --git a/docs/book/v6/extended-features/email.md b/docs/book/v6/extended-features/email.md deleted file mode 100644 index fb9079fb..00000000 --- a/docs/book/v6/extended-features/email.md +++ /dev/null @@ -1,83 +0,0 @@ -# Email sending and content parsing - -In the previous version of Dotkernel API we have been using the `mezzio/mezzio-twigrenderer` package which added unnecessary complexity to the email sending in our API platform since APIs returns JSON data, not HTML. -Besides this, it used two services (`AdminService` and `UserService`) to send emails. -It was not necessarily wrong, but their job should be only to manage Admin/User accounts. - -In order to fix this those problems, we have come up with a lighter custom solution. -Now each project can prepare the bodies of the emails by using its preferred template renderer. -`Core/src/App/src/Service/MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. - -Example from `Core/src/App/src/Service/MailService.php`: - -```php - $config - */ - #[Inject( - 'dot-mail.service.default', - 'dot-log.default_logger', - 'config', - )] - public function __construct( - protected \Dot\Mail\Service\MailService $mailService, - protected LoggerInterface $logger, - private readonly array $config, - ) { - } - - /** - * @throws MailException - */ - public function sendActivationMail(User $user, string $body): bool - { - if ($user->isActive()) { - return false; - } - - $this->mailService->getMessage()->addTo($user->getEmail(), $user->getName()); - $this->mailService->setSubject('Welcome to ' . $this->config['application']['name']); - $this->mailService->setBody($body); - - try { - return $this->mailService->send()->isValid(); - } catch (MailException | TransportExceptionInterface $exception) { - $this->logger->err($exception->getMessage()); - throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getEmail())); - } - } -} -``` - -Rending example from `src/User/src/Handler/PostUserResourceHandler.php`: - -```php -if ($user->isPending()) { - $this->mailService->sendActivationMail( - $user, - $this->renderer->render('user::activate', ['user' => $user]) - ); -} -``` - -In this case we are using the `phtml` template from `src/User/src/templates`. -It has a lighter format compared to `twig`. -It is then rendered before sending the activation email by our custom renderer from `src/App/src/Template/Rederer.php`. -The other applications that use the Core structure such as [Dotkernel Admin](https://docs.dotkernel.org/admin-documentation/) use `mezzio/mezzio-twigrenderer` for this purpose. diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index a621cdef..032b39ed 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -810,7 +810,7 @@ Doctrine can handle the table creation, run the following command: php ./vendor/bin/doctrine-migrations diff ``` -This will check for differences between your entities and database structure and create migration files if necessary, in `data/doctrine/migrations`. +This will check for differences between your entities and database structure and create migration files if necessary, in `src/Core/src/App/src/Migration`. To execute the migrations run: @@ -820,7 +820,7 @@ php ./vendor/bin/doctrine-migrations ## Checking endpoints -If we did everything as planned we can call the `http://0.0.0.0:8080/book` endpoint and create a new book: +If we did everything as planned, we can call the `http://0.0.0.0:8080/book` endpoint and create a new book: ```shell curl -X POST http://0.0.0.0:8080/book From ae15291af995de3924945d0ac1cde8afded2cf6e Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 14 May 2025 13:03:22 +0300 Subject: [PATCH 08/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index 032b39ed..bc3f4d17 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -53,7 +53,6 @@ The below files structure is what we will have at the end of this tutorial and i * `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure * `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database - ## 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. From dec02122f38a77506bd7b7a3d31f1dfdbc6773e7 Mon Sep 17 00:00:00 2001 From: horea Date: Thu, 15 May 2025 16:44:49 +0300 Subject: [PATCH 09/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 343 ++++++++----------- 1 file changed, 146 insertions(+), 197 deletions(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index bc3f4d17..a01269bd 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -11,8 +11,6 @@ The below files structure is what we will have at the end of this tutorial and i │ └── src/ │ ├── Collection/ │ │ └── BookCollection.php - │ ├── Entity/ - │ │ └── Book.php │ ├── Handler/ │ │ ├── GetBookCollectionHandler.php │ │ ├── GetBookResourceHandler.php @@ -37,207 +35,27 @@ The below files structure is what we will have at the end of this tutorial and i ├──Repository/ │ └──BookRepository.php └── ConfigProvider.php - ``` * `src/Book/src/Collection/BookCollection.php` – a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database -* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application * `src/Book/src/Handler/GetBookCollectionHandler.php` – handler that reflects the GET action for the BookCollection class * `src/Book/src/Handler/GetBookResourceHandler.php` – handler that reflects the GET action for the Book entity * `src/Book/src/Handler/PostBookResourceHandler.php` – handler that reflects the POST action for the Book entity -* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators * `src/Book/src/InputFilter/Input/*` – input filters and validator configurations -* `src/Book/src/RoutesDelegator.php` – a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application +* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators * `src/Book/src/Service/BookService.php` – is a class or component responsible for performing a specific task or providing functionality to other parts of the application -* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM +* `src/Book/src/Service/BookServiceInterface.php` – interface that reflects the publicly available methods in `BookService` +* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application +* `src/Book/src/RoutesDelegator.php` – a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application * `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure * `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database - -## 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` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. So the final structure will be like this: `src/Book/src` and `src/Core/src/Book/src`. - -In `src/Book/src` we will create 2 PHP files: `RoutesDelegator.php` and `ConfigProvider.php`. These files contain the necessary configurations. - -* `src/Book/src/RoutesDelegator.php` - -```php -get(RouteCollectorInterface::class); - - $routeCollector->group('/book') - ->post('', PostBookResourceHandler::class, 'book::create-book'); - - $routeCollector->group('/book/' . $uuid) - ->get('', GetBookResourceHandler::class, 'book::view-book'); - - $routeCollector->group('/books') - ->get('', GetBookCollectionHandler::class, 'book::list-books'); - - return $callback(); - } -} -``` - -* `src/Book/src/ConfigProvider.php` - -```php - $this->getDependencies(), - MetadataMap::class => $this->getHalConfig(), - ]; - } - - private function getDependencies(): array - { - return [ - 'delegators' => [ - Application::class => [RoutesDelegator::class], - PostBookResourceHandler::class => [HandlerDelegatorFactory::class], - GetBookResourceHandler::class => [HandlerDelegatorFactory::class], - GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], - ], - 'factories' => [ - PostBookResourceHandler::class => AttributedServiceFactory::class, - GetBookResourceHandler::class => AttributedServiceFactory::class, - GetBookCollectionHandler::class => AttributedServiceFactory::class, - BookService::class => AttributedServiceFactory::class, - ], - 'aliases' => [ - BookServiceInterface::class => BookService::class, - ], - ]; - } - - private function getHalConfig(): array - { - return [ - AppConfigProvider::getResource(Book::class, 'book::view-book'), - AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), - ]; - } -} -``` - -* `src/Core/src/Book/src/ConfigProvider.php` - -In `src/Core/src/Book/src` we will create 1 PHP file: `ConfigProvider.php`. This file contains the necessary configuration for Doctrine ORM. - -```php - $this->getDependencies(), - 'doctrine' => $this->getDoctrineConfig(), - ]; - } - - private function getDependencies(): array - { - return [ - 'factories' => [ - BookRepository::class => AttributedRepositoryFactory::class, - ], - ]; - } - - private function getDoctrineConfig(): array - { - return [ - 'driver' => [ - 'orm_default' => [ - 'drivers' => [ - 'Core\Book\Entity' => 'BookEntities', - ], - ], - 'BookEntities' => [ - 'class' => AttributeDriver::class, - 'cache' => 'array', - 'paths' => [__DIR__ . '/Entity'], - ], - ], - ]; - } -} -``` - -### Registering the module - -* register the module config by adding `Api\Book\ConfigProvider::class` and `Core\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/"` and `"Core\\Book\\": "src/Core/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. +* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM ## File creation and contents +In `src` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. +So the final structure will be like this: `src/Book/src` and `src/Core/src/Book/src`. + Each file below have a summary description above of what that file does. * `src/Book/src/Collection/BookCollection.php` @@ -258,7 +76,7 @@ class BookCollection extends ResourceCollection * `src/Core/src/Book/src/Entity/Book.php` -To keep things simple in this tutorial, our book will have 3 properties: `name`, `author` and `release date`. +To keep things simple in this tutorial, our book will have 3 properties: `name`, `author` and `releaseDate`. ```php getValidatorChain() ->attachByName(Date::class, [ - 'message' => Message::INVALID_VALUE, + 'message' => Message::invalidValue('releaseDate'), ], true); } } @@ -747,9 +565,11 @@ class PostBookResourceHandler extends AbstractHandler implements RequestHandlerI } ``` -After we have the handler, we need to register some routes in the `RoutesDelegator` using our new grouping method, the same we created when we registered the module. +In `src/Book/src` we now create the 2 PHP files: `RoutesDelegator.php` and `ConfigProvider.php`. -* `src/Book/src/RoutesDelegator.php` +`RoutesDelegator.php` contains all of our routes while `ConfigProvider` contains all the necessary configuration needed, so the above files work properly like dependency injection, aliases and so on. + +* `src/Book/src/ConfigProvider.php` ```php $this->getDependencies(), + MetadataMap::class => $this->getHalConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'delegators' => [ + Application::class => [RoutesDelegator::class], + PostBookResourceHandler::class => [HandlerDelegatorFactory::class], + GetBookResourceHandler::class => [HandlerDelegatorFactory::class], + GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], + ], + 'factories' => [ + PostBookResourceHandler::class => AttributedServiceFactory::class, + GetBookResourceHandler::class => AttributedServiceFactory::class, + GetBookCollectionHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, + ], + 'aliases' => [ + BookServiceInterface::class => BookService::class, + ], + ]; + } + + private function getHalConfig(): array + { + return [ + AppConfigProvider::getResource(Book::class, 'book::view-book'), + AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), + ]; + } +} +``` + +* `src/Book/src/RoutesDelegator.php` + +```php +get(RouteCollectorInterface::class); - $routeCollector->post('/book', PostBookHandler::class, 'book::create-book'); - $routeCollector->get('/book/' . $uuid, GetBookHandler::class, 'book::view-book'); + $routeCollector->post('/book', PostBookResourceHandler::class, 'book::create-book'); + $routeCollector->get('/book/' . $uuid, GetBookResourceHandler::class, 'book::view-book'); $routeCollector->get('/books', GetBookCollectionHandler::class, 'book::list-books'); return $callback(); @@ -790,6 +673,72 @@ class RoutesDelegator } ``` +In `src/Core/src/Book/src` we will create `ConfigProvider.php` where we configure Doctrine ORM. + +* `src/Core/src/Book/src/ConfigProvider.php`. + +```php + $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + ]; + } + + private function getDependencies(): array + { + return [ + 'factories' => [ + BookRepository::class => AttributedRepositoryFactory::class, + ], + ]; + } + + private function getDoctrineConfig(): array + { + return [ + 'driver' => [ + 'orm_default' => [ + 'drivers' => [ + 'Core\Book\Entity' => 'BookEntities', + ], + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => [__DIR__ . '/Entity'], + ], + ], + ]; + } +} +``` + +### Registering the module + +* register the module config by adding `Api\Book\ConfigProvider::class` and `Core\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/"` and `"Core\\Book\\": "src/Core/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. + We need to configure access to the newly created endpoints, add `books::list-books`, `book::view-book` and `book::create-book` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. > Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). @@ -814,7 +763,7 @@ This will check for differences between your entities and database structure and To execute the migrations run: ```shell -php ./vendor/bin/doctrine-migrations +php ./vendor/bin/doctrine-migrations migrate ``` ## Checking endpoints From 721a2a79f185e3fbb41ef119ff3ca8230456579e Mon Sep 17 00:00:00 2001 From: horea Date: Fri, 16 May 2025 19:06:39 +0300 Subject: [PATCH 10/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 62 +++++++++----------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index a01269bd..71d3ed13 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -56,8 +56,6 @@ The below files structure is what we will have at the end of this tutorial and i In `src` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. So the final structure will be like this: `src/Book/src` and `src/Core/src/Book/src`. -Each file below have a summary description above of what that file does. - * `src/Book/src/Collection/BookCollection.php` ```php @@ -176,30 +174,21 @@ namespace Core\Book\Repository; use Core\App\Repository\AbstractRepository; use Core\Book\Entity\Book; -use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; use Dot\DependencyInjection\Attribute\Entity; #[Entity(name: Book::class)] class BookRepository extends AbstractRepository { - public function saveBook(Book $book): Book - { - $this->getEntityManager()->persist($book); - $this->getEntityManager()->flush(); - - return $book; - } - - public function getBooks(array $params = [], array $filters = []): Query + public function getBooks(array $params = [], array $filters = []): QueryBuilder { return $this ->getQueryBuilder() ->select('book') ->from(Book::class, 'book') ->orderBy($filters['order'] ?? 'book.created', $filters['dir'] ?? 'desc') - ->setMaxResults($params['limit']) - ->getQuery() - ->useQueryCache(true); + ->setFirstResult($params['offset']) + ->setMaxResults($params['limit']); } } ``` @@ -213,11 +202,14 @@ declare(strict_types=1); namespace Api\Book\Service; -use Core\Book\Repository\BookRepository; +use Core\Book\Entity\Book; +use Doctrine\ORM\QueryBuilder; interface BookServiceInterface { - public function getRepository(): BookRepository; + public function saveBook(array $data): Book; + + public function getBooks(array $filters = []): QueryBuilder; } ``` @@ -234,6 +226,7 @@ use Core\App\Helper\Paginator; use Core\Book\Entity\Book; use Core\Book\Repository\BookRepository; use DateTimeImmutable; +use Doctrine\ORM\QueryBuilder; use Dot\DependencyInjection\Attribute\Inject; use Exception; @@ -244,15 +237,10 @@ class BookService implements BookServiceInterface { } - public function getRepository(): BookRepository - { - return $this->bookRepository; - } - /** * @throws Exception */ - public function createBook(array $data): Book + public function saveBook(array $data): Book { $book = new Book( $data['name'], @@ -260,13 +248,15 @@ class BookService implements BookServiceInterface new DateTimeImmutable($data['releaseDate']) ); - return $this->bookRepository->saveBook($book); + $this->bookRepository->saveResource($book); + + return $book; } - public function getBooks(array $filters = []) + public function getBooks(array $filters = []): QueryBuilder { - $params = Paginator::getParams($filters, 'book.created'); - + $params = Paginator::getParams($filters, 'book.created'); + return $this->bookRepository->getBooks($params, $filters); } } @@ -560,7 +550,7 @@ class PostBookResourceHandler extends AbstractHandler implements RequestHandlerI /** @var non-empty-array $data */ $data = (array) $this->inputFilter->getValues(); - return $this->createdResponse($request, $this->bookService->createBook($data)); + return $this->createdResponse($request, $this->bookService->saveBook($data)); } } ``` @@ -642,8 +632,8 @@ declare(strict_types=1); namespace Api\Book; use Api\Book\Handler\GetBookCollectionHandler; -use Api\Book\Handler\GetBookHandler; -use Api\Book\Handler\PostBookHandler; +use Api\Book\Handler\GetBookResourceHandler; +use Api\Book\Handler\PostBookResourceHandler; use Core\App\ConfigProvider; use Dot\Router\RouteCollectorInterface; use Mezzio\Application; @@ -739,7 +729,13 @@ composer dump-autoload That's it. The module is now registered. -We need to configure access to the newly created endpoints, add `books::list-books`, `book::view-book` and `book::create-book` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. +We need to configure access to the newly created endpoints. +Open `config/autoload/authorization.global.php` and append the below route names to the `UserRoleEnum::Guest->value` key: + +- `books::list-books` +- `book::view-book` +- `book::create-book` + > Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). ## Migrations @@ -748,8 +744,8 @@ 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 +```shell +php ./bin/doctrine orm:validate-schema ``` Doctrine can handle the table creation, run the following command: From 396ac51582c3dcac1bb74d95d5ade03d0ef9c5a8 Mon Sep 17 00:00:00 2001 From: horea Date: Mon, 19 May 2025 12:07:22 +0300 Subject: [PATCH 11/21] Issue #110: update book tutorial Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index 71d3ed13..76d62eb5 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -396,7 +396,7 @@ class CreateBookInputFilter extends AbstractInputFilter } ``` -We split all the inputs just for the purpose of this tutorial and to demonstrate a clean `CreateBookInputFilter` but you could have all the inputs created directly in the `CreateBookInputFilter` like this: +We create separate `Input` files to demonstrate their reusability and obtain a clean `CreateBookInputFilter` but you could have all the inputs created directly in the `CreateBookInputFilter` like this: ```php $nameInput = new Input(); @@ -732,10 +732,10 @@ That's it. The module is now registered. We need to configure access to the newly created endpoints. Open `config/autoload/authorization.global.php` and append the below route names to the `UserRoleEnum::Guest->value` key: -- `books::list-books` -- `book::view-book` -- `book::create-book` - +* `books::list-books` +* `book::view-book` +* `book::create-book` + > Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). ## Migrations From 99eba21264d55d7b951f360a2e67c8dc792319f0 Mon Sep 17 00:00:00 2001 From: horea Date: Mon, 19 May 2025 12:25:56 +0300 Subject: [PATCH 12/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v6/tutorials/email.md | 83 +++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 84 insertions(+) create mode 100644 docs/book/v6/tutorials/email.md diff --git a/docs/book/v6/tutorials/email.md b/docs/book/v6/tutorials/email.md new file mode 100644 index 00000000..fb9079fb --- /dev/null +++ b/docs/book/v6/tutorials/email.md @@ -0,0 +1,83 @@ +# Email sending and content parsing + +In the previous version of Dotkernel API we have been using the `mezzio/mezzio-twigrenderer` package which added unnecessary complexity to the email sending in our API platform since APIs returns JSON data, not HTML. +Besides this, it used two services (`AdminService` and `UserService`) to send emails. +It was not necessarily wrong, but their job should be only to manage Admin/User accounts. + +In order to fix this those problems, we have come up with a lighter custom solution. +Now each project can prepare the bodies of the emails by using its preferred template renderer. +`Core/src/App/src/Service/MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. + +Example from `Core/src/App/src/Service/MailService.php`: + +```php + $config + */ + #[Inject( + 'dot-mail.service.default', + 'dot-log.default_logger', + 'config', + )] + public function __construct( + protected \Dot\Mail\Service\MailService $mailService, + protected LoggerInterface $logger, + private readonly array $config, + ) { + } + + /** + * @throws MailException + */ + public function sendActivationMail(User $user, string $body): bool + { + if ($user->isActive()) { + return false; + } + + $this->mailService->getMessage()->addTo($user->getEmail(), $user->getName()); + $this->mailService->setSubject('Welcome to ' . $this->config['application']['name']); + $this->mailService->setBody($body); + + try { + return $this->mailService->send()->isValid(); + } catch (MailException | TransportExceptionInterface $exception) { + $this->logger->err($exception->getMessage()); + throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getEmail())); + } + } +} +``` + +Rending example from `src/User/src/Handler/PostUserResourceHandler.php`: + +```php +if ($user->isPending()) { + $this->mailService->sendActivationMail( + $user, + $this->renderer->render('user::activate', ['user' => $user]) + ); +} +``` + +In this case we are using the `phtml` template from `src/User/src/templates`. +It has a lighter format compared to `twig`. +It is then rendered before sending the activation email by our custom renderer from `src/App/src/Template/Rederer.php`. +The other applications that use the Core structure such as [Dotkernel Admin](https://docs.dotkernel.org/admin-documentation/) use `mezzio/mezzio-twigrenderer` for this purpose. diff --git a/mkdocs.yml b/mkdocs.yml index 033f8e57..f2e19ef8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - "Route Grouping": v6/extended-features/route-grouping.md - "Problem Details": v6/extended-features/problem-details.md - "Injectable Input Filters": v6/extended-features/injectable-input-filters.md + - "Email Sending and Parsing": v6/extended-features/email.md - Commands: - "Create admin account": v6/commands/create-admin-account.md - "Generate database migrations": v6/commands/generate-database-migrations.md From 51be99d7edf843affb73bcefef70f8d320bbe7f2 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:29:38 +0300 Subject: [PATCH 13/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- .../rendering-and-sending-emails.md | 45 ++ docs/book/v6/tutorials/create-book-module.md | 616 +++++++++--------- docs/book/v6/tutorials/email.md | 83 --- mkdocs.yml | 2 +- 4 files changed, 342 insertions(+), 404 deletions(-) create mode 100644 docs/book/v6/core-features/rendering-and-sending-emails.md delete mode 100644 docs/book/v6/tutorials/email.md diff --git a/docs/book/v6/core-features/rendering-and-sending-emails.md b/docs/book/v6/core-features/rendering-and-sending-emails.md new file mode 100644 index 00000000..2e609e35 --- /dev/null +++ b/docs/book/v6/core-features/rendering-and-sending-emails.md @@ -0,0 +1,45 @@ +# Rendering and sending emails + +In the previous versions of Dotkernel API we have been composing email bodies using Twig from the mezzio/mezzio-twigrenderer package.\ +In the current version of Dotkernel API, we introduced the core mail service Core/src/App/src/Service/MailService which is responsible for sending all emails. + + +Being a core service, MailService is used across all projects implementing the Core architecture.\ +To compose and send an email, a solid implementation of TemplateRendererInterface was required to be injected into MailService, because each method rendered and parsed their respective templates in place before sending an email. +This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, Twig had to be replaced with a lighter solution. + +The solution is a custom Api\App\Template\Renderer implementing Api\App\Template\RendererInterface. +This is a lightweight renderer, aimed at rendering a combination of PHP and HTML files with phtml extension. + +With the new solution, MailService requires no implementation of any renderer because it no longer has to render templates internally.\ +Instead, an implementation of Api\App\Template\RendererInterface is first injected in the handler: + +```php +class ExampleHandler extends AbstractHandler +{ + #[Inject( + MailService::class, + RendererInterface::class, + )] + public function __construct( + protected MailService $mailService, + protected RendererInterface $renderer, + ) { +} +``` + +Then, the handler calls the renderer and saves the rendered template in a variable: + +```php +$body = $this->renderer->render('user::welcome', ['user' => $user]) +``` + +And finally, the handler calls the mail service with the composed $body being passed as a parameter to the method which sends the email: + +```php +// $user object contains email, firstname and lastname + +$this->mailService->sendWelcomeMail($user, $body); +``` + +>Other Dotkernel applications implementing the Core architecture do the same in the handlers, but keep using Twig as the template renderer. diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index 76d62eb5..fd06d541 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -7,54 +7,139 @@ The below files structure is what we will have at the end of this tutorial and i ```markdown . └── src/ - ├── Book/ - │ └── src/ - │ ├── Collection/ - │ │ └── BookCollection.php - │ ├── Handler/ - │ │ ├── GetBookCollectionHandler.php - │ │ ├── GetBookResourceHandler.php - │ │ └── PostBookResourceHandler.php - │ ├── InputFilter/ - │ │ ├── Input/ - │ │ │ ├── AuthorInput.php - │ │ │ ├── NameInput.php - │ │ │ └── ReleaseDateInput.php - │ │ └── CreateBookInputFilter.php - │ ├── Service/ - │ │ ├── BookService.php - │ │ └── BookServiceInterface.php - │ ├── ConfigProvider.php - │ └── RoutesDelegator.php - └── Core/ + └── Book/ └── src/ - └── Book/ - └── src/ - ├──Entity/ - │ └──Book.php - ├──Repository/ - │ └──BookRepository.php - └── ConfigProvider.php + ├── Collection/ + │ └── BookCollection.php + ├── Entity/ + │ └── Book.php + ├── Handler/ + │ └── BookHandler.php + ├── InputFilter/ + │ ├── Input/ + │ │ ├── AuthorInput.php + │ │ ├── NameInput.php + │ │ └── ReleaseDateInput.php + │ └── BookInputFilter.php + ├── Repository/ + │ └── BookRepository.php + ├── Service/ + │ ├── BookService.php + │ └── BookServiceInterface.php + ├── ConfigProvider.php + └── RoutesDelegator.php ``` -* `src/Book/src/Collection/BookCollection.php` – a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database -* `src/Book/src/Handler/GetBookCollectionHandler.php` – handler that reflects the GET action for the BookCollection class -* `src/Book/src/Handler/GetBookResourceHandler.php` – handler that reflects the GET action for the Book entity -* `src/Book/src/Handler/PostBookResourceHandler.php` – handler that reflects the POST action for the Book entity -* `src/Book/src/InputFilter/Input/*` – input filters and validator configurations -* `src/Book/src/InputFilter/CreateBookInputFilter.php` – input filters and validators -* `src/Book/src/Service/BookService.php` – is a class or component responsible for performing a specific task or providing functionality to other parts of the application -* `src/Book/src/Service/BookServiceInterface.php` – interface that reflects the publicly available methods in `BookService` -* `src/Book/src/ConfigProvider.php` – is a class that provides configuration for various aspects of the framework or application -* `src/Book/src/RoutesDelegator.php` – a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application -* `src/Core/src/Book/src/Entity/Book.php` – an entity refers to a PHP class that represents a persistent object or data structure -* `src/Core/src/Book/src/Repository/BookRepository.php` – a repository is a class responsible for querying and retrieving entities from the database -* `src/Core/src/Book/src/ConfigProvider.php` – is a class that provides configuration for Doctrine ORM +* `src/Book/src/Collection/BookCollection.php` - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database +* `src/Book/src/Entity/Book.php` - an entity refers to a PHP class that represents a persistent object or data structure +* `src/Book/src/Handler/BookHandler.php` - handlers are middleware that can handle requests based on an action +* `src/Book/src/Repository/BookRepository.php` - a repository is a class responsible for querying and retrieving entities from the database +* `src/Book/src/Service/BookService.php` - is a class or component responsible for performing a specific task or providing functionality to other parts of the application +* `src/Book/src/ConfigProvider.php` - is a class that provides configuration for various aspects of the framework or application +* `src/Book/src/RoutesDelegator.php` - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application +* `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 -In `src` and `src/Core/src` folders we will create one `Book` folder and in those we will create the `src` folder. -So the final structure will be like this: `src/Book/src` and `src/Core/src/Book/src`. +Each file below have a summary description above of what that file does. * `src/Book/src/Collection/BookCollection.php` @@ -72,20 +157,20 @@ class BookCollection extends ResourceCollection } ``` -* `src/Core/src/Book/src/Entity/Book.php` +* `src/Book/src/Entity/Book.php` -To keep things simple in this tutorial, our book will have 3 properties: `name`, `author` and `releaseDate`. +To keep things simple in this tutorial our book will have 3 properties: `name`, `author` and `release date`. ```php + */ + #[Entity(name: Book::class)] +class BookRepository extends EntityRepository { - public function getBooks(array $params = [], array $filters = []): QueryBuilder + public function saveBook(Book $book): Book + { + $this->getEntityManager()->persist($book); + $this->getEntityManager()->flush(); + + return $book; + } + + public function getBooks(array $filters = []): BookCollection { - return $this - ->getQueryBuilder() + $page = PaginationHelper::getOffsetAndLimit($filters); + + $qb = $this + ->getEntityManager() + ->createQueryBuilder() ->select('book') ->from(Book::class, 'book') ->orderBy($filters['order'] ?? 'book.created', $filters['dir'] ?? 'desc') - ->setFirstResult($params['offset']) - ->setMaxResults($params['limit']); + ->setFirstResult($page['offset']) + ->setMaxResults($page['limit']); + + $qb->getQuery()->useQueryCache(true); + + return new BookCollection($qb, false); } } ``` @@ -202,14 +306,11 @@ declare(strict_types=1); namespace Api\Book\Service; -use Core\Book\Entity\Book; -use Doctrine\ORM\QueryBuilder; +use Api\Book\Repository\BookRepository; interface BookServiceInterface { - public function saveBook(array $data): Book; - - public function getBooks(array $filters = []): QueryBuilder; + public function getRepository(): BookRepository; } ``` @@ -222,13 +323,10 @@ declare(strict_types=1); namespace Api\Book\Service; -use Core\App\Helper\Paginator; -use Core\Book\Entity\Book; -use Core\Book\Repository\BookRepository; -use DateTimeImmutable; -use Doctrine\ORM\QueryBuilder; +use Api\Book\Entity\Book; +use Api\Book\Repository\BookRepository; use Dot\DependencyInjection\Attribute\Inject; -use Exception; +use DateTimeImmutable; class BookService implements BookServiceInterface { @@ -237,10 +335,12 @@ class BookService implements BookServiceInterface { } - /** - * @throws Exception - */ - public function saveBook(array $data): Book + public function getRepository(): BookRepository + { + return $this->bookRepository; + } + + public function createBook(array $data): Book { $book = new Book( $data['name'], @@ -248,16 +348,12 @@ class BookService implements BookServiceInterface new DateTimeImmutable($data['releaseDate']) ); - $this->bookRepository->saveResource($book); - - return $book; + return $this->bookRepository->saveBook($book); } - public function getBooks(array $filters = []): QueryBuilder + public function getBooks(array $filters = []) { - $params = Paginator::getParams($filters, 'book.created'); - - return $this->bookRepository->getBooks($params, $filters); + return $this->bookRepository->getBooks($filters); } } ``` @@ -268,12 +364,12 @@ When creating or updating a book, we will need some validators, so we will creat ```php getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => Message::VALIDATOR_REQUIRED_FIELD, + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'author'), ], true); } } @@ -303,12 +399,12 @@ class AuthorInput extends Input ```php getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => Message::VALIDATOR_REQUIRED_FIELD, + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); } } @@ -338,16 +434,17 @@ class NameInput extends Input ```php getValidatorChain() ->attachByName(Date::class, [ - 'message' => Message::invalidValue('releaseDate'), + 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), ], true); } } @@ -371,11 +468,11 @@ class ReleaseDateInput extends Input Now we add all the inputs together in a parent input filter. -* `src/Book/src/InputFilter/CreateBookInputFilter.php` +* `src/Book/src/InputFilter/BookInputFilter.php` ```php getFilterChain() $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ - 'message' => Message::VALIDATOR_REQUIRED_FIELD, + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); $this->add($nameInput); - -$authorInput = new Input(); -$authorInput->setRequired(true); - -$authorInput->getFilterChain() - ->attachByName(StringTrim::class) - ->attachByName(StripTags::class); - -$authorInput->getValidatorChain() - ->attachByName(NotEmpty::class, [ - 'message' => Message::VALIDATOR_REQUIRED_FIELD, - ], true); - -$this->add($authorInput); - -$releaseDateInput = new Input(); -$releaseDateInput->setRequired(true); - -$releaseDateInput->getFilterChain() - ->attachByName(StringTrim::class) - ->attachByName(StripTags::class); - -$releaseDateInput->getValidatorChain() - ->attachByName(NotEmpty::class, [ - 'message' => Message::VALIDATOR_REQUIRED_FIELD, - ], true); - -$this->add($releaseDateInput); ``` -Now it's time to create the handlers. +Now it's time to create the handler. -* `src/Book/src/Handler/GetBookCollectionHandler.php` +* `src/Book/src/Handler/BookHandler.php` ```php createResponse( - $request, - new BookCollection($this->bookService->getBooks($request->getQueryParams())) - ); - } -} -``` - -* `src/Book/src/Handler/GetBookResourceHandler.php` - -```php -bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); -use Api\App\Attribute\Resource; -use Api\App\Handler\AbstractHandler; -use Core\Book\Entity\Book; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; + if (! $book instanceof Book){ + return $this->notFoundResponse(); + } -class GetBookResourceHandler extends AbstractHandler -{ - #[Resource(entity: Book::class)] - public function handle(ServerRequestInterface $request): ResponseInterface - { - return $this->createResponse( - $request, - $request->getAttribute(Book::class) - ); + return $this->createResponse($request, $book); } -} -``` - -* `src/Book/src/Handler/PostBookResourceHandler.php` - -```php -bookService->getRepository()->getBooks($request->getQueryParams()); -class PostBookResourceHandler extends AbstractHandler implements RequestHandlerInterface -{ - #[Inject( - CreateBookInputFilter::class, - BookServiceInterface::class, - )] - public function __construct( - protected CreateBookInputFilter $inputFilter, - protected BookServiceInterface $bookService, - ) { + return $this->createResponse($request, $books); } - public function handle(ServerRequestInterface $request): ResponseInterface + public function post(ServerRequestInterface $request): ResponseInterface { - $this->inputFilter->setData((array) $request->getParsedBody()); - if (! $this->inputFilter->isValid()) { - throw BadRequestException::create( - detail: Message::VALIDATOR_INVALID_DATA, - additional: ['errors' => $this->inputFilter->getMessages()] - ); + $inputFilter = (new BookInputFilter())->setData($request->getParsedBody()); + if (! $inputFilter->isValid()) { + return $this->errorResponse($inputFilter->getMessages(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } - /** @var non-empty-array $data */ - $data = (array) $this->inputFilter->getValues(); + $book = $this->bookService->createBook($inputFilter->getValues()); - return $this->createdResponse($request, $this->bookService->saveBook($data)); + return $this->createResponse($request, $book); } } -``` - -In `src/Book/src` we now create the 2 PHP files: `RoutesDelegator.php` and `ConfigProvider.php`. - -`RoutesDelegator.php` contains all of our routes while `ConfigProvider` contains all the necessary configuration needed, so the above files work properly like dependency injection, aliases and so on. - -* `src/Book/src/ConfigProvider.php` - -```php - $this->getDependencies(), - MetadataMap::class => $this->getHalConfig(), - ]; - } - - private function getDependencies(): array - { - return [ - 'delegators' => [ - Application::class => [RoutesDelegator::class], - PostBookResourceHandler::class => [HandlerDelegatorFactory::class], - GetBookResourceHandler::class => [HandlerDelegatorFactory::class], - GetBookCollectionHandler::class => [HandlerDelegatorFactory::class], - ], - 'factories' => [ - PostBookResourceHandler::class => AttributedServiceFactory::class, - GetBookResourceHandler::class => AttributedServiceFactory::class, - GetBookCollectionHandler::class => AttributedServiceFactory::class, - BookService::class => AttributedServiceFactory::class, - ], - 'aliases' => [ - BookServiceInterface::class => BookService::class, - ], - ]; - } - private function getHalConfig(): array - { - return [ - AppConfigProvider::getResource(Book::class, 'book::view-book'), - AppConfigProvider::getCollection(BookCollection::class, 'book::list-books', 'books'), - ]; - } -} ``` +After we have the handler, we need to register some routes in the `RoutesDelegator`, the same we created when we registered the module. + * `src/Book/src/RoutesDelegator.php` ```php get( + '/books', + BookHandler::class, + 'books.list' + ); - /** @var RouteCollectorInterface $routeCollector */ - $routeCollector = $container->get(RouteCollectorInterface::class); + $app->get( + '/book/'.$uuid, + BookHandler::class, + 'book.show' + ); - $routeCollector->post('/book', PostBookResourceHandler::class, 'book::create-book'); - $routeCollector->get('/book/' . $uuid, GetBookResourceHandler::class, 'book::view-book'); - $routeCollector->get('/books', GetBookCollectionHandler::class, 'book::list-books'); + $app->post( + '/book', + BookHandler::class, + 'book.create' + ); - return $callback(); + return $app; } } ``` -In `src/Core/src/Book/src` we will create `ConfigProvider.php` where we configure Doctrine ORM. +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. -* `src/Core/src/Book/src/ConfigProvider.php`. +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. + +* `src/Book/src/ConfigProvider.php` ```php $this->getDependencies(), - 'doctrine' => $this->getDoctrineConfig(), + 'doctrine' => $this->getDoctrineConfig(), + MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class + ] + ], 'factories' => [ + BookHandler::class => AttributedServiceFactory::class, + BookService::class => AttributedServiceFactory::class, BookRepository::class => AttributedRepositoryFactory::class, ], + 'aliases' => [ + BookServiceInterface::class => BookService::class, + ], ]; } @@ -701,70 +689,58 @@ class ConfigProvider { return [ 'driver' => [ - 'orm_default' => [ + 'orm_default' => [ 'drivers' => [ - 'Core\Book\Entity' => 'BookEntities', + 'Api\Book\Entity' => 'BookEntities' ], ], - 'BookEntities' => [ + 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', - 'paths' => [__DIR__ . '/Entity'], + 'paths' => __DIR__ . '/Entity', ], ], ]; } -} -``` -### Registering the module - -* register the module config by adding `Api\Book\ConfigProvider::class` and `Core\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/"` and `"Core\\Book\\": "src/Core/src/Book/src/"`, in composer.json under the autoload.psr-4 key -* update Composer autoloader by running the command: + private function getHalConfig(): array + { + return [ + AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), + AppConfigProvider::getResource(Book::class, 'book.show') + ]; + } -```shell -composer dump-autoload +} ``` -That's it. The module is now registered. - -We need to configure access to the newly created endpoints. -Open `config/autoload/authorization.global.php` and append the below route names to the `UserRoleEnum::Guest->value` key: - -* `books::list-books` -* `book::view-book` -* `book::create-book` - -> Make sure you read and understand the rbac [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/). - ## Migrations We created the `Book` entity, but we didn't create the associated table for it. > You can check the mapping files by running: -```shell -php ./bin/doctrine orm:validate-schema +```shel +php bin/doctrine orm:validate-schema ``` Doctrine can handle the table creation, run the following command: ```shell -php ./vendor/bin/doctrine-migrations diff +vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' ``` -This will check for differences between your entities and database structure and create migration files if necessary, in `src/Core/src/App/src/Migration`. +This will check for differences between your entities and database structure and create migration files if necessary, in `data/doctrine/migrations`. To execute the migrations run: ```shell -php ./vendor/bin/doctrine-migrations migrate +vendor/bin/doctrine-migrations migrate ``` ## Checking endpoints -If we did everything as planned, we can call the `http://0.0.0.0:8080/book` endpoint and create a new book: +If we did everything as planned we can call the `http://0.0.0.0:8080/book` endpoint and create a new book: ```shell curl -X POST http://0.0.0.0:8080/book diff --git a/docs/book/v6/tutorials/email.md b/docs/book/v6/tutorials/email.md deleted file mode 100644 index fb9079fb..00000000 --- a/docs/book/v6/tutorials/email.md +++ /dev/null @@ -1,83 +0,0 @@ -# Email sending and content parsing - -In the previous version of Dotkernel API we have been using the `mezzio/mezzio-twigrenderer` package which added unnecessary complexity to the email sending in our API platform since APIs returns JSON data, not HTML. -Besides this, it used two services (`AdminService` and `UserService`) to send emails. -It was not necessarily wrong, but their job should be only to manage Admin/User accounts. - -In order to fix this those problems, we have come up with a lighter custom solution. -Now each project can prepare the bodies of the emails by using its preferred template renderer. -`Core/src/App/src/Service/MailService` is now decoupled by injecting the pre-rendered email body when calling its methods. - -Example from `Core/src/App/src/Service/MailService.php`: - -```php - $config - */ - #[Inject( - 'dot-mail.service.default', - 'dot-log.default_logger', - 'config', - )] - public function __construct( - protected \Dot\Mail\Service\MailService $mailService, - protected LoggerInterface $logger, - private readonly array $config, - ) { - } - - /** - * @throws MailException - */ - public function sendActivationMail(User $user, string $body): bool - { - if ($user->isActive()) { - return false; - } - - $this->mailService->getMessage()->addTo($user->getEmail(), $user->getName()); - $this->mailService->setSubject('Welcome to ' . $this->config['application']['name']); - $this->mailService->setBody($body); - - try { - return $this->mailService->send()->isValid(); - } catch (MailException | TransportExceptionInterface $exception) { - $this->logger->err($exception->getMessage()); - throw new MailException(sprintf(Message::MAIL_NOT_SENT_TO, $user->getEmail())); - } - } -} -``` - -Rending example from `src/User/src/Handler/PostUserResourceHandler.php`: - -```php -if ($user->isPending()) { - $this->mailService->sendActivationMail( - $user, - $this->renderer->render('user::activate', ['user' => $user]) - ); -} -``` - -In this case we are using the `phtml` template from `src/User/src/templates`. -It has a lighter format compared to `twig`. -It is then rendered before sending the activation email by our custom renderer from `src/App/src/Template/Rederer.php`. -The other applications that use the Core structure such as [Dotkernel Admin](https://docs.dotkernel.org/admin-documentation/) use `mezzio/mezzio-twigrenderer` for this purpose. diff --git a/mkdocs.yml b/mkdocs.yml index f2e19ef8..da7adbba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,13 +37,13 @@ nav: - "Exceptions": v6/core-features/exceptions.md - "Dependency Injection": v6/core-features/dependency-injection.md - "Error reporting": v6/core-features/error-reporting.md + - "Rendering and Sending emails": v6/core-features/rendering-and-sending-emails.md - Extended features: - "Core and App": v6/extended-features/core-and-app.md - "New Handler Structure": v6/extended-features/handler-structure.md - "Route Grouping": v6/extended-features/route-grouping.md - "Problem Details": v6/extended-features/problem-details.md - "Injectable Input Filters": v6/extended-features/injectable-input-filters.md - - "Email Sending and Parsing": v6/extended-features/email.md - Commands: - "Create admin account": v6/commands/create-admin-account.md - "Generate database migrations": v6/commands/generate-database-migrations.md From c642ea153a66c80967c1735be88d1fe717ff2617 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:31:51 +0300 Subject: [PATCH 14/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v6/tutorials/create-book-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index fd06d541..09fcb6ff 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -613,7 +613,7 @@ class RoutesDelegator $app->get( '/book/'.$uuid, - BookHandler::class, + BookCollection::class, 'book.show' ); From 6e34aa9f3a81205a92b0aebb976f248eb949ece6 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:33:00 +0300 Subject: [PATCH 15/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v5/tutorials/create-book-module.md | 2 +- docs/book/v6/tutorials/create-book-module.md | 2 +- 2 files 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 dcaed440..2655e3a4 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -654,7 +654,7 @@ class RoutesDelegator $app->get( '/book/' . $uuid, - BookHandler::class, + BookCollection::class, 'book.show' ); diff --git a/docs/book/v6/tutorials/create-book-module.md b/docs/book/v6/tutorials/create-book-module.md index 09fcb6ff..fd06d541 100644 --- a/docs/book/v6/tutorials/create-book-module.md +++ b/docs/book/v6/tutorials/create-book-module.md @@ -613,7 +613,7 @@ class RoutesDelegator $app->get( '/book/'.$uuid, - BookCollection::class, + BookHandler::class, 'book.show' ); From f3e17203c84751e66aba9a815fb00154f6ab5095 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:33:53 +0300 Subject: [PATCH 16/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v5/tutorials/create-book-module.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index 2655e3a4..3d40a945 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -559,6 +559,13 @@ class BookHandler extends AbstractHandler implements RequestHandlerInterface return $this->createResponse($request, $book); } + + public function getCollection(ServerRequestInterface $request): ResponseInterface + { + $books = $this->bookService->getRepository()->getBooks($request->getQueryParams()); + + return $this->createResponse($request, $books); + } public function post(ServerRequestInterface $request): ResponseInterface { From 88628e1520b12bd6c3207100657943cadd8b7684 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:34:11 +0300 Subject: [PATCH 17/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v5/tutorials/create-book-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v5/tutorials/create-book-module.md b/docs/book/v5/tutorials/create-book-module.md index 3d40a945..b6d36c90 100644 --- a/docs/book/v5/tutorials/create-book-module.md +++ b/docs/book/v5/tutorials/create-book-module.md @@ -559,7 +559,7 @@ class BookHandler extends AbstractHandler implements RequestHandlerInterface return $this->createResponse($request, $book); } - + public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->bookService->getRepository()->getBooks($request->getQueryParams()); From 3e8480a7860a067ff191df46d53e927d721f94d6 Mon Sep 17 00:00:00 2001 From: horea Date: Tue, 20 May 2025 20:35:37 +0300 Subject: [PATCH 18/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v6/core-features/rendering-and-sending-emails.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/book/v6/core-features/rendering-and-sending-emails.md b/docs/book/v6/core-features/rendering-and-sending-emails.md index 2e609e35..efee7a57 100644 --- a/docs/book/v6/core-features/rendering-and-sending-emails.md +++ b/docs/book/v6/core-features/rendering-and-sending-emails.md @@ -3,7 +3,6 @@ In the previous versions of Dotkernel API we have been composing email bodies using Twig from the mezzio/mezzio-twigrenderer package.\ In the current version of Dotkernel API, we introduced the core mail service Core/src/App/src/Service/MailService which is responsible for sending all emails. - Being a core service, MailService is used across all projects implementing the Core architecture.\ To compose and send an email, a solid implementation of TemplateRendererInterface was required to be injected into MailService, because each method rendered and parsed their respective templates in place before sending an email. This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, Twig had to be replaced with a lighter solution. From 96f916f0a98e040ee663a1360718c4f5a5353db2 Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 21 May 2025 10:46:26 +0300 Subject: [PATCH 19/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- .../rendering-and-sending-emails.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/book/v6/core-features/rendering-and-sending-emails.md b/docs/book/v6/core-features/rendering-and-sending-emails.md index efee7a57..69934437 100644 --- a/docs/book/v6/core-features/rendering-and-sending-emails.md +++ b/docs/book/v6/core-features/rendering-and-sending-emails.md @@ -1,17 +1,17 @@ # Rendering and sending emails -In the previous versions of Dotkernel API we have been composing email bodies using Twig from the mezzio/mezzio-twigrenderer package.\ -In the current version of Dotkernel API, we introduced the core mail service Core/src/App/src/Service/MailService which is responsible for sending all emails. +In the previous versions of Dotkernel API we have been composing email bodies using **Twig** from the `mezzio/mezzio-twigrenderer` package.\ +In the current version of Dotkernel API, we introduced the core mail service `Core/src/App/src/Service/MailService` which is responsible for sending all emails. -Being a core service, MailService is used across all projects implementing the Core architecture.\ -To compose and send an email, a solid implementation of TemplateRendererInterface was required to be injected into MailService, because each method rendered and parsed their respective templates in place before sending an email. -This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, Twig had to be replaced with a lighter solution. +Being a core service, `MailService` is used across all projects implementing the Core architecture.\ +To compose and send an email, a solid implementation of `TemplateRendererInterface` was required to be injected into `MailService`, because each method rendered and parsed their respective templates in place before sending an email. +This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, **Twig** had to be replaced with a lighter solution. -The solution is a custom Api\App\Template\Renderer implementing Api\App\Template\RendererInterface. -This is a lightweight renderer, aimed at rendering a combination of PHP and HTML files with phtml extension. +The solution is a custom [`Api\App\Template\Renderer`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/Renderer.php) implementing [`Api\App\Template\RendererInterface`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/RendererInterface.php).\ +This is a lightweight renderer, aimed at rendering a combination of **PHP** and **HTML** files with phtml extension. -With the new solution, MailService requires no implementation of any renderer because it no longer has to render templates internally.\ -Instead, an implementation of Api\App\Template\RendererInterface is first injected in the handler: +With the new solution, `MailService` requires no implementation of any renderer because it no longer has to render templates internally.\ +Instead, an implementation of `Api\App\Template\RendererInterface` is first injected in the handler: ```php class ExampleHandler extends AbstractHandler @@ -30,7 +30,7 @@ class ExampleHandler extends AbstractHandler Then, the handler calls the renderer and saves the rendered template in a variable: ```php -$body = $this->renderer->render('user::welcome', ['user' => $user]) +$body = $this->renderer->render('user::welcome', ['user' => $user]); ``` And finally, the handler calls the mail service with the composed $body being passed as a parameter to the method which sends the email: From a0b49b18fd260ce3c5143c2bd5b339bd586a73d0 Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 21 May 2025 10:46:48 +0300 Subject: [PATCH 20/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- docs/book/v6/core-features/rendering-and-sending-emails.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v6/core-features/rendering-and-sending-emails.md b/docs/book/v6/core-features/rendering-and-sending-emails.md index 69934437..219e9b33 100644 --- a/docs/book/v6/core-features/rendering-and-sending-emails.md +++ b/docs/book/v6/core-features/rendering-and-sending-emails.md @@ -8,7 +8,7 @@ To compose and send an email, a solid implementation of `TemplateRendererInterfa This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, **Twig** had to be replaced with a lighter solution. The solution is a custom [`Api\App\Template\Renderer`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/Renderer.php) implementing [`Api\App\Template\RendererInterface`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/RendererInterface.php).\ -This is a lightweight renderer, aimed at rendering a combination of **PHP** and **HTML** files with phtml extension. +This is a lightweight renderer, aimed at rendering a combination of **PHP** and **HTML** files with `phtml` extension. With the new solution, `MailService` requires no implementation of any renderer because it no longer has to render templates internally.\ Instead, an implementation of `Api\App\Template\RendererInterface` is first injected in the handler: From e6d0528b8f16ceb6790f73571a8a447b14586318 Mon Sep 17 00:00:00 2001 From: horea Date: Wed, 21 May 2025 12:47:17 +0300 Subject: [PATCH 21/21] Issue #104: how to send an email and parse the content Signed-off-by: horea --- .../core-features/rendering-and-sending-emails.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/book/v6/core-features/rendering-and-sending-emails.md b/docs/book/v6/core-features/rendering-and-sending-emails.md index 219e9b33..3ca12152 100644 --- a/docs/book/v6/core-features/rendering-and-sending-emails.md +++ b/docs/book/v6/core-features/rendering-and-sending-emails.md @@ -1,24 +1,24 @@ # Rendering and sending emails -In the previous versions of Dotkernel API we have been composing email bodies using **Twig** from the `mezzio/mezzio-twigrenderer` package.\ +In the previous versions of Dotkernel API we have been composing email bodies using **Twig** from the `mezzio/mezzio-twigrenderer` package. In the current version of Dotkernel API, we introduced the core mail service `Core/src/App/src/Service/MailService` which is responsible for sending all emails. -Being a core service, `MailService` is used across all projects implementing the Core architecture.\ +Being a core service, `MailService` is used across all projects implementing the Core architecture. To compose and send an email, a solid implementation of `TemplateRendererInterface` was required to be injected into `MailService`, because each method rendered and parsed their respective templates in place before sending an email. This is acceptable with other Dotkernel applications which in most cases return a rendered template, but being that Dotkernel API mostly returns JSON objects, rendered with a different renderer, **Twig** had to be replaced with a lighter solution. -The solution is a custom [`Api\App\Template\Renderer`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/Renderer.php) implementing [`Api\App\Template\RendererInterface`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/RendererInterface.php).\ +The solution is a custom [`Api\App\Template\Renderer`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/Renderer.php) implementing [`Api\App\Template\RendererInterface`](https://github.com/dotkernel/api/blob/6.0/src/App/src/Template/RendererInterface.php). This is a lightweight renderer, aimed at rendering a combination of **PHP** and **HTML** files with `phtml` extension. -With the new solution, `MailService` requires no implementation of any renderer because it no longer has to render templates internally.\ +With the new solution, `MailService` requires no implementation of any renderer because it no longer has to render templates internally. Instead, an implementation of `Api\App\Template\RendererInterface` is first injected in the handler: ```php class ExampleHandler extends AbstractHandler { #[Inject( - MailService::class, - RendererInterface::class, + MailService::class, + RendererInterface::class, )] public function __construct( protected MailService $mailService, @@ -41,4 +41,4 @@ And finally, the handler calls the mail service with the composed $body being pa $this->mailService->sendWelcomeMail($user, $body); ``` ->Other Dotkernel applications implementing the Core architecture do the same in the handlers, but keep using Twig as the template renderer. +> Other Dotkernel applications implementing the Core architecture do the same in the handlers, but keep using Twig as the template renderer.