diff --git a/docs/book/v7/commands/create-admin-account.md b/docs/book/v7/commands/create-admin-account.md index 8e84723..4724620 100644 --- a/docs/book/v7/commands/create-admin-account.md +++ b/docs/book/v7/commands/create-admin-account.md @@ -56,7 +56,8 @@ php ./bin/cli.php help admin:create **Q: Can I choose the role of the created account?** -A: No. The command always assigns the `admin` role; other roles must be set afterwards. +A: No. +The command always assigns the `admin` role; other roles must be set afterwards. See [Authorization](../core-features/authorization.md). **Q: What can I use as the identity?** diff --git a/docs/book/v7/commands/display-available-endpoints.md b/docs/book/v7/commands/display-available-endpoints.md index da33f12..47922e6 100644 --- a/docs/book/v7/commands/display-available-endpoints.md +++ b/docs/book/v7/commands/display-available-endpoints.md @@ -88,7 +88,8 @@ php ./bin/cli.php route:list --help **Q: Is the output generated from a static file?** -A: No. The command walks the application's registered routes in realtime, so it always reflects the current configuration. +A: No. +The command walks the application's registered routes in realtime, so it always reflects the current configuration. **Q: Which filters are available?** diff --git a/docs/book/v7/commands/generate-database-migrations.md b/docs/book/v7/commands/generate-database-migrations.md index cdfe917..4d2adf5 100644 --- a/docs/book/v7/commands/generate-database-migrations.md +++ b/docs/book/v7/commands/generate-database-migrations.md @@ -13,8 +13,7 @@ Run the following command in your application’s root directory: vendor/bin/doctrine-migrations diff ``` -If you have mapping modifications, this will create a new migration file under -`src/Core/src/App/src/Migration/`, in the `Core\App\Migration` namespace. +If you have mapping modifications, this will create a new migration file under `src/Core/src/App/src/Migration/`, in the `Core\App\Migration` namespace. The location comes from the `doctrine.migrations.migrations_paths` key in `Core\App\ConfigProvider`. Opening the migration file, you will notice that it contains some queries that will drop your `oauth_*` tables because they are unmapped (there is no doctrine entity describing them). You should delete your latest migration with the DROP queries in it as we will create another one, without the DROP queries in it. diff --git a/docs/book/v7/commands/generate-tokens.md b/docs/book/v7/commands/generate-tokens.md index 29bc88a..8c21a61 100644 --- a/docs/book/v7/commands/generate-tokens.md +++ b/docs/book/v7/commands/generate-tokens.md @@ -98,4 +98,5 @@ Clear it with `php ./bin/clear-config-cache.php`. **Q: Does the command store the token for me?** -A: No. It only prints the value; copying it into the configuration file is a manual step. +A: No. +It only prints the value; copying it into the configuration file is a manual step. diff --git a/docs/book/v7/core-features/authentication.md b/docs/book/v7/core-features/authentication.md index d5e60bc..043ff80 100644 --- a/docs/book/v7/core-features/authentication.md +++ b/docs/book/v7/core-features/authentication.md @@ -27,9 +27,7 @@ Authentication in Dotkernel API is built around the `mezzio/mezzio-authenticatio To customize authentication behavior (token lifetimes, algorithms, etc.), edit `config/autoload/local.php` under the `authentication` key. See the [Mezzio OAuth2 documentation](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration) for all available options. -> You can check the -> [mezzio/mezzio-authentication-oauth2](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration) -> configuration part for more info. +> You can check the [mezzio/mezzio-authentication-oauth2](https://docs.mezzio.dev/mezzio-authentication-oauth2/v1/intro/#configuration) configuration part for more info. ## How it works @@ -250,5 +248,6 @@ See [Middleware flow](../flow/middleware-flow.md). **Q: Is authenticating enough to access an endpoint?** -A: No. Authentication establishes the identity; the role still needs permission for the route. +A: No. +Authentication establishes the identity; the role still needs permission for the route. See [Authorization](authorization.md). diff --git a/docs/book/v7/core-features/authorization.md b/docs/book/v7/core-features/authorization.md index 5a50e99..184fe45 100644 --- a/docs/book/v7/core-features/authorization.md +++ b/docs/book/v7/core-features/authorization.md @@ -194,7 +194,8 @@ Because the middleware only knows `AuthorizationInterface`, no application code **Q: Do the permissions cover every route?** -A: Yes, exactly. The three populated roles grant 38 permissions across the 38 declared routes, with no route ungranted and no permission naming a route that does not exist. +A: Yes, exactly. +The three populated roles grant 38 permissions across the 38 declared routes, with no route ungranted and no permission naming a route that does not exist. `app::view-index` and `app::create-error-report` are the only two granted to two roles. **Q: What does a rejected request look like?** diff --git a/docs/book/v7/core-features/content-validation.md b/docs/book/v7/core-features/content-validation.md index 6b82723..8663ea3 100644 --- a/docs/book/v7/core-features/content-validation.md +++ b/docs/book/v7/core-features/content-validation.md @@ -129,7 +129,8 @@ A: The server attempts to deserialize the body as best it can, rather than rejec **Q: Will `application/vnd.api+json` be rejected if I only configured `application/json`?** -A: No. Validation resolves to the more generic media type, so the request is served as JSON. +A: No. +Validation resolves to the more generic media type, so the request is served as JSON. **Q: How do I configure a file upload endpoint?** diff --git a/docs/book/v7/core-features/error-reporting.md b/docs/book/v7/core-features/error-reporting.md index fa22e67..dd64edd 100644 --- a/docs/book/v7/core-features/error-reporting.md +++ b/docs/book/v7/core-features/error-reporting.md @@ -166,7 +166,8 @@ A: The log entry includes the token used, and tokens can be defined as key-value **Q: Do these tokens expire?** -A: No. Because they are indefinite, rotate them manually from time to time. +A: No. +Because they are indefinite, rotate them manually from time to time. See [Basic security](../security/basic-security.md). **Q: Where do the reports end up?** @@ -176,5 +177,6 @@ If it does not exist, it is created automatically. **Q: Does this endpoint need an auth token as well?** -A: No. It is authorized solely by the error reporting token. +A: No. +It is authorized solely by the error reporting token. See [Using the documentation](../openapi/use-documentation.md). diff --git a/docs/book/v7/core-features/rendering-and-sending-emails.md b/docs/book/v7/core-features/rendering-and-sending-emails.md index f70d82b..1d51843 100644 --- a/docs/book/v7/core-features/rendering-and-sending-emails.md +++ b/docs/book/v7/core-features/rendering-and-sending-emails.md @@ -63,7 +63,8 @@ A: Files combining PHP and HTML with the `.phtml` extension. **Q: Does `MailService` still need a renderer injected?** -A: No. Rendering happens in the handler, and the finished body is passed to the mail service as a parameter. +A: No. +Rendering happens in the handler, and the finished body is passed to the mail service as a parameter. **Q: How do I render a template and send it?** diff --git a/docs/book/v7/extended-features/core-and-app.md b/docs/book/v7/extended-features/core-and-app.md index 32b3bcc..6c8f437 100644 --- a/docs/book/v7/extended-features/core-and-app.md +++ b/docs/book/v7/extended-features/core-and-app.md @@ -73,5 +73,6 @@ See [File structure](../introduction/file-structure.md). **Q: Was this split present before version 6.0?** -A: No. It was introduced in 6.0 when common logic was moved into the Core module. +A: No. +It was introduced in 6.0 when common logic was moved into the Core module. See [Upgrading from 5.x to 6.0](../upgrading/UPGRADE-6.0.md). diff --git a/docs/book/v7/extended-features/injectable-input-filters.md b/docs/book/v7/extended-features/injectable-input-filters.md index 416695d..fa57835 100644 --- a/docs/book/v7/extended-features/injectable-input-filters.md +++ b/docs/book/v7/extended-features/injectable-input-filters.md @@ -29,7 +29,8 @@ public function handle(ServerRequestInterface $request): ResponseInterface } ``` -While simple, this ties your handler directly to a concrete class. It’s harder to reuse logic across contexts and mock or replace the filter during testing. +While simple, this ties your handler directly to a concrete class. +It’s harder to reuse logic across contexts and mock or replace the filter during testing. Our **current** approach uses constructor injection: diff --git a/docs/book/v7/extended-features/route-grouping.md b/docs/book/v7/extended-features/route-grouping.md index 151e655..7978705 100644 --- a/docs/book/v7/extended-features/route-grouping.md +++ b/docs/book/v7/extended-features/route-grouping.md @@ -8,7 +8,8 @@ The result is less duplication, easier refactoring, and routes that belong toget ## Details In Dotkernel API with the help of the new [dot-router](https://docs.dotkernel.org/dot-router/v1/overview/) package, we have managed to implement a nicer way of creating routes. -A lot of the times developers need to create sets of routes that have a similar format. As an example: +A lot of the times developers need to create sets of routes that have a similar format. +As an example: ```php $app->post('/product/create', CreateProductHandler::class, 'product:create'); diff --git a/docs/book/v7/installation/composer.md b/docs/book/v7/installation/composer.md index d01b6af..f1fe349 100644 --- a/docs/book/v7/installation/composer.md +++ b/docs/book/v7/installation/composer.md @@ -78,7 +78,8 @@ Type `0` to select `[0] Do not inject`. The next question is: -`Remember this option for other packages of the same type? (y/N)` +`Remember this option for other packages of the same type? +(y/N)` Type `y` here, and hit `enter` to complete this stage. @@ -124,7 +125,8 @@ A: It writes `composer.lock`, configures PHP CodeSniffer, generates the OAuth2 k **Q: Will re-running `composer install` overwrite my configuration?** -A: No. The post-install scripts run on every `composer install` and `composer update`, but they check whether each file already exists before writing it. +A: No. +The post-install scripts run on every `composer install` and `composer update`, but they check whether each file already exists before writing it. **Q: Composer asks where to inject `Laminas\Diactoros\ConfigProvider`. What do I answer?** diff --git a/docs/book/v7/installation/configuration-files.md b/docs/book/v7/installation/configuration-files.md index b82c2ff..06e3987 100644 --- a/docs/book/v7/installation/configuration-files.md +++ b/docs/book/v7/installation/configuration-files.md @@ -50,7 +50,8 @@ A: `config/autoload/local.php`, which is also where the API key and other enviro **Q: Should I commit these files to version control?** -A: No. The `*.local.php` files hold environment-specific values and are excluded from the repository; only the `.dist` templates are tracked. +A: No. +The `*.local.php` files hold environment-specific values and are excluded from the repository; only the `.dist` templates are tracked. **Q: When do I need to edit `cors.local.php`?** @@ -66,5 +67,6 @@ See [Rendering and sending emails](../core-features/rendering-and-sending-emails **Q: Do tests use my development database?** -A: No. `local.test.php` points the test suite at a separate in-memory database. +A: No. +`local.test.php` points the test suite at a separate in-memory database. See [Test the installation](test-the-installation.md). diff --git a/docs/book/v7/installation/doctrine-orm.md b/docs/book/v7/installation/doctrine-orm.md index f01ef41..b45fc7d 100644 --- a/docs/book/v7/installation/doctrine-orm.md +++ b/docs/book/v7/installation/doctrine-orm.md @@ -253,7 +253,8 @@ Only one active connection is allowed at a time, even if the array defines sever **Q: Do I have to name the database `dotkernel`?** -A: No. That is only an example — use any name, as long as the configuration and the database you create agree. +A: No. +That is only an example — use any name, as long as the configuration and the database you create agree. **Q: What is `table_prefix` for?** diff --git a/docs/book/v7/installation/test-the-installation.md b/docs/book/v7/installation/test-the-installation.md index 3d56ed4..05a06bc 100644 --- a/docs/book/v7/installation/test-the-installation.md +++ b/docs/book/v7/installation/test-the-installation.md @@ -109,7 +109,8 @@ The [FAQ page](faq.md) lists the exact error messages and their fixes. **Q: Do I need a virtual host?** -A: No. `php -S 0.0.0.0:8080 -t public` serves the application without one, which is convenient for a quick check. +A: No. +`php -S 0.0.0.0:8080 -t public` serves the application without one, which is convenient for a quick check. **Q: What is Bruno and why use it?** diff --git a/docs/book/v7/introduction/introduction.md b/docs/book/v7/introduction/introduction.md index 2c8c7e8..179d848 100644 --- a/docs/book/v7/introduction/introduction.md +++ b/docs/book/v7/introduction/introduction.md @@ -123,8 +123,7 @@ vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always ## Common Pitfalls -> !IMPORTANT -> Remember: +> !IMPORTANT Remember: - Change default OAuth2 client credentials in production. - Enable development mode only locally. diff --git a/docs/book/v7/introduction/packages.md b/docs/book/v7/introduction/packages.md index 05b8041..b9a5d0b 100644 --- a/docs/book/v7/introduction/packages.md +++ b/docs/book/v7/introduction/packages.md @@ -50,15 +50,13 @@ Composer refuses to install the project unless all of these are satisfied: ### Installed as dependencies These are used directly by the application but are **not** declared in `composer.json`. -They are resolved transitively through the packages listed above, so a normal install provides them — -but the project pins no version of its own for them. +They are resolved transitively through the packages listed above, so a normal install provides them — but the project pins no version of its own for them. * `doctrine/orm` - Object-Relational-Mapper for PHP; the persistence layer for every entity and repository * `doctrine/dbal` - Database abstraction and schema layer that the ORM is built on; also where the migrations and the custom `UUID` type operate * `laminas/laminas-servicemanager` - Factory-driven PSR-11 container; `config/container.php` instantiates it directly -To see which versions you actually have, run `composer show doctrine/orm` (or `composer show --tree`) -rather than relying on a constraint, since there is none to read. +To see which versions you actually have, run `composer show doctrine/orm` (or `composer show --tree`) rather than relying on a constraint, since there is none to read. ### Development requirements diff --git a/docs/book/v7/introduction/psr.md b/docs/book/v7/introduction/psr.md index 872da8a..8555411 100644 --- a/docs/book/v7/introduction/psr.md +++ b/docs/book/v7/introduction/psr.md @@ -15,7 +15,8 @@ PSR-7 (HTTP messages), PSR-15 (handlers and middleware) and PSR-11 (container) a ## PHP Standards Recommendations (PSRs) -Dotkernel API adheres to PHP Standards Recommendations (PSRs) established by the PHP-FIG (Framework Interoperability Group). These standards ensure code interoperability and allow Dotkernel API to work seamlessly with other PSR-compliant libraries. +Dotkernel API adheres to PHP Standards Recommendations (PSRs) established by the PHP-FIG (Framework Interoperability Group). +These standards ensure code interoperability and allow Dotkernel API to work seamlessly with other PSR-compliant libraries. Some PSRs are at the **core** of Dotkernel API's architecture, while others are installed as dependencies through third-party packages. @@ -68,8 +69,7 @@ Defines the standard interface for dependency injection containers. Provides a standard interface for logging libraries. -**Usage**: Error handling, debugging, audit trails -**Implemented in**: `dotkernel/dot-errorhandler` +**Usage**: Error handling, debugging, audit trails **Implemented in**: `dotkernel/dot-errorhandler` ### PSR-4: Autoloader @@ -77,8 +77,7 @@ Provides a standard interface for logging libraries. Defines how PHP files are automatically loaded based on namespaces and file paths. -**Usage**: Automatic class loading without manual `require` statements -**Implemented in**: `Laminas\Loader` +**Usage**: Automatic class loading without manual `require` statements **Implemented in**: `Laminas\Loader` ### PSR-6: Caching Interface @@ -86,8 +85,7 @@ Defines how PHP files are automatically loaded based on namespaces and file path Defines standard interfaces for caching systems to improve application performance. -**Usage**: Caching query results, configuration, templates -**Implemented in**: `dotkernel/dot-cache` +**Usage**: Caching query results, configuration, templates **Implemented in**: `dotkernel/dot-cache` ### PSR-13: Link Definition Interfaces @@ -95,8 +93,7 @@ Defines standard interfaces for caching systems to improve application performan Describes how to represent hypermedia links independently of serialization format. -**Usage**: HAL (Hypertext Application Language) resource links -**Implemented in**: `mezzio/mezzio-hal` +**Usage**: HAL (Hypertext Application Language) resource links **Implemented in**: `mezzio/mezzio-hal` ### PSR-14: Event Dispatcher @@ -104,8 +101,7 @@ Describes how to represent hypermedia links independently of serialization forma Mechanism for event-based extension and collaboration between components. -**Usage**: Triggering events on user actions, logging events, notifications -**Implemented in**: Third-party packages as needed +**Usage**: Triggering events on user actions, logging events, notifications **Implemented in**: Third-party packages as needed ### PSR-17: HTTP Factories @@ -113,8 +109,7 @@ Mechanism for event-based extension and collaboration between components. Standard for factories that create PSR-7 compliant HTTP objects. -**Usage**: Creating requests, responses, and streams programmatically -**Implemented in**: `Laminas\Diactoros` +**Usage**: Creating requests, responses, and streams programmatically **Implemented in**: `Laminas\Diactoros` ### PSR-18: HTTP Client @@ -122,8 +117,7 @@ Standard for factories that create PSR-7 compliant HTTP objects. Interface for sending HTTP requests and receiving HTTP responses. -**Usage**: Calling external APIs from your Dotkernel API -**Implemented in**: `symfony/http-client` or similar packages +**Usage**: Calling external APIs from your Dotkernel API **Implemented in**: `symfony/http-client` or similar packages ### PSR-20: Clock @@ -131,8 +125,7 @@ Interface for sending HTTP requests and receiving HTTP responses. Provides a standard interface for reading the system clock. -**Usage**: Getting current time in a testable way -**Implemented in**: Third-party packages as needed +**Usage**: Getting current time in a testable way **Implemented in**: Third-party packages as needed ### PSR Implementation Hierarchy @@ -193,7 +186,8 @@ See [File structure](file-structure.md). **Q: Do I need to install anything to use PSR-3 logging or PSR-6 caching?** -A: No. They come with `dotkernel/dot-errorhandler` and `dotkernel/dot-cache` respectively. +A: No. +They come with `dotkernel/dot-errorhandler` and `dotkernel/dot-cache` respectively. See [Packages](packages.md). **Q: How do I call an external API from Dotkernel API?** diff --git a/docs/book/v7/introduction/server-requirements.md b/docs/book/v7/introduction/server-requirements.md index bb14d08..533b88c 100644 --- a/docs/book/v7/introduction/server-requirements.md +++ b/docs/book/v7/introduction/server-requirements.md @@ -51,8 +51,7 @@ Dotkernel API v7 requires PHP 8.3, 8.4 or 8.5, as declared in `composer.json`: Earlier PHP versions are not supported. Support for PHP 8.2 was dropped in Dotkernel API 7.2.0. -> The constraint uses `~` per minor version rather than `>=`, so each supported branch is listed -> explicitly and a newly released PHP version is not assumed to work until it has been tested. +> The constraint uses `~` per minor version rather than `>=`, so each supported branch is listed explicitly and a newly released PHP version is not assumed to work until it has been tested. ### Supported PHP Configurations @@ -62,8 +61,7 @@ Support for PHP 8.2 was dropped in Dotkernel API 7.2.0. ### Why PHP 8.3+? -The floor is set by **typed class constants**, a PHP 8.3 feature used throughout the codebase — for -example in `Api\App\Middleware\ContentNegotiationMiddleware`: +The floor is set by **typed class constants**, a PHP 8.3 feature used throughout the codebase — for example in `Api\App\Middleware\ContentNegotiationMiddleware`: ```php public const string DEFAULT_HEADERS = 'default'; @@ -73,8 +71,7 @@ The project will not even parse on PHP 8.2. ## Required Settings and Modules & Extensions -These extensions are declared in the `require` section of `composer.json` as `ext-gd` and -`ext-json`, so Composer refuses to install the project without them: +These extensions are declared in the `require` section of `composer.json` as `ext-gd` and `ext-json`, so Composer refuses to install the project without them: - `gd` - must be enabled; this is the one to check on a new server - `json` - ships enabled and cannot be disabled on any supported PHP version, so in practice it needs no action @@ -107,32 +104,24 @@ All three are LTS releases, which we recommend for stability and security update #### Why 11.4 is the minimum -Dotkernel API stores every entity identifier in a MariaDB `UUID` column and generates the value as a -**UUIDv7** in PHP, via `Ramsey\Uuid\Uuid::uuid7()`. UUIDv7 is time-ordered by design, so sequential -inserts land next to each other in the index. +Dotkernel API stores every entity identifier in a MariaDB `UUID` column and generates the value as a **UUIDv7** in PHP, via `Ramsey\Uuid\Uuid::uuid7()`. +UUIDv7 is time-ordered by design, so sequential inserts land next to each other in the index. -MariaDB, however, does not always store a `UUID` in the order it was given. It rearranges the value -internally into an index-friendly layout that assumes a UUIDv1 — where the node comes first and the -timestamp second. Applied to a UUIDv7, that rearrangement scrambles exactly the ordering the type -was chosen for. +MariaDB, however, does not always store a `UUID` in the order it was given. +It rearranges the value internally into an index-friendly layout that assumes a UUIDv1 — where the node comes first and the timestamp second. +Applied to a UUIDv7, that rearrangement scrambles exactly the ordering the type was chosen for. -MariaDB 10.10 changed this: from that release on, UUIDv6 and later are stored in their native order, -with no byte-swapping. The change did not reach the older maintenance series until 10.10.7 and -10.11.6, so "MariaDB 10.11" is only correct from 10.11.6 onward. +MariaDB 10.10 changed this: from that release on, UUIDv6 and later are stored in their native order, with no byte-swapping. +The change did not reach the older maintenance series until 10.10.7 and 10.11.6, so "MariaDB 10.11" is only correct from 10.11.6 onward. -11.4 LTS is therefore the earliest LTS series in which *every* patch release stores UUIDv7 natively, -which is why it is the published floor. +11.4 LTS is therefore the earliest LTS series in which *every* patch release stores UUIDv7 natively, which is why it is the published floor. > On MariaDB 10.7, or on 10.11.0 - 10.11.5, the application still runs: inserts and reads succeed. -> The identifiers are simply stored in scrambled order, so you lose the insert locality UUIDv7 exists -> to provide. It is a silent performance problem rather than an error, which is what makes it worth -> stating explicitly. +> The identifiers are simply stored in scrambled order, so you lose the insert locality UUIDv7 exists to provide. +> It is a silent performance problem rather than an error, which is what makes it worth stating explicitly. -For the background on why identifiers are generated as UUIDv7 in PHP rather than delegated to the -database, see -[Version 7 adds PostgreSQL, native UUID and PHP 8.5](https://www.dotkernel.com/headless-platform/version-7-adds-postgresql-native-uuid-and-php-8-5/). -Generating them in the application keeps full control over which UUID version is used and avoids -depending on a database extension or a particular server version to produce the value. +For the background on why identifiers are generated as UUIDv7 in PHP rather than delegated to the database, see [Version 7 adds PostgreSQL, native UUID and PHP 8.5](https://www.dotkernel.com/headless-platform/version-7-adds-postgresql-native-uuid-and-php-8-5/). +Generating them in the application keeps full control over which UUID version is used and avoids depending on a database extension or a particular server version to produce the value. ### PostgreSQL @@ -182,8 +171,8 @@ MariaDB and PostgreSQL both do. **Q: Which database versions are tested?** A: MariaDB 11.4 LTS, 11.8 LTS and 12.3 LTS, and PostgreSQL 13 and above. -MariaDB 11.4 is also the minimum: earlier releases byte-swap `UUID` values and destroy the ordering -of the UUIDv7 identifiers this project uses. See "Why 11.4 is the minimum" above. +MariaDB 11.4 is also the minimum: earlier releases byte-swap `UUID` values and destroy the ordering of the UUIDv7 identifiers this project uses. +See "Why 11.4 is the minimum" above. **Q: What collation should I create the database with?** diff --git a/docs/book/v7/openapi/initialized-components.md b/docs/book/v7/openapi/initialized-components.md index b83787d..937723e 100644 --- a/docs/book/v7/openapi/initialized-components.md +++ b/docs/book/v7/openapi/initialized-components.md @@ -234,7 +234,8 @@ For more info, see [this page](https://spec.openapis.org/oas/latest.html#schema) ### Common schemas -We provided some schemas that are reusable across the entire project. They are defined in `src/App/src/OpenAPI.php`: +We provided some schemas that are reusable across the entire project. +They are defined in `src/App/src/OpenAPI.php`: - `#/components/schemas/Collection`: provides the default **HAL** structure to all the collections extending it - `#/components/schemas/ErrorMessage`: describes an operation that resulted in an error—may contain multiple messages diff --git a/docs/book/v7/openapi/introduction.md b/docs/book/v7/openapi/introduction.md index 1aaf8a1..7b8c681 100644 --- a/docs/book/v7/openapi/introduction.md +++ b/docs/book/v7/openapi/introduction.md @@ -19,7 +19,8 @@ Tools can consume the specification to render documentation, generate clients, o **Q: Do I write the specification by hand?** -A: No. You annotate your handlers and models with `zircote/swagger-php` attributes, then generate the specification file from them. +A: No. +You annotate your handlers and models with `zircote/swagger-php` attributes, then generate the specification file from them. See [Write documentation](write-documentation.md). **Q: Which OpenAPI version is used?** diff --git a/docs/book/v7/openapi/render-documentation.md b/docs/book/v7/openapi/render-documentation.md index 35e938c..d4fe2d2 100644 --- a/docs/book/v7/openapi/render-documentation.md +++ b/docs/book/v7/openapi/render-documentation.md @@ -41,8 +41,8 @@ Navigate to the `public` directory of your instance of Dotkernel API and create ``` -Make sure that you replace `PATH_TO_YOUR_OPENAPI_FILE` with the relative path to your documentation file -(openapi.json/openapi.yaml). The line should look similar to this: +Make sure that you replace `PATH_TO_YOUR_OPENAPI_FILE` with the relative path to your documentation file (openapi.json/openapi.yaml). +The line should look similar to this: ```js window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); @@ -54,8 +54,7 @@ From here, you can inspect each endpoint, see its URL, check if it needs authent ## Using Redoc -Navigate to the `public` directory of your instance of Dotkernel API and create an HTML (you can call it `redoc.html`, -the name is up to you) and place the following HTML content in it: +Navigate to the `public` directory of your instance of Dotkernel API and create an HTML (you can call it `redoc.html`, the name is up to you) and place the following HTML content in it: ```html @@ -101,7 +100,8 @@ The page and the OpenAPI file both need to be reachable by the browser. **Q: Does the filename matter?** -A: No. `swagger.html` and `redoc.html` are suggestions — the URL you open just has to match whatever you named the file. +A: No. +`swagger.html` and `redoc.html` are suggestions — the URL you open just has to match whatever you named the file. **Q: My page loads but shows no endpoints. What is wrong?** diff --git a/docs/book/v7/openapi/use-documentation.md b/docs/book/v7/openapi/use-documentation.md index e93f483..1be7145 100644 --- a/docs/book/v7/openapi/use-documentation.md +++ b/docs/book/v7/openapi/use-documentation.md @@ -10,21 +10,25 @@ Since Redoc is readonly, in the following section we will focus only on using Sw ## Protected endpoints -Now that you have a UI for the documentation, you can see all the endpoints. You will see that some of them have a lock symbol right before the collapse/expand arrow. +Now that you have a UI for the documentation, you can see all the endpoints. +You will see that some of them have a lock symbol right before the collapse/expand arrow. When you see this symbol next to an endpoint, it means that the endpoint is protected and can only be accessed when authenticated with an account with proper permissions. ## Authentication -In Swagger UI, you will see an `Authorize` button. Clicking it will open a modal where you will find two sections: +In Swagger UI, you will see an `Authorize` button. +Clicking it will open a modal where you will find two sections: - `AuthToken` - where you will have to enter a valid auth token - `ErrorReportingToken` - where you will have to enter a valid error reporting token -Below, we will walk you through on how to find both tokens. For now, let's close the modal. +Below, we will walk you through on how to find both tokens. +For now, let's close the modal. ### Generating AuthToken -This token is required with most of the Dotkernel API endpoints. There are two entities that generate this type of token: `(super)admin`s and `user`s. +This token is required with most of the Dotkernel API endpoints. +There are two entities that generate this type of token: `(super)admin`s and `user`s. Depending on the endpoint description, you will know which one you need to use. Examples: diff --git a/docs/book/v7/openapi/write-documentation.md b/docs/book/v7/openapi/write-documentation.md index 6561d09..f0e58ac 100644 --- a/docs/book/v7/openapi/write-documentation.md +++ b/docs/book/v7/openapi/write-documentation.md @@ -35,7 +35,8 @@ If you need help, take a look at the existing definitions found in Dotkernel API ### OA\Delete -Defines a `DELETE` HTTP request. It should specify at least the following parameters: +Defines a `DELETE` HTTP request. +It should specify at least the following parameters: - `path`: the route to the resource (example: `/resource/{id}` - where `id` is a path parameter defined below) - `description`: verbose description of the endpoint's purpose @@ -47,7 +48,8 @@ Defines a `DELETE` HTTP request. It should specify at least the following parame ### OA\Get -Defines a `GET` HTTP request. It should specify at least the following parameters: +Defines a `GET` HTTP request. +It should specify at least the following parameters: - `path`: the route to a single or collection of resources (example: `/resource/{id}` for a single resource or `/resource` for a collection of resources) - `description`: verbose description of the endpoint's purpose @@ -59,7 +61,8 @@ Defines a `GET` HTTP request. It should specify at least the following parameter ### OA\Patch -Defines a `PATCH` HTTP request. It should specify at least the following parameters: +Defines a `PATCH` HTTP request. +It should specify at least the following parameters: - `path`: the route to the resource (example: `/resource/{id}` - where `id` is a path parameter defined below) - `description`: verbose description of the endpoint's purpose @@ -72,7 +75,8 @@ Defines a `PATCH` HTTP request. It should specify at least the following paramet ### OA\Post -Defines a `POST` HTTP request. It should specify at least the following parameters: +Defines a `POST` HTTP request. +It should specify at least the following parameters: - `path`: the route to the resource (example: `/resource/{id}` - where `id` is a path parameter defined below) - `description`: verbose description of the endpoint's purpose @@ -85,7 +89,8 @@ Defines a `POST` HTTP request. It should specify at least the following paramete ### OA\Put -Defines a `PUT` HTTP request. It should specify at least the following parameters: +Defines a `PUT` HTTP request. +It should specify at least the following parameters: - `path`: the route to the resource (example: `/resource/{id}` - where `id` is a path parameter defined below) - `description`: verbose description of the endpoint's purpose diff --git a/docs/book/v7/reference/account-anonymization.md b/docs/book/v7/reference/account-anonymization.md index bbc3c08..156e20e 100644 --- a/docs/book/v7/reference/account-anonymization.md +++ b/docs/book/v7/reference/account-anonymization.md @@ -75,4 +75,5 @@ A: Both the image file and its database record are deleted. **Q: Is anonymization reversible?** -A: No. The original values are overwritten, so keep your own backup policy in mind before running it. +A: No. +The original values are overwritten, so keep your own backup policy in mind before running it. diff --git a/docs/book/v7/security/basic-security.md b/docs/book/v7/security/basic-security.md index 86cb9f3..097b30c 100644 --- a/docs/book/v7/security/basic-security.md +++ b/docs/book/v7/security/basic-security.md @@ -40,8 +40,7 @@ Update the configuration file of this package (`config/autoload/authorization.gl ## Demo Credentials -Dotkernel API ships with two demo accounts: an admin account (`admin`) and a user account (`test@dotkernel.com`), -with public identities and passwords as described in the [token authentication tutorial](https://docs.dotkernel.org/api-documentation/v7/tutorials/token-authentication/). +Dotkernel API ships with two demo accounts: an admin account (`admin`) and a user account (`test@dotkernel.com`), with public identities and passwords as described in the [token authentication tutorial](https://docs.dotkernel.org/api-documentation/v7/tutorials/token-authentication/). Make sure to **update** or **remove** these demo accounts in your production environment. @@ -107,7 +106,8 @@ A: Run `composer development-status`. **Q: Do the error reporting tokens expire?** -A: No. Because they never expire, rotate them manually on a schedule of your choosing, and set `ip_whitelist` or `domain_whitelist` in `config/autoload/error-handling.global.php` to limit who can use the endpoint. +A: No. +Because they never expire, rotate them manually on a schedule of your choosing, and set `ip_whitelist` or `domain_whitelist` in `config/autoload/error-handling.global.php` to limit who can use the endpoint. **Q: Why is committing error reporting tokens risky?** @@ -121,7 +121,8 @@ See the [CORS](../tutorials/cors.md) tutorial. **Q: Does adding a route make it protected automatically?** -A: No. Every new route and role needs an entry in `config/autoload/authorization.global.php`. +A: No. +Every new route and role needs an entry in `config/autoload/authorization.global.php`. See [Authorization](../core-features/authorization.md). **Q: Why should OpenAPI documentation stay out of production?** diff --git a/docs/book/v7/security/oauth2-security.md b/docs/book/v7/security/oauth2-security.md index 06b63ec..69b54ee 100644 --- a/docs/book/v7/security/oauth2-security.md +++ b/docs/book/v7/security/oauth2-security.md @@ -42,7 +42,8 @@ It is invoked after each `composer update` (or `composer install` with no lock f ``` **Existing keys are never overwritten.** -The script checks for `data/oauth/encryption.key`, `data/oauth/private.key` and `data/oauth/public.key`; if all three are present it prints `OAuth2 keys already exist. Skipping...` and stops. +The script checks for `data/oauth/encryption.key`, `data/oauth/private.key` and `data/oauth/public.key`; if all three are present it prints `OAuth2 keys already exist. +Skipping...` and stops. Only when one is missing does it delegate to `vendor/mezzio/mezzio-authentication-oauth2/bin/generate-oauth2-keys` to generate the set. > This guard matters in production: regenerating the keys invalidates every access token already issued. @@ -76,7 +77,8 @@ To revoke tokens on their own, use the repositories: `OAuthAccessTokenRepository **Q: When are the OAuth2 keys generated?** A: `php ./bin/generate-oauth2-keys.php` runs after every `composer update`, and after `composer install` when there is no lock file, via `scripts.post-update-cmd` in `composer.json`. -It only generates keys that are missing: if all three files in `data/oauth` exist it reports `OAuth2 keys already exist. Skipping...` and leaves them alone, so updating dependencies does not invalidate issued tokens. +It only generates keys that are missing: if all three files in `data/oauth` exist it reports `OAuth2 keys already exist. +Skipping...` and leaves them alone, so updating dependencies does not invalidate issued tokens. **Q: How do I stop the keys from being generated?** @@ -91,7 +93,8 @@ Every access token issued under the old keys stops working, so plan for clients **Q: Should the key pair be committed?** -A: No. The keys are excluded from version control by default, and the directory holding them must be secured at the filesystem level. +A: No. +The keys are excluded from version control by default, and the directory holding them must be secured at the filesystem level. **Q: Where are the OAuth2 flows themselves documented?** diff --git a/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md b/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md index 1149936..32c1aba 100644 --- a/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md +++ b/docs/book/v7/transition-from-api-tools/api-tools-vs-dotkernel-api.md @@ -31,7 +31,8 @@ API Tools is archived and MVC/event-driven; Dotkernel API is actively maintained **Q: Is Dotkernel API a drop-in replacement for API Tools?** -A: No. The two projects differ in architecture, components and functionality, so a transition is a rewrite rather than a swap. +A: No. +The two projects differ in architecture, components and functionality, so a transition is a rewrite rather than a swap. See [Transition approach](transition-approach.md). **Q: What is the biggest architectural difference?** @@ -41,7 +42,8 @@ Request handling, routing and extension points all work differently as a result. **Q: Does Dotkernel API support RPC-style endpoints?** -A: No. Dotkernel API is REST only, while API Tools supported both REST and RPC. +A: No. +Dotkernel API is REST only, while API Tools supported both REST and RPC. **Q: How is versioning handled without API Tools' version support?** diff --git a/docs/book/v7/tutorials/api-evolution.md b/docs/book/v7/tutorials/api-evolution.md index 23318e8..a78b039 100644 --- a/docs/book/v7/tutorials/api-evolution.md +++ b/docs/book/v7/tutorials/api-evolution.md @@ -99,7 +99,8 @@ The value has to be a valid date. **Q: Are `rel` and `type` required?** -A: No. They default to `sunset` and `text/html`, and both relate to the `Link` header. +A: No. +They default to `sunset` and `text/html`, and both relate to the `Link` header. **Q: Which classes can carry a deprecation?** diff --git a/docs/book/v7/tutorials/cors.md b/docs/book/v7/tutorials/cors.md index 1c146f0..b9211e2 100644 --- a/docs/book/v7/tutorials/cors.md +++ b/docs/book/v7/tutorials/cors.md @@ -7,15 +7,13 @@ This tutorial explains the mechanism, then walks through enabling it in Dotkerne ## What is CORS? -**Cross-Origin Resource Sharing** or _CORS_ is an HTTP header-based mechanism that allows a server to indicate any other -origins (domain, scheme, or port) than its own from which a browser should permit loading of resources. +**Cross-Origin Resource Sharing** or _CORS_ is an HTTP header-based mechanism that allows a server to indicate any other origins (domain, scheme, or port) than its own from which a browser should permit loading of resources. ## Why do we need CORS? When integrating an API, most developers have encountered the following error message: -> Access to fetch at _RESOURCE_URL_ from origin _ORIGIN_URL_ has been blocked by CORS policy: -> No ‘Access-Control-Allow-Origin’ header is present on the requested resource. +> Access to fetch at _RESOURCE_URL_ from origin _ORIGIN_URL_ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. This happens because the API (_RESOURCE_URL_) is not configured to accept requests from the client (_ORIGIN_URL_). @@ -130,5 +128,6 @@ A: It lists response headers the browser should make readable to client-side cod **Q: Do I need to install the package on a fresh Dotkernel API?** -A: No. `mezzio/mezzio-cors` ships with the project and `cors.local.php` is created during installation — you only need to review its values. +A: No. +`mezzio/mezzio-cors` ships with the project and `cors.local.php` is created during installation — you only need to review its values. See [Configuration files](../installation/configuration-files.md). diff --git a/docs/book/v7/tutorials/create-book-module-via-dot-maker.md b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md index 9a4a322..a7d6694 100644 --- a/docs/book/v7/tutorials/create-book-module-via-dot-maker.md +++ b/docs/book/v7/tutorials/create-book-module-via-dot-maker.md @@ -452,7 +452,11 @@ To list the books, use: curl http://0.0.0.0:8080/book ``` -To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . `books` . * . `_links` . `self` . `href`. +To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . +`books` . * . +`_links` . +`self` . +`href`. The link should have the following format: @@ -491,7 +495,8 @@ A: `Book.php` to add the three properties with their accessors and constructor, **Q: Does `dot-maker` create the `Input` classes as part of the module?** -A: No. Generate them separately with `./vendor/bin/dot-maker input`, entering `Author`, `Name` and `ReleaseDate`. +A: No. +Generate them separately with `./vendor/bin/dot-maker input`, entering `Author`, `Name` and `ReleaseDate`. As generated they need no further changes. **Q: Can I define the inputs inline instead?** diff --git a/docs/book/v7/tutorials/create-book-module.md b/docs/book/v7/tutorials/create-book-module.md index c9f84b0..b1a59ea 100644 --- a/docs/book/v7/tutorials/create-book-module.md +++ b/docs/book/v7/tutorials/create-book-module.md @@ -835,7 +835,8 @@ class ConfigProvider composer dump-autoload ``` -That's it. The module is now registered. +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: @@ -892,7 +893,11 @@ To list the books, use: curl http://0.0.0.0:8080/book ``` -To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . `books` . * . `_links` . `self` . `href`. +To fetch a book, `curl` one of the links found in the output of the **list books** command, under `_embedded` . +`books` . * . +`_links` . +`self` . +`href`. The link should have the following format: diff --git a/docs/book/v7/tutorials/find-user-by-identity.md b/docs/book/v7/tutorials/find-user-by-identity.md index 88ccabe..6bc1872 100644 --- a/docs/book/v7/tutorials/find-user-by-identity.md +++ b/docs/book/v7/tutorials/find-user-by-identity.md @@ -1,263 +1,264 @@ -# A practical example: Find a user by identity - -## Summary - -A worked example of adding an endpoint by following an existing one. -Starting from `user.view`, which fetches a user by UUID, it builds an `IdentityHandler` that looks a user up by identity, registers it in the module's `ConfigProvider` and `RoutesDelegator`, grants the route a permission, and covers it with functional tests. - -## Our goal - -Create a new endpoint that fetches a user record by its identity column. - -We already have an endpoint that retrieves a user based on their UUID, so we can review it and create something similar. - -## What we have - -Let's print out all available endpoints using : - -```shell -php ./bin/cli.php route:list -``` - -This command will list all available endpoints, which looks like this: - -```text -+--------+---------------------------------+--------------------------------+ -| Method | Name | Path | -+--------+---------------------------------+--------------------------------+ -| POST | account.activate.request | /account/activate | -| PATCH | account.activate | /account/activate/{hash} | -| PATCH | account.modify-password | /account/reset-password/{hash} | -............................................................................. -............................................................................. -............................................................................. -| GET | user.my-avatar.view | /user/my-avatar | -| GET | user.role.list | /user/role | -| GET | user.role.view | /user/role/{id} | -| PATCH | user.update | /user/{id} | -| GET | user.view | /user/{id} | -+--------+---------------------------------+--------------------------------+ -``` - -### Note - -> **The above output is just an example.** -> -> More info about listing available endpoints can be found in `../commands/display-available-endpoints.md`. - -The endpoint we're focusing on is the last one, `user.view`, so let's take a closer look at its functionality. - -If we search for the route name `user.view` we will find its definition in the `src/User/src/RoutesDelegator.php` class, where all user-related endpoints are found. - -```php -$app->get('/user/' . $id, UserHandler::class, 'user.view'); -``` - -Our route points to `get` method from `UserHandler` so let's navigate to that method. - -```php -public function get(ServerRequestInterface $request): ResponseInterface -{ - $user = $this->userService->findOneBy(['id' => $request->getAttribute('id')]); - - return $this->createResponse($request, $user); -} -``` - -As we can see, the method will query the database for the user based on its id taken from the endpoint. - -We now have an understanding of how things work, and we can start to implement our own endpoint. - -### Implementation - -We need to create a new handler that will process our request, we can call it `IdentityHandler`. - -Create a new PHP class called `IdentityHandler.php` in `src/User/src/Handler` folder. - -```php -getAttribute('identity'); - if (empty($identity)) { - throw (new BadRequestException())->setMessages([sprintf(Message::INVALID_VALUE, 'identity')]); - } - - $user = $this->userService->findByIdentity($identity); - if (! $user instanceof User) { - throw new NotFoundException(Message::USER_NOT_FOUND); - } - - return $this->createResponse($request, $user); - } -} -``` - -Our handler is very similar to the existing one, with some extra steps: - -* We store the identity from the request in the `$identity` variable for later use. -* If the identity is empty we throw a `BadRequestException` with an appropriate message. -* If we can't find the user in the database, we throw an `NotFoundException`. -* If the record is found, we generate and return the response. - -The next step is to register the new handler. -To do this, go to `src/User/src/ConfigProvider.php`. -In the `getDependencies()` method under the `factories` key add `IdentityHandler::class => AttributedServiceFactory::class,` - -Next, create the route in `src/User/src/RoutesDelegator.php`: - -```php - $app->get( - '/user/{identity}', - IdentityHandler::class, - 'user.view.identity' - ); -``` - -### Note - -> Make sure to register the endpoint as the last one to not shadow existing endpoints. - -The last step is to set permissions on the newly created route. - -Go to `config/autoload/authorization.global.php` and add our route name (`user.view.identity`) under the `UserRole::ROLE_GUEST` key. -This will give access to every user, including guests, to view other accounts (for the sake of simplicity). - -### Writing tests - -Because every new piece of code should be tested, we will write some tests for this endpoint also. - -In the `test/Functional` folder create a new php class `IdentityTest.php`: - -```php -get('/user/'); - - $this->assertResponseNotFound($response); - } - - public function testInvalidIdentityReturnsNotFound(): void - { - $response = $this->get('/user/invalid_identity'); - $messages = json_decode($response->getBody()->getContents(), true); - - $this->assertResponseNotFound($response); - $this->assertNotEmpty($messages); - $this->assertIsArray($messages); - $this->assertNotEmpty($messages['error']['messages'][0]); - $this->assertIsString($messages['error']['messages'][0]); - $this->assertSame(Message::USER_NOT_FOUND, $messages['error']['messages'][0]); - } - - public function testValidIdentityReturnsUser(): void - { - $this->createUser([ - 'identity' => 'valid_user', - ]); - - $response = $this->get('/user/valid_user'); - - $this->assertResponseOk($response); - $user = json_decode($response->getBody()->getContents(), true); - - $this->assertSame('valid_user', $user['identity']); - } -} -``` - -Planning and coding a new feature can be challenging at times, but reviewing our existing code or tutorials can serve as a source of inspiration. - -## FAQ - -**Q: How do I find the code behind an existing endpoint?** - -A: List the routes with `php ./bin/cli.php route:list`, then search for the route name in the module's `RoutesDelegator.php` to find the handler it points to. -See [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md). - -**Q: What are the steps to add an endpoint?** - -A: Create the handler, register it in the module's `ConfigProvider` under `factories`, declare the route in `RoutesDelegator.php`, and grant the route name a permission in `config/autoload/authorization.global.php`. - -**Q: Why must the new route be registered last?** - -A: Because `/user/{identity}` and `/user/{id}` match the same shape. -Registering the new route last stops it from shadowing the existing ones. - -**Q: Which factory do I register the handler with?** - -A: `AttributedServiceFactory::class`, which resolves the dependencies declared by the handler's `#[Inject]` attribute. -See [Dependency injection](../core-features/dependency-injection.md). - -**Q: Why does the handler throw two different exceptions?** - -A: `BadRequestException` covers a missing identity in the request (a client error in the input), while `NotFoundException` covers a valid identity with no matching record. -They map to 400 and 404 respectively. -See [Exceptions](../core-features/exceptions.md). - -**Q: Why is the route added under `UserRole::ROLE_GUEST`?** - -A: Only to keep the example simple — it lets everyone, including guests, view accounts. -Real deployments should grant it to the narrowest role that needs it. -See [Authorization](../core-features/authorization.md). - -**Q: Will the endpoint work without an authorization entry?** - -A: No. A route with no permission granted to the caller's role is refused, even though the handler and route exist. - -**Q: What should the tests cover?** - -A: The three outcomes: an empty identity, an identity with no matching user, and a valid identity returning the expected record. - -**Q: Where do functional tests live?** - -A: In the `test/Functional` folder, extending `AbstractFunctionalTest`. -See [Test the installation](../installation/test-the-installation.md). +# A practical example: Find a user by identity + +## Summary + +A worked example of adding an endpoint by following an existing one. +Starting from `user.view`, which fetches a user by UUID, it builds an `IdentityHandler` that looks a user up by identity, registers it in the module's `ConfigProvider` and `RoutesDelegator`, grants the route a permission, and covers it with functional tests. + +## Our goal + +Create a new endpoint that fetches a user record by its identity column. + +We already have an endpoint that retrieves a user based on their UUID, so we can review it and create something similar. + +## What we have + +Let's print out all available endpoints using : + +```shell +php ./bin/cli.php route:list +``` + +This command will list all available endpoints, which looks like this: + +```text ++--------+---------------------------------+--------------------------------+ +| Method | Name | Path | ++--------+---------------------------------+--------------------------------+ +| POST | account.activate.request | /account/activate | +| PATCH | account.activate | /account/activate/{hash} | +| PATCH | account.modify-password | /account/reset-password/{hash} | +............................................................................. +............................................................................. +............................................................................. +| GET | user.my-avatar.view | /user/my-avatar | +| GET | user.role.list | /user/role | +| GET | user.role.view | /user/role/{id} | +| PATCH | user.update | /user/{id} | +| GET | user.view | /user/{id} | ++--------+---------------------------------+--------------------------------+ +``` + +### Note + +> **The above output is just an example.** +> +> More info about listing available endpoints can be found in `../commands/display-available-endpoints.md`. + +The endpoint we're focusing on is the last one, `user.view`, so let's take a closer look at its functionality. + +If we search for the route name `user.view` we will find its definition in the `src/User/src/RoutesDelegator.php` class, where all user-related endpoints are found. + +```php +$app->get('/user/' . $id, UserHandler::class, 'user.view'); +``` + +Our route points to `get` method from `UserHandler` so let's navigate to that method. + +```php +public function get(ServerRequestInterface $request): ResponseInterface +{ + $user = $this->userService->findOneBy(['id' => $request->getAttribute('id')]); + + return $this->createResponse($request, $user); +} +``` + +As we can see, the method will query the database for the user based on its id taken from the endpoint. + +We now have an understanding of how things work, and we can start to implement our own endpoint. + +### Implementation + +We need to create a new handler that will process our request, we can call it `IdentityHandler`. + +Create a new PHP class called `IdentityHandler.php` in `src/User/src/Handler` folder. + +```php +getAttribute('identity'); + if (empty($identity)) { + throw (new BadRequestException())->setMessages([sprintf(Message::INVALID_VALUE, 'identity')]); + } + + $user = $this->userService->findByIdentity($identity); + if (! $user instanceof User) { + throw new NotFoundException(Message::USER_NOT_FOUND); + } + + return $this->createResponse($request, $user); + } +} +``` + +Our handler is very similar to the existing one, with some extra steps: + +* We store the identity from the request in the `$identity` variable for later use. +* If the identity is empty we throw a `BadRequestException` with an appropriate message. +* If we can't find the user in the database, we throw an `NotFoundException`. +* If the record is found, we generate and return the response. + +The next step is to register the new handler. +To do this, go to `src/User/src/ConfigProvider.php`. +In the `getDependencies()` method under the `factories` key add `IdentityHandler::class => AttributedServiceFactory::class,` + +Next, create the route in `src/User/src/RoutesDelegator.php`: + +```php + $app->get( + '/user/{identity}', + IdentityHandler::class, + 'user.view.identity' + ); +``` + +### Note + +> Make sure to register the endpoint as the last one to not shadow existing endpoints. + +The last step is to set permissions on the newly created route. + +Go to `config/autoload/authorization.global.php` and add our route name (`user.view.identity`) under the `UserRole::ROLE_GUEST` key. +This will give access to every user, including guests, to view other accounts (for the sake of simplicity). + +### Writing tests + +Because every new piece of code should be tested, we will write some tests for this endpoint also. + +In the `test/Functional` folder create a new php class `IdentityTest.php`: + +```php +get('/user/'); + + $this->assertResponseNotFound($response); + } + + public function testInvalidIdentityReturnsNotFound(): void + { + $response = $this->get('/user/invalid_identity'); + $messages = json_decode($response->getBody()->getContents(), true); + + $this->assertResponseNotFound($response); + $this->assertNotEmpty($messages); + $this->assertIsArray($messages); + $this->assertNotEmpty($messages['error']['messages'][0]); + $this->assertIsString($messages['error']['messages'][0]); + $this->assertSame(Message::USER_NOT_FOUND, $messages['error']['messages'][0]); + } + + public function testValidIdentityReturnsUser(): void + { + $this->createUser([ + 'identity' => 'valid_user', + ]); + + $response = $this->get('/user/valid_user'); + + $this->assertResponseOk($response); + $user = json_decode($response->getBody()->getContents(), true); + + $this->assertSame('valid_user', $user['identity']); + } +} +``` + +Planning and coding a new feature can be challenging at times, but reviewing our existing code or tutorials can serve as a source of inspiration. + +## FAQ + +**Q: How do I find the code behind an existing endpoint?** + +A: List the routes with `php ./bin/cli.php route:list`, then search for the route name in the module's `RoutesDelegator.php` to find the handler it points to. +See [Displaying Dotkernel API endpoints](../commands/display-available-endpoints.md). + +**Q: What are the steps to add an endpoint?** + +A: Create the handler, register it in the module's `ConfigProvider` under `factories`, declare the route in `RoutesDelegator.php`, and grant the route name a permission in `config/autoload/authorization.global.php`. + +**Q: Why must the new route be registered last?** + +A: Because `/user/{identity}` and `/user/{id}` match the same shape. +Registering the new route last stops it from shadowing the existing ones. + +**Q: Which factory do I register the handler with?** + +A: `AttributedServiceFactory::class`, which resolves the dependencies declared by the handler's `#[Inject]` attribute. +See [Dependency injection](../core-features/dependency-injection.md). + +**Q: Why does the handler throw two different exceptions?** + +A: `BadRequestException` covers a missing identity in the request (a client error in the input), while `NotFoundException` covers a valid identity with no matching record. +They map to 400 and 404 respectively. +See [Exceptions](../core-features/exceptions.md). + +**Q: Why is the route added under `UserRole::ROLE_GUEST`?** + +A: Only to keep the example simple — it lets everyone, including guests, view accounts. +Real deployments should grant it to the narrowest role that needs it. +See [Authorization](../core-features/authorization.md). + +**Q: Will the endpoint work without an authorization entry?** + +A: No. +A route with no permission granted to the caller's role is refused, even though the handler and route exist. + +**Q: What should the tests cover?** + +A: The three outcomes: an empty identity, an identity with no matching user, and a valid identity returning the expected record. + +**Q: Where do functional tests live?** + +A: In the `test/Functional` folder, extending `AbstractFunctionalTest`. +See [Test the installation](../installation/test-the-installation.md). diff --git a/docs/book/v7/tutorials/token-authentication.md b/docs/book/v7/tutorials/token-authentication.md index 362b220..b93db45 100644 --- a/docs/book/v7/tutorials/token-authentication.md +++ b/docs/book/v7/tutorials/token-authentication.md @@ -383,7 +383,8 @@ The `client_id` and `client_secret` must match the account you are authenticatin **Q: Do I have to generate a token for every request?** -A: No. Generate it once, store it, and reuse it until it expires — then refresh rather than re-authenticate. +A: No. +Generate it once, store it, and reuse it until it expires — then refresh rather than re-authenticate. **Q: What is the difference between the generate and refresh requests?** @@ -404,7 +405,8 @@ A: The refresh token could not be decrypted — it is malformed, has expired, or **Q: Are the shipped credentials safe to keep?** -A: No. The `admin` / `dotadmin` and `test@dotkernel.com` / `dotkernel` accounts, and the OAuth clients whose secrets equal their names, must be changed or removed before production. +A: No. +The `admin` / `dotadmin` and `test@dotkernel.com` / `dotkernel` accounts, and the OAuth clients whose secrets equal their names, must be changed or removed before production. See [OAuth2 security](../security/oauth2-security.md). **Q: Where should the tokens be stored on the client?** diff --git a/docs/book/v7/upgrading/UPGRADE-7.0.md b/docs/book/v7/upgrading/UPGRADE-7.0.md index 656d157..e438f6e 100644 --- a/docs/book/v7/upgrading/UPGRADE-7.0.md +++ b/docs/book/v7/upgrading/UPGRADE-7.0.md @@ -19,7 +19,8 @@ The headline items are native UUIDs in the database, PostgreSQL support, and the **Q: Is there an automated upgrade from 6.x to 7.0?** -A: No. You implement each listed change manually in your own project. +A: No. +You implement each listed change manually in your own project. See [Upgrades](upgrading.md) for the recommended procedure. **Q: What does the switch to native UUIDs mean for my database?** @@ -29,7 +30,8 @@ Review pull request 456 before touching production data. **Q: Do I have to move to PostgreSQL in 7.0?** -A: No. PostgreSQL is now supported in addition to MariaDB; either is a valid choice. +A: No. +PostgreSQL is now supported in addition to MariaDB; either is a valid choice. **Q: `MethodDeprecation` was removed — how do I deprecate an endpoint now?**