# Implementing a book module in Dotkernel Admin using DotMaker

## Summary

This tutorial uses `dotkernel/dot-maker` to scaffold a complete `Book` module in Dotkernel Admin, from folder structure and file contents to migrations, authorization and route verification.

## Details

The `dotkernel/dot-maker` library can be used to programmatically generate project files and directories.
It can be added to your Admin installation by following the [official documentation](https://docs.dotkernel.org/dot-maker/).

## Folder and files structure

The below files structure is what we will have at the end of this tutorial and is just an example; you can have multiple components such as event listeners, wrappers, etc.

```markdown
.
└── src/
    ├── Book/
    │   ├── src/
    │   │   ├── Handler/
    │   │   │   ├── GetCreateBookFormHandler.php
    │   │   │   ├── GetDeleteBookFormHandler.php
    │   │   │   ├── GetEditBookFormHandler.php
    │   │   │   ├── GetListBookHandler.php
    │   │   │   ├── PostCreateBookHandler.php
    │   │   │   ├── PostDeleteBookHandler.php
    │   │   │   └── PostEditBookHandler.php
    │   │   ├── InputFilter/
    │   │   │   ├── Input/
    │   │   │   │   └── ConfirmDeleteBookInput.php
    │   │   │   ├── CreateBookInputFilter.php
    │   │   │   ├── DeleteBookInputFilter.php
    │   │   │   └── EditBookInputFilter.php
    │   │   ├── Service/
    │   │   │   ├── BookService.php
    │   │   │   └── BookServiceInterface.php
    │   │   ├── ConfigProvider.php
    │   │   └── RoutesDelegator.php
    │   └── templates/
    │       └── book/
    │           ├── create-book-form.html.twig
    │           ├── delete-book-form.html.twig
    │           ├── edit-book-form.html.twig
    │           └── list-book.html.twig
    └── Core/
        └── src/
            └── Book/
                └── src/
                    ├──Entity/
                    │   └──Book.php
                    ├──Repository/
                    │   └──BookRepository.php
                    └── ConfigProvider.php
```

* `src/Book/src/Handler/GetCreateBookFormHandler.php` – handler that reflects the GET action for the `CreateBookForm` class
* `src/Book/src/Handler/GetDeleteBookFormHandler.php` – handler that reflects the GET action for the `DeleteBookForm` class
* `src/Book/src/Handler/GetEditBookFormHandler.php` – handler that reflects the GET action for the `EditBookForm` class
* `src/Book/src/Handler/GetListBookHandler.php` – handler that reflects the GET action for a configurable list of `Book` entities
* `src/Book/src/Handler/PostCreateBookHandler.php` – handler that reflects the POST action for creating a `Book` entity
* `src/Book/src/Handler/PostDeleteBookHandler.php` – handler that reflects the POST action for deleting a `Book` entity
* `src/Book/src/Handler/PostEditBookHandler.php` – handler that reflects the POST action for editing a `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/InputFilter/EditBookInputFilter.php` – input filters and validators
* `src/Book/src/InputFilter/DeleteBookInputFilter.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/Book/templates/book/create-book-form.html.twig` – a Twig template for generating the view for the `CreateBookForm` class
* `src/Book/templates/book/delete-book-form.html.twig` – a Twig template for generating the view for the `DeleteBookForm` class
* `src/Book/templates/book/edit-book-form.html.twig` – a Twig template for generating the view for the `EditBookForm` class
* `src/Book/templates/book/list-book.html.twig` – a Twig template for generating the view for the list of `Book` entities
* `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

## File creation and contents

After successfully installing `dot-maker`, it can be used to generate the Book module.
Invoke `dot-maker` by executing `./vendor/bin/dot-maker` or via the optional script described in the documentation - `composer make`.
This will list all component types that can be created - for this tutorial, enter `module`:

```shell
./vendor/bin/dot-maker module
```

Type `book` when prompted to enter the module name.

Next you will be prompted to add the relevant components of a module, accepting `y(es)`, `n(o)` and `Enter` (defaults to `yes`):

> Note that `dot-maker` will automatically split the files into the described `Admin` and `Core` structure without a further input needed.

* `Entity and repository` (Y): will generate the `Book.php` entity and the associated `BookRepository.php`.
* `Service` and `service interface` (Y): will generate the `BookService` and the `BookServiceInterface`.
* `Command`, followed by `middleware`(N): not necessary for the module described in this tutorial.
* `Handler` (Y): this option is needed, and will further prompt you for the required actions.
    * `Allow listing Books?` (Y): this will generate the `GetListBookHandler.php` class and the `list-book.html.twig`.
    * `Allow viewing Books?` (N): not necessary for the module described in this tutorial.
    * `Allow creating Books?` (Y): will generate all files used for creating `Book` entities, as follows:
        * The form used for creation `CreateBookForm` as well as the input filter it uses `CreateBookInputFilter`
        * The handler that fetches the form `GetCreateBookFormHandler`
        * The handler for the POST action `PostCreateBookHandler`
        * The template file used for the form `create-book-form.html.twig`
    * `Allow deleting Books?` (Y): similar to the previous step, this step will generate multiple files:
        * The form used for creation `DeleteBookForm`, the input filter it uses `DeleteBookInputFilter` as well as a singular Input class it uses - `ConfirmDeleteBookInput`
        * The handler that fetches the form `GetDeleteBookFormHandler`
        * The handler for the POST action `PostDeleteBookHandler`
        * The template file used for the form `delete-book-form.html.twig`
    * `Allow editing Books?` (Y): as the previous two cases, multiple files are generated on this step as well:
        * The form used for creation `EditBookForm` and the input filter it uses `EditBookInputFilter`
        * The handler that fetches the form `GetEditBookFormHandler`
        * The handler for the POST action `PostEditBookHandler`
        * The template file used for the form `edit-book-form.html.twig`
* Following this step, `dot-maker` will automatically generate the `ConfigProvider.php` classes for both the `Admin` and `Core` namespaces,
as well as the `RoutesDelegator` class containing all the relevant routes.

You will then be instructed to:

* Register the `ConfigProvider` classes by adding `Admin\Book\ConfigProvider::class` and `Core\Book\ConfigProvider::class` to `config/config.php`
* Register the new `Book` namespace by adding `"Admin\\Book\\": "src/Book/src/"` and `"Core\\Book\\": "src/Core/src/Book/src/"` to `composer.json` under the `autoload.psr-4` key.
    * After registering the namespace, run the following command to regenerate the autoloaded files, as notified by `dot-maker`:

```shell
composer dump
```

* `dot-maker` will by default prompt you to generate the migrations for the new entity, but for this tutorial we will run this after updating the generated entity.

The next step is filling in the required logic for the proposed flow of this module.
While `dot-maker` does also include common logic in the relevant files, the tutorial adds custom functionality.
As such, the following section will go over the files that require changes.

* `src/Book/src/Handler/GetListBookHandler.php`

The overall class structure is fully generated, but for this tutorial you will need to send the `indentifier` key to the template, as shown below:

```php
return new HtmlResponse(
    $this->template->render('book::book-list', [
        'pagination' => $this->bookService->getBooks($request->getQueryParams()),
        'identifier' => SettingIdentifierEnum::IdentifierTableUserListSelectedColumns->value,
    ])
);
```

* `src/Core/src/App/src/Message.php`

The generated `PostCreateBookHandler`, `PostEditBookHandler` and `PostDeleteBookHandler` classes will by default make use of the `Message::BOOK_CREATED`, `Message::BOOK_UPDATED` and `Message::BOOK_DELETED` constants which you will have to manually add:

```php
public const BOOK_CREATED = 'Book created successfully.';
public const BOOK_UPDATED = 'Book updated successfully.';
public const BOOK_DELETED = 'Book deleted successfully.';
```

* `src/Core/src/Book/src/Entity/Book.php`

To keep things simple in this tutorial, our book will have three properties: `name`, `author` and `releaseDate`.
Add the three properties and their getters and setters, while making sure to update the generated constructor method.

```php
<?php

declare(strict_types=1);

namespace Core\Book\Entity;

use Core\App\Entity\AbstractEntity;
use Core\App\Entity\TimestampsTrait;
use Core\Book\Repository\BookRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: BookRepository::class)]
#[ORM\Table("book")]
#[ORM\HasLifecycleCallbacks]
class Book extends AbstractEntity
{
    use TimestampsTrait;

