From 8885603ded7525f0e86f245c358b48d77e895a1c Mon Sep 17 00:00:00 2001 From: MarioRadu Date: Tue, 30 Apr 2024 16:21:55 +0300 Subject: [PATCH] dk3 api documentation WIP --- docs/book/v4/introduction.md | 0 docs/book/v4/introduction/features.md | 0 docs/book/v4/introduction/introduction.md | 106 +++ docs/book/v4/tutorials.md | 0 docs/book/v4/tutorials/create-book-module.md | 636 ++++++++++++++++++ .../tutorials/create-hello-world-handler.md | 0 mkdocs.yml | 24 + 7 files changed, 766 insertions(+) delete mode 100644 docs/book/v4/introduction.md delete mode 100644 docs/book/v4/introduction/features.md create mode 100644 docs/book/v4/introduction/introduction.md delete mode 100644 docs/book/v4/tutorials.md create mode 100644 docs/book/v4/tutorials/create-book-module.md delete mode 100644 docs/book/v4/tutorials/create-hello-world-handler.md create mode 100644 mkdocs.yml diff --git a/docs/book/v4/introduction.md b/docs/book/v4/introduction.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/book/v4/introduction/features.md b/docs/book/v4/introduction/features.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/book/v4/introduction/introduction.md b/docs/book/v4/introduction/introduction.md new file mode 100644 index 0000000..d233b8d --- /dev/null +++ b/docs/book/v4/introduction/introduction.md @@ -0,0 +1,106 @@ +### Introduction + +Based on Enrico Zimuel’s Zend Expressive API – Skeleton example, DotKernel API runs on Laminas and Mezzio components and implements standards like PSR-3, PSR-4, PSR-7, PSR-11 and PSR-15. + +Here is a list of the core components: + +* Middleware Microframework (mezzio/mezzio) +* Error Handler (dotkernel/dot-errorhandler) +* Problem Details (mezzio-problem-details) +* CORS (mezzio-cors) +* Routing (mezzio/mezzio-fastroute) +* Authentication (mezzio-authentication) +* Authorization (mezzio-authorization) +* Config Aggregator (laminas/laminas-config-aggregator) +* Container (roave/psr-container-doctrine) +* Implicit Head, Options and Method Not Allowed +* Annotations (dotkernel/dot-annotated-services) +* Input Filter (laminas/laminas-inputfilter) +* Doctrine +* Hydrator (laminas/laminas-hydrator) +* Paginator (laminas/laminas-paginator) +* HAL (mezzio-hal) +* CLI (dotkernel/dot-cli) +* TwigRenderer (mezzio/mezzio-twigrenderer) +* Fixtures (dotkernel/dot-data-fixtures) +* UUID (ramsey/uuid-doctrine) + +## Doctrine 2 ORM + +For the persistence in a relational database management system we chose Doctrine ORM (object-relational mapper) . + +The benefit of Doctrine for the programmer is the ability to focus on the object-oriented business logic and worry about persistence only as a secondary priority. + +## Documentation + +Our documentation is Postman based. We use the following files in which we store information about every available endpoint ready to be tested: + + documentation/DotKernel_API.postman_collection.json + documentation/DotKernel_API.postman_environment.json + +## Hypertext Application Language + +For our API payloads ( a value object for describing the API resource, its relational links and any embedded/child resources related to it ) we chose mezzio-hal. + +## CORS + +By using `MezzioCorsMiddlewareCorsMiddleware`, the CORS preflight will be recognized and the middleware will start to detect the proper CORS configuration. The Router is used to detect every allowed request method by executing a route match with all possible request methods. Therefore, for every preflight request, there is at least one Router request. + + +## OAuth 2 + +OAuth 2 is an authorization framework that enables applications to obtain limited access to user accounts on your Dotkernel API. We are using mezzio/mezzio-authentication-oauth2 which provides OAuth2 authentication for Mezzio and PSR-7/PSR-15 applications by using league/oauth2-server package. + +## Email + +It is not unlikely for an API to send emails depending on the use case. Here is another area where Dotkernel API shines. Using `DotMailServiceMailService` provided by dotkernel/dot-mail you can easily send custom email templates. + +## Configuration + +From authorization at request route level to API keys for your application, you can find every configuration variable in the config directory. + +Registering a new module can be done by including its ConfigProvider.php in config.php. + +Brand new middlewares should go into pipeline.php. Here you can edit the order in which they run and find more info about the currently included ones. + +You can further customize your api within the autoload directory where each configuration category has its own file. + +## Routing + +Each Module has a `RoutesDelegator.php` file for managing existing routes inside that specific module. It also allows a quick way of adding new routes by providing the route path, Middlewares that the route will use and the route name. + +You can allocate permissions per route name in order to restrict access for a user role to a specific route in `config/autoload/authorization.global.php`. + +## Commands + +For registering new commands first make sure your command class extends `SymfonyComponentConsoleCommandCommand`. Then you can enable it by registering it in `config/autoload/cli.global.php`. + +## File locker + +Here you will also find our brand-new file locker configuration, so you can easily turn it on or off ( by default: `'enabled' => true` ) + +Note: The File Locker System will create a `command-{command-default-name}.lock` file which will not let another instance of the same command to run until the previous one has finished. + +## PSR Standards + +* PSR-3: Logger Interface – the application uses `LoggerInterface` for error logging +* PSR-4: Autoloader – the application locates classes using an autoloader +* PSR-7: HTTP message interfaces – the handlers return `ResponseInterface` +* PSR-11: Container interface – the application is container-based +* PSR-15: HTTP Server Request Handlers – the handlers implement `RequestHandlerInterface` + +## Tests + +One of the best ways to ensure the quality of your product is to create and run functional and unit tests. You can find factory-made tests in the tests/AppTest/ folder, and you can also register your own. + +We have 2 types of tests: functional and unit tests, you can run both types at the same type by executing this command: + + php vendor/bin/phpunit + +#### Running unit tests + + vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always + +#### Running functional tests + + vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always diff --git a/docs/book/v4/tutorials.md b/docs/book/v4/tutorials.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/book/v4/tutorials/create-book-module.md b/docs/book/v4/tutorials/create-book-module.md new file mode 100644 index 0000000..35c68c0 --- /dev/null +++ b/docs/book/v4/tutorials/create-book-module.md @@ -0,0 +1,636 @@ +## Implementing a book module in DotKernel 3 API + +### File structure + +The below file structure is just an example, you can have multiple components such as event listeners, wrappers, etc. + + /src/ + /Book/ + /src/ + /Collection/ + /BookCollection.php + /Entity/ + /Book.php + /Handler/ + /BookHandler.php + /Repository/ + /BookRepository.php + /Service/ + /BookService.php + /InputFilter/ + /Input/ + /NameInput.php + /AuthorInput.php + /ReleaseDateInput.php + /BookInputFilter.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/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 + +### File creation and contents + +* `src/Book/src/Collection/BookCollection.php` + +```php +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(), + ]; + } +} +``` + +* `src/Book/src/Repository/BookRepository.php` + +```php + + */ +class BookRepository extends EntityRepository +{ + public function saveBook(Book $book): Book + { + $this->getEntityManager()->persist($book); + $this->getEntityManager()->flush(); + + return $book; + } + + public function getBooks(array $filters = []): BookCollection + { + $page = PaginationHelper::getOffsetAndLimit($filters); + + $qb = $this + ->getEntityManager() + ->createQueryBuilder() + ->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); + } +} +``` +* `src/Book/src/Service/BookService.php` + +```php +bookRepository->saveBook($book); + } + + public function getBooks(array $filters = []) + { + return $this->bookRepository->getBooks($filters); + } +} +``` + +* `src/Book/src/Service/BookServiceInterface.php` + +```php + $this->getDependencies(), + MetadataMap::class => $this->getHalConfig(), + ]; + } + + public function getDependencies(): array + { + return [ + 'factories' => [ + BookHandler::class => AnnotatedServiceFactory::class, + BookService::class => AnnotatedServiceFactory::class, + BookRepository::class => AnnotatedRepositoryFactory::class, + ], + 'aliases' => [ + BookServiceInterface::class => BookService::class, + ], + ]; + } + + public function getHalConfig(): array + { + return [ + AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), + AppConfigProvider::getResource(Book::class, 'book.create'), + ]; + } +} +``` + +* `src/Book/src/RoutesDelegator.php` + +```php +get( + '/books', + BookHandler::class, + 'books.list' + ); + + $app->post( + '/book', + BookHandler::class, + 'book.create' + ); + + return $app; + } +} +``` + +* `src/Book/src/InputFilter/BookInputFilter.php` + + +```php +add(new NameInput('name')); + $this->add(new AuthorInput('author')); + $this->add(new ReleaseDateInput('releaseDate')); + } +} +``` + +* `src/Book/src/InputFilter/Input/AuthorInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'author'), + ], true); + } +} +``` + +* `src/Book/src/InputFilter/Input/NameInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(NotEmpty::class, [ + 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), + ], true); + } +} +``` + +* `src/Book/src/InputFilter/Input/ReleaseDateInput.php` + +```php +setRequired($isRequired); + + $this->getFilterChain() + ->attachByName(StringTrim::class) + ->attachByName(StripTags::class); + + $this->getValidatorChain() + ->attachByName(Date::class, [ + 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), + ], true); + } +} +``` + +* `src/Book/src/Handler/BookHandler.php` + +```php +bookService->getBooks($request->getQueryParams()); + + return $this->createResponse($request, $books); + } + + public function post(ServerRequestInterface $request): ResponseInterface + { + $inputFilter = (new BookInputFilter())->setData($request->getParsedBody()); + if (! $inputFilter->isValid()) { + return $this->errorResponse($inputFilter->getMessages()); + } + + $book = $this->bookService->createBook($inputFilter->getValues()); + + return $this->createResponse($request, $book); + } +} +``` + +### Configuring and registering the new module + +Once you set up all the files as in the example above, you will need to do a few additional configurations: + +* Register the namespace by adding this line `"Api\\Book\\": "src/Book/src/",` in `composer.json` under the `autoload.psr-4` key. +* Register the module by adding `Api\Book\ConfigProvider::class,` under `Api\User\ConfigProvider::class,`. +* Register the module's routes by adding `\Api\Book\RoutesDelegator::class,` under `\Api\User\RoutesDelegator::class,` in `src/App/src/ConfigProvider.php`. + +It should look like this: + +```php +public function getDependencies(): array +{ + return [ + 'delegators' => [ + Application::class => [ + RoutesDelegator::class, + \Api\Admin\RoutesDelegator::class, + \Api\User\RoutesDelegator::class, + \Api\Book\RoutesDelegator::class, + ], + ], + 'factories' => [ + ... + ] + ... +``` + +* In `src/config/autoload/doctrine.global.php` add under the `doctrine.driver` key: + * ```php + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/../../src/Book/src/Entity', + ], + ``` + add this under the `doctrine.driver` key: + * ``'Api\\Book\Entity' => 'BookEntities',`` add this under the `doctrine.driver.drivers` class + +Example: + +```php + [ + ... + 'driver' => [ + 'orm_default' => [ + 'class' => MappingDriverChain::class, + 'drivers' => [ + 'Api\\App\Entity' => 'AppEntities', + 'Api\\Admin\\Entity' => 'AdminEntities', + 'Api\\User\\Entity' => 'UserEntities', + 'Api\\Book\Entity' => 'BookEntities', + ], + ], + 'AdminEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/../../src/Admin/src/Entity', + ], + 'UserEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/../../src/User/src/Entity', + ], + 'AppEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/../../src/App/src/Entity', + ], + 'BookEntities' => [ + 'class' => AttributeDriver::class, + 'cache' => 'array', + 'paths' => __DIR__ . '/../../src/Book/src/Entity', + ], + ], + ... +``` + +Next we need to configure access to the newly created endpoints, add `books.list` and `book.create` to the authorization rbac array, under the `UserRole::ROLE_GUEST` key. +> Make sure you read and understand the rbac documentation. + +### Migrations + +We created the `Book` entity, but we didn't create the associated table for it. + +Doctrine can handle the table creation, run the following command: + + 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 `data/doctrine/migrations`. + +To execute the migrations run: + + vendor/bin/doctrine-migrations migrate + +### Checking endpoints + +If we did everything as planned we can call the `http://localhost/books` endpoint and create a new book: + + curl -X POST http://localhost/book + -H "Content-Type: application/json" + -d '{"name": "test", "author": "author name", "releaseDate": "2023-03-03"}' + +To list the books use : + + curl http://localhost/books \ No newline at end of file diff --git a/docs/book/v4/tutorials/create-hello-world-handler.md b/docs/book/v4/tutorials/create-hello-world-handler.md deleted file mode 100644 index e69de29..0000000 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..ffed7a7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,24 @@ +docs_dir: docs/book +site_dir: docs/html +extra: + project: DotKernel 3 API + current_version: v4 + versions: + - v4 +nav: + - Home: index.md + - v4: + - Introduction: + - "Introduction": v4/introduction/introduction.md + - "Getting Started": v4/introduction/getting-started.md + - "Server Requirements": v4/introduction/server-requirements.md + - "File Structure": v4/introduction/file-structure.md + - "Installation": v4/introduction/installation.md + - "Packages": v4/introduction/packages.md + - Tutorials: + - "Creating a book module": v4/tutorials/create-book-module.md +site_name: api +site_description: "DotKernel 3 API" +repo_url: "https://github.com/dotkernel/api" +plugins: + - search \ No newline at end of file