diff --git a/docs/book/v1/chapter-1.md b/docs/book/v1/chapter-1.md index 66dcb07..0de7a2d 100644 --- a/docs/book/v1/chapter-1.md +++ b/docs/book/v1/chapter-1.md @@ -165,8 +165,6 @@ private function getDoctrineConfig(): array Now that everything has been configured we only need to do one last thing, to create an executable for the Doctrine CLI. In our case we will create a `doctrine` file inside the application's `bin` directory: -![doctrine](images/chapter-1/doctrine.png) - ```php #!/usr/bin/env php + */ +public function getTitles(): array +{ + $qb = $this->getQueryBuilder() + ->select('book.id, book.title') + ->from(Book::class, 'book'); + + return $qb->getQuery()->getResult(); +} + +/** + * @return array + */ +public function getAuthors(): array +{ + $qb = $this->getQueryBuilder() + ->select('book.id, book.author') + ->from(Book::class, 'book'); + + return $qb->getQuery()->getResult(); +} +``` + +The functions listed above will **retrieve** the `title`/`author` along with the `ID` of the Book database row for reference. + +## Setting up the Route + +We will now work on setting up the route from our new module through which we will display our data. +For this tutorial our route will be `{{base_url}}/books`. + +In order to register this path we need to create a `RoutesDelegator.php` file in `src/Book/src`. + +```php +get('/books', [GetBooksHandler::class], 'books::list'); + + return $app; + } +} +``` + +## Handling the Request + +Now that the route exists, we need to create the class that will handle our request. +Create a new directory `src/Book/src/Handler` and create the file `GetBooksHandler.php`. + +```php +bookRepository->getTitles(); + $authors = $this->bookRepository->getAuthors(); + + return new HtmlResponse( + $this->template->render('page::books', [ + 'titles' => $titles, + 'authors' => $authors, + ]) + ); + } +} +``` + +The custom functions that we have created in the `BookRepository.php` retrieve our desired values and pass it to the `books` HTML template that is being rendered. +We will crate the HTML file in the next part of this chapter. + +## Twig Template for Book Listing + +The `books` template is currently missing, we now need to create it under the following path: `src/Book/src/templates/page/books.html.twig`. + +```html +{% extends '@layout/default.html.twig' %} + +{% block title %}Books{% endblock %} + +{% block content %} +
+

Book Titles

+ + + + + + + + + {% for title in titles %} + + + + + {% endfor %} + +
Book IDTitle
{{ title.id }}{{ title.title }}
+ +

Book Authors