    #[ORM\Column(name: "name", type: "string", length: 100)]
    protected string $name;

    #[ORM\Column(name: "author", type: "string", length: 100)]
    protected string $author;

    #[ORM\Column(name: "releaseDate", type: "datetime_immutable")]
    protected DateTimeImmutable $releaseDate;

    public function __construct(string $name, string $author, DateTimeImmutable $releaseDate)
    {
        parent::__construct();

        $this->setName($name);
        $this->setAuthor($author);
        $this->setReleaseDate($releaseDate);
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getAuthor(): string
    {
        return $this->author;
    }

    public function setAuthor(string $author): self
    {
        $this->author = $author;

        return $this;
    }

    public function getReleaseDate(): DateTimeImmutable
    {
        return $this->releaseDate;
    }

    public function setReleaseDate(DateTimeImmutable $releaseDate): self
    {
        $this->releaseDate = $releaseDate;

        return $this;
    }

    public function getArrayCopy(): array
    {
        return [
            'uuid'        => $this->getUuid()->toString(),
            'name'        => $this->getName(),
            'author'      => $this->getAuthor(),
            'releaseDate' => $this->getReleaseDate(),
        ];
    }
}

```

The `BookService` class will require minor modifications for the `getBooks()` and `saveBook()` methods, to add the custom properties added in the previous step.
The class should look like the following after updating the methods.

* `src/Book/src/Service/BookService.php`

```php
<?php

declare(strict_types=1);

namespace Admin\Book\Service;

use Admin\App\Exception\NotFoundException;
use Core\App\Helper\Paginator;
use Core\App\Message;
use Core\Book\Entity\Book;
use Core\Book\Repository\BookRepository;
use DateTimeImmutable;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
use Dot\DependencyInjection\Attribute\Inject;

use function array_key_exists;
use function in_array;

class BookService implements BookServiceInterface
{
    #[Inject(
        BookRepository::class,
    )]
    public function __construct(
        protected BookRepository $bookRepository,
    ) {
    }

    public function getBookRepository(): BookRepository
    {
        return $this->bookRepository;
    }

    public function deleteBook(
        Book $book,
    ): void {
        $this->bookRepository->deleteResource($book);
    }

    /**
     * @param array<non-empty-string, mixed> $params
     */
    public function getBooks(
        array $params,
    ): array {
        $filters = $params['filters'] ?? [];
        $params  = Paginator::getParams($params, 'book.created');

       $sortableColumns = [
            'book.name',
            'book.author',
            'book.releaseDate',
            'book.created',
            'book.updated',
        ];
        if (! in_array($params['sort'], $sortableColumns, true)) {
            $params['sort'] = 'book.created';
        }

        $paginator = new DoctrinePaginator($this->bookRepository->getBooks($params, $filters)->getQuery());

        return Paginator::wrapper($paginator, $params, $filters);
    }

    /**
     * @param array<non-empty-string, mixed> $data
     * @throws \DateMalformedStringException
     */
    public function saveBook(
        array $data,
        ?Book $book = null,
    ): Book {
        if (! $book instanceof Book) {
            $book = new Book(
                $data['name'],
                $data['author'],
                new DateTimeImmutable($data['releaseDate'])
            );
        } else {
            if (array_key_exists('name', $data) && $data['name'] !== null) {
                $book->setName($data['name']);
            }

            if (array_key_exists('author', $data) && $data['author'] !== null) {
                $book->setAuthor($data['author']);
            }

            if (array_key_exists('releaseDate', $data) && $data['releaseDate'] !== null) {
                $book->setReleaseDate(new DateTimeImmutable($data['releaseDate']));
            }
        }

        $this->bookRepository->saveResource($book);

        return $book;
    }

    /**
     * @throws NotFoundException
     */
    public function findBook(
        string $uuid,
    ): Book {
        $book = $this->bookRepository->find($uuid);
        if (! $book instanceof Book) {
            throw new NotFoundException(Message::resourceNotFound('Book'));
        }

        return $book;
    }
}

```

When creating a book, we will need some validators, so we will create a form and the input filter that will be used to validate the data received in the request.

* `src/Book/src/Form/CreateBookForm.php`

The default `Csrf` and `Submit` Inputs will be automatically added to the `CreateBookForm.php` class that `dot-maker` will create for you.
For this tutorial, you will have to add the custom inputs, by copying the following code in the `init` function of `CreateBookForm`:

```php
$this->add(
    (new Text('name'))
        ->setLabel('Name')
        ->setAttribute('required', true)
)->add(
    (new Text('author'))
        ->setLabel('Author')
        ->setAttribute('required', true)
)->add(
    (new Date('releaseDate'))
        ->setLabel('Release Date')
        ->setAttribute('required', true)
);
```

* `src/Book/src/Form/EditBookForm.php`

A similar sequence is used for the `init` function of `EditBookForm`, with the `required` attributes removed, as leaving the inputs empty is allowed for keeping the original data:

```php
$this->add(
    (new Text('name'))
        ->setLabel('Name')
)->add(
    (new Text('author'))
        ->setLabel('Author')
)->add(
    (new Date('releaseDate'))
        ->setLabel('Release Date')
);
```

By creating a `module` with `dot-maker`, separate inputs will not be created.
However, you can still generate them as using these steps:

* Run the following to start adding `Input` classes:

```shell
./vendor/bin/dot-maker input
```

* When prompted, enter the names `Author`, `Name` and `ReleaseDate` one by one to generate the classes.
* The resulting `AuthorInput.php`, `NameInput.php` and `ReleaseDateInput.php` classes require no further changes for the tutorial use case.

The module creation process has generated the parent input filters `CreateBookInputFilter.php` and `EditBookInputFilter.php` containing only the default `CsrfInput`.
Now we add all the inputs together in the parent input filters' `init` functions, as below:

* `src/Book/src/InputFilter/CreateBookInputFilter.php` and ``src/Book/src/InputFilter/EditBookInputFilter.php``

```php
$this->add(new NameInput('name'))
    ->add(new AuthorInput('author'))
    ->add(new ReleaseDateInput('releaseDate'));