+ + + + + + + + + {% for author in authors %} + + + + + {% endfor %} + +
Book IDAuthor
{{ author.id }}{{ author.author }}
+
+{% endblock %} +``` + +## Registering our Changes + +Now that our route, handler, repository methods and view are ready, we need to **register them so the application can use them**. +In `src/Book/src/ConfigProvider.php` we need to add the following changes: + +![book-config](images/chapter-3/book-config.png) + +```php +public function __invoke(): array +{ + return [ + 'dependencies' => $this->getDependencies(), + 'doctrine' => $this->getDoctrineConfig(), + 'templates' => $this->getTemplates(), + ]; +} + +private function getDependencies(): array +{ + return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class, + ], + ], + 'factories' => [ + GetBooksHandler::class => BookHandlerFactory::class, + BookRepository::class => BookRepositoryFactory::class, + ], + ]; +} + +/** + * @return array{ + * paths: array{page: array{literal-string&non-falsy-string}} + * } + */ +private function getTemplates(): array +{ + return [ + 'paths' => [ + 'page' => [__DIR__ . '/../templates/page'], + ], + ]; +} +``` + +In Dotkernel (and Mezzio/Laminas in general), most classes cannot be created directly with new ClassName() because they require dependencies (such as the template engine or a repository). +To provide those dependencies, we create Factory classes. + +A factory’s only responsibility is to build an object with everything it needs. +For that, we have created two factories - one for our Handler and one for our Repository. + +`src/App/src/Factory/BookHandlerFactory.php`: + +```php +get(BookRepository::class); + $template = $container->get(TemplateRendererInterface::class); + + assert($repository instanceof BookRepository); + assert($template instanceof TemplateRendererInterface); + + return new GetBooksHandler($template, $repository); + } +} +``` + +`src/App/src/Factory/BookRepositoryFactory.php`: + +```php +get(EntityManager::class); + + $repository = $entityManager->getRepository(Book::class); + assert($repository instanceof BookRepository); + + return $repository; + } +} +``` + +All the changes are now registered. + +## Populating the Table + +In order to have values for us to display, we now have to populate our `books` table from our database with some values. +There are two options in this case: + +- Use Doctrine Fixtures +- Add entries manually from the Database Client. + +**We recommend using fixtures**, but if you choose the alternative approach, feel free to skip directly to the [final](#listing-result) section of this chapter. + +## Doctrine Fixtures + +Doctrine Fixtures are tools that let us preload sample data into the database. +They make it easy to populate entities automatically, so we can test and develop our application without adding data manually. + +Our first step is to add our required Doctrine packages to our `composer.json` file: + +![composer-fixtures](images/chapter-3/composer-fixtures.png) + +```text +"doctrine/data-fixtures": "^2.2", +"doctrine/doctrine-fixtures-bundle": "^4.3", +``` + +After importing our packages, we need to register the path for our Fixture files in `src/App/src/ConfigProvider.php`. + +![config-fixtures](images/chapter-3/config-fixtures.png) + +```php +'fixtures' => getcwd() . '/src/App/src/Fixture', +``` + +We now need to create our Fixture Loader in `src/App/src/Fixture/BookLoader.php`: + +```php +setTitle('A Game of Thrones'); + $book1->setAuthor('George Martin'); + $manager->persist($book1); + + $book2 = new Book(); + $book2->setTitle('The Lord of the Rings'); + $book2->setAuthor('J.R.R. Tolkien'); + $manager->persist($book2); + + $book3 = new Book(); + $book3->setTitle('Dune'); + $book3->setAuthor('Frank Herbert'); + $manager->persist($book3); + + $manager->flush(); + } +} +``` + +For our last step we only need to create an executable for our fixtures in `bin/doctrine-fixtures`: + +```php +#!/usr/bin/env php +get(EntityManager::class); +$config = $container->get('config'); + +// Get fixtures directory from config +$fixturesPath = $config['doctrine']['fixtures']; + +if (! is_dir($fixturesPath)) { + echo "Fixtures directory not found: {$fixturesPath}\n"; + exit(1); +} + +// Load fixtures +$loader = new Loader(); +$loader->loadFromDirectory($fixturesPath); + +// Execute fixtures +$purger = new ORMPurger(); +$executor = new ORMExecutor($entityManager, $purger); + +echo "Loading fixtures from: {$fixturesPath}\n"; + +$executor->execute($loader->getFixtures()); + +echo "Fixtures loaded successfully!\n"; +``` + +Now we can run: + +```shell +php bin/doctrine-fixtures +``` + +The output should be similar to this: + +```terminaloutput +Loading fixtures from: {{ your_path }} +Fixtures loaded successfully! +``` + +## Listing Result + +In order to see your changes either go to `{{base_url}}/books` in your browser or you can add for example a button in `src/App/templates/layout/default.html`. + +![button](images/chapter-3/button.png) + +```html +Books +``` + +We should now see a page similar to the following: + +![tables](images/chapter-3/tables.png) + +### Summary + +- [Modified] src/Book/src/Repository/BookRepository.php +- [+] src/Book/src/RoutesDelegator.php +- [+] src/Book/src/Handler/GetBooksHandler.php +- [+] src/Book/templates/page/books.html.twig +- [Modified] src/Book/src/ConfigProvider.php +- [+] src/App/src/Factory/BookHandlerFactory.php +- [+] src/App/src/Factory/BookRepositoryFactory.php + +Fixtures (Optional): + +- [Modified] composer.json +- [Modified] src/App/src/ConfigProvider.php +- [+] src/App/src/Fixture/BookLoader.php +- [+] bin/doctrine-fixtures +- [Modified] src/App/templates/layout/default.html.twig diff --git a/docs/book/v1/images/chapter-3/book-repository.png b/docs/book/v1/images/chapter-3/book-repository.png new file mode 100644 index 0000000..44cb9e6 Binary files /dev/null and b/docs/book/v1/images/chapter-3/book-repository.png differ diff --git a/docs/book/v1/images/chapter-3/button.png b/docs/book/v1/images/chapter-3/button.png new file mode 100644 index 0000000..2c65600 Binary files /dev/null and b/docs/book/v1/images/chapter-3/button.png differ diff --git a/docs/book/v1/images/chapter-3/composer-fixtures.png b/docs/book/v1/images/chapter-3/composer-fixtures.png new file mode 100644 index 0000000..9deac8b Binary files /dev/null and b/docs/book/v1/images/chapter-3/composer-fixtures.png differ diff --git a/docs/book/v1/images/chapter-3/config-fixtures.png b/docs/book/v1/images/chapter-3/config-fixtures.png new file mode 100644 index 0000000..4ba253e Binary files /dev/null and b/docs/book/v1/images/chapter-3/config-fixtures.png differ diff --git a/docs/book/v1/images/chapter-3/tables.png b/docs/book/v1/images/chapter-3/tables.png new file mode 100644 index 0000000..7636535 Binary files /dev/null and b/docs/book/v1/images/chapter-3/tables.png differ diff --git a/mkdocs.yml b/mkdocs.yml index cd61629..577c75b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - Introduction: v1/introduction.md - 1. Installing Doctrine: v1/chapter-1.md - 2. Entities and Migrations: v1/chapter-2.md + - 3. Fixtures and Custom Listing: v1/chapter-3.md site_name: Dotkernel 101 site_description: "Beginner tutorial for using Dotkernel" repo_url: "https://github.com/dotkernel/tutorial-101"