```

We create separate `Input` files to demonstrate their reusability and obtain a clean `InputFilter`s, but you could have all the inputs created directly in the `InputFilter` like this:

> Note that `dot-maker` will not generate inputs in the `init` method, so the following are to be added by hand before the default `CsrfInput`, **if** going for this approach.

`CreateBookInputFilter`

```php
$nameInput = new Input('name');
$nameInput->setRequired(true);

$nameInput->getFilterChain()
    ->attachByName(StringTrim::class)
    ->attachByName(StripTags::class);

$nameInput->getValidatorChain()
    ->attachByName(NotEmpty::class, [
        'message' => Message::VALIDATOR_REQUIRED_FIELD,
    ], true);

$this->add($nameInput);

$authorInput = new Input('author');
$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('releaseDate');
$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);
```

`EditBookInputFilter`

```php
$nameInput = new Input('name');
$nameInput->setRequired(false);

$nameInput->getFilterChain()
    ->attachByName(StringTrim::class)
    ->attachByName(StripTags::class);

$nameInput->getValidatorChain()
    ->attachByName(NotEmpty::class, [
        'message' => Message::VALIDATOR_REQUIRED_FIELD,
    ], true);

$this->add($nameInput);

$authorInput = new Input('author');
$authorInput->setRequired(false);

$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('releaseDate');
$releaseDateInput->setRequired(false);

$releaseDateInput->getFilterChain()
    ->attachByName(StringTrim::class)
    ->attachByName(StripTags::class);

$releaseDateInput->getValidatorChain()
    ->attachByName(NotEmpty::class, [
        'message' => Message::VALIDATOR_REQUIRED_FIELD,
    ], true);

$this->add($releaseDateInput);
```

* `src/App/assets/js/components/_book.js`

As the listing pages make use of JavaScript, you will need to manually create your module specific `_book.js` file and register it in `webpack.config.js` for building.

You may copy this sample `_book.js` file to the `src/App/assets/js/components/` directory:

```js
$(document).ready(() => {
    const request = async(url, options = {}) => {
        try {
            const response = await fetch(url, options);
            const body     = await response.text();
            if (! response.ok) {
                throw {
                    data: body,
                }
            }
            return body;
        } catch (error) {
            throw {
                data: error.data,
            }
        }
    }

    $("#add-book-modal").on('show.bs.modal', function () {
        const modal = $(this);
        request(modal.data('add-url'), {
            method: 'GET'
        }).then(data => {
            modal.find('.modal-dialog').html(data);
        }).catch(error => {
            console.error(error);
            location.reload();
        });
    }).on('hidden.bs.modal', function () {
        const modal = $(this);
        modal.find('.modal-dialog').find('.modal-body').html('Loading...');
    });

    $("#edit-book-modal").on('show.bs.modal', function () {
        const selectedElement = $('.ui-checkbox:checked');
        if (selectedElement.length !== 1) {
            return;
        }

        const modal = $(this);
        request(selectedElement.data('edit-url'), {
            method: 'GET'
        }).then(data => {
            modal.find('.modal-dialog').html(data);
        }).catch(error => {
            console.error(error);
            location.reload();
        });
    }).on('hidden.bs.modal', function () {
        const modal = $(this);
        modal.find('.modal-dialog').find('.modal-body').html('Loading...');
    });

    $("#delete-book-modal").on('show.bs.modal', function () {
        const selectedElement = $('.ui-checkbox:checked');
        if (selectedElement.length !== 1) {
            return;
        }

        const modal = $(this);
        request(selectedElement.data('delete-url'), {
            method: 'GET'
        }).then(data => {
            modal.find('.modal-dialog').html(data);
        }).catch(error => {
            console.error(error);
            location.reload();
        });
    }).on('hidden.bs.modal', function () {
        const modal = $(this);
        modal.find('.modal-dialog').find('.modal-body').html('Loading...');
    });

    $(document).on("submit", "#book-form", (event) => {
        event.preventDefault();

        const form = event.target;
        if (! form.checkValidity()) {
            event.stopPropagation();
            form.classList.add('was-validated');
            return;
        }

        const modal = $(form.closest('.modal'));
        request(form.getAttribute('action'), {
            method: 'POST',
            body: new FormData(form),
        }).then(() => {
            location.reload();
        }).catch(error => {
            modal.find('.modal-dialog').html(error.data);
        });
    });

    $(document).on("submit", "#delete-book-form", (event) => {
        event.preventDefault();

        const form = event.target;
        if (! form.checkValidity()) {
            event.stopPropagation();
            form.classList.add('was-validated');
            return;
        }

        const modal = $(form.closest('.modal'));
        request(form.getAttribute('action'), {
            method: 'POST',
            body: new FormData(form),
        }).then(() => {
            location.reload();
        }).catch(error => {
            modal.find('.modal-dialog').html(error.data);
        });
    });
});
```

Next you have to register the file in the `entries` array of `webpack.config.js` by adding the following key:

```js
book: [
    './App/assets/js/components/_book.js'
]
```

To make use of the newly added scripts, make sure to build your assets by running the command:

```shell
npm run prod
```

* `src/Book/templates/book/*`

The next step is creating the page structures in the `.twig` files `dot-maker` automatically generated for you.

For this tutorial you may copy the following default page layout in the `list-book.html.twig`:

```html
{% from '@partial/macros.html.twig' import sortableColumn %}

{% extends '@layout/default.html.twig' %}

{% block title %}Manage books{% endblock %}

{% block content %}
<div class="container-fluid">
    <h4 class="c-grey-900 mT-10 mB-30">Manage books</h4>
    <div class="row">
        <div class="col-md-12">
            <div class="bgc-white bd bdrs-3 pL-10 pR-20 pT-20 pB-3 mB-20">
                <form class="row g-3" method="get" action="{{ path('book::list-book') }}">
                    <input type="hidden" name="offset" value="0" />
                    <input type="hidden" name="limit" value="{{ pagination.limit }}" />
                    <input type="hidden" name="sort" value="{{ pagination.sort }}" />
                    <input type="hidden" name="order" value="{{ pagination.dir }}" />
                    <div class="col-sm-auto btn-group-sm">
                        <button type="button" class="btn btn-default btn-sm" id="btn-add-resource" data-bs-toggle="modal" data-bs-target="#add-book-modal">
                            <i class="fa fa-plus"></i>
                        </button>
                        <button type="button" class="btn btn-default btn-sm" id="btn-edit-resource" data-bs-toggle="modal" data-bs-target="#edit-book-modal" disabled>
                            <i class="fa fa-pencil"></i>
                        </button>
                        <button type="button" class="btn btn-default btn-sm" id="btn-delete-resource" data-bs-toggle="modal" data-bs-target="#delete-book-modal" disabled>
                            <i class="fa fa-trash-o"></i>
                        </button>
                    </div>
                    <div class="col-sm-auto ms-auto">
                        <div class="dropdown" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-custom-class="custom-tooltip" data-bs-title="Toggle columns">
                            <button class="btn btn-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                                <i class="fa fa-columns"></i>
                            </button>
                            <ul class="dropdown-menu" id="column-selector"></ul>
                        </div>
                    </div>
                </form>
            </div>
        </div>
        <div class="col-md-12">
            <div class="table-responsive">
                <table id="book-table" class="table table-bordered table-hover table-striped table-light" style="display: none;">
                    <thead>
                    <tr>
                        <th class="column-book-uuid"></th>
                        <th class="column-book-name">
                            {{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.name', 'Name') }}
                        </th>
                        <th class="column-book-author">
                            {{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.author', 'Author') }}
                        </th>
                        <th class="column-book-release-date">
                            {{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.release-date', 'Release Date') }}
                        </th>
                        <th class="column-book-created">
                            {{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.created', 'Created') }}
                        </th>
                        <th class="column-book-updated">
                            {{ sortableColumn('book::list-book', {}, pagination.queryParams, 'book.updated', 'Updated') }}
                        </th>
                    </tr>
                    </thead>
                    <tbody>
                    {% for book in pagination.items %}
                    <tr class="table-row">
                        <td class="column-book-uuid" style="width: 1vw;">
                            <label>
                                <input type="checkbox"
                                       class="checkbox ui-checkbox"
                                       value="{{ book.uuid }}"
                                       data-edit-url="{{ path('book::edit-book', {uuid: book.uuid}) }}"
                                       data-delete-url="{{ path('book::delete-book', {uuid: book.uuid}) }}"
                                >
                            </label>
                        </td>
                        <td class="column-book-name">{{ book.name }}</td>
                        <td class="column-book-author">{{ book.author }}</td>
                        <td class="column-book-release-date">{{ book.releaseDate|date('Y-m-d') }}</td>
                        <td class="column-book-created">{{ book.getCreated()|date('Y-m-d H:i:s') }}</td>
                        <td class="column-book-updated">{{ book.getUpdated() is not null ? book.getUpdated()|date('Y-m-d H:i:s') : '' }}</td>
                    </tr>
                    {% endfor %}
                    </tbody>
                </table>
                {% if pagination.isOutOfBounds %}
                <div class="alert alert-warning text-center text-black fw-bold" role="alert">
                    Out of bounds! Return to
                    <a href="{{ path('book::list-book', {}, pagination.queryParams|merge({offset: pagination.lastOffset})) }}">page {{ pagination.lastPage }}</a>
                </div>
                {% endif %}
            </div>
        </div>
        <div class="col-md-12">
            <div class="bgc-white bd bdrs-3 p-20 mB-20">
                {{ include('@partial/pagination.html.twig', {pagination: pagination, path: 'book::list-book'}, false) }}
            </div>
        </div>
    </div>

    <div class="modal fade" id="add-book-modal" tabindex="-1" aria-labelledby="add-book-modal-content" aria-hidden="true" data-add-url="{{ path('book::create-book') }}">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="add-book-modal-content">Create book</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">Loading...</div>
            </div>
        </div>
    </div>

    <div class="modal fade" id="edit-book-modal" tabindex="-1" aria-labelledby="edit-book-modal-content" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="edit-book-modal-content">Edit book</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">Loading...</div>
            </div>
        </div>
    </div>

    <div class="modal fade" id="delete-book-modal" tabindex="-1" aria-labelledby="delete-book-modal-content" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="delete-book-modal-content">Delete book</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">Loading...</div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block javascript %}
{{ parent() }}
<script>
    const tableId = '#book-table';
    const storeSettingsUrl = '{{ path('setting::store-setting', {identifier: identifier}) }}';
    const getSettingsUrl = '{{ path('setting::view-setting', {identifier: identifier}) }}';
</script>
<script src="{{ asset('js/table_settings.js') }}" defer></script>
<script src="{{ asset('js/book.js') }}" defer></script>
{% endblock %}
```

To add books, a modal must be generated based on the `CreateBookForm.php` class.
You may copy the following structure in `create-book-form.html.twig`:

```html
{% from '@partial/macros.html.twig' import inputElement, submitElement %}

<div class="modal-content">
  <div class="modal-header">
    <h5 class="modal-title" id="add-book-modal-content">Create book</h5>
    <button type="button" class="btn-close btn-sm" data-bs-dismiss="modal" aria-label="Close"></button>
  </div>
  <div class="modal-body">
    {{ form().openTag(form)|raw }}
    {% set fieldsets = form.getFieldsets() %}

    {{ inputElement(form.get('name')) }}
    {{ inputElement(form.get('author')) }}
    {{ inputElement(form.get('releaseDate')) }}

    {{ inputElement(form.get('createBookCsrf')) }}
    {{ submitElement(form.get('submit')) }}
    {{ form().closeTag()|raw }}

    {% if messages is defined and messages is iterable %}
    {% for type, message in messages %}
    <div class="mt-3 alert alert-{% if type == 'success' %}success{% elseif type == 'warning' %}warning{% else %}danger{% endif %}" role="alert">{{ message }}</div>
    {% endfor %}
    {% endif %}
  </div>
</div>
```

For the "edit" action, use the following modal in the `edit-book-form.html.twig`:

```html
{% from '@partial/macros.html.twig' import inputElement, submitElement %}

<div class="modal-content">
    <div class="modal-header">
        <h5 class="modal-title" id="edit-book-modal-content">Edit book</h5>
        <button type="button" class="btn-close btn-sm" data-bs-dismiss="modal" aria-label="Close"></button>
    </div>
    <div class="modal-body">
        {{ form().openTag(form)|raw }}
        {% set fieldsets = form.getFieldsets() %}

        {{ inputElement(form.get('name')) }}
        {{ inputElement(form.get('author')) }}
        {{ inputElement(form.get('releaseDate')) }}

        {{ inputElement(form.get('editBookCsrf')) }}
        {{ submitElement(form.get('submit')) }}
        {{ form().closeTag()|raw }}

        {% if messages is defined and messages is iterable %}
            {% for type, message in messages %}
                <div class="mt-3 alert alert-{% if type == 'success' %}success{% elseif type == 'warning' %}warning{% else %}danger{% endif %}" role="alert">{{ message }}</div>
            {% endfor %}
        {% endif %}
    </div>
</div>
```

Add the following structure to the `delete-book-form.html.twig` file:

```html
<div class="modal-content">
    <div class="modal-header">
        <h5 class="modal-title" id="delete-book-modal-content">Delete book</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
    </div>
    <div class="modal-body">
        {{ form().openTag(form)|raw }}
        <div class="row">book
            <div class="col-md-12">
                <p>Are you sure you want to delete the following book: <span id="book" class="fw-bold">{{ book.name }} by {{ book.author }}</span> ?</p>
                <div class="form-check">
                    {{ formElement(form.get('confirmation')) }}
                    <label class="form-check-label" for="confirmation">Yes, I want to delete <span class="fw-bold">{{ book.name }} by {{ book.author }}</span></label>
                </div>
                <div class="d-flex justify-content-end">
                    {{ formElement(form.get('submit')) }}
                    {{ formElement(form.get('deleteBookCsrf')) }}
                </div>
            </div>
        </div>
        {{ form().closeTag()|raw }}
    </div>
</div>
```

* `/config/autoload/navigation.global.php`

Lastly, link the new module to the admin side-menu by adding the following array to `navigation.global.php`, under the `dot_navigation.containers.main_menu.options.items` key:

```php
[
    'options' => [
        'label' => 'Book',
        'route' => [
            'route_name' => 'book::list-book',
        ],
        'icon'  => 'c-blue-500 fa fa-book',
    ],
],
```

## Migrations

All changes are done, so at this point the migration file can be generated to create the associated table for the `Book` entity.

> You can check the mapping files by running:

```shell
php ./bin/doctrine orm:validate-schema
```

> Generate the migration files by running:

```shell
php ./vendor/bin/doctrine-migrations diff
```

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:

```shell
php ./vendor/bin/doctrine-migrations migrate
```

## Update the authorization file

We need to configure access to the newly created endpoints.
Open `config/autoload/authorization-guards.global.php` and append the below routes to the `dot_authorization.guards_provider.options.guards[0].options.rules` key:

```php
'book::create-book-form' => ['authenticated'],
'book::create-book'      => ['authenticated'],
'book::list-book'        => ['authenticated'],
```

> Make sure you read and understand the `rbac` [documentation](https://docs.dotkernel.org/dot-rbac-guard/v4/configuration/).

## Checking routes

The module should now be accessible via the `Book` section of the `Admin` main menu, linking to the newly created `/list-book` route.

New book entities can be added via the new "Create book" modal accessible from the `+` button on the management page.

## FAQ

**Q: What tool generates the files for the book module?**

A: The `dotkernel/dot-maker` library, which programmatically generates project files and directories.

**Q: How do I create the database table for the new `Book` entity?**

A: Generate a migration by running `php ./vendor/bin/doctrine-migrations diff`, then apply it with `php ./vendor/bin/doctrine-migrations migrate`.

**Q: How do I make the new endpoints accessible?**

A: Configure access to them by appending the new routes to the `guards.options.rules` key in `config/autoload/authorization-guards.global.php`.

**Q: How do I verify the module works after implementing it?**

A: Check that the `Book` section appears in the `Admin` main menu, linking to `/list-book`, and that new book entities can be added via the "Create book" modal.

Once selected with the checkbox, existing entries can be edited via the `-` button , or deleted via the "trash" icon.
