diff --git a/index.html b/index.html index cb29f1c..19703ae 100644 --- a/index.html +++ b/index.html @@ -752,7 +752,44 @@
Dotkernel API
+@@ -942,5 +979,5 @@Search
diff --git a/search/search_index.json b/search/search_index.json index 7e405db..0f13769 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"DotKernel API 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.","title":"Home"},{"location":"#dotkernel-api","text":"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.","title":"DotKernel API"},{"location":"v4/commands/create-admin-account/","text":"Creating admin accounts in Dotkernel API Usage Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Create admin account"},{"location":"v4/commands/create-admin-account/#creating-admin-accounts-in-dotkernel-api","text":"","title":"Creating admin accounts in Dotkernel API"},{"location":"v4/commands/create-admin-account/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Usage"},{"location":"v4/commands/display-available-endpoints/","text":"Displaying Dotkernel API endpoints using dot-cli Usage Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ Filtering results The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Display available endpoints"},{"location":"v4/commands/display-available-endpoints/#displaying-dotkernel-api-endpoints-using-dot-cli","text":"","title":"Displaying Dotkernel API endpoints using dot-cli"},{"location":"v4/commands/display-available-endpoints/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"Usage"},{"location":"v4/commands/display-available-endpoints/#filtering-results","text":"The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Filtering results"},{"location":"v4/commands/generate-database-migrations/","text":"Generate a database migration without dropping custom tables Usage 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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' Filtering multiple unmapped table patterns If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' Troubleshooting On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt Help You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Generate database migrations"},{"location":"v4/commands/generate-database-migrations/#generate-a-database-migration-without-dropping-custom-tables","text":"","title":"Generate a database migration without dropping custom tables"},{"location":"v4/commands/generate-database-migrations/#usage","text":"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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/'","title":"Usage"},{"location":"v4/commands/generate-database-migrations/#filtering-multiple-unmapped-table-patterns","text":"If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/'","title":"Filtering multiple unmapped table patterns"},{"location":"v4/commands/generate-database-migrations/#troubleshooting","text":"On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt","title":"Troubleshooting"},{"location":"v4/commands/generate-database-migrations/#help","text":"You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Help"},{"location":"v4/commands/generate-tokens/","text":"Generating tokens in Dotkernel API This is a multipurpose command that allows creating tokens required by different parts of the API. Usage Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help Generate error reporting token You can generate an error reporting token by executing the following command: php ./bin/cli.php token:generate error-reporting The output should look similar to this: Error reporting token: 0123456789abcdef0123456789abcdef01234567 Copy the generated token. Open config/autoload/error-handling.global.php and paste the copied token as shown below: return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Save and close config/autoload/error-handling.global.php . Note : If your application is NOT in development mode, make sure you clear your config cache by executing: php ./bin/clear-config-cache.php","title":"Generate tokens"},{"location":"v4/commands/generate-tokens/#generating-tokens-in-dotkernel-api","text":"This is a multipurpose command that allows creating tokens required by different parts of the API.","title":"Generating tokens in Dotkernel API"},{"location":"v4/commands/generate-tokens/#usage","text":"Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help","title":"Usage"},{"location":"v4/core-features/authentication/","text":"Authentication Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface . Configuration Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info. How it works Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware . Database When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope. Issuing API Tokens Token generation in Dotkernel API is done using the password grand_type scenario, which in this case allows authentication to an API using the user's credentials (generally a username and password). The client sends a POST request to the /security/generate-token with the following parameters: grant_type = password. client_id = column name from the oauth_clients table client_secret = column secret from the oauth_clients table scope = column scope from the oauth_scopes table username = column identity from table admin / user password = column password from table admin / user POST /security/generate-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" } Next time when you make a request to the server to an authenticated endpoint, the client should use the Authorization header request. GET /users/1 HTTP/1.1 Accept: application/json Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... Refreshing tokens Dotkernel API can refresh the access token, based on the expired access token's refresh_token . The clients need to send a POST request to the /security/refresh-token with the following request POST /security/refresh-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\" : \"def5020087199939a49d0f2f818...\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" }","title":"Authentication"},{"location":"v4/core-features/authentication/#authentication","text":"Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface .","title":"Authentication"},{"location":"v4/core-features/authentication/#configuration","text":"Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info.","title":"Configuration"},{"location":"v4/core-features/authentication/#how-it-works","text":"Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware .","title":"How it works"},{"location":"v4/core-features/authentication/#database","text":"When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope.","title":"Database"},{"location":"v4/core-features/authorization/","text":"Authorization Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC). How it works In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware. Configuration Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information. Usage Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Authorization"},{"location":"v4/core-features/authorization/#authorization","text":"Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC).","title":"Authorization"},{"location":"v4/core-features/authorization/#how-it-works","text":"In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware.","title":"How it works"},{"location":"v4/core-features/authorization/#configuration","text":"Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information.","title":"Configuration"},{"location":"v4/core-features/authorization/#usage","text":"Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Usage"},{"location":"v4/core-features/content-validation/","text":"Content Negotiation Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary. Configuration In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against. Accept Negotiation This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything. Content-Type Negotiation The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can. The Request <-> Response validation In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"Content Validation"},{"location":"v4/core-features/content-validation/#content-negotiation","text":"Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary.","title":"Content Negotiation"},{"location":"v4/core-features/content-validation/#configuration","text":"In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against.","title":"Configuration"},{"location":"v4/core-features/content-validation/#accept-negotiation","text":"This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything.","title":"Accept Negotiation"},{"location":"v4/core-features/content-validation/#content-type-negotiation","text":"The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can.","title":"Content-Type Negotiation"},{"location":"v4/core-features/content-validation/#the-request-response-validation","text":"In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"The Request <-> Response validation"},{"location":"v4/core-features/cors/","text":"CORS 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. 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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ). How to fix? Dotkernel API fixes this issue using the mezzio/mezzio-cors library. Step 1: Install library In order to install mezzio/mezzio-cors , run the following command: composer require mezzio/mezzio-cors Step 2: Configure your API Register ConfigProvider Register mezzio/mezzio-cors in your application by adding its ConfigProvider to your application's config aggregator. Open the file config/config.php and paste the below lines at the beginning of the array passed to ConfigAggregator : Laminas\\Diactoros\\ConfigProvider::class, Mezzio\\Cors\\ConfigProvider::class, Save and close the file. Add middleware Add mezzio/mezzio-cors middleware to your application's pipeline. Open config/pipeline.php and paste the below line before the one with RouteMiddleware::class : $app->pipe(\\Mezzio\\Cors\\Middleware\\CorsMiddleware::class); Save and close the file. Create config file Create and open file config/autoload/cors.local.php and add the following code inside it: <?php declare(strict_types=1); use Mezzio\\Cors\\Configuration\\ConfigurationInterface; return [ ConfigurationInterface::CONFIGURATION_IDENTIFIER => [ 'allowed_origins' => [ ConfigurationInterface::ANY_ORIGIN, ], 'allowed_headers' => ['Accept', 'Content-Type', 'Authorization'], 'allowed_max_age' => '600', 'credentials_allowed' => true, 'exposed_headers' => [], ], ]; This list explains the above configuration values: allowed_origins : an array of domains that are allowed to interact with the API (default ConfigurationInterface::ANY_ORIGIN which means that any domain can make requests to the API) allowed_headers : an array of allowed custom headers allowed_max_age : the maximum duration, since the preflight response may be cached by a client credentials_allowed : allows a request to pass cookies exposed_headers : an array of headers which are being exposed by the endpoint Save and close the file. On the production environment, make sure you allow only specific origins by adding them to the allowed_origins array and removing the current value of ConfigurationInterface::ANY_ORIGIN . For more info, see mezzio/mezzio-cors documentation .","title":"CORS"},{"location":"v4/core-features/cors/#cors","text":"","title":"CORS"},{"location":"v4/core-features/cors/#what-is-cors","text":"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.","title":"What is CORS?"},{"location":"v4/core-features/cors/#why-do-we-need-cors","text":"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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ).","title":"Why do we need CORS?"},{"location":"v4/core-features/cors/#how-to-fix","text":"Dotkernel API fixes this issue using the mezzio/mezzio-cors library.","title":"How to fix?"},{"location":"v4/core-features/exceptions/","text":"Exceptions What are exceptions? Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code. How we use exceptions? When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Out-of-the-box we provide the following custom exceptions: BadRequestException thrown when client tries to create/update resource, but the data from the request is invalid/incomplete (example: client tries to create an account, but does not send the required identity field) ConflictException thrown when resource cannot be created because a different resource with the same identifier already exists (example: cannot change existing user's identity because another user with the same identity already exists) resource cannot change its state because it is already in the specified state (example: user cannot be activated because it is already active) ExpiredException thrown when resource cannot be accessed because it expired (example: account activation link) resource cannot be accessed because it has been consumed (example: one-time password) ForbiddenException thrown when resource cannot be accessed by the authenticated client (example: client authenticated as regular user sends a GET /admin request) MethodNotAllowedException thrown when client tries to interact with a resource via an invalid HTTP request method (example: client sends a PATCH /avatar request) NotFoundException thrown when client tries to interact with a resource that does not exist on the server (example: client sends a GET /resource-does-not-exist request) UnauthorizedException thrown when resource cannot be accessed because the client is not authenticated (example: unauthenticated client sends a GET /admin request) How it works? During a request, if there is no uncaught exception Dotkernel API will return a JSON response with the data provided by the handler that handled the request. Else, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response How to extend? In this example we will create a custom exception called CustomException , place it next to the already existing custom exceptions (you can use your preferred location) and finally return a custom HTTP status code when CustomException is encountered. Step 1: Create exception file Navigate to the directory src/App/src/Handler/Exception and create a PHP class called CustomException.php . Open CustomException.php and add the following content: <?php declare(strict_types=1); namespace Api\\App\\Exception; use Exception; class CustomException extends Exception { } Save and close the file. Step 2: Use exception file Open the file src/App/src/Handler/HomeHandler.php and at the beginning of the get method, place the following code: throw new \\Api\\App\\Exception\\CustomException('some message'); Save and close the file. Step 3: Test for failure Access your API's home page URL and make sure it returns 500 Internal Server Error HTTP status code and the following content: { \"error\": { \"messages\": [ \"some message\" ] } } Step 4: Prepare for success Open the file src/App/src/Handler/HandlerTrait.php and locate the handle method. Insert the following lines of code before the first catch statement: } catch (\\Api\\App\\Exception\\CustomException $exception) { return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_IM_A_TEAPOT); Save and close the file. Step 5: Test for success Again, access your API's home page URL, which should return the same content. Notice that this time it returns 418 I'm a teapot HTTP status code.","title":"Exceptions"},{"location":"v4/core-features/exceptions/#exceptions","text":"","title":"Exceptions"},{"location":"v4/core-features/exceptions/#what-are-exceptions","text":"Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code.","title":"What are exceptions?"},{"location":"v4/core-features/exceptions/#how-we-use-exceptions","text":"When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Out-of-the-box we provide the following custom exceptions:","title":"How we use exceptions?"},{"location":"v4/core-features/exceptions/#how-it-works","text":"During a request, if there is no uncaught exception Dotkernel API will return a JSON response with the data provided by the handler that handled the request. Else, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response","title":"How it works?"},{"location":"v4/core-features/exceptions/#how-to-extend","text":"In this example we will create a custom exception called CustomException , place it next to the already existing custom exceptions (you can use your preferred location) and finally return a custom HTTP status code when CustomException is encountered.","title":"How to extend?"},{"location":"v4/flow/default-library-flow/","text":"Default Library Flow The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v4/flow/default-library-flow/#default-library-flow","text":"The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v4/flow/library-flow-for-email/","text":"Library Flow for Email The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v4/flow/library-flow-for-email/#library-flow-for-email","text":"The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v4/flow/middleware-flow/","text":"Middleware flow The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware Flow"},{"location":"v4/flow/middleware-flow/#middleware-flow","text":"The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware flow"},{"location":"v4/installation/composer/","text":"Composer Installation of Packages Install dependencies composer install Development mode If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Composer"},{"location":"v4/installation/composer/#composer-installation-of-packages","text":"","title":"Composer Installation of Packages"},{"location":"v4/installation/composer/#install-dependencies","text":"composer install","title":"Install dependencies"},{"location":"v4/installation/composer/#development-mode","text":"If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Development mode"},{"location":"v4/installation/configuration-files/","text":"Configuration Files Prepare config files duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php Note if your API will be consumed by another application, make sure to configure the allowed_origins variable duplicate config/autoload/local.php.dist as config/autoload/local.php duplicate config/autoload/mail.local.php.dist as config/autoload/mail.local.php Note if your API will send emails, make sure to fill in SMTP connection params optional : in order to run/create tests, duplicate config/autoload/local.test.php.dist as config/autoload/local.test.php Note this creates a new in-memory database that your tests will run on.","title":"Configuration Files"},{"location":"v4/installation/configuration-files/#configuration-files","text":"","title":"Configuration Files"},{"location":"v4/installation/configuration-files/#prepare-config-files","text":"duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php","title":"Prepare config files"},{"location":"v4/installation/doctrine-orm/","text":"Doctrine ORM Setup database Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci Running migrations Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation. Executing fixtures Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found here: https://github.com/dotkernel/dot-data-fixtures#creating-fixtures","title":"Doctrine ORM"},{"location":"v4/installation/doctrine-orm/#doctrine-orm","text":"","title":"Doctrine ORM"},{"location":"v4/installation/doctrine-orm/#setup-database","text":"Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci","title":"Setup database"},{"location":"v4/installation/doctrine-orm/#running-migrations","text":"Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation.","title":"Running migrations"},{"location":"v4/installation/doctrine-orm/#executing-fixtures","text":"Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found here: https://github.com/dotkernel/dot-data-fixtures#creating-fixtures","title":"Executing fixtures"},{"location":"v4/installation/faq/","text":"Frequently Asked Questions How do I fix common permission issues? If running your project you encounter some permission issues, follow the below steps. Errors PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache/doctrine\" is not writable... Fix: chmod -R 777 data Error PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/public/uploads\" is not writable... Fix: chmod -R 777 public/uploads Error PHP Fatal error: Uncaught ErrorException: fopen(/var/www/ example.local /config/autoload/../../log/error-log- yyyy-mm-dd.log ): Failed to open stream: Permission denied... Fix: chmod -R 777 log","title":"FAQ"},{"location":"v4/installation/faq/#frequently-asked-questions","text":"","title":"Frequently Asked Questions"},{"location":"v4/installation/faq/#how-do-i-fix-common-permission-issues","text":"If running your project you encounter some permission issues, follow the below steps.","title":"How do I fix common permission issues?"},{"location":"v4/installation/getting-started/","text":"Clone the project Recommended development environment If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Getting Started"},{"location":"v4/installation/getting-started/#clone-the-project","text":"","title":"Clone the project"},{"location":"v4/installation/getting-started/#recommended-development-environment","text":"If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Recommended development environment"},{"location":"v4/installation/test-the-installation/","text":"Test the installation Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 4\"} Old way of doing things, using PHP built-in server php -S 0.0.0.0:8080 -t public Running tests The project has 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","title":"Test the Installation"},{"location":"v4/installation/test-the-installation/#test-the-installation","text":"Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 4\"}","title":"Test the installation"},{"location":"v4/installation/test-the-installation/#old-way-of-doing-things-using-php-built-in-server","text":"php -S 0.0.0.0:8080 -t public","title":"Old way of doing things, using PHP built-in server"},{"location":"v4/installation/test-the-installation/#running-tests","text":"The project has 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","title":"Running tests"},{"location":"v4/installation/test-the-installation/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v4/installation/test-the-installation/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v4/introduction/file-structure/","text":"File structure Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default: Main directories bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files Special purpose folders .github - containes workflow files .laminas-ci - contains laminas-ci workflow files src directory This directory contains all source code related to the Module. It should contain following directories, if they’re not empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - For database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but these should give you an idea of how the structure should look like. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory should also contain 2 files: ConfigProvider.php - Provides configuration data RoutesDelegator.php - Module main routes entry file templates directory This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig data directory This directory contains project-related data (such as cache, file uploads) We recommend using the following directory structure: data/cache - location where caches are stored data/oauth - encryption, private and public keys needed for authentication. data/doctrine - fixtures and migrations data/lock - lock files generated by dotkernel/dot-cli See more","title":"File Structure"},{"location":"v4/introduction/file-structure/#file-structure","text":"Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default:","title":"File structure"},{"location":"v4/introduction/file-structure/#main-directories","text":"bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files","title":"Main directories"},{"location":"v4/introduction/file-structure/#special-purpose-folders","text":".github - containes workflow files .laminas-ci - contains laminas-ci workflow files","title":"Special purpose folders"},{"location":"v4/introduction/file-structure/#src-directory","text":"This directory contains all source code related to the Module. It should contain following directories, if they’re not empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - For database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but these should give you an idea of how the structure should look like. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory should also contain 2 files: ConfigProvider.php - Provides configuration data RoutesDelegator.php - Module main routes entry file","title":"src directory"},{"location":"v4/introduction/file-structure/#templates-directory","text":"This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig","title":"templates directory"},{"location":"v4/introduction/file-structure/#data-directory","text":"This directory contains project-related data (such as cache, file uploads) We recommend using the following directory structure: data/cache - location where caches are stored data/oauth - encryption, private and public keys needed for authentication. data/doctrine - fixtures and migrations data/lock - lock files generated by dotkernel/dot-cli See more","title":"data directory"},{"location":"v4/introduction/introduction/","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Annotations (dotkernel/dot-annotated-services) Input Filter (laminas/laminas-inputfilter) Doctrine 2 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/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.0 OAuth 2.0 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 OAuth 2.0 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","title":"Introduction"},{"location":"v4/introduction/introduction/#introduction","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Annotations (dotkernel/dot-annotated-services) Input Filter (laminas/laminas-inputfilter) Doctrine 2 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/mezzio-hal) CLI (dotkernel/dot-cli) TwigRenderer (mezzio/mezzio-twigrenderer) Fixtures (dotkernel/dot-data-fixtures) UUID (ramsey/uuid-doctrine)","title":"Introduction"},{"location":"v4/introduction/introduction/#doctrine-2-orm","text":"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.","title":"Doctrine 2 ORM"},{"location":"v4/introduction/introduction/#documentation","text":"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","title":"Documentation"},{"location":"v4/introduction/introduction/#hypertext-application-language","text":"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.","title":"Hypertext Application Language"},{"location":"v4/introduction/introduction/#cors","text":"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.","title":"CORS"},{"location":"v4/introduction/introduction/#oauth-20","text":"OAuth 2.0 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 OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using league/oauth2-server package.","title":"OAuth 2.0"},{"location":"v4/introduction/introduction/#email","text":"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.","title":"Email"},{"location":"v4/introduction/introduction/#configuration","text":"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.","title":"Configuration"},{"location":"v4/introduction/introduction/#routing","text":"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 .","title":"Routing"},{"location":"v4/introduction/introduction/#commands","text":"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 .","title":"Commands"},{"location":"v4/introduction/introduction/#file-locker","text":"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.","title":"File locker"},{"location":"v4/introduction/introduction/#psr-standards","text":"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","title":"PSR Standards"},{"location":"v4/introduction/introduction/#tests","text":"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","title":"Tests"},{"location":"v4/introduction/introduction/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v4/introduction/introduction/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v4/introduction/packages/","text":"Packages dotkernel/dot-annotated-services - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-http - Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-paginator - Paginate collections of data from arbitrary sources laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more laminas/laminas-text - Create FIGlets and text-based tables mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v4/introduction/packages/#packages","text":"dotkernel/dot-annotated-services - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-http - Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-paginator - Paginate collections of data from arbitrary sources laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more laminas/laminas-text - Create FIGlets and text-based tables mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v4/introduction/server-requirements/","text":"Server Requirements For production, we highly recommend a *nix based system. Webserver Apache >= 2.2 or Nginx mod_rewrite .htaccess support (AllowOverride All) PHP >= 8.2 Both mod_php and FCGI (FPM) are supported. Required Settings and Modules & Extensions memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH) RDBMS MySQL / MariaDB >= 5.5.3 Recommended extensions opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used)","title":"Server Requirements"},{"location":"v4/introduction/server-requirements/#server-requirements","text":"For production, we highly recommend a *nix based system.","title":"Server Requirements"},{"location":"v4/introduction/server-requirements/#webserver","text":"Apache >= 2.2 or Nginx mod_rewrite .htaccess support (AllowOverride All)","title":"Webserver"},{"location":"v4/introduction/server-requirements/#php-82","text":"Both mod_php and FCGI (FPM) are supported.","title":"PHP >= 8.2"},{"location":"v4/introduction/server-requirements/#required-settings-and-modules-extensions","text":"memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH)","title":"Required Settings and Modules & Extensions"},{"location":"v4/introduction/server-requirements/#rdbms","text":"MySQL / MariaDB >= 5.5.3","title":"RDBMS"},{"location":"v4/introduction/server-requirements/#recommended-extensions","text":"opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used)","title":"Recommended extensions"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/","text":"Laminas API Tools compared to Dotkernel API API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15 Note Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Laminas API Tools vs Dotkernel API"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/#laminas-api-tools-compared-to-dotkernel-api","text":"API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15","title":"Laminas API Tools compared to Dotkernel API"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/#note","text":"Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Note"},{"location":"v4/transition-from-api-tools/discovery-phase/","text":"Discovery phase for a current system built using API Tools [WIP] In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it. Database there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else ) Note Dotkernel API is tested only with MariaDB version 10.6 and 10.11 LTS Authentication and Authorization how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac) Modules analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations Custom functionalities Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Discovery Phase"},{"location":"v4/transition-from-api-tools/discovery-phase/#discovery-phase-for-a-current-system-built-using-api-tools-wip","text":"In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it.","title":"Discovery phase for a current system built using API Tools [WIP]"},{"location":"v4/transition-from-api-tools/discovery-phase/#database","text":"there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else )","title":"Database"},{"location":"v4/transition-from-api-tools/discovery-phase/#authentication-and-authorization","text":"how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac)","title":"Authentication and Authorization"},{"location":"v4/transition-from-api-tools/discovery-phase/#modules","text":"analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations","title":"Modules"},{"location":"v4/transition-from-api-tools/discovery-phase/#custom-functionalities","text":"Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Custom functionalities"},{"location":"v4/transition-from-api-tools/transition-approach/","text":"Transition approach [WIP] Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools Business cases There are at least 2 approaches for this transition: Clone 1:1 and recreate all endpoints and entities Build a new version of the current API using Dotkernel API and keep it running as separate platforms until the sunset of the current version of api-tools","title":"Transition Approach"},{"location":"v4/transition-from-api-tools/transition-approach/#transition-approach-wip","text":"Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools","title":"Transition approach [WIP]"},{"location":"v4/transition-from-api-tools/transition-approach/#business-cases","text":"There are at least 2 approaches for this transition:","title":"Business cases"},{"location":"v4/tutorials/create-book-module/","text":"Implementing a book module in Dotkernel 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 ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/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 declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\Book\\Repository\\BookRepository; use DateTimeImmutable; use Doctrine\\ORM\\Mapping as ORM; #[ORM\\Entity(repositoryClass: BookRepository::class)] #[ORM\\Table(\"book\")] class Book extends AbstractEntity { #[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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\AnnotatedServices\\Annotation\\Entity; /** * @Entity(name=\"Api\\Book\\Entity\\Book\") * @extends EntityRepository<object> */ 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 declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\AnnotatedServices\\Annotation\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { /** * @Inject({ * BookRepository::class, * }) */ public function __construct(protected BookRepository $bookRepository) { } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } src/Book/src/Service/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; interface BookServiceInterface { } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Dot\\AnnotatedServices\\Factory\\AnnotatedRepositoryFactory; use Dot\\AnnotatedServices\\Factory\\AnnotatedServiceFactory; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $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 namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $app->get( '/books', BookHandler::class, 'books.list' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\ResponseTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\AnnotatedServices\\Annotation\\Inject; class BookHandler implements RequestHandlerInterface { use ResponseTrait; /** * @Inject({ * HalResponseFactory::class, * ResourceGenerator::class, * BookServiceInterface::class * }) */ public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService ) { } public function get(ServerRequestInterface $request): ResponseInterface { $books = $this->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 update Composer autoloader by running the command: composer dump-autoload It should look like this: 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 this under the doctrine.driver key: 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/../../src/Book/src/Entity', ], Api\\\\Book\\Entity' => 'BookEntities', add this under the doctrine.driver.drivers key Example: <?php ... return [ 'doctrine' => [ ... '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://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books","title":"Creating a book module"},{"location":"v4/tutorials/create-book-module/#implementing-a-book-module-in-dotkernel-api","text":"","title":"Implementing a book module in Dotkernel API"},{"location":"v4/tutorials/create-book-module/#file-structure","text":"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 ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/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","title":"File structure"},{"location":"v4/tutorials/create-book-module/#file-creation-and-contents","text":"src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\Book\\Repository\\BookRepository; use DateTimeImmutable; use Doctrine\\ORM\\Mapping as ORM; #[ORM\\Entity(repositoryClass: BookRepository::class)] #[ORM\\Table(\"book\")] class Book extends AbstractEntity { #[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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\AnnotatedServices\\Annotation\\Entity; /** * @Entity(name=\"Api\\Book\\Entity\\Book\") * @extends EntityRepository<object> */ 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 declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\AnnotatedServices\\Annotation\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { /** * @Inject({ * BookRepository::class, * }) */ public function __construct(protected BookRepository $bookRepository) { } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } src/Book/src/Service/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; interface BookServiceInterface { } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Dot\\AnnotatedServices\\Factory\\AnnotatedRepositoryFactory; use Dot\\AnnotatedServices\\Factory\\AnnotatedServiceFactory; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $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 namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $app->get( '/books', BookHandler::class, 'books.list' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\ResponseTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\AnnotatedServices\\Annotation\\Inject; class BookHandler implements RequestHandlerInterface { use ResponseTrait; /** * @Inject({ * HalResponseFactory::class, * ResourceGenerator::class, * BookServiceInterface::class * }) */ public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService ) { } public function get(ServerRequestInterface $request): ResponseInterface { $books = $this->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); } }","title":"File creation and contents"},{"location":"v4/tutorials/create-book-module/#configuring-and-registering-the-new-module","text":"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 update Composer autoloader by running the command: composer dump-autoload It should look like this: 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 this under the doctrine.driver key: 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/../../src/Book/src/Entity', ], Api\\\\Book\\Entity' => 'BookEntities', add this under the doctrine.driver.drivers key Example: <?php ... return [ 'doctrine' => [ ... '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.","title":"Configuring and registering the new module"},{"location":"v4/tutorials/create-book-module/#migrations","text":"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","title":"Migrations"},{"location":"v4/tutorials/create-book-module/#checking-endpoints","text":"If we did everything as planned we can call the http://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books","title":"Checking endpoints"},{"location":"v4/tutorials/token-authentication/","text":"Token authentication What is token authentication? Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string. How does it work? In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel Flow client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource Note The first two steps need to executed only once. Access token should be stored and reused for all upcoming requests. Refresh token should be stored and used to refresh expired access token. For a better overview of the flow, see the below image: Generate admin access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your admin account's identity and <password> with your admin account's password . Both fields come from table admin . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' Generate user access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your user account's identity and <password> with your user account's password . Both fields come from table user . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (modify in: config/autoload/local.php > authentication . access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 400 Bad Request response with the following JSON body: { \"error\": \"Invalid credentials.\", \"error_description\": \"Invalid credentials.\", \"message\": \"Invalid credentials.\" } Refresh admin access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Refresh user access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (change here: config/autoload/local.php authentication -> access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 401 Unauthorized response with the following JSON body: { \"error\": \"invalid_request\", \"error_description\": \"The refresh token is invalid.\", \"hint\": \"Cannot decrypt the refresh token\", \"message\": \"The refresh token is invalid.\" } Test admin authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body. Test user authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body.","title":"Token authentication"},{"location":"v4/tutorials/token-authentication/#token-authentication","text":"","title":"Token authentication"},{"location":"v4/tutorials/token-authentication/#what-is-token-authentication","text":"Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string.","title":"What is token authentication?"},{"location":"v4/tutorials/token-authentication/#how-does-it-work","text":"In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel","title":"How does it work?"},{"location":"v4/tutorials/token-authentication/#flow","text":"client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource","title":"Flow"},{"location":"v4/tutorials/token-authentication/#generate-admin-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate admin access token"},{"location":"v4/tutorials/token-authentication/#generate-user-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate user access token"},{"location":"v4/tutorials/token-authentication/#refresh-admin-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh admin access token"},{"location":"v4/tutorials/token-authentication/#refresh-user-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh user access token"},{"location":"v4/tutorials/token-authentication/#test-admin-authentication-flow","text":"","title":"Test admin authentication flow"},{"location":"v4/tutorials/token-authentication/#test-user-authentication-flow","text":"","title":"Test user authentication flow"},{"location":"v5/upgrading/","text":"Upgrades Dotkernel API does not provide an automatic upgrade path. Instead, the recommended procedure is to manually implement each modification listed in releases . Additionally, release info can also be accessed as an RSS feed. Upgrade procedure Once you clone Dotkernel API, you will find a CHANGELOG.md file in the root of the project. This file contains a list of already implemented features in reverse chronological order. You can use this file to track the version of your copy of Dotkernel API. For each new release you need implement the modifications from its pull requests in your project. It is recommended to copy the release info into your project's CHANGELOG.md file. This allows you to track your API's version and keep your project up-to-date with future releases.","title":"Upgrading"},{"location":"v5/upgrading/#upgrades","text":"Dotkernel API does not provide an automatic upgrade path. Instead, the recommended procedure is to manually implement each modification listed in releases . Additionally, release info can also be accessed as an RSS feed.","title":"Upgrades"},{"location":"v5/upgrading/#upgrade-procedure","text":"Once you clone Dotkernel API, you will find a CHANGELOG.md file in the root of the project. This file contains a list of already implemented features in reverse chronological order. You can use this file to track the version of your copy of Dotkernel API. For each new release you need implement the modifications from its pull requests in your project. It is recommended to copy the release info into your project's CHANGELOG.md file. This allows you to track your API's version and keep your project up-to-date with future releases.","title":"Upgrade procedure"},{"location":"v5/commands/create-admin-account/","text":"Creating admin accounts in Dotkernel API Usage Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Create admin account"},{"location":"v5/commands/create-admin-account/#creating-admin-accounts-in-dotkernel-api","text":"","title":"Creating admin accounts in Dotkernel API"},{"location":"v5/commands/create-admin-account/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Usage"},{"location":"v5/commands/display-available-endpoints/","text":"Displaying Dotkernel API endpoints using dot-cli Usage Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ Filtering results The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Display available endpoints"},{"location":"v5/commands/display-available-endpoints/#displaying-dotkernel-api-endpoints-using-dot-cli","text":"","title":"Displaying Dotkernel API endpoints using dot-cli"},{"location":"v5/commands/display-available-endpoints/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"Usage"},{"location":"v5/commands/display-available-endpoints/#filtering-results","text":"The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Filtering results"},{"location":"v5/commands/generate-database-migrations/","text":"Generate a database migration without dropping custom tables Usage 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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' Filtering multiple unmapped table patterns If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' Troubleshooting On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt Help You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Generate database migrations"},{"location":"v5/commands/generate-database-migrations/#generate-a-database-migration-without-dropping-custom-tables","text":"","title":"Generate a database migration without dropping custom tables"},{"location":"v5/commands/generate-database-migrations/#usage","text":"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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/'","title":"Usage"},{"location":"v5/commands/generate-database-migrations/#filtering-multiple-unmapped-table-patterns","text":"If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/'","title":"Filtering multiple unmapped table patterns"},{"location":"v5/commands/generate-database-migrations/#troubleshooting","text":"On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt","title":"Troubleshooting"},{"location":"v5/commands/generate-database-migrations/#help","text":"You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Help"},{"location":"v5/commands/generate-tokens/","text":"Generating tokens in Dotkernel API This is a multipurpose command that allows creating tokens required by different parts of the API. Usage Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help Generate error reporting token You can generate an error reporting token by executing the following command: php ./bin/cli.php token:generate error-reporting The output should look similar to this: Error reporting token: 0123456789abcdef0123456789abcdef01234567 Copy the generated token. Open config/autoload/error-handling.global.php and paste the copied token as shown below: return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Save and close config/autoload/error-handling.global.php . Note : If your application is NOT in development mode, make sure you clear your config cache by executing: php ./bin/clear-config-cache.php","title":"Generate tokens"},{"location":"v5/commands/generate-tokens/#generating-tokens-in-dotkernel-api","text":"This is a multipurpose command that allows creating tokens required by different parts of the API.","title":"Generating tokens in Dotkernel API"},{"location":"v5/commands/generate-tokens/#usage","text":"Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help","title":"Usage"},{"location":"v5/core-features/authentication/","text":"Authentication Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface . Configuration Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info. How it works Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware . Database When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope. Issuing API Tokens Token generation in Dotkernel API is done using the password grand_type scenario, which in this case allows authentication to an API using the user's credentials (generally a username and password). The client sends a POST request to the /security/generate-token with the following parameters: grant_type = password. client_id = column name from the oauth_clients table client_secret = column secret from the oauth_clients table scope = column scope from the oauth_scopes table username = column identity from table admin / user password = column password from table admin / user POST /security/generate-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" } Next time when you make a request to the server to an authenticated endpoint, the client should use the Authorization header request. GET /users/1 HTTP/1.1 Accept: application/json Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... Refreshing tokens Dotkernel API can refresh the access token, based on the expired access token's refresh_token . The clients need to send a POST request to the /security/refresh-token with the following request POST /security/refresh-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\" : \"def5020087199939a49d0f2f818...\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" }","title":"Authentication"},{"location":"v5/core-features/authentication/#authentication","text":"Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface .","title":"Authentication"},{"location":"v5/core-features/authentication/#configuration","text":"Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info.","title":"Configuration"},{"location":"v5/core-features/authentication/#how-it-works","text":"Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware .","title":"How it works"},{"location":"v5/core-features/authentication/#database","text":"When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope.","title":"Database"},{"location":"v5/core-features/authorization/","text":"Authorization Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC). How it works In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware. Configuration Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information. Usage Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Authorization"},{"location":"v5/core-features/authorization/#authorization","text":"Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC).","title":"Authorization"},{"location":"v5/core-features/authorization/#how-it-works","text":"In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware.","title":"How it works"},{"location":"v5/core-features/authorization/#configuration","text":"Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information.","title":"Configuration"},{"location":"v5/core-features/authorization/#usage","text":"Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Usage"},{"location":"v5/core-features/content-validation/","text":"Content Negotiation Introduced in Dotkernel API 5.0.0 Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary. Configuration In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against. Accept Negotiation This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything. Content-Type Negotiation The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can. The Request <-> Response validation In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"Content Validation"},{"location":"v5/core-features/content-validation/#content-negotiation","text":"Introduced in Dotkernel API 5.0.0 Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary.","title":"Content Negotiation"},{"location":"v5/core-features/content-validation/#configuration","text":"In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against.","title":"Configuration"},{"location":"v5/core-features/content-validation/#accept-negotiation","text":"This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything.","title":"Accept Negotiation"},{"location":"v5/core-features/content-validation/#content-type-negotiation","text":"The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can.","title":"Content-Type Negotiation"},{"location":"v5/core-features/content-validation/#the-request-response-validation","text":"In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"The Request <-> Response validation"},{"location":"v5/core-features/dependency-injection/","text":"Dependency Injection Dependency injection is a design pattern used in software development to implement inversion of control. In simpler terms, it's the act of providing dependencies for an object during instantiation. In PHP, dependency injection can be implemented in various ways, including through constructor injection, setter injection and property injection. Introduced in Dotkernel API 5.0.0 Dotkernel API, through its dot-dependency-injection package focuses only on constructor injection. Usage Dotkernel API comes out of the box with the dot-dependency-injection package, which provides all we need for injecting dependencies into any object you want. dot-dependency-injection determines the dependencies by looking at the #[Inject] attribute, added to the constructor of a class. Dependencies are specified as separate parameters of the #[Inject] attribute. For our example we will inject UserService and config dependencies into a UseHandler . use Dot\\DependencyInjection\\Attribute\\Inject; class UserHandler implements RequestHandlerInterface { #[Inject( UserService::class, \"config\", )] public function __construct( protected UserServiceInterface $userService, protected array $config, ) { } } If your class needs the value of a specific configuration key, you can specify the path using dot notation config.example . The next step is to register the class in the ConfigProvider under factories using Dot\\DependencyInjection\\Factory\\AttributedServiceFactory::class public function getDependencies(): array { return [ 'factories' => [ UserHandler::class => AttributedServiceFactory::class ] ]; } That's it. When your object is instantiated from the container, it will automatically have its dependencies resolved. Dependencies injection is available to any object within Dotkernel API. For example, you can inject dependencies in a service, a handler and so on, simply by registering it in the ConfigProvider .","title":"Dependency Injection"},{"location":"v5/core-features/dependency-injection/#dependency-injection","text":"Dependency injection is a design pattern used in software development to implement inversion of control. In simpler terms, it's the act of providing dependencies for an object during instantiation. In PHP, dependency injection can be implemented in various ways, including through constructor injection, setter injection and property injection. Introduced in Dotkernel API 5.0.0 Dotkernel API, through its dot-dependency-injection package focuses only on constructor injection.","title":"Dependency Injection"},{"location":"v5/core-features/dependency-injection/#usage","text":"Dotkernel API comes out of the box with the dot-dependency-injection package, which provides all we need for injecting dependencies into any object you want. dot-dependency-injection determines the dependencies by looking at the #[Inject] attribute, added to the constructor of a class. Dependencies are specified as separate parameters of the #[Inject] attribute. For our example we will inject UserService and config dependencies into a UseHandler . use Dot\\DependencyInjection\\Attribute\\Inject; class UserHandler implements RequestHandlerInterface { #[Inject( UserService::class, \"config\", )] public function __construct( protected UserServiceInterface $userService, protected array $config, ) { } } If your class needs the value of a specific configuration key, you can specify the path using dot notation config.example . The next step is to register the class in the ConfigProvider under factories using Dot\\DependencyInjection\\Factory\\AttributedServiceFactory::class public function getDependencies(): array { return [ 'factories' => [ UserHandler::class => AttributedServiceFactory::class ] ]; } That's it. When your object is instantiated from the container, it will automatically have its dependencies resolved. Dependencies injection is available to any object within Dotkernel API. For example, you can inject dependencies in a service, a handler and so on, simply by registering it in the ConfigProvider .","title":"Usage"},{"location":"v5/core-features/error-reporting/","text":"Error reporting endpoint The error reporting endpoint was designed to allow the frontend developers of your API to report any bugs they encounter in a secure way that is fully under your control. To prevent unauthorized usage, the endpoint is protected by a token in the request's header. Example case usage Frontend developed in Angular. Frontend developer will use try-catch in the code in order to send frontend errors back to the API. How to use it on the API side Error reporting is done by sending a POST request to the /error-report endpoint, together with a token in the header. In the sections below we will detail how to configure error reporting in your API and how the endpoint is used by the frontend developers. Generating a token and adding it to your API config First you need to generate a token for your request. This is done by using the command php ./bin/cli.php token:generate error-reporting The resulting token has this format 0123456789abcdef0123456789abcdef01234567 . Note: this example is not a valid token, it just lets you know what to look for. Copy the generated token in your config/autoload/error-handling.global.php file. It should look similar to the example below. Your API can have multiple tokens, if needed. return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Validation mechanism Behind the scenes, the API validates your configuration and lets you know if any config items prevent the submission of the error report. Below are the requirements for an application to be able to send error messages to Dotkernel API. Server-side requirements stored in config/autoload/error-handling.global.php (these can be set/overwritten in config/autoload/local.php ): All keys ( enabled , path , tokens , domain_whitelist and ip_whitelist ) must exist under ErrorReportServiceInterface::class . The error reporting feature must be enabled by setting ErrorReportServiceInterface::class . enabled to true . ErrorReportServiceInterface::class . path must have a value; if the destination file does not exist, it will be created automatically. ErrorReportServiceInterface::class . tokens must contain at least one token. At least one of ErrorReportServiceInterface::class . domain_whitelist / ip_whitelist must have at least one value. Note: In src/App/src/Service/ErrorReportService.php , the method checkRequest() tries to validate the request by checking matches for domain_whitelist with isMatchingDomain() and for ip_whitelist with isMatchingIpAddress() . If both return false , a ForbiddenException is thrown and the error message does not get stored. Application-side requirements : Send the Error-Reporting-Token header with a valid token previously stored in config/autoload/error-handling.global.php in the ErrorReportServiceInterface::class . tokens array. Send the Origin header set to the application's URL; this is the application that sends the error message. Note: The tokens under ErrorReportServiceInterface::class . tokens do not expire. The log file stores the token value too, making it easy to identify which application sent the error message. If your request passes all the checks, the message is saved in the log file specified in ErrorReportServiceInterface::class . path . Tips and tricks If there are multiple applications that report errors to your API, you can assign a different error reporting token for each. The tokens support key-value pairs where: The key is an alias relevant to the assigned application that uses it. The value is the token itself. Example: // ... return [ ... ErrorReportServiceInterface::class => [ // ... 'tokens' => [ 'frontend' => '0123456789abcdef0123456789abcdef01234567', 'admin' => '9876543210abcdef0123456789abcdef7654321', // other tokens ], ], ]; The log file will have entries similar to the below: [2024-08-29 12:47:00] [0123456789abcdef0123456789abcdef01234567] Demo error message The inclusion of the token helps you identify the source of the error message. In our example, it's the application that uses the 0123456789abcdef0123456789abcdef01234567 token, which is assigned to the application frontend . How to use it on the Frontend side (Angular example) The API developer sends a generated token to the frontend developer who will save it in their environment.staging.ts and/or environment.prod.ts . From then on, it's the frontend developer's job to set up an error reporting function similar to the one below. postError(body: object): Promise<any> { return new Promise((resolve, reject) => { return this.http.post(API_ENDPOINT + 'error-report', body , {headers: new HttpHeaders({'Error-Reporting-Token': 'TOKEN', 'Origin': 'https://example.com'})})).subscribe({ next: (response: any) => { resolve(response); }, error: (e: HttpErrorResponse) => reject(e), complete: () => console.info('Error on sending error'), }); }); } Whenever an error is found, the frontend will call postError() with a relevant description under message . apiService.postError({message: 'ERROR MESSAGE'})","title":"Error reporting"},{"location":"v5/core-features/error-reporting/#error-reporting-endpoint","text":"The error reporting endpoint was designed to allow the frontend developers of your API to report any bugs they encounter in a secure way that is fully under your control. To prevent unauthorized usage, the endpoint is protected by a token in the request's header.","title":"Error reporting endpoint"},{"location":"v5/core-features/error-reporting/#example-case-usage","text":"Frontend developed in Angular. Frontend developer will use try-catch in the code in order to send frontend errors back to the API.","title":"Example case usage"},{"location":"v5/core-features/error-reporting/#how-to-use-it-on-the-api-side","text":"Error reporting is done by sending a POST request to the /error-report endpoint, together with a token in the header. In the sections below we will detail how to configure error reporting in your API and how the endpoint is used by the frontend developers.","title":"How to use it on the API side"},{"location":"v5/core-features/error-reporting/#how-to-use-it-on-the-frontend-side-angular-example","text":"The API developer sends a generated token to the frontend developer who will save it in their environment.staging.ts and/or environment.prod.ts . From then on, it's the frontend developer's job to set up an error reporting function similar to the one below. postError(body: object): Promise<any> { return new Promise((resolve, reject) => { return this.http.post(API_ENDPOINT + 'error-report', body , {headers: new HttpHeaders({'Error-Reporting-Token': 'TOKEN', 'Origin': 'https://example.com'})})).subscribe({ next: (response: any) => { resolve(response); }, error: (e: HttpErrorResponse) => reject(e), complete: () => console.info('Error on sending error'), }); }); } Whenever an error is found, the frontend will call postError() with a relevant description under message . apiService.postError({message: 'ERROR MESSAGE'})","title":"How to use it on the Frontend side (Angular example)"},{"location":"v5/core-features/exceptions/","text":"Exceptions What are exceptions? Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code. How we use exceptions When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Below we will list the available custom exceptions. BadRequestException thrown when The Client tries to create/update resource , but the request data is invalid/incomplete (example: client tries to create an account, but does not send the required identity field) ConflictException thrown when The resource cannot be created because a different resource with the same identifier already exists (example: cannot change existing user's identity because another user with the same identity already exists) The resource cannot change its state because it is already in the specified state (example: user cannot be activated because it is already active) ExpiredException thrown when The resource cannot be accessed because it has expired (example: account activation link) because it has been consumed (example: one-time password) ForbiddenException thrown when The resource cannot be accessed by the authenticated client's role (example: client authenticated as regular user sends a GET /admin request) MethodNotAllowedException thrown when The client tries to interact with a resource via an invalid HTTP request method (example: client sends a PATCH /avatar request) NotFoundException thrown when The client tries to interact with a resource that does not exist on the server (example: client sends a GET /resource-does-not-exist request) UnauthorizedException thrown when The resource cannot be accessed because the client is not authenticated (example: unauthenticated client sends a GET /admin request) How it works During a request, if there is no uncaught exception, Dotkernel API will return a JSON response with the data provided by the handler that processed the request. Otherwise, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response How to extend In this example we will Create a custom exception called CustomException Place it next to the already existing custom exceptions (you can use your preferred location) Return a custom HTTP status code when CustomException is encountered. Step 1: Create exception file Navigate to the directory src/App/src/Handler/Exception and create a PHP class called CustomException.php . Open CustomException.php and add the following content: <?php declare(strict_types=1); namespace Api\\App\\Exception; use Exception; class CustomException extends Exception { } Save and close the file. Step 2: Use exception file Open the file src/App/src/Handler/HomeHandler.php and at the beginning of the get method, place the following code: throw new \\Api\\App\\Exception\\CustomException('some message'); Save and close the file. Step 3: Test for failure Access your API's home page URL and make sure it returns 500 Internal Server Error HTTP status code and the following content: { \"error\": { \"messages\": [ \"some message\" ] } } Step 4: Prepare for success Open the file src/App/src/Handler/HandlerTrait.php and locate the handle method. Insert the following lines of code before the first catch statement: } catch (\\Api\\App\\Exception\\CustomException $exception) { return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_IM_A_TEAPOT); Save and close the file. Step 5: Test for success Access your API's home page URL, which should return the same content. Notice that this time it returns 418 I'm a teapot HTTP status code.","title":"Exceptions"},{"location":"v5/core-features/exceptions/#exceptions","text":"","title":"Exceptions"},{"location":"v5/core-features/exceptions/#what-are-exceptions","text":"Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code.","title":"What are exceptions?"},{"location":"v5/core-features/exceptions/#how-we-use-exceptions","text":"When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Below we will list the available custom exceptions.","title":"How we use exceptions"},{"location":"v5/core-features/exceptions/#how-it-works","text":"During a request, if there is no uncaught exception, Dotkernel API will return a JSON response with the data provided by the handler that processed the request. Otherwise, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response","title":"How it works"},{"location":"v5/core-features/exceptions/#how-to-extend","text":"In this example we will Create a custom exception called CustomException Place it next to the already existing custom exceptions (you can use your preferred location) Return a custom HTTP status code when CustomException is encountered.","title":"How to extend"},{"location":"v5/flow/default-library-flow/","text":"Default Library Flow The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v5/flow/default-library-flow/#default-library-flow","text":"The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v5/flow/library-flow-for-email/","text":"Library Flow for Email The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v5/flow/library-flow-for-email/#library-flow-for-email","text":"The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v5/flow/middleware-flow/","text":"Middleware flow The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware Flow"},{"location":"v5/flow/middleware-flow/#middleware-flow","text":"The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware flow"},{"location":"v5/installation/composer/","text":"Composer Installation of Packages Composer is required to install Dotkernel api . You can install Composer from the official site . First make sure that you have navigated your command prompt to the folder where you copied the files in the previous step. Install dependencies Run this command in the command prompt. Use the CLI in order to ensure interactivity for proper configuration. composer install You should see this text below, along with a long list of packages to be installed instead of the [...] . In this example there are 164 packages, though the number can change in future updates. You will find the packages in the vendor folder. No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Lock file operations: 164 installs, 0 updates, 0 removals [...] Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 164 installs, 0 updates, 0 removals [...] The setup script may prompt for some configuration settings, for example the lines below. If you don't see them, you can skip to the next section. Please select which config file you wish to inject 'Laminas\\Diactoros\\ConfigProvider' into: [0] Do not inject [1] config/config.php Make your selection (default is 1): Type 0 to select [0] Do not inject . We choose 0 because Dotkernel includes its own ConfigProvider which already contains the prompted configurations. If you choose [1] config/config.php , an extra ConfigProvider will be injected. The next question is: Remember this option for other packages of the same type? (y/N) Type y here, and hit enter to complete this stage. Development mode If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Composer"},{"location":"v5/installation/composer/#composer-installation-of-packages","text":"Composer is required to install Dotkernel api . You can install Composer from the official site . First make sure that you have navigated your command prompt to the folder where you copied the files in the previous step.","title":"Composer Installation of Packages"},{"location":"v5/installation/composer/#install-dependencies","text":"Run this command in the command prompt. Use the CLI in order to ensure interactivity for proper configuration. composer install You should see this text below, along with a long list of packages to be installed instead of the [...] . In this example there are 164 packages, though the number can change in future updates. You will find the packages in the vendor folder. No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Lock file operations: 164 installs, 0 updates, 0 removals [...] Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 164 installs, 0 updates, 0 removals [...] The setup script may prompt for some configuration settings, for example the lines below. If you don't see them, you can skip to the next section. Please select which config file you wish to inject 'Laminas\\Diactoros\\ConfigProvider' into: [0] Do not inject [1] config/config.php Make your selection (default is 1): Type 0 to select [0] Do not inject . We choose 0 because Dotkernel includes its own ConfigProvider which already contains the prompted configurations. If you choose [1] config/config.php , an extra ConfigProvider will be injected. The next question is: Remember this option for other packages of the same type? (y/N) Type y here, and hit enter to complete this stage.","title":"Install dependencies"},{"location":"v5/installation/composer/#development-mode","text":"If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Development mode"},{"location":"v5/installation/configuration-files/","text":"Configuration Files Prepare config files duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php Note if your API will be consumed by another application, make sure to configure the allowed_origins variable duplicate config/autoload/local.php.dist as config/autoload/local.php duplicate config/autoload/mail.local.php.dist as config/autoload/mail.local.php Note if your API will send emails, make sure to fill in SMTP connection params optional : in order to run/create tests, duplicate config/autoload/local.test.php.dist as config/autoload/local.test.php Note this creates a new in-memory database that your tests will run on.","title":"Configuration Files"},{"location":"v5/installation/configuration-files/#configuration-files","text":"","title":"Configuration Files"},{"location":"v5/installation/configuration-files/#prepare-config-files","text":"duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php","title":"Prepare config files"},{"location":"v5/installation/doctrine-orm/","text":"Doctrine ORM Setup database Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci Running migrations Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation. Executing fixtures Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found on dot-data-fixtures documentation","title":"Doctrine ORM"},{"location":"v5/installation/doctrine-orm/#doctrine-orm","text":"","title":"Doctrine ORM"},{"location":"v5/installation/doctrine-orm/#setup-database","text":"Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci","title":"Setup database"},{"location":"v5/installation/doctrine-orm/#running-migrations","text":"Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation.","title":"Running migrations"},{"location":"v5/installation/doctrine-orm/#executing-fixtures","text":"Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found on dot-data-fixtures documentation","title":"Executing fixtures"},{"location":"v5/installation/faq/","text":"Frequently Asked Questions How do I fix common permission issues? If running your project you encounter some permission issues, follow the below steps. Errors PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache/doctrine\" is not writable... Fix: chmod -R 777 data Error PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/public/uploads\" is not writable... Fix: chmod -R 777 public/uploads Error PHP Fatal error: Uncaught ErrorException: fopen(/var/www/ example.local /config/autoload/../../log/error-log- yyyy-mm-dd.log ): Failed to open stream: Permission denied... Fix: chmod -R 777 log","title":"FAQ"},{"location":"v5/installation/faq/#frequently-asked-questions","text":"","title":"Frequently Asked Questions"},{"location":"v5/installation/faq/#how-do-i-fix-common-permission-issues","text":"If running your project you encounter some permission issues, follow the below steps.","title":"How do I fix common permission issues?"},{"location":"v5/installation/getting-started/","text":"Clone the project Recommended development environment If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Getting Started"},{"location":"v5/installation/getting-started/#clone-the-project","text":"","title":"Clone the project"},{"location":"v5/installation/getting-started/#recommended-development-environment","text":"If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Recommended development environment"},{"location":"v5/installation/test-the-installation/","text":"Test the installation Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 5\"} Old way of doing things, using PHP built-in server php -S 0.0.0.0:8080 -t public Running tests The project has 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","title":"Test the Installation"},{"location":"v5/installation/test-the-installation/#test-the-installation","text":"Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 5\"}","title":"Test the installation"},{"location":"v5/installation/test-the-installation/#old-way-of-doing-things-using-php-built-in-server","text":"php -S 0.0.0.0:8080 -t public","title":"Old way of doing things, using PHP built-in server"},{"location":"v5/installation/test-the-installation/#running-tests","text":"The project has 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","title":"Running tests"},{"location":"v5/installation/test-the-installation/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v5/installation/test-the-installation/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v5/introduction/file-structure/","text":"File structure Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default: Main directories bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files Special purpose folders .github - containes workflow files .laminas-ci - contains laminas-ci workflow files bin directory This directory contents are clear-config-cache.php which removes the config cache file ( data/cache/config-cache.php - available only when development mode is enabled). cli.php used to build console applications based on laminas-cli doctrine used by the doctrine fixtures to populate the database tables config directory This directory contains all application-related config files: cli-config.php : command line interface configuration used by migrations, fixtures, crons config.php : registers ConfigProviders for installing packages container.php : main service container that provides access to all registered services development.config.php.dist : activates debug mode; gets symlinked as development.config.php when enabling development mode migrations.php : configuration for database migration, like migration file location and table to save the migration log pipeline.php : contains a list of middlewares, in the order of their execution twig-cs-fixer.php : configuration file for Twig code style checker/fixer config/autoload directory This directory contains all service-related local and global config files: authorization.global.php : configures access per route for user roles cli.global.php : configures cli content-negotiation.global.php : configures request and response formats cors.local.php.dist : configures Cross-Origin Resource Sharing, like call origin, headers, cookies dependencies.global.php : config file to set global dependencies that should be accessible by all modules development.local.php.dist : gets symlinked as development.local.php when enabling development mode - activates error handlers doctrine.global.php : configuration used by Object–relational mapping error-handling.global.php : configures and activates error logs local.php.dist : local config file where you can overwrite application name and URL local.test.php.dist : local configuration for functional tests mail.local.php.dist : mail configuration; e.g. sendmail vs smtp, message configuration, mail logging mezzio.global.php : Mezzio core config file mezzio-tooling-factories.global.php : add or remove factory definitions response-header.global.php : defines headers per route templates.global.php : dotkernel/dot-twigrenderer config file data directory This directory is a storage for project data files and service caches. It contains these folders: cache : cache for e.g. Twig files doctrine : database migrations and fixtures oauth : encryption, private and public keys needed for authentication data/lock - lock files generated by dotkernel/dot-cli AVOID storing sensitive data on VCS. log directory This directory stores daily log files. When you access the application from the browser, (if not already created) a new log file gets created in the format specified in the config/autoload/error-handling.global.php config file under the stream array key. public directory This directory contains all publicly available assets and serves as the entry point of the application: uploads : a folder that normally contains files uploaded via the application .htaccess : server configuration file used by Apache web server; it enables the URL rewrite functionality index.php : the application's main entry point robots.txt.dist : a sample robots.txt file that allows/denies bot access to certain areas of your application; activate it by duplicating the file as robots.txt and comment out the lines that don't match your environment src directory This folder contains a separate folder for each Module. Each Module folder, in turn, should contain following directories, unless they are empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - Used by database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but they should give you an idea about the recommended structure. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory normally also contains these files: ConfigProvider.php - Configuration data for the module OpenAPI.php - Detailed descriptions for each endpoint in the OpenAPI format RoutesDelegator.php - Module specific route registrations Module main routes entry file templates directory This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig","title":"File Structure"},{"location":"v5/introduction/file-structure/#file-structure","text":"Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default:","title":"File structure"},{"location":"v5/introduction/file-structure/#main-directories","text":"bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files","title":"Main directories"},{"location":"v5/introduction/file-structure/#special-purpose-folders","text":".github - containes workflow files .laminas-ci - contains laminas-ci workflow files","title":"Special purpose folders"},{"location":"v5/introduction/file-structure/#src-directory","text":"This folder contains a separate folder for each Module. Each Module folder, in turn, should contain following directories, unless they are empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - Used by database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but they should give you an idea about the recommended structure. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory normally also contains these files: ConfigProvider.php - Configuration data for the module OpenAPI.php - Detailed descriptions for each endpoint in the OpenAPI format RoutesDelegator.php - Module specific route registrations Module main routes entry file","title":"src directory"},{"location":"v5/introduction/file-structure/#templates-directory","text":"This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig","title":"templates directory"},{"location":"v5/introduction/introduction/","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Dependency Injection (dotkernel/dot-dependency-injection) Input Filter (laminas/laminas-inputfilter) Doctrine 3 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/mezzio-hal) CLI (dotkernel/dot-cli) TwigRenderer (mezzio/mezzio-twigrenderer) Fixtures (dotkernel/dot-data-fixtures) UUID (ramsey/uuid-doctrine) Doctrine 3 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.0 OAuth 2.0 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 OAuth 2.0 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 Symfony\\Component\\Console\\Command\\Command . 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","title":"Introduction"},{"location":"v5/introduction/introduction/#introduction","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Dependency Injection (dotkernel/dot-dependency-injection) Input Filter (laminas/laminas-inputfilter) Doctrine 3 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/mezzio-hal) CLI (dotkernel/dot-cli) TwigRenderer (mezzio/mezzio-twigrenderer) Fixtures (dotkernel/dot-data-fixtures) UUID (ramsey/uuid-doctrine)","title":"Introduction"},{"location":"v5/introduction/introduction/#doctrine-3-orm","text":"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.","title":"Doctrine 3 ORM"},{"location":"v5/introduction/introduction/#documentation","text":"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","title":"Documentation"},{"location":"v5/introduction/introduction/#hypertext-application-language","text":"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.","title":"Hypertext Application Language"},{"location":"v5/introduction/introduction/#cors","text":"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.","title":"CORS"},{"location":"v5/introduction/introduction/#oauth-20","text":"OAuth 2.0 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 OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using league/oauth2-server package.","title":"OAuth 2.0"},{"location":"v5/introduction/introduction/#email","text":"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.","title":"Email"},{"location":"v5/introduction/introduction/#configuration","text":"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.","title":"Configuration"},{"location":"v5/introduction/introduction/#routing","text":"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 .","title":"Routing"},{"location":"v5/introduction/introduction/#commands","text":"For registering new commands first make sure your command class extends Symfony\\Component\\Console\\Command\\Command . Then you can enable it by registering it in config/autoload/cli.global.php .","title":"Commands"},{"location":"v5/introduction/introduction/#file-locker","text":"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.","title":"File locker"},{"location":"v5/introduction/introduction/#psr-standards","text":"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","title":"PSR Standards"},{"location":"v5/introduction/introduction/#tests","text":"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","title":"Tests"},{"location":"v5/introduction/introduction/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v5/introduction/introduction/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v5/introduction/packages/","text":"Packages Version 5.1.1 had these packages removed or moved where noted: laminas/laminas-http was moved to require-dev laminas/laminas-paginator laminas/laminas-text dotkernel/dot-dependency-injection - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v5/introduction/packages/#packages","text":"Version 5.1.1 had these packages removed or moved where noted: laminas/laminas-http was moved to require-dev laminas/laminas-paginator laminas/laminas-text dotkernel/dot-dependency-injection - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v5/introduction/psr/","text":"PSRs Some of the PSRs on this list are at the core of Dotkernel API, but several others are installed with the 3rd party packages used in the application. Below is the full list of PSRs present in Dotkernel API and their purpose. PSR-3: Logger Interface Interface for logging libraries Interfaces implemented in php-fig/log PSR-4: Autoloader Autoloading classes from file paths Interfaces implemented in laminas/laminas-loader PSR-6: Caching Interface Interface for caching systems to improve the performance of any project Interfaces implemented in php-fig/cache PSR-7: HTTP message interfaces Interfaces for representing HTTP messages and URIs for use with HTTP messages Interfaces implemented in php-fig/http-message PSR-11: Container interface Interface for dependency injection containers Interfaces implemented in php-fig/container PSR-13: Link definition interfaces Way of representing a hypermedia link independently of the serialization format Interfaces implemented in php-fig/link PSR-14: Event Dispatcher Mechanism for event-based extension and collaboration Interfaces implemented in php-fig/event-dispatcher PSR-15: HTTP Server Request Handlers Interfaces for HTTP server request handlers and HTTP server middleware components that use HTTP messages Interfaces implemented in php-fig/http-server-handler and php-fig/http-server-middleware PSR-17: HTTP Factories Standard for factories that create PSR-7 compliant HTTP objects Interfaces implemented in php-fig/http-factory PSR-18: HTTP Client Interface for sending HTTP requests and receiving HTTP responses Interfaces implemented in php-fig/http-client PSR-20: Clock Interface for reading the system clock Interfaces implemented in php-fig/clock","title":"PSRs"},{"location":"v5/introduction/psr/#psrs","text":"Some of the PSRs on this list are at the core of Dotkernel API, but several others are installed with the 3rd party packages used in the application. Below is the full list of PSRs present in Dotkernel API and their purpose. PSR-3: Logger Interface Interface for logging libraries Interfaces implemented in php-fig/log PSR-4: Autoloader Autoloading classes from file paths Interfaces implemented in laminas/laminas-loader PSR-6: Caching Interface Interface for caching systems to improve the performance of any project Interfaces implemented in php-fig/cache PSR-7: HTTP message interfaces Interfaces for representing HTTP messages and URIs for use with HTTP messages Interfaces implemented in php-fig/http-message PSR-11: Container interface Interface for dependency injection containers Interfaces implemented in php-fig/container PSR-13: Link definition interfaces Way of representing a hypermedia link independently of the serialization format Interfaces implemented in php-fig/link PSR-14: Event Dispatcher Mechanism for event-based extension and collaboration Interfaces implemented in php-fig/event-dispatcher PSR-15: HTTP Server Request Handlers Interfaces for HTTP server request handlers and HTTP server middleware components that use HTTP messages Interfaces implemented in php-fig/http-server-handler and php-fig/http-server-middleware PSR-17: HTTP Factories Standard for factories that create PSR-7 compliant HTTP objects Interfaces implemented in php-fig/http-factory PSR-18: HTTP Client Interface for sending HTTP requests and receiving HTTP responses Interfaces implemented in php-fig/http-client PSR-20: Clock Interface for reading the system clock Interfaces implemented in php-fig/clock","title":"PSRs"},{"location":"v5/introduction/server-requirements/","text":"Server Requirements For production, we highly recommend a *nix based system. Webserver Apache >= 2.2 mod_rewrite .htaccess support (AllowOverride All) The repository includes a default .htaccess file in the public folder. Nginx You need to convert the provided Apache related .htaccess file into Nginx configuration instructions. PHP >= 8.2 Both mod_php and FCGI (FPM) are supported. Required Settings and Modules & Extensions memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH) RDBMS Tested with MariaDB 10.11 LTS and MariaDB 11.4 LTS Recommended extensions opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used) sqlite3 - for tests","title":"Server Requirements"},{"location":"v5/introduction/server-requirements/#server-requirements","text":"For production, we highly recommend a *nix based system.","title":"Server Requirements"},{"location":"v5/introduction/server-requirements/#webserver","text":"","title":"Webserver"},{"location":"v5/introduction/server-requirements/#php-82","text":"Both mod_php and FCGI (FPM) are supported.","title":"PHP >= 8.2"},{"location":"v5/introduction/server-requirements/#required-settings-and-modules-extensions","text":"memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH)","title":"Required Settings and Modules & Extensions"},{"location":"v5/introduction/server-requirements/#rdbms","text":"Tested with MariaDB 10.11 LTS and MariaDB 11.4 LTS","title":"RDBMS"},{"location":"v5/introduction/server-requirements/#recommended-extensions","text":"opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used) sqlite3 - for tests","title":"Recommended extensions"},{"location":"v5/openapi/generate-documentation/","text":"Generating the documentation file Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . Using your terminal, move to the root directory of your project. Dotkernel API stores the OpenAPI attributes in the src directory, so that's the path we will use for generating the static documentation file. Methods of generating documentation file Without saving it to a file ./vendor/bin/openapi ./src This will output the generated content to the terminal. Place it in a custom location ./vendor/bin/openapi ./src --output public/openapi.yaml This will place the generated file openapi.yaml in the public directory. Specify OpenAPI version Supported OpenAPI versions are 3.0.0 and 3.1.0 , 3.0.0 being the default version. The below command will specify both the output location and the OpenAPI version: ./vendor/bin/openapi ./src --version 3.1.0 Specify output file format Supported file formats are yaml and json , yaml being the default format. The below command will specify the output location and zircote/swagger-php will determine the file format: ./vendor/bin/openapi ./src --output public/openapi.json Or be specific about the format by appending the --format argument: ./vendor/bin/openapi ./src --output public/openapi.json --format json These will place the generated file openapi.json in the public directory.","title":"Generate Documentation"},{"location":"v5/openapi/generate-documentation/#generating-the-documentation-file","text":"Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . Using your terminal, move to the root directory of your project. Dotkernel API stores the OpenAPI attributes in the src directory, so that's the path we will use for generating the static documentation file.","title":"Generating the documentation file"},{"location":"v5/openapi/generate-documentation/#methods-of-generating-documentation-file","text":"","title":"Methods of generating documentation file"},{"location":"v5/openapi/getting-help/","text":"Getting help consult the OpenAPI specs for a complete reference of the presented objects see more examples of OpenAPI object representations in zircote/swagger-php 's GitHub repository consult zircote/swagger-php 's online documentation or run the following command to see their help page: ./vendor/bin/openapi --help","title":"Getting Help"},{"location":"v5/openapi/getting-help/#getting-help","text":"consult the OpenAPI specs for a complete reference of the presented objects see more examples of OpenAPI object representations in zircote/swagger-php 's GitHub repository consult zircote/swagger-php 's online documentation or run the following command to see their help page: ./vendor/bin/openapi --help","title":"Getting help"},{"location":"v5/openapi/initialized-components/","text":"Initialized OpenAPI components Below you will find details on some prepopulated OpenAPI components we added to Dotkernel API. OA\\Info Defined in src/App/src/OpenAPI.php , this object provides general info about the API: version : API version (example: 1.0.0 ) title : title shown in the UI (example: Dotkernel API ) For more info, see this page . OA\\Server Defined in src/App/src/OpenAPI.php , this object provides API server entries: url : API server URL (example: https://api.example.com - use no trailing slash!) description : describes the purpose of the server (example: Dev , Staging , Production or even Auth if you use a separate authentication server) You can have multiple Server definitions, one for each of your Dotkernel API instances. For more info, see this page . OA\\SecurityScheme Defined in src/App/src/OpenAPI.php , you will find an object for the AuthToken security header: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) bearerFormat : a hint to the client to identify how the bearer token is formatted scheme : the name of the authorization scheme to be used And another object for the ErrorReportingToken security token: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) name : the name of the header For more info, see this page . OA\\ExternalDocumentation Defined in src/App/src/OpenAPI.php , in this object we provide the following details: description : describes the purpose of the document url : external documentation URL For more info, see this page . OA\\Schema Schemas are OpenAPI objects describing an object or collection of objects existing in your project. Schemas describing objects In order to describe an object (entity) you will need to transform in into a schema. Object: <?php declare(strict_types=1); namespace Api\\User\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\RoleInterface; use Api\\App\\Entity\\TimestampsTrait; use Doctrine\\ORM\\Mapping as ORM; class UserRole extends AbstractEntity implements RoleInterface { use TimestampsTrait; #[ORM\\Column(name: \"name\", type: \"string\", length: 20, unique: true)] protected ?string $name = null; // methods } Schema: <?php declare(strict_types=1); namespace Api\\User; use Api\\User\\Entity\\UserRole; use OpenApi\\Attributes as OA; ... /** * @see UserRole */ #[OA\\Schema( schema: 'UserRole', properties: [ new OA\\Property(property: 'uuid', type: 'string', example: '1234abcd-abcd-4321-12ab-123456abcdef'), new OA\\Property(property: 'name', type: 'string', example: UserRole::ROLE_USER), new OA\\Property( property: '_links', properties: [ new OA\\Property( property: 'self', properties: [ new OA\\Property( property: 'href', type: 'string', example: 'https://example.com/user/role/1234abcd-abcd-4321-12ab-123456abcdef', ), ], type: 'object', ), ], type: 'object', ), ], type: 'object', )] Then, when generating the documentation file, OpenAPI will transform it into the specified format ( json / yaml ). UserRole: properties: uuid: type: string example: 1234abcd-abcd-4321-12ab-123456abcdef name: type: string example: user _links: properties: self: properties: href: type: string example: 'https://example.com/user/role/1234abcd-abcd-4321-12ab-123456abcdef' type: object type: object type: object Schemas describing collections of objects Collections of objects are just as easy to describe in OpenAPI as they are in PHP. PHP collection: <?php declare(strict_types=1); namespace Api\\User\\Collection; use Api\\App\\Collection\\ResourceCollection; class UserRoleCollection extends ResourceCollection { } Schema: #[OA\\Schema( schema: 'UserRoleCollection', properties: [ new OA\\Property( property: '_embedded', properties: [ new OA\\Property( property: 'roles', type: 'array', items: new OA\\Items( ref: '#/components/schemas/UserRole', ), ), ], type: 'object', ), ], type: 'object', allOf: [ new OA\\Schema(ref: '#/components/schemas/Collection'), ], )] Using ref: '#/components/schemas/UserRole', in our code, we instruct OpenAPI to grab the existing schema UserRole (not the entity, but the schema) that we just described earlier. This way we do not need to repeat code by describing again the same object and any future modifications will happen in only one place. Then, when generating the documentation file, OpenAPI will transform it into the specified format ( json / yaml ). UserRoleCollection: type: object allOf: - $ref: '#/components/schemas/Collection' - properties: _embedded: properties: roles: type: array items: { $ref: '#/components/schemas/UserRole' } type: object type: object Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . You can add multiple servers (for staging, production etc) by duplicating the existing one. For more info, see this page . Common schemas 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 #/components/schemas/InfoMessage : describes an operation that completed successfully - may contain multiple messages","title":"Initialized Components"},{"location":"v5/openapi/initialized-components/#initialized-openapi-components","text":"Below you will find details on some prepopulated OpenAPI components we added to Dotkernel API.","title":"Initialized OpenAPI components"},{"location":"v5/openapi/initialized-components/#oainfo","text":"Defined in src/App/src/OpenAPI.php , this object provides general info about the API: version : API version (example: 1.0.0 ) title : title shown in the UI (example: Dotkernel API ) For more info, see this page .","title":"OA\\Info"},{"location":"v5/openapi/initialized-components/#oaserver","text":"Defined in src/App/src/OpenAPI.php , this object provides API server entries: url : API server URL (example: https://api.example.com - use no trailing slash!) description : describes the purpose of the server (example: Dev , Staging , Production or even Auth if you use a separate authentication server) You can have multiple Server definitions, one for each of your Dotkernel API instances. For more info, see this page .","title":"OA\\Server"},{"location":"v5/openapi/initialized-components/#oasecurityscheme","text":"Defined in src/App/src/OpenAPI.php , you will find an object for the AuthToken security header: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) bearerFormat : a hint to the client to identify how the bearer token is formatted scheme : the name of the authorization scheme to be used And another object for the ErrorReportingToken security token: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) name : the name of the header For more info, see this page .","title":"OA\\SecurityScheme"},{"location":"v5/openapi/initialized-components/#oaexternaldocumentation","text":"Defined in src/App/src/OpenAPI.php , in this object we provide the following details: description : describes the purpose of the document url : external documentation URL For more info, see this page .","title":"OA\\ExternalDocumentation"},{"location":"v5/openapi/initialized-components/#oaschema","text":"Schemas are OpenAPI objects describing an object or collection of objects existing in your project.","title":"OA\\Schema"},{"location":"v5/openapi/introduction/","text":"OpenAPI documentation In order to provide an interactive documentation, Dotkernel API implemented zircote/swagger-php . Using this library, developers are able to automatically generate documentation files that later can be used to provide a comprehensive overview of the available endpoints, all the details on the requests that it can receive and the responses these can return.","title":"Introduction"},{"location":"v5/openapi/introduction/#openapi-documentation","text":"In order to provide an interactive documentation, Dotkernel API implemented zircote/swagger-php . Using this library, developers are able to automatically generate documentation files that later can be used to provide a comprehensive overview of the available endpoints, all the details on the requests that it can receive and the responses these can return.","title":"OpenAPI documentation"},{"location":"v5/openapi/render-documentation/","text":"Rendering the documentation file At this step, you only have a static documentation file. You will need an interface that can render it so that you will be able to interact with your Dotkernel API. In order to do this, we recommend using either of: swagger-api/swagger-ui Redocly/redoc Using Swagger UI Navigate to the public directory of your instance of Dotkernel API and create an HTML (you can call it swagger.html , the name is up to you) and place the following HTML content in it: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css\" /> </head> <body> <div id=\"swagger-ui\"></div> <script src=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js\" crossorigin></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({url: 'PATH_TO_YOUR_OPENAPI_FILE', dom_id: '#swagger-ui'}); }; </script> </body> </html> 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: window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /swagger.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s). 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: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <script src=\"https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js\"></script> </head> <body> <div id=\"redoc-container\"></div> <script> Redoc.init('PATH_TO_YOUR_OPENAPI_FILE', {}, document.getElementById('redoc-container')); </script> </body> </html> 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: Redoc.init('./openapi.yaml', {}, document.getElementById('redoc-container')); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /redoc.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Render Documentation"},{"location":"v5/openapi/render-documentation/#rendering-the-documentation-file","text":"At this step, you only have a static documentation file. You will need an interface that can render it so that you will be able to interact with your Dotkernel API. In order to do this, we recommend using either of: swagger-api/swagger-ui Redocly/redoc","title":"Rendering the documentation file"},{"location":"v5/openapi/render-documentation/#using-swagger-ui","text":"Navigate to the public directory of your instance of Dotkernel API and create an HTML (you can call it swagger.html , the name is up to you) and place the following HTML content in it: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css\" /> </head> <body> <div id=\"swagger-ui\"></div> <script src=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js\" crossorigin></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({url: 'PATH_TO_YOUR_OPENAPI_FILE', dom_id: '#swagger-ui'}); }; </script> </body> </html> 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: window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /swagger.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Using Swagger UI"},{"location":"v5/openapi/render-documentation/#using-redoc","text":"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: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <script src=\"https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js\"></script> </head> <body> <div id=\"redoc-container\"></div> <script> Redoc.init('PATH_TO_YOUR_OPENAPI_FILE', {}, document.getElementById('redoc-container')); </script> </body> </html> 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: Redoc.init('./openapi.yaml', {}, document.getElementById('redoc-container')); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /redoc.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Using Redoc"},{"location":"v5/openapi/use-documentation/","text":"Using the documentation Since Redoc is readonly, in the following section we will focus only on using Swagger UI. 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. 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: 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. 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. Depending on the endpoint description, you will know which one you need to use. Examples: /user : the description says Admin lists user accounts - it means that you need an AccessToken with (super)admin privileges /user/my-account : the description says User fetches their own account - it means that you need an AccessToken with user privileges In the UI, find a section called AccessToken , toggle the /security/generate-token ( Generate access token ) endpoint and click the Try it out button. Under the Access token generation request you will find a textarea prepopulated with a JSON object. You will have to change the value of username and password . See this guide for the credentials. After you have filled out the credentials, click on the Execute button below the textarea. This will send the request to your instance of Dotkernel API. If everything went well, under the textarea you should see: the curl request that was made the Request URL the request was sent to the Server response with 200 OK response code and the Response body with a JSON object containing token_type , expires_in , access_token and refresh_token . Save the refresh_token somewhere, you will need it later Now copy the value of access_token (make sure you copy all the characters, without the surrounding double quotes) and go back up to the Authorize button and click it to open the auth modal. Paste the copied token as the value of the AuthToken and click on the Authorize button you see under the input field. The Authorize button has now changed to Logout . You can close the modal. From here, Swagger UI will remember the AuthToken until you close/refresh the browser tab. Also, it will automatically append the Authorization header to each request, allowing you to make authorized API calls. If you need to switch to an account with different privileges, you go again to the Authorize button, click on it to open the auth modal, and click Logout for the AuthToken . Then paste the new token as the value of the AuthToken , click on the Authorize button, close the modal and continue using the UI authenticated with the new account. Refreshing AuthToken By default, auth tokens expire in 1 day. If you make an API call, and you receive an error telling you that your auth token is expired, you need to either generate a new token (as seen above) or refresh the existing one using the refresh_token received when generating the current token. In order to refresh the auth token, you find the same section called AccessToken , toggle the /security/refresh-token ( Refresh access token ) endpoint and click the Try it out button. Under the Access token refresh request you will find a textarea prepopulated with a JSON object. You will have to change the value of refresh_token to the refresh token of your current auth token. Once done, click on the Execute button below the textarea. This will send the request to your instance of Dotkernel API. If everything went well, under the textarea you should see the same details: the curl request that was made the Request URL the request was sent to the Server response with 200 OK response code and the Response body with a JSON object containing token_type , expires_in , access_token and refresh_token . From here, you will follow the same steps: copy the access_token go to the Authorize button to open the auth modal paste the new token and click on Authorize close the modal Generating ErrorReportingToken Just like the AuthTokens, ErrorReportingTokens are used to make authorized API calls. The difference is that this token applies only to one specific endpoint: /error-report ( Report an error to the API ). This endpoint is intended to be used by third-party applications and frontends to report an error back to the API. This endpoint does not require AuthTokens In order to generate this token, follow this guide . Once you have the error reporting token, go again to the Authorize button, paste the new token as the value of the ErrorReportingToken , click on the Authorize button and close the modal. Now you're ready to report errors to your instance of Dotkernel API. Making API calls The UI does not use confirmation messages before making an API call so double check any operation before executing it. Once authorized in the UI, you can click on any endpoint to expand it. There you will find an overview of the endpoint, including: Request method ( DELETE , GET , PATCH , POST , PUT ) request URL (example: /resource ) Short description Long description Parameters - if this area says No parameters , then there are no parameters to fill out; else, make sure you fill out all the required parameters Request body - if present, provides a textarea prepopulated with a JSON object describing the request Responses - a list of possible HTTP status codes and their respective response bodies Clicking the Try it out button will activate any parameter input fields and the request body textarea (if any). Clicking Cancel will deactivate them. Make sure you fill out all the necessary data, then click on the Execute found button above Responses . This will send the request and return and display the API response. Once finished, you will see the response as the first item under Responses , including the HTTP status code and the response body. You can repeat the request by clicking again on the Execute button. This will first clear the previous output and display the new response in the same place. Additionally, between two executions, you can manually clear any previous output using the Clear button next to the Execute button.","title":"Use Documentation"},{"location":"v5/openapi/use-documentation/#using-the-documentation","text":"Since Redoc is readonly, in the following section we will focus only on using Swagger UI.","title":"Using the documentation"},{"location":"v5/openapi/use-documentation/#protected-endpoints","text":"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.","title":"Protected endpoints"},{"location":"v5/openapi/use-documentation/#authentication","text":"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.","title":"Authentication"},{"location":"v5/openapi/use-documentation/#making-api-calls","text":"The UI does not use confirmation messages before making an API call so double check any operation before executing it. Once authorized in the UI, you can click on any endpoint to expand it. There you will find an overview of the endpoint, including: Request method ( DELETE , GET , PATCH , POST , PUT ) request URL (example: /resource ) Short description Long description Parameters - if this area says No parameters , then there are no parameters to fill out; else, make sure you fill out all the required parameters Request body - if present, provides a textarea prepopulated with a JSON object describing the request Responses - a list of possible HTTP status codes and their respective response bodies Clicking the Try it out button will activate any parameter input fields and the request body textarea (if any). Clicking Cancel will deactivate them. Make sure you fill out all the necessary data, then click on the Execute found button above Responses . This will send the request and return and display the API response. Once finished, you will see the response as the first item under Responses , including the HTTP status code and the response body. You can repeat the request by clicking again on the Execute button. This will first clear the previous output and display the new response in the same place. Additionally, between two executions, you can manually clear any previous output using the Clear button next to the Execute button.","title":"Making API calls"},{"location":"v5/openapi/write-documentation/","text":"Writing documentation In order to avoid polluting PHP files with maybe thousands of lines of OpenAPI attributes, we opted for storing them in separate files, called OpenAPI.php , one for each module. We already covered all the endpoints available in Dotkernel API, you can consult the existing documentation in each module's own OpenAPI.php file. After you add more functionalities to your API, you will have to document the new endpoints. This is easier than it sounds because in most cases you will do the same: add a request by method, describe the request payload (if any), add request parameters (if any) and describe the possible responses. Common objects To do this, you will use the following request objects: OA\\Delete : delete an API resource identified by its unique id OA\\Get : fetch API single or collections of API resources OA\\Post : create a new API resource (unless if it already exists) OA\\Patch : update an existing API resource OA\\Put : create a new API resource (if it already exists, it is overwritten) Also, the following components describing PHP objects: OA\\Schema : describe an object sent in a request or received as a response - read more OA\\Parameter : describe a query / path parameter - read more OA\\RequestBody : describe the body of a request - read more There are lot more, but these are the most often used ones. 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: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Get 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/{uuid} for a single resource or /resource for a collection of resources) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Patch Defines a PATCH HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Post Defines a POST HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Put Defines a PUT HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies Conclusion To summarize, the typical scenario on working on your own instance of Dotkernel API would follow these steps: create new module (example: Book ) add functionality to your new module (routes, entities, repositories, handlers, services, tests etc) create file OpenAPI.php in the new module and describe each new endpoint generate latest version of documentation file as described here","title":"Write Documentation"},{"location":"v5/openapi/write-documentation/#writing-documentation","text":"In order to avoid polluting PHP files with maybe thousands of lines of OpenAPI attributes, we opted for storing them in separate files, called OpenAPI.php , one for each module. We already covered all the endpoints available in Dotkernel API, you can consult the existing documentation in each module's own OpenAPI.php file. After you add more functionalities to your API, you will have to document the new endpoints. This is easier than it sounds because in most cases you will do the same: add a request by method, describe the request payload (if any), add request parameters (if any) and describe the possible responses.","title":"Writing documentation"},{"location":"v5/openapi/write-documentation/#common-objects","text":"To do this, you will use the following request objects: OA\\Delete : delete an API resource identified by its unique id OA\\Get : fetch API single or collections of API resources OA\\Post : create a new API resource (unless if it already exists) OA\\Patch : update an existing API resource OA\\Put : create a new API resource (if it already exists, it is overwritten) Also, the following components describing PHP objects: OA\\Schema : describe an object sent in a request or received as a response - read more OA\\Parameter : describe a query / path parameter - read more OA\\RequestBody : describe the body of a request - read more There are lot more, but these are the most often used ones. If you need help, take a look at the existing definitions found in Dotkernel API.","title":"Common objects"},{"location":"v5/openapi/write-documentation/#conclusion","text":"To summarize, the typical scenario on working on your own instance of Dotkernel API would follow these steps: create new module (example: Book ) add functionality to your new module (routes, entities, repositories, handlers, services, tests etc) create file OpenAPI.php in the new module and describe each new endpoint generate latest version of documentation file as described here","title":"Conclusion"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/","text":"Laminas API Tools compared to Dotkernel API API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15 Note Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Laminas API Tools vs Dotkernel API"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/#laminas-api-tools-compared-to-dotkernel-api","text":"API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15","title":"Laminas API Tools compared to Dotkernel API"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/#note","text":"Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Note"},{"location":"v5/transition-from-api-tools/discovery-phase/","text":"Discovery phase for a current system built using API Tools [WIP] In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it. Database there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else ) Note Dotkernel API is tested only with MariaDB version 10.6 and 10.11 LTS Authentication and Authorization how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac) Modules analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations Custom functionalities Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Discovery Phase"},{"location":"v5/transition-from-api-tools/discovery-phase/#discovery-phase-for-a-current-system-built-using-api-tools-wip","text":"In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it.","title":"Discovery phase for a current system built using API Tools [WIP]"},{"location":"v5/transition-from-api-tools/discovery-phase/#database","text":"there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else )","title":"Database"},{"location":"v5/transition-from-api-tools/discovery-phase/#authentication-and-authorization","text":"how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac)","title":"Authentication and Authorization"},{"location":"v5/transition-from-api-tools/discovery-phase/#modules","text":"analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations","title":"Modules"},{"location":"v5/transition-from-api-tools/discovery-phase/#custom-functionalities","text":"Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Custom functionalities"},{"location":"v5/transition-from-api-tools/transition-approach/","text":"Transition approach [WIP] Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools Business cases There are at least 2 approaches for this transition: Clone 1:1 and recreate all endpoints and entities Build a new version of the current API using Dotkernel API and keep it running as separate platforms until the sunset of the current version of api-tools","title":"Transition Approach"},{"location":"v5/transition-from-api-tools/transition-approach/#transition-approach-wip","text":"Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools","title":"Transition approach [WIP]"},{"location":"v5/transition-from-api-tools/transition-approach/#business-cases","text":"There are at least 2 approaches for this transition:","title":"Business cases"},{"location":"v5/tutorials/api-evolution/","text":"API Evolution pattern API evolution: Updating an API while keeping it compatible for existing consumers by adding new features, fixing bugs, planning and removing outdated features. How it works In Dotkernel API we can mark an entire endpoint or a single method as deprecated using attributes on handlers. We use response headers to inform the consumers about the future changes by using 2 new headers: Link - it's a link to the official documentation pointing out the changes that will take place. Sunset - this header is a date, indicating when the deprecated resource will potentially become unresponsive. Both headers are independent, you can use them separately. Make sure you have the DeprecationMiddleware:class piped in your pipeline list. In our case it's config/pipeline.php . Marking an entire endpoint as deprecated When you want to mark an entire resource as deprecated you have to use the ResourceDeprecation attribute. ... #[ResourceDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Resource deprecation example.', rel: 'sunset', type: 'text/html' )] class HomeHandler implements RequestHandlerInterface { ... In the example above, the ResourceDeprecation attribute is attached to the class, marking the entire / (home) endpoint as deprecated starting from 2038-01-01 . Running the following curl will print out the response headers where we can see the Sunset and Link headers. curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:23:11 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin Marking a method as deprecated Most of the time you want to deprecate only an endpoint, so you will need to use the MethodDeprecation attribute which has the same parameters, but it attaches to a handler method. ... class HomeHandler implements RequestHandlerInterface { ... use Api\\App\\Attribute\\MethodDeprecation; #[MethodDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Method deprecation example.', rel: 'sunset', type: 'text/html' )] public function get(): ResponseInterface { ... } } Attaching the MethodDeprecation can only be done to HTTP verb methods ( GET , POST , PUT , PATCH and DELETE ). If you followed along you can run the below curl: curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" The response lists the Sunset and Link headers. HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:54:57 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin NOTES If Link or Sunset do not have a value they will not appear in the response headers. Sunset has to be a valid date, otherwise it will throw an error. You cannot use both ResourceDeprecation and MethodDeprecation in the same handler. Deprecations can only be attached to handler classes that implement RequestHandlerInterface . The rel and type arguments are optional, they default to sunset and text/html if no value was provided and are Link related parts.","title":"API Evolution"},{"location":"v5/tutorials/api-evolution/#api-evolution-pattern","text":"API evolution: Updating an API while keeping it compatible for existing consumers by adding new features, fixing bugs, planning and removing outdated features.","title":"API Evolution pattern"},{"location":"v5/tutorials/api-evolution/#how-it-works","text":"In Dotkernel API we can mark an entire endpoint or a single method as deprecated using attributes on handlers. We use response headers to inform the consumers about the future changes by using 2 new headers: Link - it's a link to the official documentation pointing out the changes that will take place. Sunset - this header is a date, indicating when the deprecated resource will potentially become unresponsive. Both headers are independent, you can use them separately. Make sure you have the DeprecationMiddleware:class piped in your pipeline list. In our case it's config/pipeline.php .","title":"How it works"},{"location":"v5/tutorials/api-evolution/#marking-an-entire-endpoint-as-deprecated","text":"When you want to mark an entire resource as deprecated you have to use the ResourceDeprecation attribute. ... #[ResourceDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Resource deprecation example.', rel: 'sunset', type: 'text/html' )] class HomeHandler implements RequestHandlerInterface { ... In the example above, the ResourceDeprecation attribute is attached to the class, marking the entire / (home) endpoint as deprecated starting from 2038-01-01 . Running the following curl will print out the response headers where we can see the Sunset and Link headers. curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:23:11 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin","title":"Marking an entire endpoint as deprecated"},{"location":"v5/tutorials/api-evolution/#marking-a-method-as-deprecated","text":"Most of the time you want to deprecate only an endpoint, so you will need to use the MethodDeprecation attribute which has the same parameters, but it attaches to a handler method. ... class HomeHandler implements RequestHandlerInterface { ... use Api\\App\\Attribute\\MethodDeprecation; #[MethodDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Method deprecation example.', rel: 'sunset', type: 'text/html' )] public function get(): ResponseInterface { ... } } Attaching the MethodDeprecation can only be done to HTTP verb methods ( GET , POST , PUT , PATCH and DELETE ). If you followed along you can run the below curl: curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" The response lists the Sunset and Link headers. HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:54:57 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin","title":"Marking a method as deprecated"},{"location":"v5/tutorials/api-evolution/#notes","text":"If Link or Sunset do not have a value they will not appear in the response headers. Sunset has to be a valid date, otherwise it will throw an error. You cannot use both ResourceDeprecation and MethodDeprecation in the same handler. Deprecations can only be attached to handler classes that implement RequestHandlerInterface . The rel and type arguments are optional, they default to sunset and text/html if no value was provided and are Link related parts.","title":"NOTES"},{"location":"v5/tutorials/cors/","text":"CORS 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. 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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ). How to fix? Dotkernel API fixes this issue using the mezzio/mezzio-cors library. Step 1: Install library In order to install mezzio/mezzio-cors , run the following command: composer require mezzio/mezzio-cors Step 2: Configure your API Register ConfigProvider Register mezzio/mezzio-cors in your application by adding its ConfigProvider to your application's config aggregator. Open the file config/config.php and paste the below lines at the beginning of the array passed to ConfigAggregator : Laminas\\Diactoros\\ConfigProvider::class, Mezzio\\Cors\\ConfigProvider::class, Save and close the file. Add middleware Add mezzio/mezzio-cors middleware to your application's pipeline. Open config/pipeline.php and paste the below line before the one with RouteMiddleware::class : $app->pipe(\\Mezzio\\Cors\\Middleware\\CorsMiddleware::class); Save and close the file. Create config file Create and open file config/autoload/cors.local.php and add the following code inside it: <?php declare(strict_types=1); use Mezzio\\Cors\\Configuration\\ConfigurationInterface; return [ ConfigurationInterface::CONFIGURATION_IDENTIFIER => [ 'allowed_origins' => [ ConfigurationInterface::ANY_ORIGIN, ], 'allowed_headers' => ['Accept', 'Content-Type', 'Authorization'], 'allowed_max_age' => '600', 'credentials_allowed' => true, 'exposed_headers' => [], ], ]; This list explains the above configuration values: allowed_origins : an array of domains that are allowed to interact with the API (default ConfigurationInterface::ANY_ORIGIN which means that any domain can make requests to the API) allowed_headers : an array of allowed custom headers allowed_max_age : the maximum duration, since the preflight response may be cached by a client credentials_allowed : allows a request to pass cookies exposed_headers : an array of headers which are being exposed by the endpoint Save and close the file. On the production environment, make sure you allow only specific origins by adding them to the allowed_origins array and removing the current value of ConfigurationInterface::ANY_ORIGIN . For more info, see mezzio/mezzio-cors documentation .","title":"Setting up CORS"},{"location":"v5/tutorials/cors/#cors","text":"","title":"CORS"},{"location":"v5/tutorials/cors/#what-is-cors","text":"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.","title":"What is CORS?"},{"location":"v5/tutorials/cors/#why-do-we-need-cors","text":"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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ).","title":"Why do we need CORS?"},{"location":"v5/tutorials/cors/#how-to-fix","text":"Dotkernel API fixes this issue using the mezzio/mezzio-cors library.","title":"How to fix?"},{"location":"v5/tutorials/create-book-module/","text":"Implementing a book module in Dotkernel API 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. . └── src/ └── Book/ └── src/ ├── Collection/ │ └── BookCollection.php ├── Entity/ │ └── Book.php ├── Handler/ │ └── BookHandler.php ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/Entity/Book.php - an entity refers to a PHP class that represents a persistent object or data structure src/Book/src/Handler/BookHandler.php - handlers are middleware that can handle requests based on an action src/Book/src/Repository/BookRepository.php - a repository is a class responsible for querying and retrieving entities from the database src/Book/src/Service/BookService.php - is a class or component responsible for performing a specific task or providing functionality to other parts of the application src/Book/src/ConfigProvider.php - is a class that provides configuration for various aspects of the framework or application src/Book/src/RoutesDelegator.php - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application src/Book/src/InputFilter/BookInputFilter.php - input filters and validators src/Book/src/InputFilter/Input/* - input filters and validator configurations Creating and configuring the module Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. In src folder we will create the Book folder and in this we will create the src folder. So the final structure will be like this: src/Book/src . In src/Book/src we will create 2 php files: RoutesDelegator.php and ConfigProvider.php . This files will be updated later with all needed configuration. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); return $app; } } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ ], 'aliases' => [ ], ]; } private function getDoctrineConfig(): array { return [ ]; } private function getHalConfig(): array { return [ ]; } } Registering the module register the module config by adding the Api\\Book\\ConfigProvider::class in config/config.php under the Api\\User\\ConfigProvider::class register the namespace by adding this line \"Api\\\\Book\\\\\": \"src/Book/src/\" , in composer.json under the autoload.psr-4 key update Composer autoloader by running the command: composer dump-autoload That's it. The module is now registered and, we can continue creating Handlers, Services, Repositories and whatever is needed for out tutorial. File creation and contents Each file below have a summary description above of what that file does. src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\TimestampsTrait; use Api\\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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\DependencyInjection\\Attribute\\Entity; /** * @extends EntityRepository<object> */ #[Entity(name: Book::class)] 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/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Repository\\BookRepository; interface BookServiceInterface { public function getRepository(): BookRepository; } src/Book/src/Service/BookService.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\DependencyInjection\\Attribute\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { #[Inject(BookRepository::class)] public function __construct(protected BookRepository $bookRepository) { } public function getRepository(): BookRepository { return $this->bookRepository; } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->setRequired($isRequired); $this->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $this->getValidatorChain() ->attachByName(Date::class, [ 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), ], true); } } Now we add all the inputs together in a parent input filter. src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } We split all the inputs just for the purpose of this tutorial and to demonstrate a clean BookInputFiler but you could have all the inputs created directly in the BookInputFilter like this: $nameInput = new Input(); $nameInput->setRequired(true); $nameInput->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); $this->add($nameInput); Now it's time to create the handler. src/Book/src/Handler/BookHandler.php <?php declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\HandlerTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Fig\\Http\\Message\\StatusCodeInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\DependencyInjection\\Attribute\\Inject; class BookHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, BookServiceInterface::class, \"config\" )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService, protected array $config ) { } public function get(ServerRequestInterface $request): ResponseInterface { $book = $this->bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); if (! $book instanceof Book){ return $this->notFoundResponse(); } return $this->createResponse($request, $book); } public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->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(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } $book = $this->bookService->createBook($inputFilter->getValues()); return $this->createResponse($request, $book); } } After we have the handler, we need to register some routes in the RoutesDelegator , the same we created when we registered the module. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $uuid = \\Api\\App\\RoutesDelegator::REGEXP_UUID; $app->get( '/books', BookHandler::class, 'books.list' ); $app->get( '/book/'.$uuid, BookHandler::class, 'book.show' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } We need to configure access to the newly created endpoints, add books.list , book.show and book.create to the authorization rbac array, under the UserRole::ROLE_GUEST key. Make sure you read and understand the rbac documentation. It's time to update the ConfigProvider with all the necessary configuration needed, so the above files to work properly like dependency injection, aliases, doctrine mapping and so on. src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Doctrine\\ORM\\Mapping\\Driver\\AttributeDriver; use Dot\\DependencyInjection\\Factory\\AttributedRepositoryFactory; use Dot\\DependencyInjection\\Factory\\AttributedServiceFactory; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ BookHandler::class => AttributedServiceFactory::class, BookService::class => AttributedServiceFactory::class, BookRepository::class => AttributedRepositoryFactory::class, ], 'aliases' => [ BookServiceInterface::class => BookService::class, ], ]; } private function getDoctrineConfig(): array { return [ 'driver' => [ 'orm_default' => [ 'drivers' => [ 'Api\\Book\\Entity' => 'BookEntities' ], ], 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/Entity', ], ], ]; } private function getHalConfig(): array { return [ AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), AppConfigProvider::getResource(Book::class, 'book.show') ]; } } Migrations We created the Book entity, but we didn't create the associated table for it. You can check the mapping files by running: php bin/doctrine orm:validate-schema 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://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books To retrieve a book use: curl http://0.0.0.0:8080/book/{uuid}","title":"Creating a book module"},{"location":"v5/tutorials/create-book-module/#implementing-a-book-module-in-dotkernel-api","text":"","title":"Implementing a book module in Dotkernel API"},{"location":"v5/tutorials/create-book-module/#folder-and-files-structure","text":"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. . └── src/ └── Book/ └── src/ ├── Collection/ │ └── BookCollection.php ├── Entity/ │ └── Book.php ├── Handler/ │ └── BookHandler.php ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php 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","title":"Folder and files structure"},{"location":"v5/tutorials/create-book-module/#creating-and-configuring-the-module","text":"Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. In src folder we will create the Book folder and in this we will create the src folder. So the final structure will be like this: src/Book/src . In src/Book/src we will create 2 php files: RoutesDelegator.php and ConfigProvider.php . This files will be updated later with all needed configuration. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); return $app; } } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ ], 'aliases' => [ ], ]; } private function getDoctrineConfig(): array { return [ ]; } private function getHalConfig(): array { return [ ]; } }","title":"Creating and configuring the module"},{"location":"v5/tutorials/create-book-module/#file-creation-and-contents","text":"Each file below have a summary description above of what that file does. src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\TimestampsTrait; use Api\\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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\DependencyInjection\\Attribute\\Entity; /** * @extends EntityRepository<object> */ #[Entity(name: Book::class)] 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/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Repository\\BookRepository; interface BookServiceInterface { public function getRepository(): BookRepository; } src/Book/src/Service/BookService.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\DependencyInjection\\Attribute\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { #[Inject(BookRepository::class)] public function __construct(protected BookRepository $bookRepository) { } public function getRepository(): BookRepository { return $this->bookRepository; } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->setRequired($isRequired); $this->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $this->getValidatorChain() ->attachByName(Date::class, [ 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), ], true); } } Now we add all the inputs together in a parent input filter. src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } We split all the inputs just for the purpose of this tutorial and to demonstrate a clean BookInputFiler but you could have all the inputs created directly in the BookInputFilter like this: $nameInput = new Input(); $nameInput->setRequired(true); $nameInput->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); $this->add($nameInput); Now it's time to create the handler. src/Book/src/Handler/BookHandler.php <?php declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\HandlerTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Fig\\Http\\Message\\StatusCodeInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\DependencyInjection\\Attribute\\Inject; class BookHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, BookServiceInterface::class, \"config\" )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService, protected array $config ) { } public function get(ServerRequestInterface $request): ResponseInterface { $book = $this->bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); if (! $book instanceof Book){ return $this->notFoundResponse(); } return $this->createResponse($request, $book); } public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->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(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } $book = $this->bookService->createBook($inputFilter->getValues()); return $this->createResponse($request, $book); } } After we have the handler, we need to register some routes in the RoutesDelegator , the same we created when we registered the module. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $uuid = \\Api\\App\\RoutesDelegator::REGEXP_UUID; $app->get( '/books', BookHandler::class, 'books.list' ); $app->get( '/book/'.$uuid, BookHandler::class, 'book.show' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } We need to configure access to the newly created endpoints, add books.list , book.show and book.create to the authorization rbac array, under the UserRole::ROLE_GUEST key. Make sure you read and understand the rbac documentation. It's time to update the ConfigProvider with all the necessary configuration needed, so the above files to work properly like dependency injection, aliases, doctrine mapping and so on. src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Doctrine\\ORM\\Mapping\\Driver\\AttributeDriver; use Dot\\DependencyInjection\\Factory\\AttributedRepositoryFactory; use Dot\\DependencyInjection\\Factory\\AttributedServiceFactory; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ BookHandler::class => AttributedServiceFactory::class, BookService::class => AttributedServiceFactory::class, BookRepository::class => AttributedRepositoryFactory::class, ], 'aliases' => [ BookServiceInterface::class => BookService::class, ], ]; } private function getDoctrineConfig(): array { return [ 'driver' => [ 'orm_default' => [ 'drivers' => [ 'Api\\Book\\Entity' => 'BookEntities' ], ], 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/Entity', ], ], ]; } private function getHalConfig(): array { return [ AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), AppConfigProvider::getResource(Book::class, 'book.show') ]; } }","title":"File creation and contents"},{"location":"v5/tutorials/create-book-module/#migrations","text":"We created the Book entity, but we didn't create the associated table for it. You can check the mapping files by running: php bin/doctrine orm:validate-schema 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","title":"Migrations"},{"location":"v5/tutorials/create-book-module/#checking-endpoints","text":"If we did everything as planned we can call the http://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books To retrieve a book use: curl http://0.0.0.0:8080/book/{uuid}","title":"Checking endpoints"},{"location":"v5/tutorials/find-user-by-identity/","text":"A practical example: Find user by identity 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 : php ./bin/cli.php route:list This command will list all available endpoints, which looks like this: +--------+---------------------------------+--------------------------------+ | 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/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ 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. $app->get('/user/' . $uuid, UserHandler::class, 'user.view'); Our route points to get method from UserHandler so let's navigate to that method. public function get(ServerRequestInterface $request): ResponseInterface { $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); return $this->createResponse($request, $user); } As we can see, the method will query the database for the user based on its uuid 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 declare(strict_types=1); namespace Api\\User\\Handler; use Api\\App\\Exception\\BadRequestException; use Api\\App\\Exception\\NotFoundException; use Api\\App\\Handler\\HandlerTrait; use Api\\App\\Message; use Api\\User\\Entity\\User; use Api\\User\\Service\\UserServiceInterface; use Dot\\DependencyInjection\\Attribute\\Inject; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use function sprintf; class IdentityHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, UserServiceInterface::class, )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected UserServiceInterface $userService, ) { } /** * @throws NotFoundException * @throws BadRequestException */ public function get(ServerRequestInterface $request): ResponseInterface { $identity = $request->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 : $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 namespace ApiTest\\Functional; use Api\\App\\Message; class IdentityTest extends AbstractFunctionalTest { public function testEmptyIdentityReturnsNotFound(): void { $response = $this->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.","title":"Find user by identity"},{"location":"v5/tutorials/find-user-by-identity/#a-practical-example-find-user-by-identity","text":"","title":"A practical example: Find user by identity"},{"location":"v5/tutorials/find-user-by-identity/#our-goal","text":"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.","title":"Our goal"},{"location":"v5/tutorials/find-user-by-identity/#what-we-have","text":"Let's print out all available endpoints using : php ./bin/cli.php route:list This command will list all available endpoints, which looks like this: +--------+---------------------------------+--------------------------------+ | 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/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"What we have"},{"location":"v5/tutorials/token-authentication/","text":"Token authentication What is token authentication? Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string. How does it work? In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. Credentials The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel Flow client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource Note The first two steps need to executed only once. Access token should be stored and reused for all upcoming requests. Refresh token should be stored and used to refresh expired access token. For a better overview of the flow, see the below image: Generate admin access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your admin account's identity and <password> with your admin account's password . Both fields come from table admin . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' Generate user access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your user account's identity and <password> with your user account's password . Both fields come from table user . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (modify in: config/autoload/local.php > authentication . access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 400 Bad Request response with the following JSON body: { \"error\": \"Invalid credentials.\", \"error_description\": \"Invalid credentials.\", \"message\": \"Invalid credentials.\" } Refresh admin access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Refresh user access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (change here: config/autoload/local.php authentication -> access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 401 Unauthorized response with the following JSON body: { \"error\": \"invalid_request\", \"error_description\": \"The refresh token is invalid.\", \"hint\": \"Cannot decrypt the refresh token\", \"message\": \"The refresh token is invalid.\" } Test admin authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body. Test user authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body.","title":"Token authentication"},{"location":"v5/tutorials/token-authentication/#token-authentication","text":"","title":"Token authentication"},{"location":"v5/tutorials/token-authentication/#what-is-token-authentication","text":"Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string.","title":"What is token authentication?"},{"location":"v5/tutorials/token-authentication/#how-does-it-work","text":"In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account.","title":"How does it work?"},{"location":"v5/tutorials/token-authentication/#flow","text":"client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource","title":"Flow"},{"location":"v5/tutorials/token-authentication/#generate-admin-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate admin access token"},{"location":"v5/tutorials/token-authentication/#generate-user-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate user access token"},{"location":"v5/tutorials/token-authentication/#refresh-admin-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh admin access token"},{"location":"v5/tutorials/token-authentication/#refresh-user-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh user access token"},{"location":"v5/tutorials/token-authentication/#test-admin-authentication-flow","text":"","title":"Test admin authentication flow"},{"location":"v5/tutorials/token-authentication/#test-user-authentication-flow","text":"","title":"Test user authentication flow"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"DotKernel API 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.","title":"Home"},{"location":"#dotkernel-api","text":"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.","title":"DotKernel API"},{"location":"v4/commands/create-admin-account/","text":"Creating admin accounts in Dotkernel API Usage Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Create admin account"},{"location":"v4/commands/create-admin-account/#creating-admin-accounts-in-dotkernel-api","text":"","title":"Creating admin accounts in Dotkernel API"},{"location":"v4/commands/create-admin-account/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Usage"},{"location":"v4/commands/display-available-endpoints/","text":"Displaying Dotkernel API endpoints using dot-cli Usage Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ Filtering results The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Display available endpoints"},{"location":"v4/commands/display-available-endpoints/#displaying-dotkernel-api-endpoints-using-dot-cli","text":"","title":"Displaying Dotkernel API endpoints using dot-cli"},{"location":"v4/commands/display-available-endpoints/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"Usage"},{"location":"v4/commands/display-available-endpoints/#filtering-results","text":"The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Filtering results"},{"location":"v4/commands/generate-database-migrations/","text":"Generate a database migration without dropping custom tables Usage 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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' Filtering multiple unmapped table patterns If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' Troubleshooting On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt Help You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Generate database migrations"},{"location":"v4/commands/generate-database-migrations/#generate-a-database-migration-without-dropping-custom-tables","text":"","title":"Generate a database migration without dropping custom tables"},{"location":"v4/commands/generate-database-migrations/#usage","text":"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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/'","title":"Usage"},{"location":"v4/commands/generate-database-migrations/#filtering-multiple-unmapped-table-patterns","text":"If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/'","title":"Filtering multiple unmapped table patterns"},{"location":"v4/commands/generate-database-migrations/#troubleshooting","text":"On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt","title":"Troubleshooting"},{"location":"v4/commands/generate-database-migrations/#help","text":"You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Help"},{"location":"v4/commands/generate-tokens/","text":"Generating tokens in Dotkernel API This is a multipurpose command that allows creating tokens required by different parts of the API. Usage Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help Generate error reporting token You can generate an error reporting token by executing the following command: php ./bin/cli.php token:generate error-reporting The output should look similar to this: Error reporting token: 0123456789abcdef0123456789abcdef01234567 Copy the generated token. Open config/autoload/error-handling.global.php and paste the copied token as shown below: return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Save and close config/autoload/error-handling.global.php . Note : If your application is NOT in development mode, make sure you clear your config cache by executing: php ./bin/clear-config-cache.php","title":"Generate tokens"},{"location":"v4/commands/generate-tokens/#generating-tokens-in-dotkernel-api","text":"This is a multipurpose command that allows creating tokens required by different parts of the API.","title":"Generating tokens in Dotkernel API"},{"location":"v4/commands/generate-tokens/#usage","text":"Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help","title":"Usage"},{"location":"v4/core-features/authentication/","text":"Authentication Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface . Configuration Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info. How it works Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware . Database When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope. Issuing API Tokens Token generation in Dotkernel API is done using the password grand_type scenario, which in this case allows authentication to an API using the user's credentials (generally a username and password). The client sends a POST request to the /security/generate-token with the following parameters: grant_type = password. client_id = column name from the oauth_clients table client_secret = column secret from the oauth_clients table scope = column scope from the oauth_scopes table username = column identity from table admin / user password = column password from table admin / user POST /security/generate-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" } Next time when you make a request to the server to an authenticated endpoint, the client should use the Authorization header request. GET /users/1 HTTP/1.1 Accept: application/json Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... Refreshing tokens Dotkernel API can refresh the access token, based on the expired access token's refresh_token . The clients need to send a POST request to the /security/refresh-token with the following request POST /security/refresh-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\" : \"def5020087199939a49d0f2f818...\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" }","title":"Authentication"},{"location":"v4/core-features/authentication/#authentication","text":"Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface .","title":"Authentication"},{"location":"v4/core-features/authentication/#configuration","text":"Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info.","title":"Configuration"},{"location":"v4/core-features/authentication/#how-it-works","text":"Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware .","title":"How it works"},{"location":"v4/core-features/authentication/#database","text":"When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope.","title":"Database"},{"location":"v4/core-features/authorization/","text":"Authorization Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC). How it works In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware. Configuration Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information. Usage Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Authorization"},{"location":"v4/core-features/authorization/#authorization","text":"Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC).","title":"Authorization"},{"location":"v4/core-features/authorization/#how-it-works","text":"In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware.","title":"How it works"},{"location":"v4/core-features/authorization/#configuration","text":"Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information.","title":"Configuration"},{"location":"v4/core-features/authorization/#usage","text":"Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Usage"},{"location":"v4/core-features/content-validation/","text":"Content Negotiation Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary. Configuration In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against. Accept Negotiation This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything. Content-Type Negotiation The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can. The Request <-> Response validation In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"Content Validation"},{"location":"v4/core-features/content-validation/#content-negotiation","text":"Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary.","title":"Content Negotiation"},{"location":"v4/core-features/content-validation/#configuration","text":"In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against.","title":"Configuration"},{"location":"v4/core-features/content-validation/#accept-negotiation","text":"This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything.","title":"Accept Negotiation"},{"location":"v4/core-features/content-validation/#content-type-negotiation","text":"The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can.","title":"Content-Type Negotiation"},{"location":"v4/core-features/content-validation/#the-request-response-validation","text":"In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"The Request <-> Response validation"},{"location":"v4/core-features/cors/","text":"CORS 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. 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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ). How to fix? Dotkernel API fixes this issue using the mezzio/mezzio-cors library. Step 1: Install library In order to install mezzio/mezzio-cors , run the following command: composer require mezzio/mezzio-cors Step 2: Configure your API Register ConfigProvider Register mezzio/mezzio-cors in your application by adding its ConfigProvider to your application's config aggregator. Open the file config/config.php and paste the below lines at the beginning of the array passed to ConfigAggregator : Laminas\\Diactoros\\ConfigProvider::class, Mezzio\\Cors\\ConfigProvider::class, Save and close the file. Add middleware Add mezzio/mezzio-cors middleware to your application's pipeline. Open config/pipeline.php and paste the below line before the one with RouteMiddleware::class : $app->pipe(\\Mezzio\\Cors\\Middleware\\CorsMiddleware::class); Save and close the file. Create config file Create and open file config/autoload/cors.local.php and add the following code inside it: <?php declare(strict_types=1); use Mezzio\\Cors\\Configuration\\ConfigurationInterface; return [ ConfigurationInterface::CONFIGURATION_IDENTIFIER => [ 'allowed_origins' => [ ConfigurationInterface::ANY_ORIGIN, ], 'allowed_headers' => ['Accept', 'Content-Type', 'Authorization'], 'allowed_max_age' => '600', 'credentials_allowed' => true, 'exposed_headers' => [], ], ]; This list explains the above configuration values: allowed_origins : an array of domains that are allowed to interact with the API (default ConfigurationInterface::ANY_ORIGIN which means that any domain can make requests to the API) allowed_headers : an array of allowed custom headers allowed_max_age : the maximum duration, since the preflight response may be cached by a client credentials_allowed : allows a request to pass cookies exposed_headers : an array of headers which are being exposed by the endpoint Save and close the file. On the production environment, make sure you allow only specific origins by adding them to the allowed_origins array and removing the current value of ConfigurationInterface::ANY_ORIGIN . For more info, see mezzio/mezzio-cors documentation .","title":"CORS"},{"location":"v4/core-features/cors/#cors","text":"","title":"CORS"},{"location":"v4/core-features/cors/#what-is-cors","text":"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.","title":"What is CORS?"},{"location":"v4/core-features/cors/#why-do-we-need-cors","text":"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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ).","title":"Why do we need CORS?"},{"location":"v4/core-features/cors/#how-to-fix","text":"Dotkernel API fixes this issue using the mezzio/mezzio-cors library.","title":"How to fix?"},{"location":"v4/core-features/exceptions/","text":"Exceptions What are exceptions? Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code. How we use exceptions? When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Out-of-the-box we provide the following custom exceptions: BadRequestException thrown when client tries to create/update resource, but the data from the request is invalid/incomplete (example: client tries to create an account, but does not send the required identity field) ConflictException thrown when resource cannot be created because a different resource with the same identifier already exists (example: cannot change existing user's identity because another user with the same identity already exists) resource cannot change its state because it is already in the specified state (example: user cannot be activated because it is already active) ExpiredException thrown when resource cannot be accessed because it expired (example: account activation link) resource cannot be accessed because it has been consumed (example: one-time password) ForbiddenException thrown when resource cannot be accessed by the authenticated client (example: client authenticated as regular user sends a GET /admin request) MethodNotAllowedException thrown when client tries to interact with a resource via an invalid HTTP request method (example: client sends a PATCH /avatar request) NotFoundException thrown when client tries to interact with a resource that does not exist on the server (example: client sends a GET /resource-does-not-exist request) UnauthorizedException thrown when resource cannot be accessed because the client is not authenticated (example: unauthenticated client sends a GET /admin request) How it works? During a request, if there is no uncaught exception Dotkernel API will return a JSON response with the data provided by the handler that handled the request. Else, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response How to extend? In this example we will create a custom exception called CustomException , place it next to the already existing custom exceptions (you can use your preferred location) and finally return a custom HTTP status code when CustomException is encountered. Step 1: Create exception file Navigate to the directory src/App/src/Handler/Exception and create a PHP class called CustomException.php . Open CustomException.php and add the following content: <?php declare(strict_types=1); namespace Api\\App\\Exception; use Exception; class CustomException extends Exception { } Save and close the file. Step 2: Use exception file Open the file src/App/src/Handler/HomeHandler.php and at the beginning of the get method, place the following code: throw new \\Api\\App\\Exception\\CustomException('some message'); Save and close the file. Step 3: Test for failure Access your API's home page URL and make sure it returns 500 Internal Server Error HTTP status code and the following content: { \"error\": { \"messages\": [ \"some message\" ] } } Step 4: Prepare for success Open the file src/App/src/Handler/HandlerTrait.php and locate the handle method. Insert the following lines of code before the first catch statement: } catch (\\Api\\App\\Exception\\CustomException $exception) { return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_IM_A_TEAPOT); Save and close the file. Step 5: Test for success Again, access your API's home page URL, which should return the same content. Notice that this time it returns 418 I'm a teapot HTTP status code.","title":"Exceptions"},{"location":"v4/core-features/exceptions/#exceptions","text":"","title":"Exceptions"},{"location":"v4/core-features/exceptions/#what-are-exceptions","text":"Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code.","title":"What are exceptions?"},{"location":"v4/core-features/exceptions/#how-we-use-exceptions","text":"When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Out-of-the-box we provide the following custom exceptions:","title":"How we use exceptions?"},{"location":"v4/core-features/exceptions/#how-it-works","text":"During a request, if there is no uncaught exception Dotkernel API will return a JSON response with the data provided by the handler that handled the request. Else, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response","title":"How it works?"},{"location":"v4/core-features/exceptions/#how-to-extend","text":"In this example we will create a custom exception called CustomException , place it next to the already existing custom exceptions (you can use your preferred location) and finally return a custom HTTP status code when CustomException is encountered.","title":"How to extend?"},{"location":"v4/flow/default-library-flow/","text":"Default Library Flow The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v4/flow/default-library-flow/#default-library-flow","text":"The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v4/flow/library-flow-for-email/","text":"Library Flow for Email The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v4/flow/library-flow-for-email/#library-flow-for-email","text":"The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v4/flow/middleware-flow/","text":"Middleware flow The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware Flow"},{"location":"v4/flow/middleware-flow/#middleware-flow","text":"The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware flow"},{"location":"v4/installation/composer/","text":"Composer Installation of Packages Install dependencies composer install Development mode If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Composer"},{"location":"v4/installation/composer/#composer-installation-of-packages","text":"","title":"Composer Installation of Packages"},{"location":"v4/installation/composer/#install-dependencies","text":"composer install","title":"Install dependencies"},{"location":"v4/installation/composer/#development-mode","text":"If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Development mode"},{"location":"v4/installation/configuration-files/","text":"Configuration Files Prepare config files duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php Note if your API will be consumed by another application, make sure to configure the allowed_origins variable duplicate config/autoload/local.php.dist as config/autoload/local.php duplicate config/autoload/mail.local.php.dist as config/autoload/mail.local.php Note if your API will send emails, make sure to fill in SMTP connection params optional : in order to run/create tests, duplicate config/autoload/local.test.php.dist as config/autoload/local.test.php Note this creates a new in-memory database that your tests will run on.","title":"Configuration Files"},{"location":"v4/installation/configuration-files/#configuration-files","text":"","title":"Configuration Files"},{"location":"v4/installation/configuration-files/#prepare-config-files","text":"duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php","title":"Prepare config files"},{"location":"v4/installation/doctrine-orm/","text":"Doctrine ORM Setup database Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci Running migrations Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation. Executing fixtures Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found here: https://github.com/dotkernel/dot-data-fixtures#creating-fixtures","title":"Doctrine ORM"},{"location":"v4/installation/doctrine-orm/#doctrine-orm","text":"","title":"Doctrine ORM"},{"location":"v4/installation/doctrine-orm/#setup-database","text":"Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci","title":"Setup database"},{"location":"v4/installation/doctrine-orm/#running-migrations","text":"Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation.","title":"Running migrations"},{"location":"v4/installation/doctrine-orm/#executing-fixtures","text":"Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found here: https://github.com/dotkernel/dot-data-fixtures#creating-fixtures","title":"Executing fixtures"},{"location":"v4/installation/faq/","text":"Frequently Asked Questions How do I fix common permission issues? If running your project you encounter some permission issues, follow the below steps. Errors PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache/doctrine\" is not writable... Fix: chmod -R 777 data Error PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/public/uploads\" is not writable... Fix: chmod -R 777 public/uploads Error PHP Fatal error: Uncaught ErrorException: fopen(/var/www/ example.local /config/autoload/../../log/error-log- yyyy-mm-dd.log ): Failed to open stream: Permission denied... Fix: chmod -R 777 log","title":"FAQ"},{"location":"v4/installation/faq/#frequently-asked-questions","text":"","title":"Frequently Asked Questions"},{"location":"v4/installation/faq/#how-do-i-fix-common-permission-issues","text":"If running your project you encounter some permission issues, follow the below steps.","title":"How do I fix common permission issues?"},{"location":"v4/installation/getting-started/","text":"Clone the project Recommended development environment If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Getting Started"},{"location":"v4/installation/getting-started/#clone-the-project","text":"","title":"Clone the project"},{"location":"v4/installation/getting-started/#recommended-development-environment","text":"If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Recommended development environment"},{"location":"v4/installation/test-the-installation/","text":"Test the installation Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 4\"} Old way of doing things, using PHP built-in server php -S 0.0.0.0:8080 -t public Running tests The project has 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","title":"Test the Installation"},{"location":"v4/installation/test-the-installation/#test-the-installation","text":"Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 4\"}","title":"Test the installation"},{"location":"v4/installation/test-the-installation/#old-way-of-doing-things-using-php-built-in-server","text":"php -S 0.0.0.0:8080 -t public","title":"Old way of doing things, using PHP built-in server"},{"location":"v4/installation/test-the-installation/#running-tests","text":"The project has 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","title":"Running tests"},{"location":"v4/installation/test-the-installation/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v4/installation/test-the-installation/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v4/introduction/file-structure/","text":"File structure Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default: Main directories bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files Special purpose folders .github - containes workflow files .laminas-ci - contains laminas-ci workflow files src directory This directory contains all source code related to the Module. It should contain following directories, if they’re not empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - For database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but these should give you an idea of how the structure should look like. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory should also contain 2 files: ConfigProvider.php - Provides configuration data RoutesDelegator.php - Module main routes entry file templates directory This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig data directory This directory contains project-related data (such as cache, file uploads) We recommend using the following directory structure: data/cache - location where caches are stored data/oauth - encryption, private and public keys needed for authentication. data/doctrine - fixtures and migrations data/lock - lock files generated by dotkernel/dot-cli See more","title":"File Structure"},{"location":"v4/introduction/file-structure/#file-structure","text":"Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default:","title":"File structure"},{"location":"v4/introduction/file-structure/#main-directories","text":"bin - executable files from CLI config - various configuration files data - should contain project-related data (AVOID storing sensitive data on VCS) documentation - should contain project-related documentation log - storage of log files generated by dot-error-log library public - publicly visible files. The webserver need to have this folder as www-document root folder. src - should contain the source code files test - should contain the test files","title":"Main directories"},{"location":"v4/introduction/file-structure/#special-purpose-folders","text":".github - containes workflow files .laminas-ci - contains laminas-ci workflow files","title":"Special purpose folders"},{"location":"v4/introduction/file-structure/#src-directory","text":"This directory contains all source code related to the Module. It should contain following directories, if they’re not empty: Handler - Action classes (similar to Controllers but can only perform one action) Entity - For database entities Service - Service classes Collection - Database entities collections Repository - Entity repository folder The above example is just some of the directories a project may include, but these should give you an idea of how the structure should look like. Other classes in the src directory may include InputFilter , EventListener , Helper , Command , Factory etc. The src directory should also contain 2 files: ConfigProvider.php - Provides configuration data RoutesDelegator.php - Module main routes entry file","title":"src directory"},{"location":"v4/introduction/file-structure/#templates-directory","text":"This directory contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig","title":"templates directory"},{"location":"v4/introduction/file-structure/#data-directory","text":"This directory contains project-related data (such as cache, file uploads) We recommend using the following directory structure: data/cache - location where caches are stored data/oauth - encryption, private and public keys needed for authentication. data/doctrine - fixtures and migrations data/lock - lock files generated by dotkernel/dot-cli See more","title":"data directory"},{"location":"v4/introduction/introduction/","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Annotations (dotkernel/dot-annotated-services) Input Filter (laminas/laminas-inputfilter) Doctrine 2 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/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.0 OAuth 2.0 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 OAuth 2.0 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","title":"Introduction"},{"location":"v4/introduction/introduction/#introduction","text":"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/mezzio-problem-details) CORS (mezzio/mezzio-cors) Routing (mezzio/mezzio-fastroute) Authentication (mezzio/mezzio-authentication) Authorization (mezzio/mezzio-authorization) Config Aggregator (laminas/laminas-config-aggregator) Container (roave/psr-container-doctrine) Annotations (dotkernel/dot-annotated-services) Input Filter (laminas/laminas-inputfilter) Doctrine 2 ORM (doctrine/orm) Serializer/Deserializer (laminas/laminas-hydrator) Paginator (laminas/laminas-paginator) HAL (mezzio/mezzio-hal) CLI (dotkernel/dot-cli) TwigRenderer (mezzio/mezzio-twigrenderer) Fixtures (dotkernel/dot-data-fixtures) UUID (ramsey/uuid-doctrine)","title":"Introduction"},{"location":"v4/introduction/introduction/#doctrine-2-orm","text":"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.","title":"Doctrine 2 ORM"},{"location":"v4/introduction/introduction/#documentation","text":"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","title":"Documentation"},{"location":"v4/introduction/introduction/#hypertext-application-language","text":"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.","title":"Hypertext Application Language"},{"location":"v4/introduction/introduction/#cors","text":"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.","title":"CORS"},{"location":"v4/introduction/introduction/#oauth-20","text":"OAuth 2.0 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 OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using league/oauth2-server package.","title":"OAuth 2.0"},{"location":"v4/introduction/introduction/#email","text":"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.","title":"Email"},{"location":"v4/introduction/introduction/#configuration","text":"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.","title":"Configuration"},{"location":"v4/introduction/introduction/#routing","text":"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 .","title":"Routing"},{"location":"v4/introduction/introduction/#commands","text":"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 .","title":"Commands"},{"location":"v4/introduction/introduction/#file-locker","text":"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.","title":"File locker"},{"location":"v4/introduction/introduction/#psr-standards","text":"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","title":"PSR Standards"},{"location":"v4/introduction/introduction/#tests","text":"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","title":"Tests"},{"location":"v4/introduction/introduction/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v4/introduction/introduction/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v4/introduction/packages/","text":"Packages dotkernel/dot-annotated-services - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-http - Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-paginator - Paginate collections of data from arbitrary sources laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more laminas/laminas-text - Create FIGlets and text-based tables mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v4/introduction/packages/#packages","text":"dotkernel/dot-annotated-services - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-http - Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-paginator - Paginate collections of data from arbitrary sources laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more laminas/laminas-text - Create FIGlets and text-based tables mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v4/introduction/server-requirements/","text":"Server Requirements For production, we highly recommend a *nix based system. Webserver Apache >= 2.2 or Nginx mod_rewrite .htaccess support (AllowOverride All) PHP >= 8.2 Both mod_php and FCGI (FPM) are supported. Required Settings and Modules & Extensions memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH) RDBMS MySQL / MariaDB >= 5.5.3 Recommended extensions opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used)","title":"Server Requirements"},{"location":"v4/introduction/server-requirements/#server-requirements","text":"For production, we highly recommend a *nix based system.","title":"Server Requirements"},{"location":"v4/introduction/server-requirements/#webserver","text":"Apache >= 2.2 or Nginx mod_rewrite .htaccess support (AllowOverride All)","title":"Webserver"},{"location":"v4/introduction/server-requirements/#php-82","text":"Both mod_php and FCGI (FPM) are supported.","title":"PHP >= 8.2"},{"location":"v4/introduction/server-requirements/#required-settings-and-modules-extensions","text":"memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH)","title":"Required Settings and Modules & Extensions"},{"location":"v4/introduction/server-requirements/#rdbms","text":"MySQL / MariaDB >= 5.5.3","title":"RDBMS"},{"location":"v4/introduction/server-requirements/#recommended-extensions","text":"opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used)","title":"Recommended extensions"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/","text":"Laminas API Tools compared to Dotkernel API API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15 Note Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Laminas API Tools vs Dotkernel API"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/#laminas-api-tools-compared-to-dotkernel-api","text":"API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15","title":"Laminas API Tools compared to Dotkernel API"},{"location":"v4/transition-from-api-tools/api-tools-vs-dotkernel-api/#note","text":"Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Note"},{"location":"v4/transition-from-api-tools/discovery-phase/","text":"Discovery phase for a current system built using API Tools [WIP] In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it. Database there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else ) Note Dotkernel API is tested only with MariaDB version 10.6 and 10.11 LTS Authentication and Authorization how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac) Modules analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations Custom functionalities Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Discovery Phase"},{"location":"v4/transition-from-api-tools/discovery-phase/#discovery-phase-for-a-current-system-built-using-api-tools-wip","text":"In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it.","title":"Discovery phase for a current system built using API Tools [WIP]"},{"location":"v4/transition-from-api-tools/discovery-phase/#database","text":"there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else )","title":"Database"},{"location":"v4/transition-from-api-tools/discovery-phase/#authentication-and-authorization","text":"how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac)","title":"Authentication and Authorization"},{"location":"v4/transition-from-api-tools/discovery-phase/#modules","text":"analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations","title":"Modules"},{"location":"v4/transition-from-api-tools/discovery-phase/#custom-functionalities","text":"Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Custom functionalities"},{"location":"v4/transition-from-api-tools/transition-approach/","text":"Transition approach [WIP] Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools Business cases There are at least 2 approaches for this transition: Clone 1:1 and recreate all endpoints and entities Build a new version of the current API using Dotkernel API and keep it running as separate platforms until the sunset of the current version of api-tools","title":"Transition Approach"},{"location":"v4/transition-from-api-tools/transition-approach/#transition-approach-wip","text":"Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools","title":"Transition approach [WIP]"},{"location":"v4/transition-from-api-tools/transition-approach/#business-cases","text":"There are at least 2 approaches for this transition:","title":"Business cases"},{"location":"v4/tutorials/create-book-module/","text":"Implementing a book module in Dotkernel 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 ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/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 declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\Book\\Repository\\BookRepository; use DateTimeImmutable; use Doctrine\\ORM\\Mapping as ORM; #[ORM\\Entity(repositoryClass: BookRepository::class)] #[ORM\\Table(\"book\")] class Book extends AbstractEntity { #[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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\AnnotatedServices\\Annotation\\Entity; /** * @Entity(name=\"Api\\Book\\Entity\\Book\") * @extends EntityRepository<object> */ 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 declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\AnnotatedServices\\Annotation\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { /** * @Inject({ * BookRepository::class, * }) */ public function __construct(protected BookRepository $bookRepository) { } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } src/Book/src/Service/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; interface BookServiceInterface { } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Dot\\AnnotatedServices\\Factory\\AnnotatedRepositoryFactory; use Dot\\AnnotatedServices\\Factory\\AnnotatedServiceFactory; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $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 namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $app->get( '/books', BookHandler::class, 'books.list' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\ResponseTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\AnnotatedServices\\Annotation\\Inject; class BookHandler implements RequestHandlerInterface { use ResponseTrait; /** * @Inject({ * HalResponseFactory::class, * ResourceGenerator::class, * BookServiceInterface::class * }) */ public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService ) { } public function get(ServerRequestInterface $request): ResponseInterface { $books = $this->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 update Composer autoloader by running the command: composer dump-autoload It should look like this: 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 this under the doctrine.driver key: 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/../../src/Book/src/Entity', ], Api\\\\Book\\Entity' => 'BookEntities', add this under the doctrine.driver.drivers key Example: <?php ... return [ 'doctrine' => [ ... '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://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books","title":"Creating a book module"},{"location":"v4/tutorials/create-book-module/#implementing-a-book-module-in-dotkernel-api","text":"","title":"Implementing a book module in Dotkernel API"},{"location":"v4/tutorials/create-book-module/#file-structure","text":"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 ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/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","title":"File structure"},{"location":"v4/tutorials/create-book-module/#file-creation-and-contents","text":"src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\Book\\Repository\\BookRepository; use DateTimeImmutable; use Doctrine\\ORM\\Mapping as ORM; #[ORM\\Entity(repositoryClass: BookRepository::class)] #[ORM\\Table(\"book\")] class Book extends AbstractEntity { #[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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\AnnotatedServices\\Annotation\\Entity; /** * @Entity(name=\"Api\\Book\\Entity\\Book\") * @extends EntityRepository<object> */ 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 declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\AnnotatedServices\\Annotation\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { /** * @Inject({ * BookRepository::class, * }) */ public function __construct(protected BookRepository $bookRepository) { } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } src/Book/src/Service/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; interface BookServiceInterface { } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Dot\\AnnotatedServices\\Factory\\AnnotatedRepositoryFactory; use Dot\\AnnotatedServices\\Factory\\AnnotatedServiceFactory; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $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 namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $app->get( '/books', BookHandler::class, 'books.list' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\ResponseTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\AnnotatedServices\\Annotation\\Inject; class BookHandler implements RequestHandlerInterface { use ResponseTrait; /** * @Inject({ * HalResponseFactory::class, * ResourceGenerator::class, * BookServiceInterface::class * }) */ public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService ) { } public function get(ServerRequestInterface $request): ResponseInterface { $books = $this->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); } }","title":"File creation and contents"},{"location":"v4/tutorials/create-book-module/#configuring-and-registering-the-new-module","text":"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 update Composer autoloader by running the command: composer dump-autoload It should look like this: 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 this under the doctrine.driver key: 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/../../src/Book/src/Entity', ], Api\\\\Book\\Entity' => 'BookEntities', add this under the doctrine.driver.drivers key Example: <?php ... return [ 'doctrine' => [ ... '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.","title":"Configuring and registering the new module"},{"location":"v4/tutorials/create-book-module/#migrations","text":"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","title":"Migrations"},{"location":"v4/tutorials/create-book-module/#checking-endpoints","text":"If we did everything as planned we can call the http://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books","title":"Checking endpoints"},{"location":"v4/tutorials/token-authentication/","text":"Token authentication What is token authentication? Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string. How does it work? In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel Flow client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource Note The first two steps need to executed only once. Access token should be stored and reused for all upcoming requests. Refresh token should be stored and used to refresh expired access token. For a better overview of the flow, see the below image: Generate admin access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your admin account's identity and <password> with your admin account's password . Both fields come from table admin . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' Generate user access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your user account's identity and <password> with your user account's password . Both fields come from table user . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (modify in: config/autoload/local.php > authentication . access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 400 Bad Request response with the following JSON body: { \"error\": \"Invalid credentials.\", \"error_description\": \"Invalid credentials.\", \"message\": \"Invalid credentials.\" } Refresh admin access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Refresh user access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (change here: config/autoload/local.php authentication -> access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 401 Unauthorized response with the following JSON body: { \"error\": \"invalid_request\", \"error_description\": \"The refresh token is invalid.\", \"hint\": \"Cannot decrypt the refresh token\", \"message\": \"The refresh token is invalid.\" } Test admin authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body. Test user authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body.","title":"Token authentication"},{"location":"v4/tutorials/token-authentication/#token-authentication","text":"","title":"Token authentication"},{"location":"v4/tutorials/token-authentication/#what-is-token-authentication","text":"Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string.","title":"What is token authentication?"},{"location":"v4/tutorials/token-authentication/#how-does-it-work","text":"In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel","title":"How does it work?"},{"location":"v4/tutorials/token-authentication/#flow","text":"client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource","title":"Flow"},{"location":"v4/tutorials/token-authentication/#generate-admin-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate admin access token"},{"location":"v4/tutorials/token-authentication/#generate-user-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate user access token"},{"location":"v4/tutorials/token-authentication/#refresh-admin-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh admin access token"},{"location":"v4/tutorials/token-authentication/#refresh-user-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh user access token"},{"location":"v4/tutorials/token-authentication/#test-admin-authentication-flow","text":"","title":"Test admin authentication flow"},{"location":"v4/tutorials/token-authentication/#test-user-authentication-flow","text":"","title":"Test user authentication flow"},{"location":"v5/upgrading/","text":"Upgrades Dotkernel API does not provide an automatic upgrade path. Instead, the recommended procedure is to manually implement each modification listed in releases . Additionally, release info can also be accessed as an RSS feed. Upgrade procedure Once you clone Dotkernel API, you will find a CHANGELOG.md file in the root of the project. This file contains a list of already implemented features in reverse chronological order. You can use this file to track the version of your copy of Dotkernel API. For each new release you need implement the modifications from its pull requests in your project. It is recommended to copy the release info into your project's CHANGELOG.md file. This allows you to track your API's version and keep your project up-to-date with future releases.","title":"Upgrading"},{"location":"v5/upgrading/#upgrades","text":"Dotkernel API does not provide an automatic upgrade path. Instead, the recommended procedure is to manually implement each modification listed in releases . Additionally, release info can also be accessed as an RSS feed.","title":"Upgrades"},{"location":"v5/upgrading/#upgrade-procedure","text":"Once you clone Dotkernel API, you will find a CHANGELOG.md file in the root of the project. This file contains a list of already implemented features in reverse chronological order. You can use this file to track the version of your copy of Dotkernel API. For each new release you need implement the modifications from its pull requests in your project. It is recommended to copy the release info into your project's CHANGELOG.md file. This allows you to track your API's version and keep your project up-to-date with future releases.","title":"Upgrade procedure"},{"location":"v5/commands/create-admin-account/","text":"Creating admin accounts in Dotkernel API Usage Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Create admin account"},{"location":"v5/commands/create-admin-account/#creating-admin-accounts-in-dotkernel-api","text":"","title":"Creating admin accounts in Dotkernel API"},{"location":"v5/commands/create-admin-account/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php admin:create -i {IDENTITY} -p {PASSWORD} -f {FIRST_NAME} -l {LAST_NAME} OR php ./bin/cli.php admin:create --identity {IDENTITY} --password {PASSWORD} --firstName {FIRST_NAME} --lastName {LAST_NAME} after replacing: {IDENTITY} with a valid username OR email address {PASSWORD} with a valid password {FIRST_NAME} and {LAST_NAME} with valid names NOTE: if the specified fields contain special characters, make sure you surround them with double quote signs this method does not allow specifying an admin role – newly created accounts will have role of admin If the submitted data is valid, the outputted response is: Admin account has been created. The new admin account is ready to use. You can get more help with this command by running: php ./bin/cli.php help admin:create","title":"Usage"},{"location":"v5/commands/display-available-endpoints/","text":"Displaying Dotkernel API endpoints using dot-cli Usage Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ Filtering results The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Display available endpoints"},{"location":"v5/commands/display-available-endpoints/#displaying-dotkernel-api-endpoints-using-dot-cli","text":"","title":"Displaying Dotkernel API endpoints using dot-cli"},{"location":"v5/commands/display-available-endpoints/#usage","text":"Run the following command in your application’s root directory: php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: +--------+---------------------------------+--------------------------------+ | Method | Name | Path | +--------+---------------------------------+--------------------------------+ | POST | account.activate.request | /account/activate | | PATCH | account.activate | /account/activate/{hash} | | PATCH | account.modify-password | /account/reset-password/{hash} | | POST | account.recover-identity | /account/recover-identity | | POST | account.register | /account/register | | POST | account.reset-password.request | /account/reset-password | | GET | account.reset-password.validate | /account/reset-password/{hash} | | POST | admin.create | /admin | | DELETE | admin.delete | /admin/{uuid} | | GET | admin.list | /admin | | PATCH | admin.my-account.update | /admin/my-account | | GET | admin.my-account.view | /admin/my-account | | GET | admin.role.list | /admin/role | | GET | admin.role.view | /admin/role/{uuid} | | PATCH | admin.update | /admin/{uuid} | | GET | admin.view | /admin/{uuid} | | POST | error.report | /error-report | | GET | home | / | | POST | security.generate-token | /security/generate-token | | POST | security.refresh-token | /security/refresh-token | | POST | user.activate | /user/{uuid}/activate | | POST | user.avatar.create | /user/{uuid}/avatar | | DELETE | user.avatar.delete | /user/{uuid}/avatar | | GET | user.avatar.view | /user/{uuid}/avatar | | POST | user.create | /user | | DELETE | user.delete | /user/{uuid} | | GET | user.list | /user | | DELETE | user.my-account.delete | /user/my-account | | PATCH | user.my-account.update | /user/my-account | | GET | user.my-account.view | /user/my-account | | POST | user.my-avatar.create | /user/my-avatar | | DELETE | user.my-avatar.delete | /user/my-avatar | | GET | user.my-avatar.view | /user/my-avatar | | GET | user.role.list | /user/role | | GET | user.role.view | /user/role/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"Usage"},{"location":"v5/commands/display-available-endpoints/#filtering-results","text":"The following filters can be applied when displaying the routes list: Filter routes by name, using: -i|--name[=NAME] Filter routes by path, using: -p|--path[=PATH] Filter routes by method, using: -m|--method[=METHOD] The filters are case-insensitive and can be combined. Get more help by running this command: php ./bin/cli.php route:list --help","title":"Filtering results"},{"location":"v5/commands/generate-database-migrations/","text":"Generate a database migration without dropping custom tables Usage 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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/' Filtering multiple unmapped table patterns If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/' Troubleshooting On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt Help You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Generate database migrations"},{"location":"v5/commands/generate-database-migrations/#generate-a-database-migration-without-dropping-custom-tables","text":"","title":"Generate a database migration without dropping custom tables"},{"location":"v5/commands/generate-database-migrations/#usage","text":"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 data/doctrine/migrations/ directory. 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. In order to avoid dropping these tables, you need to add a parameter called filter-expression . The command to be executed without dropping these tables looks like this: On Windows (use double quotes): vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!oauth_)/\" On Linux/macOS (use single quotes): vendor/bin/doctrine-migrations diff --filter-expression='/^(?!oauth_)/'","title":"Usage"},{"location":"v5/commands/generate-database-migrations/#filtering-multiple-unmapped-table-patterns","text":"If your database contains multiple unmapped table groups, then the pattern in filter-expression should hold all table prefixes concatenated by pipe character ( | ). For example, if you need to filter tables prefixed with foo_ and bar_ , then the command should look like this: On Windows: vendor/bin/doctrine-migrations diff --filter-expression=\"/^(?!foo_|bar_)/\" On Linux/macOS: vendor/bin/doctrine-migrations diff --filter-expression='/^(?!foo_|bar_)/'","title":"Filtering multiple unmapped table patterns"},{"location":"v5/commands/generate-database-migrations/#troubleshooting","text":"On Windows, running the command in PowerShell might still add the DROP TABLE oauth_* queries to the migration file. This happens because for PowerShell the caret ( ^ ) is a special character, so it gets dropped ( \"/^(?!oauth_)/\" becomes \"/(?!oauth_)/\" when it reaches your command). Escaping it will not help either. In this case, we recommend running the command: directly from your IDE using Linux shell from the Command Prompt","title":"Troubleshooting"},{"location":"v5/commands/generate-database-migrations/#help","text":"You can get more help with this command by running: vendor/bin/doctrine-migrations help diff","title":"Help"},{"location":"v5/commands/generate-tokens/","text":"Generating tokens in Dotkernel API This is a multipurpose command that allows creating tokens required by different parts of the API. Usage Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help Generate error reporting token You can generate an error reporting token by executing the following command: php ./bin/cli.php token:generate error-reporting The output should look similar to this: Error reporting token: 0123456789abcdef0123456789abcdef01234567 Copy the generated token. Open config/autoload/error-handling.global.php and paste the copied token as shown below: return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Save and close config/autoload/error-handling.global.php . Note : If your application is NOT in development mode, make sure you clear your config cache by executing: php ./bin/clear-config-cache.php","title":"Generate tokens"},{"location":"v5/commands/generate-tokens/#generating-tokens-in-dotkernel-api","text":"This is a multipurpose command that allows creating tokens required by different parts of the API.","title":"Generating tokens in Dotkernel API"},{"location":"v5/commands/generate-tokens/#usage","text":"Go to your application's root directory. Run the token generator command by executing the following command: php ./bin/cli.php token:generate <type> Where <type> is one of the following: error-reporting If you need help using the command, execute the following command: php ./bin/cli.php token:generate --help","title":"Usage"},{"location":"v5/core-features/authentication/","text":"Authentication Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface . Configuration Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info. How it works Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware . Database When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope. Issuing API Tokens Token generation in Dotkernel API is done using the password grand_type scenario, which in this case allows authentication to an API using the user's credentials (generally a username and password). The client sends a POST request to the /security/generate-token with the following parameters: grant_type = password. client_id = column name from the oauth_clients table client_secret = column secret from the oauth_clients table scope = column scope from the oauth_scopes table username = column identity from table admin / user password = column password from table admin / user POST /security/generate-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" } Next time when you make a request to the server to an authenticated endpoint, the client should use the Authorization header request. GET /users/1 HTTP/1.1 Accept: application/json Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... Refreshing tokens Dotkernel API can refresh the access token, based on the expired access token's refresh_token . The clients need to send a POST request to the /security/refresh-token with the following request POST /security/refresh-token HTTP/1.1 Accept: application/json Content-Type: application/json { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\" : \"def5020087199939a49d0f2f818...\" } The server responds with a JSON as follows: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...\", \"refresh_token\": \"def5020087199939a49d0f2f818...\" }","title":"Authentication"},{"location":"v5/core-features/authentication/#authentication","text":"Authentication is the process by which an identity is presented to the application. It ensures that the entity making the request has the proper credentials to access the API. Dotkernel API identities are delivered to the application from the client through the Authorization request. If it is present, the application tries to find and assign the identity to the application. If it is not presented, Dotkernel API assigns a default guest identity, represented by an instance of the class Mezzio\\Authentication\\UserInterface .","title":"Authentication"},{"location":"v5/core-features/authentication/#configuration","text":"Authentication in Dotkernel API is built around the mezzio/mezzio-authentication-oauth2 component and is already configured out of the box. But if you want to dig more, the configuration is stored in config/autoload/local.php under the authentication key. You can check the mezzio/mezzio-authentication-oauth2 configuration part for more info.","title":"Configuration"},{"location":"v5/core-features/authentication/#how-it-works","text":"Dotkernels API authentication system can be used for SPAs (single-page applications), mobile applications, and simple, token-based APIs. It allows each user of your application to generate API tokens for their accounts. The authentication happens through the middleware in the Api\\App\\Middleware\\AuthenticationMiddleware .","title":"How it works"},{"location":"v5/core-features/authentication/#database","text":"When you install Dotkernel API for the first time, you need to run the migrations and seeders. All the tables required for authentication are automatically created and populated. In Dotkernel API, authenticated users come from either the admin or the user table. We choose to keep the admin table separated from the users to prevent users of the application from accessing sensitive data, which only the administrators of the application should access. The oauth_clients table is pre-populated with the default admin and frontend clients with the same password as their names ( we recommend you change the default passwords ). As you guessed each client serves to authenticate admin or user . Another table that is pre-populated is the oauth_scopes table, with the api scope.","title":"Database"},{"location":"v5/core-features/authorization/","text":"Authorization Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC). How it works In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware. Configuration Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information. Usage Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Authorization"},{"location":"v5/core-features/authorization/#authorization","text":"Authorization is the process by which a system takes a validated identity and checks if that identity has access to a given resource. Dotkernel API 's implementation of authorization uses Mezzio\\Authorization\\Rbac\\LaminasRbac as a model of Role-Based Access Control (RBAC).","title":"Authorization"},{"location":"v5/core-features/authorization/#how-it-works","text":"In Dotkernel API each authenticatable entity (admin/user) comes with their roles table where you can define roles for each entity. RBAC comes in to ensure that each entity has the appropriate role and permission to access a resource. The authorization happens through the Api\\App\\Middleware\\AuthorizationMiddleware middleware.","title":"How it works"},{"location":"v5/core-features/authorization/#configuration","text":"Dotkernel API makes use of mezzio-authorization-rbac and includes the full configuration. The configuration file for the role and permission definitions is config/autoload/authorization.global.php . 'mezzio-authorization-rbac' => [ 'roles' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ AdminRole::ROLE_SUPERUSER, ], UserRole::ROLE_GUEST => [ UserRole::ROLE_USER, ], ], 'permissions' => [ AdminRole::ROLE_SUPERUSER => [], AdminRole::ROLE_ADMIN => [ 'other.routes' 'admin.list', 'home' ], UserRole::ROLE_USER => [ 'other.routes', 'user.my-account.update', 'user.my-account.view', ], UserRole::ROLE_GUEST => [ 'other.routes', 'security.refresh-token', 'error.report', 'home', ], ], ], See mezzio-authorization-rbac for more information.","title":"Configuration"},{"location":"v5/core-features/authorization/#usage","text":"Based on the configuration file above, we have 2 admins roles ( superuser , admin ) and 2 users roles ( user , guest ). Roles inherit the permissions from their parents: superuser has no parent admin has superuser as a parent which means superuser also has admin permissions user has no parent guest has user as a parent which means user also has guest permissions For each role we defined an array of permissions. A permission in Dotkernel API is basically a route name. As you can see, the superuser does not have its own permissions, because it gains all the permissions from admin , no need to define explicit permissions. The user role, gains all the permission from guest so no need to define that user can access home route, but guest cannot access user-specific routes.","title":"Usage"},{"location":"v5/core-features/content-validation/","text":"Content Negotiation Introduced in Dotkernel API 5.0.0 Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary. Configuration In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against. Accept Negotiation This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything. Content-Type Negotiation The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can. The Request <-> Response validation In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"Content Validation"},{"location":"v5/core-features/content-validation/#content-negotiation","text":"Introduced in Dotkernel API 5.0.0 Content Negotiation is performed by an application in order : To match the requested representation as specified by the client via the Accept header with a representation the application can deliver. To determine the Content-Type of incoming data and deserialize it so the application can utilize it. Essentially, content negotiation is the client telling the server what it is sending and what it wants in return, and the server determining if it can do what the client requests. Content negotiation validation in Dotkernel API happens through middleware, and it ensures that the incoming request and the outgoing response conform to the content types specified in the config file for all routes or for a specific route. It performs validation on the Accept and Content-Type headers of the request and response and returning appropriate errors responses when necessary.","title":"Content Negotiation"},{"location":"v5/core-features/content-validation/#configuration","text":"In Dotkernel API the configuration file for content negotiation is held in config/autoload/content-negotiation.global.php and the array looks like this: return [ 'content-negotiation' => [ 'default' => [ 'Accept' => [ 'application/json', 'application/hal+json', ], 'Content-Type' => [ 'application/json', 'application/hal+json', ], ], 'your.route.name' => [ 'Accept' => [], 'Content-Type' => [], ], ], ]; Except the default key, all your keys must match the route name, for example in Dotkernel API we have the route to list all admins, which name is admin.list . If you did not specify a route name to configure your specifications about content negotiation, the default one will be in place. The default key is mandatory . Every route configuration must come with Accept and Content-Type keys, basically this will be the keys that the request headers will be validated against.","title":"Configuration"},{"location":"v5/core-features/content-validation/#accept-negotiation","text":"This specifies that your server can return that representation, or at least one of the representation sent by the client. GET /admin HTTP/1.1 Accept: application/json This request indicates the client wants application/json in return. Now the server, through the config file will try to validate if that representation can be returned, basically if application/json is presented in the Accept key. If the representation cannot be returned, a status code 406 - Not Acceptable will be returned. If the representation can be returned, the server should report the media type through Content-Type header of the response. Due to how these validations are made, for a json media type, the server can return a more generic media type, for example, if the clients send Accept: application/vnd.api+json and you configured your Accept key as application/json the representation will still be returned as json . If the Accept header of the request contains */* it means that whatever the server can return it is OK, so it can return anything.","title":"Accept Negotiation"},{"location":"v5/core-features/content-validation/#content-type-negotiation","text":"The second aspect of content negotiation is the Content-Type header and determine the server can deserialize the data. POST /admin/1 HTTP/1.1 Accept: application/json Content-Type: application/json { \"foo\": \"bar\" } The server will try to validate the Content-Type header against your configured Content-Type key from the config file, and if the format is not supported, a status code 415 - Unsupported Media Type will be returned. For example, if you have a route that needs a file to be uploaded , normally you will configure the Content-Type of that route to be multipart/form-data . The above request will fail as the client send application/json as Content-Type . If the request does not contain \"Content-Type\" header, that means that the server will try to deserialize the data as it can.","title":"Content-Type Negotiation"},{"location":"v5/core-features/content-validation/#the-request-response-validation","text":"In addition to the validation described above, a third one is happening and is the last one: the server will check if the request Accept header can really be returned by the response. Through the way Dotkernel API is returning a response in handler, a content type is always set. This cannot be the case in any custom response but in any case the server will check what Content-Type the response is returning and will try to validate that against the Accept header of the request. If the validation fails, a status code 406 - Not Acceptable will be returned.","title":"The Request <-> Response validation"},{"location":"v5/core-features/dependency-injection/","text":"Dependency Injection Dependency injection is a design pattern used in software development to implement inversion of control. In simpler terms, it's the act of providing dependencies for an object during instantiation. In PHP, dependency injection can be implemented in various ways, including through constructor injection, setter injection and property injection. Introduced in Dotkernel API 5.0.0 Dotkernel API, through its dot-dependency-injection package focuses only on constructor injection. Usage Dotkernel API comes out of the box with the dot-dependency-injection package, which provides all we need for injecting dependencies into any object you want. dot-dependency-injection determines the dependencies by looking at the #[Inject] attribute, added to the constructor of a class. Dependencies are specified as separate parameters of the #[Inject] attribute. For our example we will inject UserService and config dependencies into a UseHandler . use Dot\\DependencyInjection\\Attribute\\Inject; class UserHandler implements RequestHandlerInterface { #[Inject( UserService::class, \"config\", )] public function __construct( protected UserServiceInterface $userService, protected array $config, ) { } } If your class needs the value of a specific configuration key, you can specify the path using dot notation config.example . The next step is to register the class in the ConfigProvider under factories using Dot\\DependencyInjection\\Factory\\AttributedServiceFactory::class public function getDependencies(): array { return [ 'factories' => [ UserHandler::class => AttributedServiceFactory::class ] ]; } That's it. When your object is instantiated from the container, it will automatically have its dependencies resolved. Dependencies injection is available to any object within Dotkernel API. For example, you can inject dependencies in a service, a handler and so on, simply by registering it in the ConfigProvider .","title":"Dependency Injection"},{"location":"v5/core-features/dependency-injection/#dependency-injection","text":"Dependency injection is a design pattern used in software development to implement inversion of control. In simpler terms, it's the act of providing dependencies for an object during instantiation. In PHP, dependency injection can be implemented in various ways, including through constructor injection, setter injection and property injection. Introduced in Dotkernel API 5.0.0 Dotkernel API, through its dot-dependency-injection package focuses only on constructor injection.","title":"Dependency Injection"},{"location":"v5/core-features/dependency-injection/#usage","text":"Dotkernel API comes out of the box with the dot-dependency-injection package, which provides all we need for injecting dependencies into any object you want. dot-dependency-injection determines the dependencies by looking at the #[Inject] attribute, added to the constructor of a class. Dependencies are specified as separate parameters of the #[Inject] attribute. For our example we will inject UserService and config dependencies into a UseHandler . use Dot\\DependencyInjection\\Attribute\\Inject; class UserHandler implements RequestHandlerInterface { #[Inject( UserService::class, \"config\", )] public function __construct( protected UserServiceInterface $userService, protected array $config, ) { } } If your class needs the value of a specific configuration key, you can specify the path using dot notation config.example . The next step is to register the class in the ConfigProvider under factories using Dot\\DependencyInjection\\Factory\\AttributedServiceFactory::class public function getDependencies(): array { return [ 'factories' => [ UserHandler::class => AttributedServiceFactory::class ] ]; } That's it. When your object is instantiated from the container, it will automatically have its dependencies resolved. Dependencies injection is available to any object within Dotkernel API. For example, you can inject dependencies in a service, a handler and so on, simply by registering it in the ConfigProvider .","title":"Usage"},{"location":"v5/core-features/error-reporting/","text":"Error reporting endpoint The error reporting endpoint was designed to allow the frontend developers of your API to report any bugs they encounter in a secure way that is fully under your control. To prevent unauthorized usage, the endpoint is protected by a token in the request's header. Example case usage Frontend developed in Angular. Frontend developer will use try-catch in the code in order to send frontend errors back to the API. How to use it on the API side Error reporting is done by sending a POST request to the /error-report endpoint, together with a token in the header. In the sections below we will detail how to configure error reporting in your API and how the endpoint is used by the frontend developers. Generating a token and adding it to your API config First you need to generate a token for your request. This is done by using the command php ./bin/cli.php token:generate error-reporting The resulting token has this format 0123456789abcdef0123456789abcdef01234567 . Note: this example is not a valid token, it just lets you know what to look for. Copy the generated token in your config/autoload/error-handling.global.php file. It should look similar to the example below. Your API can have multiple tokens, if needed. return [ ... ErrorReportServiceInterface::class => [ ... 'tokens' => [ '0123456789abcdef0123456789abcdef01234567', ], ... ] ] Validation mechanism Behind the scenes, the API validates your configuration and lets you know if any config items prevent the submission of the error report. Below are the requirements for an application to be able to send error messages to Dotkernel API. Server-side requirements stored in config/autoload/error-handling.global.php (these can be set/overwritten in config/autoload/local.php ): All keys ( enabled , path , tokens , domain_whitelist and ip_whitelist ) must exist under ErrorReportServiceInterface::class . The error reporting feature must be enabled by setting ErrorReportServiceInterface::class . enabled to true . ErrorReportServiceInterface::class . path must have a value; if the destination file does not exist, it will be created automatically. ErrorReportServiceInterface::class . tokens must contain at least one token. At least one of ErrorReportServiceInterface::class . domain_whitelist / ip_whitelist must have at least one value. Note: In src/App/src/Service/ErrorReportService.php , the method checkRequest() tries to validate the request by checking matches for domain_whitelist with isMatchingDomain() and for ip_whitelist with isMatchingIpAddress() . If both return false , a ForbiddenException is thrown and the error message does not get stored. Application-side requirements : Send the Error-Reporting-Token header with a valid token previously stored in config/autoload/error-handling.global.php in the ErrorReportServiceInterface::class . tokens array. Send the Origin header set to the application's URL; this is the application that sends the error message. Note: The tokens under ErrorReportServiceInterface::class . tokens do not expire. The log file stores the token value too, making it easy to identify which application sent the error message. If your request passes all the checks, the message is saved in the log file specified in ErrorReportServiceInterface::class . path . Tips and tricks If there are multiple applications that report errors to your API, you can assign a different error reporting token for each. The tokens support key-value pairs where: The key is an alias relevant to the assigned application that uses it. The value is the token itself. Example: // ... return [ ... ErrorReportServiceInterface::class => [ // ... 'tokens' => [ 'frontend' => '0123456789abcdef0123456789abcdef01234567', 'admin' => '9876543210abcdef0123456789abcdef7654321', // other tokens ], ], ]; The log file will have entries similar to the below: [2024-08-29 12:47:00] [0123456789abcdef0123456789abcdef01234567] Demo error message The inclusion of the token helps you identify the source of the error message. In our example, it's the application that uses the 0123456789abcdef0123456789abcdef01234567 token, which is assigned to the application frontend . How to use it on the Frontend side (Angular example) The API developer sends a generated token to the frontend developer who will save it in their environment.staging.ts and/or environment.prod.ts . From then on, it's the frontend developer's job to set up an error reporting function similar to the one below. postError(body: object): Promise<any> { return new Promise((resolve, reject) => { return this.http.post(API_ENDPOINT + 'error-report', body , {headers: new HttpHeaders({'Error-Reporting-Token': 'TOKEN', 'Origin': 'https://example.com'})})).subscribe({ next: (response: any) => { resolve(response); }, error: (e: HttpErrorResponse) => reject(e), complete: () => console.info('Error on sending error'), }); }); } Whenever an error is found, the frontend will call postError() with a relevant description under message . apiService.postError({message: 'ERROR MESSAGE'})","title":"Error reporting"},{"location":"v5/core-features/error-reporting/#error-reporting-endpoint","text":"The error reporting endpoint was designed to allow the frontend developers of your API to report any bugs they encounter in a secure way that is fully under your control. To prevent unauthorized usage, the endpoint is protected by a token in the request's header.","title":"Error reporting endpoint"},{"location":"v5/core-features/error-reporting/#example-case-usage","text":"Frontend developed in Angular. Frontend developer will use try-catch in the code in order to send frontend errors back to the API.","title":"Example case usage"},{"location":"v5/core-features/error-reporting/#how-to-use-it-on-the-api-side","text":"Error reporting is done by sending a POST request to the /error-report endpoint, together with a token in the header. In the sections below we will detail how to configure error reporting in your API and how the endpoint is used by the frontend developers.","title":"How to use it on the API side"},{"location":"v5/core-features/error-reporting/#how-to-use-it-on-the-frontend-side-angular-example","text":"The API developer sends a generated token to the frontend developer who will save it in their environment.staging.ts and/or environment.prod.ts . From then on, it's the frontend developer's job to set up an error reporting function similar to the one below. postError(body: object): Promise<any> { return new Promise((resolve, reject) => { return this.http.post(API_ENDPOINT + 'error-report', body , {headers: new HttpHeaders({'Error-Reporting-Token': 'TOKEN', 'Origin': 'https://example.com'})})).subscribe({ next: (response: any) => { resolve(response); }, error: (e: HttpErrorResponse) => reject(e), complete: () => console.info('Error on sending error'), }); }); } Whenever an error is found, the frontend will call postError() with a relevant description under message . apiService.postError({message: 'ERROR MESSAGE'})","title":"How to use it on the Frontend side (Angular example)"},{"location":"v5/core-features/exceptions/","text":"Exceptions What are exceptions? Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code. How we use exceptions When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Below we will list the available custom exceptions. BadRequestException thrown when The Client tries to create/update resource , but the request data is invalid/incomplete (example: client tries to create an account, but does not send the required identity field) ConflictException thrown when The resource cannot be created because a different resource with the same identifier already exists (example: cannot change existing user's identity because another user with the same identity already exists) The resource cannot change its state because it is already in the specified state (example: user cannot be activated because it is already active) ExpiredException thrown when The resource cannot be accessed because it has expired (example: account activation link) because it has been consumed (example: one-time password) ForbiddenException thrown when The resource cannot be accessed by the authenticated client's role (example: client authenticated as regular user sends a GET /admin request) MethodNotAllowedException thrown when The client tries to interact with a resource via an invalid HTTP request method (example: client sends a PATCH /avatar request) NotFoundException thrown when The client tries to interact with a resource that does not exist on the server (example: client sends a GET /resource-does-not-exist request) UnauthorizedException thrown when The resource cannot be accessed because the client is not authenticated (example: unauthenticated client sends a GET /admin request) How it works During a request, if there is no uncaught exception, Dotkernel API will return a JSON response with the data provided by the handler that processed the request. Otherwise, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response How to extend In this example we will Create a custom exception called CustomException Place it next to the already existing custom exceptions (you can use your preferred location) Return a custom HTTP status code when CustomException is encountered. Step 1: Create exception file Navigate to the directory src/App/src/Handler/Exception and create a PHP class called CustomException.php . Open CustomException.php and add the following content: <?php declare(strict_types=1); namespace Api\\App\\Exception; use Exception; class CustomException extends Exception { } Save and close the file. Step 2: Use exception file Open the file src/App/src/Handler/HomeHandler.php and at the beginning of the get method, place the following code: throw new \\Api\\App\\Exception\\CustomException('some message'); Save and close the file. Step 3: Test for failure Access your API's home page URL and make sure it returns 500 Internal Server Error HTTP status code and the following content: { \"error\": { \"messages\": [ \"some message\" ] } } Step 4: Prepare for success Open the file src/App/src/Handler/HandlerTrait.php and locate the handle method. Insert the following lines of code before the first catch statement: } catch (\\Api\\App\\Exception\\CustomException $exception) { return $this->errorResponse($exception->getMessage(), StatusCodeInterface::STATUS_IM_A_TEAPOT); Save and close the file. Step 5: Test for success Access your API's home page URL, which should return the same content. Notice that this time it returns 418 I'm a teapot HTTP status code.","title":"Exceptions"},{"location":"v5/core-features/exceptions/#exceptions","text":"","title":"Exceptions"},{"location":"v5/core-features/exceptions/#what-are-exceptions","text":"Exceptions are a powerful mechanism for handling errors and other exceptional conditions that may occur during the execution of a script. They provide a way to manage errors in a structured and controlled manner, separating error-handling code from regular code.","title":"What are exceptions?"},{"location":"v5/core-features/exceptions/#how-we-use-exceptions","text":"When it comes to handling exceptions, Dotkernel API relies on the usage of easy-to-understand, problem-specific exceptions. Below we will list the available custom exceptions.","title":"How we use exceptions"},{"location":"v5/core-features/exceptions/#how-it-works","text":"During a request, if there is no uncaught exception, Dotkernel API will return a JSON response with the data provided by the handler that processed the request. Otherwise, it will build and send a response based on the exception thrown: BadRequestException will return a 400 Bad Request response UnauthorizedException will return a 401 Unauthorized response ForbiddenException will return a 403 Forbidden response OutOfBoundsException and NotFoundException will return a 404 Not Found response MethodNotAllowedException will return a 405 Method Not Allowed response ConflictException will return a 409 Conflict response ExpiredException will return a 410 Gone response MailException , RuntimeException and the generic Exception will return a 500 Internal Server Error response","title":"How it works"},{"location":"v5/core-features/exceptions/#how-to-extend","text":"In this example we will Create a custom exception called CustomException Place it next to the already existing custom exceptions (you can use your preferred location) Return a custom HTTP status code when CustomException is encountered.","title":"How to extend"},{"location":"v5/flow/default-library-flow/","text":"Default Library Flow The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v5/flow/default-library-flow/#default-library-flow","text":"The graph below demonstrates a default flow between Dotkernel's libraries.","title":"Default Library Flow"},{"location":"v5/flow/library-flow-for-email/","text":"Library Flow for Email The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v5/flow/library-flow-for-email/#library-flow-for-email","text":"The graph below demonstrates the simplified flow between Dotkernel's libraries for sending an email.","title":"Library Flow for Email"},{"location":"v5/flow/middleware-flow/","text":"Middleware flow The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware Flow"},{"location":"v5/flow/middleware-flow/#middleware-flow","text":"The graph below demonstrates a default flow between Dotkernel's middlewares.","title":"Middleware flow"},{"location":"v5/installation/composer/","text":"Composer Installation of Packages Composer is required to install Dotkernel api . You can install Composer from the official site . First make sure that you have navigated your command prompt to the folder where you copied the files in the previous step. Install dependencies Run this command in the command prompt. Use the CLI in order to ensure interactivity for proper configuration. composer install You should see this text below, along with a long list of packages to be installed instead of the [...] . In this example there are 164 packages, though the number can change in future updates. You will find the packages in the vendor folder. No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Lock file operations: 164 installs, 0 updates, 0 removals [...] Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 164 installs, 0 updates, 0 removals [...] The setup script may prompt for some configuration settings, for example the lines below. If you don't see them, you can skip to the next section. Please select which config file you wish to inject 'Laminas\\Diactoros\\ConfigProvider' into: [0] Do not inject [1] config/config.php Make your selection (default is 1): Type 0 to select [0] Do not inject . We choose 0 because Dotkernel includes its own ConfigProvider which already contains the prompted configurations. If you choose [1] config/config.php , an extra ConfigProvider will be injected. The next question is: Remember this option for other packages of the same type? (y/N) Type y here, and hit enter to complete this stage. Development mode If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Composer"},{"location":"v5/installation/composer/#composer-installation-of-packages","text":"Composer is required to install Dotkernel api . You can install Composer from the official site . First make sure that you have navigated your command prompt to the folder where you copied the files in the previous step.","title":"Composer Installation of Packages"},{"location":"v5/installation/composer/#install-dependencies","text":"Run this command in the command prompt. Use the CLI in order to ensure interactivity for proper configuration. composer install You should see this text below, along with a long list of packages to be installed instead of the [...] . In this example there are 164 packages, though the number can change in future updates. You will find the packages in the vendor folder. No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Lock file operations: 164 installs, 0 updates, 0 removals [...] Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 164 installs, 0 updates, 0 removals [...] The setup script may prompt for some configuration settings, for example the lines below. If you don't see them, you can skip to the next section. Please select which config file you wish to inject 'Laminas\\Diactoros\\ConfigProvider' into: [0] Do not inject [1] config/config.php Make your selection (default is 1): Type 0 to select [0] Do not inject . We choose 0 because Dotkernel includes its own ConfigProvider which already contains the prompted configurations. If you choose [1] config/config.php , an extra ConfigProvider will be injected. The next question is: Remember this option for other packages of the same type? (y/N) Type y here, and hit enter to complete this stage.","title":"Install dependencies"},{"location":"v5/installation/composer/#development-mode","text":"If you're installing the project for development, make sure you have development mode enabled, by running: composer development-enable You can disable development mode by running: composer development-disable You can check if you have development mode enabled by running: composer development-status","title":"Development mode"},{"location":"v5/installation/configuration-files/","text":"Configuration Files Prepare config files duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php Note if your API will be consumed by another application, make sure to configure the allowed_origins variable duplicate config/autoload/local.php.dist as config/autoload/local.php duplicate config/autoload/mail.local.php.dist as config/autoload/mail.local.php Note if your API will send emails, make sure to fill in SMTP connection params optional : in order to run/create tests, duplicate config/autoload/local.test.php.dist as config/autoload/local.test.php Note this creates a new in-memory database that your tests will run on.","title":"Configuration Files"},{"location":"v5/installation/configuration-files/#configuration-files","text":"","title":"Configuration Files"},{"location":"v5/installation/configuration-files/#prepare-config-files","text":"duplicate config/autoload/cors.local.php.dist as config/autoload/cors.local.php","title":"Prepare config files"},{"location":"v5/installation/doctrine-orm/","text":"Doctrine ORM Setup database Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci Running migrations Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation. Executing fixtures Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found on dot-data-fixtures documentation","title":"Doctrine ORM"},{"location":"v5/installation/doctrine-orm/#doctrine-orm","text":"","title":"Doctrine ORM"},{"location":"v5/installation/doctrine-orm/#setup-database","text":"Make sure you fill out the database credentials in config/autoload/local.php under $databases['default'] . Create a new MySQL database - set collation to utf8mb4_general_ci","title":"Setup database"},{"location":"v5/installation/doctrine-orm/#running-migrations","text":"Run the database migrations by using the following command: php vendor/bin/doctrine-migrations migrate This command will prompt you to confirm that you want to run it. WARNING! You are about to execute a migration in database \"...\" that could result in schema changes and data loss. Are you sure you wish to continue? (yes/no) [yes]: Hit Enter to confirm the operation.","title":"Running migrations"},{"location":"v5/installation/doctrine-orm/#executing-fixtures","text":"Fixtures are used to seed the database with initial values and should be executed after migrating the database. To list all the fixtures, run: php bin/doctrine fixtures:list This will output all the fixtures in the order of execution. To execute all fixtures, run: php bin/doctrine fixtures:execute To execute a specific fixture, run: php bin/doctrine fixtures:execute --class=FixtureClassName More details on how fixtures work can be found on dot-data-fixtures documentation","title":"Executing fixtures"},{"location":"v5/installation/faq/","text":"Frequently Asked Questions How do I fix common permission issues? If running your project you encounter some permission issues, follow the below steps. Errors PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache\" is not writable... PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/data/cache/doctrine\" is not writable... Fix: chmod -R 777 data Error PHP Fatal error: Uncaught InvalidArgumentException: The directory \"/var/www/ example.local /html/public/uploads\" is not writable... Fix: chmod -R 777 public/uploads Error PHP Fatal error: Uncaught ErrorException: fopen(/var/www/ example.local /config/autoload/../../log/error-log- yyyy-mm-dd.log ): Failed to open stream: Permission denied... Fix: chmod -R 777 log","title":"FAQ"},{"location":"v5/installation/faq/#frequently-asked-questions","text":"","title":"Frequently Asked Questions"},{"location":"v5/installation/faq/#how-do-i-fix-common-permission-issues","text":"If running your project you encounter some permission issues, follow the below steps.","title":"How do I fix common permission issues?"},{"location":"v5/installation/getting-started/","text":"Clone the project Recommended development environment If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Getting Started"},{"location":"v5/installation/getting-started/#clone-the-project","text":"","title":"Clone the project"},{"location":"v5/installation/getting-started/#recommended-development-environment","text":"If you are using Windows as OS on your machine, you can use WSL2 as development environment. Read more here: PHP-Mariadb-on-WLS2 Using your terminal, navigate inside the directory you want to download the project files into. Make sure that the directory is empty before proceeding to the download process. Once there, run the following command: git clone https://github.com/dotkernel/api.git .","title":"Recommended development environment"},{"location":"v5/installation/test-the-installation/","text":"Test the installation Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 5\"} Old way of doing things, using PHP built-in server php -S 0.0.0.0:8080 -t public Running tests The project has 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","title":"Test the Installation"},{"location":"v5/installation/test-the-installation/#test-the-installation","text":"Sending a GET request to the home page should output the following message: {\"message\": \"Dotkernel API version 5\"}","title":"Test the installation"},{"location":"v5/installation/test-the-installation/#old-way-of-doing-things-using-php-built-in-server","text":"php -S 0.0.0.0:8080 -t public","title":"Old way of doing things, using PHP built-in server"},{"location":"v5/installation/test-the-installation/#running-tests","text":"The project has 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","title":"Running tests"},{"location":"v5/installation/test-the-installation/#running-unit-tests","text":"vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always","title":"Running unit tests"},{"location":"v5/installation/test-the-installation/#running-functional-tests","text":"vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Running functional tests"},{"location":"v5/introduction/file-structure/","text":"File structure Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default: Special purpose folders .github - contains GitHub workflow files .laminas-ci - contains laminas-ci workflow files bin folder This folder contents are clear-config-cache.php - removes the config cache file ( data/cache/config-cache.php - available only when development mode is enabled). cli.php used to build console applications based on laminas-cli doctrine - used by the doctrine fixtures to populate the database tables config folder This folder contains all application-related config files: cli-config.php : command line interface configuration used by migrations, fixtures, crons config.php : registers ConfigProviders for installing packages container.php : main service container that provides access to all registered services development.config.php.dist : activates debug mode; gets symlinked as development.config.php when enabling development mode migrations.php : configuration for database migration, like migration file location and table to save the migration log pipeline.php : contains a list of middlewares, in the order of their execution twig-cs-fixer.php : configuration file for Twig code style checker/fixer config/autoload folder This folder contains all service-related local and global config files: authorization.global.php : configures access per route for user roles cli.global.php : configures cli content-negotiation.global.php : configures request and response formats cors.local.php.dist : configures Cross-Origin Resource Sharing, like call origin, headers, cookies dependencies.global.php : config file to set global dependencies that should be accessible by all modules development.local.php.dist : gets symlinked as development.local.php when enabling development mode - activates error handlers doctrine.global.php : configuration used by Object–relational mapping error-handling.global.php : configures and activates error logs local.php.dist : local config file where you can overwrite application name and URL local.test.php.dist : local configuration for functional tests mail.local.php.dist : mail configuration; e.g. sendmail vs smtp, message configuration, mail logging mezzio.global.php : Mezzio core config file mezzio-tooling-factories.global.php : add or remove factory definitions response-header.global.php : defines headers per route templates.global.php : dotkernel/dot-twigrenderer config file data folder This folder is a storage for project data files and service caches. It contains these folders: cache : cache for e.g. Twig files doctrine : database migrations and fixtures oauth : encryption, private and public keys needed for authentication data/lock - lock files generated by dotkernel/dot-cli AVOID storing sensitive data on the repository! log folder This folder stores daily log files. When you access the application from the browser, (if not already created) a new log file gets created in the format specified in the config/autoload/error-handling.global.php config file under the stream array key. public folder This folder contains all publicly available assets and serves as the entry point of the application: uploads : a folder that normally contains files uploaded via the application .htaccess : server configuration file used by Apache web server; it enables the URL rewrite functionality index.php : the application's main entry point robots.txt.dist : a sample robots.txt file that allows/denies bot access to certain areas of your application; activate it by duplicating the file as robots.txt and comment out the lines that don't match your environment src folder This folder contains a separate folder for each Module. These are the modules included by default: Admin - contains functionality for managing users with admin role; note these are users save in the admin database table App - contains core functionality, from authentication, to rendering, to error reporting User - contains functionality for managing regular users Module contents Each Module folder, in turn, should contain the following folders, unless they are empty: src/Handler - Action classes (similar to Controllers but can only perform one action) src/Entity - Used by database entities src/Service - Service classes src/Collection - Database entities collections src/Repository - Entity repository folder The above example is just some of the folders a project may include, but they should give you an idea about the recommended structure. Other classes the src folder may include are InputFilter , EventListener , Helper , Command , Factory etc. The src folder in each Module folder normally also contains these files: ConfigProvider.php - Configuration data for the module OpenAPI.php - Detailed descriptions for each endpoint in the OpenAPI format RoutesDelegator.php - Module specific route registrations templates folder in Modules This folder contains the template files, used for example to help render e-mail templates. Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig","title":"File Structure"},{"location":"v5/introduction/file-structure/#file-structure","text":"Dotkernel API follows the PSR-4 standards. It is a good practice to standardize the file structure of projects. When using Dotkernel API the following structure is installed by default:","title":"File structure"},{"location":"v5/introduction/file-structure/#special-purpose-folders","text":".github - contains GitHub workflow files .laminas-ci - contains laminas-ci workflow files","title":"Special purpose folders"},{"location":"v5/introduction/file-structure/#bin-folder","text":"This folder contents are clear-config-cache.php - removes the config cache file ( data/cache/config-cache.php - available only when development mode is enabled). cli.php used to build console applications based on laminas-cli doctrine - used by the doctrine fixtures to populate the database tables","title":"bin folder"},{"location":"v5/introduction/file-structure/#config-folder","text":"This folder contains all application-related config files: cli-config.php : command line interface configuration used by migrations, fixtures, crons config.php : registers ConfigProviders for installing packages container.php : main service container that provides access to all registered services development.config.php.dist : activates debug mode; gets symlinked as development.config.php when enabling development mode migrations.php : configuration for database migration, like migration file location and table to save the migration log pipeline.php : contains a list of middlewares, in the order of their execution twig-cs-fixer.php : configuration file for Twig code style checker/fixer","title":"config folder"},{"location":"v5/introduction/file-structure/#data-folder","text":"This folder is a storage for project data files and service caches. It contains these folders: cache : cache for e.g. Twig files doctrine : database migrations and fixtures oauth : encryption, private and public keys needed for authentication data/lock - lock files generated by dotkernel/dot-cli AVOID storing sensitive data on the repository!","title":"data folder"},{"location":"v5/introduction/file-structure/#log-folder","text":"This folder stores daily log files. When you access the application from the browser, (if not already created) a new log file gets created in the format specified in the config/autoload/error-handling.global.php config file under the stream array key.","title":"log folder"},{"location":"v5/introduction/file-structure/#public-folder","text":"This folder contains all publicly available assets and serves as the entry point of the application: uploads : a folder that normally contains files uploaded via the application .htaccess : server configuration file used by Apache web server; it enables the URL rewrite functionality index.php : the application's main entry point robots.txt.dist : a sample robots.txt file that allows/denies bot access to certain areas of your application; activate it by duplicating the file as robots.txt and comment out the lines that don't match your environment","title":"public folder"},{"location":"v5/introduction/file-structure/#src-folder","text":"This folder contains a separate folder for each Module. These are the modules included by default: Admin - contains functionality for managing users with admin role; note these are users save in the admin database table App - contains core functionality, from authentication, to rendering, to error reporting User - contains functionality for managing regular users","title":"src folder"},{"location":"v5/introduction/introduction/","text":"Introduction Below is a quick overview of features in Dotkernel API. Doctrine 3 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 use mezzio/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.0 OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on your Dotkernel API. We use mezzio/mezzio-authentication-oauth2 which provides OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using the [thephpleague/oauth2-server]https://github.com/thephpleague/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 that holds configuration files for each category. 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 Symfony\\Component\\Console\\Command\\Command . Then you can enable it by registering it in config/autoload/cli.global.php . File locker Here you will also find our file locker configuration, so you can easily enable and disable it (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. 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 test 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 Alternatively, you can run each test category separately with these commands: vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Introduction"},{"location":"v5/introduction/introduction/#introduction","text":"Below is a quick overview of features in Dotkernel API.","title":"Introduction"},{"location":"v5/introduction/introduction/#doctrine-3-orm","text":"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.","title":"Doctrine 3 ORM"},{"location":"v5/introduction/introduction/#documentation","text":"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","title":"Documentation"},{"location":"v5/introduction/introduction/#hypertext-application-language","text":"For our API payloads (a value object for describing the API resource, its relational links and any embedded/child resources related to it) we use mezzio/mezzio-hal .","title":"Hypertext Application Language"},{"location":"v5/introduction/introduction/#cors","text":"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.","title":"CORS"},{"location":"v5/introduction/introduction/#oauth-20","text":"OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on your Dotkernel API. We use mezzio/mezzio-authentication-oauth2 which provides OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using the [thephpleague/oauth2-server]https://github.com/thephpleague/oauth2-server package.","title":"OAuth 2.0"},{"location":"v5/introduction/introduction/#email","text":"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.","title":"Email"},{"location":"v5/introduction/introduction/#configuration","text":"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 that holds configuration files for each category.","title":"Configuration"},{"location":"v5/introduction/introduction/#routing","text":"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 .","title":"Routing"},{"location":"v5/introduction/introduction/#commands","text":"For registering new commands first make sure your command class extends Symfony\\Component\\Console\\Command\\Command . Then you can enable it by registering it in config/autoload/cli.global.php .","title":"Commands"},{"location":"v5/introduction/introduction/#file-locker","text":"Here you will also find our file locker configuration, so you can easily enable and disable it (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.","title":"File locker"},{"location":"v5/introduction/introduction/#tests","text":"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 test 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 Alternatively, you can run each test category separately with these commands: vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always","title":"Tests"},{"location":"v5/introduction/packages/","text":"Packages Version 5.1.1 had these packages removed or moved where noted: laminas/laminas-http was moved to require-dev laminas/laminas-paginator laminas/laminas-text dotkernel/dot-dependency-injection - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v5/introduction/packages/#packages","text":"Version 5.1.1 had these packages removed or moved where noted: laminas/laminas-http was moved to require-dev laminas/laminas-paginator laminas/laminas-text dotkernel/dot-dependency-injection - Dependency injection component using class attributes. dotkernel/dot-cache - Cache component extending symfony-cache dotkernel/dot-cli - Component for creating console applications based on laminas-cli dotkernel/dot-data-fixtures - Provides a CLI interface for listing & executing doctrine data fixtures dotkernel/dot-errorhandler - Logging Error Handler for Middleware Applications dotkernel/dot-mail - Mail component based on laminas-mail dotkernel/dot-response-header - Middleware for setting custom response headers. laminas/laminas-component-installer - Composer plugin for injecting modules and configuration providers into application configuration laminas/laminas-config - Provides a nested object property based user interface for accessing this configuration data within application code laminas/laminas-config-aggregator - Lightweight library for collecting and merging configuration from different sources laminas/laminas-hydrator - Serialize objects to arrays, and vice versa laminas/laminas-inputfilter - Normalize and validate input sets from the web, APIs, the CLI, and more, including files laminas/laminas-stdlib - SPL extensions, array utilities, error handlers, and more mezzio/mezzio - PSR-15 Middleware Microframework mezzio/mezzio-authentication-oauth2 - OAuth2 (server) authentication middleware for Mezzio and PSR-7 applications mezzio/mezzio-authorization-acl - laminas-permissions-acl adapter for mezzio-authorization mezzio/mezzio-authorization-rbac - mezzio authorization rbac adapter for laminas/laminas-permissions-rbac mezzio/mezzio-cors - CORS component for Mezzio and other PSR-15 middleware runners mezzio/mezzio-fastroute - FastRoute integration for Mezzio mezzio/mezzio-hal - Hypertext Application Language implementation for PHP and PSR-7 mezzio/mezzio-problem-details - Problem Details for PSR-7 HTTP APIs addressing the RFC 7807 standard mezzio/mezzio-twigrenderer - Twig integration for Mezzio ramsey/uuid-doctrine - Use ramsey/uuid as a Doctrine field type roave/psr-container-doctrine - Doctrine Factories for PSR-11 Containers symfony/filesystem - Provides basic utilities for the filesystem","title":"Packages"},{"location":"v5/introduction/psr/","text":"PSRs Some of the PSRs on this list are at the core of Dotkernel API, but several others are installed with the 3rd party packages used in the application. Below is the full list of PSRs present in Dotkernel API and their purpose. PSR-3: Logger Interface Interface for logging libraries Interfaces implemented in php-fig/log PSR-4: Autoloader Autoloading classes from file paths Interfaces implemented in laminas/laminas-loader PSR-6: Caching Interface Interface for caching systems to improve the performance of any project Interfaces implemented in php-fig/cache PSR-7: HTTP message interfaces Interfaces for representing HTTP messages and URIs for use with HTTP messages Interfaces implemented in php-fig/http-message PSR-11: Container interface Interface for dependency injection containers Interfaces implemented in php-fig/container PSR-13: Link definition interfaces Way of representing a hypermedia link independently of the serialization format Interfaces implemented in php-fig/link PSR-14: Event Dispatcher Mechanism for event-based extension and collaboration Interfaces implemented in php-fig/event-dispatcher PSR-15: HTTP Server Request Handlers Interfaces for HTTP server request handlers and HTTP server middleware components that use HTTP messages Interfaces implemented in php-fig/http-server-handler and php-fig/http-server-middleware PSR-17: HTTP Factories Standard for factories that create PSR-7 compliant HTTP objects Interfaces implemented in php-fig/http-factory PSR-18: HTTP Client Interface for sending HTTP requests and receiving HTTP responses Interfaces implemented in php-fig/http-client PSR-20: Clock Interface for reading the system clock Interfaces implemented in php-fig/clock","title":"PSRs"},{"location":"v5/introduction/psr/#psrs","text":"Some of the PSRs on this list are at the core of Dotkernel API, but several others are installed with the 3rd party packages used in the application. Below is the full list of PSRs present in Dotkernel API and their purpose. PSR-3: Logger Interface Interface for logging libraries Interfaces implemented in php-fig/log PSR-4: Autoloader Autoloading classes from file paths Interfaces implemented in laminas/laminas-loader PSR-6: Caching Interface Interface for caching systems to improve the performance of any project Interfaces implemented in php-fig/cache PSR-7: HTTP message interfaces Interfaces for representing HTTP messages and URIs for use with HTTP messages Interfaces implemented in php-fig/http-message PSR-11: Container interface Interface for dependency injection containers Interfaces implemented in php-fig/container PSR-13: Link definition interfaces Way of representing a hypermedia link independently of the serialization format Interfaces implemented in php-fig/link PSR-14: Event Dispatcher Mechanism for event-based extension and collaboration Interfaces implemented in php-fig/event-dispatcher PSR-15: HTTP Server Request Handlers Interfaces for HTTP server request handlers and HTTP server middleware components that use HTTP messages Interfaces implemented in php-fig/http-server-handler and php-fig/http-server-middleware PSR-17: HTTP Factories Standard for factories that create PSR-7 compliant HTTP objects Interfaces implemented in php-fig/http-factory PSR-18: HTTP Client Interface for sending HTTP requests and receiving HTTP responses Interfaces implemented in php-fig/http-client PSR-20: Clock Interface for reading the system clock Interfaces implemented in php-fig/clock","title":"PSRs"},{"location":"v5/introduction/server-requirements/","text":"Server Requirements For production, we highly recommend a *nix based system. Webserver Apache >= 2.2 mod_rewrite .htaccess support (AllowOverride All) The repository includes a default .htaccess file in the public folder. Nginx You need to convert the provided Apache related .htaccess file into Nginx configuration instructions. PHP >= 8.2 Both mod_php and FCGI (FPM) are supported. Required Settings and Modules & Extensions memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH) RDBMS Tested with MariaDB 10.11 LTS and MariaDB 11.4 LTS Recommended extensions opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used) sqlite3 - for tests","title":"Server Requirements"},{"location":"v5/introduction/server-requirements/#server-requirements","text":"For production, we highly recommend a *nix based system.","title":"Server Requirements"},{"location":"v5/introduction/server-requirements/#webserver","text":"","title":"Webserver"},{"location":"v5/introduction/server-requirements/#php-82","text":"Both mod_php and FCGI (FPM) are supported.","title":"PHP >= 8.2"},{"location":"v5/introduction/server-requirements/#required-settings-and-modules-extensions","text":"memory_limit >= 128M upload_max_filesize and post_max_size >= 100M (depending on your data) mbstring CLI SAPI (for Cron Jobs) Composer (added to $PATH)","title":"Required Settings and Modules & Extensions"},{"location":"v5/introduction/server-requirements/#rdbms","text":"Tested with MariaDB 10.11 LTS and MariaDB 11.4 LTS","title":"RDBMS"},{"location":"v5/introduction/server-requirements/#recommended-extensions","text":"opcache pdo_mysql or mysqli (if using MySQL or MariaDB as RDBMS) dom - if working with markup files structure (html, xml, etc) simplexml - working with xml files gd, exif - if working with images zlib, zip, bz2 - if compessing files curl (required if APIs are used) sqlite3 - for tests","title":"Recommended extensions"},{"location":"v5/openapi/generate-documentation/","text":"Generating the documentation file Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . Using your terminal, move to the root directory of your project. Dotkernel API stores the OpenAPI attributes in the src directory, so that's the path we will use for generating the static documentation file. Methods of generating documentation file Without saving it to a file ./vendor/bin/openapi ./src This will output the generated content to the terminal. Place it in a custom location ./vendor/bin/openapi ./src --output public/openapi.yaml This will place the generated file openapi.yaml in the public directory. Specify OpenAPI version Supported OpenAPI versions are 3.0.0 and 3.1.0 , 3.0.0 being the default version. The below command will specify both the output location and the OpenAPI version: ./vendor/bin/openapi ./src --version 3.1.0 Specify output file format Supported file formats are yaml and json , yaml being the default format. The below command will specify the output location and zircote/swagger-php will determine the file format: ./vendor/bin/openapi ./src --output public/openapi.json Or be specific about the format by appending the --format argument: ./vendor/bin/openapi ./src --output public/openapi.json --format json These will place the generated file openapi.json in the public directory.","title":"Generate Documentation"},{"location":"v5/openapi/generate-documentation/#generating-the-documentation-file","text":"Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . Using your terminal, move to the root directory of your project. Dotkernel API stores the OpenAPI attributes in the src directory, so that's the path we will use for generating the static documentation file.","title":"Generating the documentation file"},{"location":"v5/openapi/generate-documentation/#methods-of-generating-documentation-file","text":"","title":"Methods of generating documentation file"},{"location":"v5/openapi/getting-help/","text":"Getting help consult the OpenAPI specs for a complete reference of the presented objects see more examples of OpenAPI object representations in zircote/swagger-php 's GitHub repository consult zircote/swagger-php 's online documentation or run the following command to see their help page: ./vendor/bin/openapi --help","title":"Getting Help"},{"location":"v5/openapi/getting-help/#getting-help","text":"consult the OpenAPI specs for a complete reference of the presented objects see more examples of OpenAPI object representations in zircote/swagger-php 's GitHub repository consult zircote/swagger-php 's online documentation or run the following command to see their help page: ./vendor/bin/openapi --help","title":"Getting help"},{"location":"v5/openapi/initialized-components/","text":"Initialized OpenAPI components Below you will find details on some prepopulated OpenAPI components we added to Dotkernel API. OA\\Info Defined in src/App/src/OpenAPI.php , this object provides general info about the API: version : API version (example: 1.0.0 ) title : title shown in the UI (example: Dotkernel API ) For more info, see this page . OA\\Server Defined in src/App/src/OpenAPI.php , this object provides API server entries: url : API server URL (example: https://api.example.com - use no trailing slash!) description : describes the purpose of the server (example: Dev , Staging , Production or even Auth if you use a separate authentication server) You can have multiple Server definitions, one for each of your Dotkernel API instances. For more info, see this page . OA\\SecurityScheme Defined in src/App/src/OpenAPI.php , you will find an object for the AuthToken security header: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) bearerFormat : a hint to the client to identify how the bearer token is formatted scheme : the name of the authorization scheme to be used And another object for the ErrorReportingToken security token: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) name : the name of the header For more info, see this page . OA\\ExternalDocumentation Defined in src/App/src/OpenAPI.php , in this object we provide the following details: description : describes the purpose of the document url : external documentation URL For more info, see this page . OA\\Schema Schemas are OpenAPI objects describing an object or collection of objects existing in your project. Schemas describing objects In order to describe an object (entity) you will need to transform in into a schema. Object: <?php declare(strict_types=1); namespace Api\\User\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\RoleInterface; use Api\\App\\Entity\\TimestampsTrait; use Doctrine\\ORM\\Mapping as ORM; class UserRole extends AbstractEntity implements RoleInterface { use TimestampsTrait; #[ORM\\Column(name: \"name\", type: \"string\", length: 20, unique: true)] protected ?string $name = null; // methods } Schema: <?php declare(strict_types=1); namespace Api\\User; use Api\\User\\Entity\\UserRole; use OpenApi\\Attributes as OA; ... /** * @see UserRole */ #[OA\\Schema( schema: 'UserRole', properties: [ new OA\\Property(property: 'uuid', type: 'string', example: '1234abcd-abcd-4321-12ab-123456abcdef'), new OA\\Property(property: 'name', type: 'string', example: UserRole::ROLE_USER), new OA\\Property( property: '_links', properties: [ new OA\\Property( property: 'self', properties: [ new OA\\Property( property: 'href', type: 'string', example: 'https://example.com/user/role/1234abcd-abcd-4321-12ab-123456abcdef', ), ], type: 'object', ), ], type: 'object', ), ], type: 'object', )] Then, when generating the documentation file, OpenAPI will transform it into the specified format ( json / yaml ). UserRole: properties: uuid: type: string example: 1234abcd-abcd-4321-12ab-123456abcdef name: type: string example: user _links: properties: self: properties: href: type: string example: 'https://example.com/user/role/1234abcd-abcd-4321-12ab-123456abcdef' type: object type: object type: object Schemas describing collections of objects Collections of objects are just as easy to describe in OpenAPI as they are in PHP. PHP collection: <?php declare(strict_types=1); namespace Api\\User\\Collection; use Api\\App\\Collection\\ResourceCollection; class UserRoleCollection extends ResourceCollection { } Schema: #[OA\\Schema( schema: 'UserRoleCollection', properties: [ new OA\\Property( property: '_embedded', properties: [ new OA\\Property( property: 'roles', type: 'array', items: new OA\\Items( ref: '#/components/schemas/UserRole', ), ), ], type: 'object', ), ], type: 'object', allOf: [ new OA\\Schema(ref: '#/components/schemas/Collection'), ], )] Using ref: '#/components/schemas/UserRole', in our code, we instruct OpenAPI to grab the existing schema UserRole (not the entity, but the schema) that we just described earlier. This way we do not need to repeat code by describing again the same object and any future modifications will happen in only one place. Then, when generating the documentation file, OpenAPI will transform it into the specified format ( json / yaml ). UserRoleCollection: type: object allOf: - $ref: '#/components/schemas/Collection' - properties: _embedded: properties: roles: type: array items: { $ref: '#/components/schemas/UserRole' } type: object type: object Make sure that in src/App/src/OpenAPI.php , on the line with #[OA\\Server the value of url is set to the of URL of your instance of Dotkernel API . You can add multiple servers (for staging, production etc) by duplicating the existing one. For more info, see this page . Common schemas 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 #/components/schemas/InfoMessage : describes an operation that completed successfully - may contain multiple messages","title":"Initialized Components"},{"location":"v5/openapi/initialized-components/#initialized-openapi-components","text":"Below you will find details on some prepopulated OpenAPI components we added to Dotkernel API.","title":"Initialized OpenAPI components"},{"location":"v5/openapi/initialized-components/#oainfo","text":"Defined in src/App/src/OpenAPI.php , this object provides general info about the API: version : API version (example: 1.0.0 ) title : title shown in the UI (example: Dotkernel API ) For more info, see this page .","title":"OA\\Info"},{"location":"v5/openapi/initialized-components/#oaserver","text":"Defined in src/App/src/OpenAPI.php , this object provides API server entries: url : API server URL (example: https://api.example.com - use no trailing slash!) description : describes the purpose of the server (example: Dev , Staging , Production or even Auth if you use a separate authentication server) You can have multiple Server definitions, one for each of your Dotkernel API instances. For more info, see this page .","title":"OA\\Server"},{"location":"v5/openapi/initialized-components/#oasecurityscheme","text":"Defined in src/App/src/OpenAPI.php , you will find an object for the AuthToken security header: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) bearerFormat : a hint to the client to identify how the bearer token is formatted scheme : the name of the authorization scheme to be used And another object for the ErrorReportingToken security token: securityScheme : the name of the security scheme - you will provide this to indicate that an endpoint is protected type : whether it's an API key, an authorization header etc in : indicates where the scheme is applied ( query / header / cookie ) name : the name of the header For more info, see this page .","title":"OA\\SecurityScheme"},{"location":"v5/openapi/initialized-components/#oaexternaldocumentation","text":"Defined in src/App/src/OpenAPI.php , in this object we provide the following details: description : describes the purpose of the document url : external documentation URL For more info, see this page .","title":"OA\\ExternalDocumentation"},{"location":"v5/openapi/initialized-components/#oaschema","text":"Schemas are OpenAPI objects describing an object or collection of objects existing in your project.","title":"OA\\Schema"},{"location":"v5/openapi/introduction/","text":"OpenAPI documentation In order to provide an interactive documentation, Dotkernel API implemented zircote/swagger-php . Using this library, developers are able to automatically generate documentation files that later can be used to provide a comprehensive overview of the available endpoints, all the details on the requests that it can receive and the responses these can return.","title":"Introduction"},{"location":"v5/openapi/introduction/#openapi-documentation","text":"In order to provide an interactive documentation, Dotkernel API implemented zircote/swagger-php . Using this library, developers are able to automatically generate documentation files that later can be used to provide a comprehensive overview of the available endpoints, all the details on the requests that it can receive and the responses these can return.","title":"OpenAPI documentation"},{"location":"v5/openapi/render-documentation/","text":"Rendering the documentation file At this step, you only have a static documentation file. You will need an interface that can render it so that you will be able to interact with your Dotkernel API. In order to do this, we recommend using either of: swagger-api/swagger-ui Redocly/redoc Using Swagger UI Navigate to the public directory of your instance of Dotkernel API and create an HTML (you can call it swagger.html , the name is up to you) and place the following HTML content in it: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css\" /> </head> <body> <div id=\"swagger-ui\"></div> <script src=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js\" crossorigin></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({url: 'PATH_TO_YOUR_OPENAPI_FILE', dom_id: '#swagger-ui'}); }; </script> </body> </html> 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: window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /swagger.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s). 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: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <script src=\"https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js\"></script> </head> <body> <div id=\"redoc-container\"></div> <script> Redoc.init('PATH_TO_YOUR_OPENAPI_FILE', {}, document.getElementById('redoc-container')); </script> </body> </html> 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: Redoc.init('./openapi.yaml', {}, document.getElementById('redoc-container')); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /redoc.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Render Documentation"},{"location":"v5/openapi/render-documentation/#rendering-the-documentation-file","text":"At this step, you only have a static documentation file. You will need an interface that can render it so that you will be able to interact with your Dotkernel API. In order to do this, we recommend using either of: swagger-api/swagger-ui Redocly/redoc","title":"Rendering the documentation file"},{"location":"v5/openapi/render-documentation/#using-swagger-ui","text":"Navigate to the public directory of your instance of Dotkernel API and create an HTML (you can call it swagger.html , the name is up to you) and place the following HTML content in it: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css\" /> </head> <body> <div id=\"swagger-ui\"></div> <script src=\"https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js\" crossorigin></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({url: 'PATH_TO_YOUR_OPENAPI_FILE', dom_id: '#swagger-ui'}); }; </script> </body> </html> 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: window.ui = SwaggerUIBundle({url: './openapi.yaml', dom_id: '#swagger-ui'}); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /swagger.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Using Swagger UI"},{"location":"v5/openapi/render-documentation/#using-redoc","text":"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: <!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <meta name=\"description\" content=\"Dotkernel API Documentation\" /> <title>Dotkernel API Documentation</title> <script src=\"https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js\"></script> </head> <body> <div id=\"redoc-container\"></div> <script> Redoc.init('PATH_TO_YOUR_OPENAPI_FILE', {}, document.getElementById('redoc-container')); </script> </body> </html> 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: Redoc.init('./openapi.yaml', {}, document.getElementById('redoc-container')); Using your browser, open a new tab and type in the URL of your instance of Dotkernel API and append /redoc.html to it. You should see the Redoc interface with your documentation file loaded in it. From here, you can inspect each endpoint, see it's URL, check if it needs authentication, the request payload (if any) and the possible response(s).","title":"Using Redoc"},{"location":"v5/openapi/use-documentation/","text":"Using the documentation Since Redoc is readonly, in the following section we will focus only on using Swagger UI. 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. 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: 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. 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. Depending on the endpoint description, you will know which one you need to use. Examples: /user : the description says Admin lists user accounts - it means that you need an AccessToken with (super)admin privileges /user/my-account : the description says User fetches their own account - it means that you need an AccessToken with user privileges In the UI, find a section called AccessToken , toggle the /security/generate-token ( Generate access token ) endpoint and click the Try it out button. Under the Access token generation request you will find a textarea prepopulated with a JSON object. You will have to change the value of username and password . See this guide for the credentials. After you have filled out the credentials, click on the Execute button below the textarea. This will send the request to your instance of Dotkernel API. If everything went well, under the textarea you should see: the curl request that was made the Request URL the request was sent to the Server response with 200 OK response code and the Response body with a JSON object containing token_type , expires_in , access_token and refresh_token . Save the refresh_token somewhere, you will need it later Now copy the value of access_token (make sure you copy all the characters, without the surrounding double quotes) and go back up to the Authorize button and click it to open the auth modal. Paste the copied token as the value of the AuthToken and click on the Authorize button you see under the input field. The Authorize button has now changed to Logout . You can close the modal. From here, Swagger UI will remember the AuthToken until you close/refresh the browser tab. Also, it will automatically append the Authorization header to each request, allowing you to make authorized API calls. If you need to switch to an account with different privileges, you go again to the Authorize button, click on it to open the auth modal, and click Logout for the AuthToken . Then paste the new token as the value of the AuthToken , click on the Authorize button, close the modal and continue using the UI authenticated with the new account. Refreshing AuthToken By default, auth tokens expire in 1 day. If you make an API call, and you receive an error telling you that your auth token is expired, you need to either generate a new token (as seen above) or refresh the existing one using the refresh_token received when generating the current token. In order to refresh the auth token, you find the same section called AccessToken , toggle the /security/refresh-token ( Refresh access token ) endpoint and click the Try it out button. Under the Access token refresh request you will find a textarea prepopulated with a JSON object. You will have to change the value of refresh_token to the refresh token of your current auth token. Once done, click on the Execute button below the textarea. This will send the request to your instance of Dotkernel API. If everything went well, under the textarea you should see the same details: the curl request that was made the Request URL the request was sent to the Server response with 200 OK response code and the Response body with a JSON object containing token_type , expires_in , access_token and refresh_token . From here, you will follow the same steps: copy the access_token go to the Authorize button to open the auth modal paste the new token and click on Authorize close the modal Generating ErrorReportingToken Just like the AuthTokens, ErrorReportingTokens are used to make authorized API calls. The difference is that this token applies only to one specific endpoint: /error-report ( Report an error to the API ). This endpoint is intended to be used by third-party applications and frontends to report an error back to the API. This endpoint does not require AuthTokens In order to generate this token, follow this guide . Once you have the error reporting token, go again to the Authorize button, paste the new token as the value of the ErrorReportingToken , click on the Authorize button and close the modal. Now you're ready to report errors to your instance of Dotkernel API. Making API calls The UI does not use confirmation messages before making an API call so double check any operation before executing it. Once authorized in the UI, you can click on any endpoint to expand it. There you will find an overview of the endpoint, including: Request method ( DELETE , GET , PATCH , POST , PUT ) request URL (example: /resource ) Short description Long description Parameters - if this area says No parameters , then there are no parameters to fill out; else, make sure you fill out all the required parameters Request body - if present, provides a textarea prepopulated with a JSON object describing the request Responses - a list of possible HTTP status codes and their respective response bodies Clicking the Try it out button will activate any parameter input fields and the request body textarea (if any). Clicking Cancel will deactivate them. Make sure you fill out all the necessary data, then click on the Execute found button above Responses . This will send the request and return and display the API response. Once finished, you will see the response as the first item under Responses , including the HTTP status code and the response body. You can repeat the request by clicking again on the Execute button. This will first clear the previous output and display the new response in the same place. Additionally, between two executions, you can manually clear any previous output using the Clear button next to the Execute button.","title":"Use Documentation"},{"location":"v5/openapi/use-documentation/#using-the-documentation","text":"Since Redoc is readonly, in the following section we will focus only on using Swagger UI.","title":"Using the documentation"},{"location":"v5/openapi/use-documentation/#protected-endpoints","text":"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.","title":"Protected endpoints"},{"location":"v5/openapi/use-documentation/#authentication","text":"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.","title":"Authentication"},{"location":"v5/openapi/use-documentation/#making-api-calls","text":"The UI does not use confirmation messages before making an API call so double check any operation before executing it. Once authorized in the UI, you can click on any endpoint to expand it. There you will find an overview of the endpoint, including: Request method ( DELETE , GET , PATCH , POST , PUT ) request URL (example: /resource ) Short description Long description Parameters - if this area says No parameters , then there are no parameters to fill out; else, make sure you fill out all the required parameters Request body - if present, provides a textarea prepopulated with a JSON object describing the request Responses - a list of possible HTTP status codes and their respective response bodies Clicking the Try it out button will activate any parameter input fields and the request body textarea (if any). Clicking Cancel will deactivate them. Make sure you fill out all the necessary data, then click on the Execute found button above Responses . This will send the request and return and display the API response. Once finished, you will see the response as the first item under Responses , including the HTTP status code and the response body. You can repeat the request by clicking again on the Execute button. This will first clear the previous output and display the new response in the same place. Additionally, between two executions, you can manually clear any previous output using the Clear button next to the Execute button.","title":"Making API calls"},{"location":"v5/openapi/write-documentation/","text":"Writing documentation In order to avoid polluting PHP files with maybe thousands of lines of OpenAPI attributes, we opted for storing them in separate files, called OpenAPI.php , one for each module. We already covered all the endpoints available in Dotkernel API, you can consult the existing documentation in each module's own OpenAPI.php file. After you add more functionalities to your API, you will have to document the new endpoints. This is easier than it sounds because in most cases you will do the same: add a request by method, describe the request payload (if any), add request parameters (if any) and describe the possible responses. Common objects To do this, you will use the following request objects: OA\\Delete : delete an API resource identified by its unique id OA\\Get : fetch API single or collections of API resources OA\\Post : create a new API resource (unless if it already exists) OA\\Patch : update an existing API resource OA\\Put : create a new API resource (if it already exists, it is overwritten) Also, the following components describing PHP objects: OA\\Schema : describe an object sent in a request or received as a response - read more OA\\Parameter : describe a query / path parameter - read more OA\\RequestBody : describe the body of a request - read more There are lot more, but these are the most often used ones. 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: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Get 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/{uuid} for a single resource or /resource for a collection of resources) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Patch Defines a PATCH HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Post Defines a POST HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies OA\\Put Defines a PUT HTTP request. It should specify at least the following parameters: path : the route to the resource (example: /resource/{uuid} - where uuid is a path parameter defined below) description : verbose description of the endpoint's purpose summary : short description of the endpoint's purpose security : an array of security scheme(s) to be used - omit if the endpoint is not protected requestBody : a OA\\RequestBody object describing the data being sent in the request tags : an array of tags to help grouping related requests (example: user-related requests could have a User tag) parameters : an array of query / path parameters - each parameter is specified as a new OA\\Parameter object responses : an array of OA\\Response objects, each describing a combination of HTTP status codes and their respective response bodies Conclusion To summarize, the typical scenario on working on your own instance of Dotkernel API would follow these steps: create new module (example: Book ) add functionality to your new module (routes, entities, repositories, handlers, services, tests etc) create file OpenAPI.php in the new module and describe each new endpoint generate latest version of documentation file as described here","title":"Write Documentation"},{"location":"v5/openapi/write-documentation/#writing-documentation","text":"In order to avoid polluting PHP files with maybe thousands of lines of OpenAPI attributes, we opted for storing them in separate files, called OpenAPI.php , one for each module. We already covered all the endpoints available in Dotkernel API, you can consult the existing documentation in each module's own OpenAPI.php file. After you add more functionalities to your API, you will have to document the new endpoints. This is easier than it sounds because in most cases you will do the same: add a request by method, describe the request payload (if any), add request parameters (if any) and describe the possible responses.","title":"Writing documentation"},{"location":"v5/openapi/write-documentation/#common-objects","text":"To do this, you will use the following request objects: OA\\Delete : delete an API resource identified by its unique id OA\\Get : fetch API single or collections of API resources OA\\Post : create a new API resource (unless if it already exists) OA\\Patch : update an existing API resource OA\\Put : create a new API resource (if it already exists, it is overwritten) Also, the following components describing PHP objects: OA\\Schema : describe an object sent in a request or received as a response - read more OA\\Parameter : describe a query / path parameter - read more OA\\RequestBody : describe the body of a request - read more There are lot more, but these are the most often used ones. If you need help, take a look at the existing definitions found in Dotkernel API.","title":"Common objects"},{"location":"v5/openapi/write-documentation/#conclusion","text":"To summarize, the typical scenario on working on your own instance of Dotkernel API would follow these steps: create new module (example: Book ) add functionality to your new module (routes, entities, repositories, handlers, services, tests etc) create file OpenAPI.php in the new module and describe each new endpoint generate latest version of documentation file as described here","title":"Conclusion"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/","text":"Laminas API Tools compared to Dotkernel API API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15 Note Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Laminas API Tools vs Dotkernel API"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/#laminas-api-tools-compared-to-dotkernel-api","text":"API Tools (formerly Apigility) Dotkernel API URL api-tools Dotkernel API First Release 2012 2018 PHP Version <= 8.2 >= 8.1 Architecture MVC, Event Driven Middleware OSS Lifecycle Archived Style REST, RPC REST Versioning Yes Deprecations * Documentation Swagger (Automated) Postman (Manual) * Content-Negotiation Custom Custom License BSD-3 MIT Default DB Layer laminas-db doctrine-orm Authorization ACL RBAC-guard Authentication HTTP Basic/Digest OAuth2.0 OAuth2.0 CI/CD Yes Yes Unit Tests Yes Yes Endpoint Generator Yes Under development PSR PSR-7 PSR-7, PSR-15","title":"Laminas API Tools compared to Dotkernel API"},{"location":"v5/transition-from-api-tools/api-tools-vs-dotkernel-api/#note","text":"Versioning is replaced by Deprecations, using evolution strategy Version 5 ( Roadmap ) will implement OpenAPi 3.0","title":"Note"},{"location":"v5/transition-from-api-tools/discovery-phase/","text":"Discovery phase for a current system built using API Tools [WIP] In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it. Database there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else ) Note Dotkernel API is tested only with MariaDB version 10.6 and 10.11 LTS Authentication and Authorization how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac) Modules analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations Custom functionalities Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Discovery Phase"},{"location":"v5/transition-from-api-tools/discovery-phase/#discovery-phase-for-a-current-system-built-using-api-tools-wip","text":"In order to transition a system built using api-tools to Dotkernel API , we need to analyze the core components of it.","title":"Discovery phase for a current system built using API Tools [WIP]"},{"location":"v5/transition-from-api-tools/discovery-phase/#database","text":"there is a database in the current API ? which is the connection to database which library is used for database interaction ( laminas-db, doctrine 2, eloquent, or else )","title":"Database"},{"location":"v5/transition-from-api-tools/discovery-phase/#authentication-and-authorization","text":"how authentication is done ? (basic, digest, oauth2, etc.) how authorization is done ? (acl, rbac)","title":"Authentication and Authorization"},{"location":"v5/transition-from-api-tools/discovery-phase/#modules","text":"analyze configuration files of the modules (what needs to be configured in order to use a module) analyze routes (which are the routes, protection rules, which one need auth, etc.) analyze response format (content negotiation and validation, which ones are json, hal, views, etc.) analyze input field validations","title":"Modules"},{"location":"v5/transition-from-api-tools/discovery-phase/#custom-functionalities","text":"Analyze the custom code (code that cannot be generated through Admin UI and require manual implementation) For instance: caching events services extra installed packages and libraries jobs and queues third-parties tests","title":"Custom functionalities"},{"location":"v5/transition-from-api-tools/transition-approach/","text":"Transition approach [WIP] Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools Business cases There are at least 2 approaches for this transition: Clone 1:1 and recreate all endpoints and entities Build a new version of the current API using Dotkernel API and keep it running as separate platforms until the sunset of the current version of api-tools","title":"Transition Approach"},{"location":"v5/transition-from-api-tools/transition-approach/#transition-approach-wip","text":"Dotkernel API is not a one-to-one replacement of api-tools ( former Apigility), but is only a potential solution to migrate to. Functionalities, components and architecture are different. See the Comparison between Dotkernel APi and api-tools","title":"Transition approach [WIP]"},{"location":"v5/transition-from-api-tools/transition-approach/#business-cases","text":"There are at least 2 approaches for this transition:","title":"Business cases"},{"location":"v5/tutorials/api-evolution/","text":"API Evolution pattern API evolution: Updating an API while keeping it compatible for existing consumers by adding new features, fixing bugs, planning and removing outdated features. How it works In Dotkernel API we can mark an entire endpoint or a single method as deprecated using attributes on handlers. We use response headers to inform the consumers about the future changes by using 2 new headers: Link - it's a link to the official documentation pointing out the changes that will take place. Sunset - this header is a date, indicating when the deprecated resource will potentially become unresponsive. Both headers are independent, you can use them separately. Make sure you have the DeprecationMiddleware:class piped in your pipeline list. In our case it's config/pipeline.php . Marking an entire endpoint as deprecated When you want to mark an entire resource as deprecated you have to use the ResourceDeprecation attribute. ... #[ResourceDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Resource deprecation example.', rel: 'sunset', type: 'text/html' )] class HomeHandler implements RequestHandlerInterface { ... In the example above, the ResourceDeprecation attribute is attached to the class, marking the entire / (home) endpoint as deprecated starting from 2038-01-01 . Running the following curl will print out the response headers where we can see the Sunset and Link headers. curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:23:11 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin Marking a method as deprecated Most of the time you want to deprecate only an endpoint, so you will need to use the MethodDeprecation attribute which has the same parameters, but it attaches to a handler method. ... class HomeHandler implements RequestHandlerInterface { ... use Api\\App\\Attribute\\MethodDeprecation; #[MethodDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Method deprecation example.', rel: 'sunset', type: 'text/html' )] public function get(): ResponseInterface { ... } } Attaching the MethodDeprecation can only be done to HTTP verb methods ( GET , POST , PUT , PATCH and DELETE ). If you followed along you can run the below curl: curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" The response lists the Sunset and Link headers. HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:54:57 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin NOTES If Link or Sunset do not have a value they will not appear in the response headers. Sunset has to be a valid date, otherwise it will throw an error. You cannot use both ResourceDeprecation and MethodDeprecation in the same handler. Deprecations can only be attached to handler classes that implement RequestHandlerInterface . The rel and type arguments are optional, they default to sunset and text/html if no value was provided and are Link related parts.","title":"API Evolution"},{"location":"v5/tutorials/api-evolution/#api-evolution-pattern","text":"API evolution: Updating an API while keeping it compatible for existing consumers by adding new features, fixing bugs, planning and removing outdated features.","title":"API Evolution pattern"},{"location":"v5/tutorials/api-evolution/#how-it-works","text":"In Dotkernel API we can mark an entire endpoint or a single method as deprecated using attributes on handlers. We use response headers to inform the consumers about the future changes by using 2 new headers: Link - it's a link to the official documentation pointing out the changes that will take place. Sunset - this header is a date, indicating when the deprecated resource will potentially become unresponsive. Both headers are independent, you can use them separately. Make sure you have the DeprecationMiddleware:class piped in your pipeline list. In our case it's config/pipeline.php .","title":"How it works"},{"location":"v5/tutorials/api-evolution/#marking-an-entire-endpoint-as-deprecated","text":"When you want to mark an entire resource as deprecated you have to use the ResourceDeprecation attribute. ... #[ResourceDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Resource deprecation example.', rel: 'sunset', type: 'text/html' )] class HomeHandler implements RequestHandlerInterface { ... In the example above, the ResourceDeprecation attribute is attached to the class, marking the entire / (home) endpoint as deprecated starting from 2038-01-01 . Running the following curl will print out the response headers where we can see the Sunset and Link headers. curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:23:11 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin","title":"Marking an entire endpoint as deprecated"},{"location":"v5/tutorials/api-evolution/#marking-a-method-as-deprecated","text":"Most of the time you want to deprecate only an endpoint, so you will need to use the MethodDeprecation attribute which has the same parameters, but it attaches to a handler method. ... class HomeHandler implements RequestHandlerInterface { ... use Api\\App\\Attribute\\MethodDeprecation; #[MethodDeprecation( sunset: '2038-01-01', link: 'https://docs.dotkernel.org/api-documentation/v5/core-features/versioning', deprecationReason: 'Method deprecation example.', rel: 'sunset', type: 'text/html' )] public function get(): ResponseInterface { ... } } Attaching the MethodDeprecation can only be done to HTTP verb methods ( GET , POST , PUT , PATCH and DELETE ). If you followed along you can run the below curl: curl --head -X GET http://0.0.0.0:8080 -H \"Content-Type: application/json\" The response lists the Sunset and Link headers. HTTP/1.1 200 OK Host: 0.0.0.0:8080 Date: Mon, 24 Jun 2024 10:54:57 GMT Connection: close X-Powered-By: PHP/6.4.20 Content-Type: application/json Permissions-Policy: interest-cohort=() Sunset: 2038-01-01 Link: https://docs.dotkernel.org/api-documentation/v5/core-features/versioning;rel=\"sunset\";type=\"text/html\" Vary: Origin","title":"Marking a method as deprecated"},{"location":"v5/tutorials/api-evolution/#notes","text":"If Link or Sunset do not have a value they will not appear in the response headers. Sunset has to be a valid date, otherwise it will throw an error. You cannot use both ResourceDeprecation and MethodDeprecation in the same handler. Deprecations can only be attached to handler classes that implement RequestHandlerInterface . The rel and type arguments are optional, they default to sunset and text/html if no value was provided and are Link related parts.","title":"NOTES"},{"location":"v5/tutorials/cors/","text":"CORS 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. 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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ). How to fix? Dotkernel API fixes this issue using the mezzio/mezzio-cors library. Step 1: Install library In order to install mezzio/mezzio-cors , run the following command: composer require mezzio/mezzio-cors Step 2: Configure your API Register ConfigProvider Register mezzio/mezzio-cors in your application by adding its ConfigProvider to your application's config aggregator. Open the file config/config.php and paste the below lines at the beginning of the array passed to ConfigAggregator : Laminas\\Diactoros\\ConfigProvider::class, Mezzio\\Cors\\ConfigProvider::class, Save and close the file. Add middleware Add mezzio/mezzio-cors middleware to your application's pipeline. Open config/pipeline.php and paste the below line before the one with RouteMiddleware::class : $app->pipe(\\Mezzio\\Cors\\Middleware\\CorsMiddleware::class); Save and close the file. Create config file Create and open file config/autoload/cors.local.php and add the following code inside it: <?php declare(strict_types=1); use Mezzio\\Cors\\Configuration\\ConfigurationInterface; return [ ConfigurationInterface::CONFIGURATION_IDENTIFIER => [ 'allowed_origins' => [ ConfigurationInterface::ANY_ORIGIN, ], 'allowed_headers' => ['Accept', 'Content-Type', 'Authorization'], 'allowed_max_age' => '600', 'credentials_allowed' => true, 'exposed_headers' => [], ], ]; This list explains the above configuration values: allowed_origins : an array of domains that are allowed to interact with the API (default ConfigurationInterface::ANY_ORIGIN which means that any domain can make requests to the API) allowed_headers : an array of allowed custom headers allowed_max_age : the maximum duration, since the preflight response may be cached by a client credentials_allowed : allows a request to pass cookies exposed_headers : an array of headers which are being exposed by the endpoint Save and close the file. On the production environment, make sure you allow only specific origins by adding them to the allowed_origins array and removing the current value of ConfigurationInterface::ANY_ORIGIN . For more info, see mezzio/mezzio-cors documentation .","title":"Setting up CORS"},{"location":"v5/tutorials/cors/#cors","text":"","title":"CORS"},{"location":"v5/tutorials/cors/#what-is-cors","text":"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.","title":"What is CORS?"},{"location":"v5/tutorials/cors/#why-do-we-need-cors","text":"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. This happens because the API ( RESOURCE_URL ) is not configured to accept requests from the client ( ORIGIN_URL ).","title":"Why do we need CORS?"},{"location":"v5/tutorials/cors/#how-to-fix","text":"Dotkernel API fixes this issue using the mezzio/mezzio-cors library.","title":"How to fix?"},{"location":"v5/tutorials/create-book-module/","text":"Implementing a book module in Dotkernel API 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. . └── src/ └── Book/ └── src/ ├── Collection/ │ └── BookCollection.php ├── Entity/ │ └── Book.php ├── Handler/ │ └── BookHandler.php ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php src/Book/src/Collection/BookCollection.php - a collection refers to a container for a group of related objects, typically used to manage sets of related entities fetched from a database src/Book/src/Entity/Book.php - an entity refers to a PHP class that represents a persistent object or data structure src/Book/src/Handler/BookHandler.php - handlers are middleware that can handle requests based on an action src/Book/src/Repository/BookRepository.php - a repository is a class responsible for querying and retrieving entities from the database src/Book/src/Service/BookService.php - is a class or component responsible for performing a specific task or providing functionality to other parts of the application src/Book/src/ConfigProvider.php - is a class that provides configuration for various aspects of the framework or application src/Book/src/RoutesDelegator.php - a routes delegator is a delegator factory responsible for configuring routing middleware based on routing configuration provided by the application src/Book/src/InputFilter/BookInputFilter.php - input filters and validators src/Book/src/InputFilter/Input/* - input filters and validator configurations Creating and configuring the module Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. In src folder we will create the Book folder and in this we will create the src folder. So the final structure will be like this: src/Book/src . In src/Book/src we will create 2 php files: RoutesDelegator.php and ConfigProvider.php . This files will be updated later with all needed configuration. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); return $app; } } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ ], 'aliases' => [ ], ]; } private function getDoctrineConfig(): array { return [ ]; } private function getHalConfig(): array { return [ ]; } } Registering the module register the module config by adding the Api\\Book\\ConfigProvider::class in config/config.php under the Api\\User\\ConfigProvider::class register the namespace by adding this line \"Api\\\\Book\\\\\": \"src/Book/src/\" , in composer.json under the autoload.psr-4 key update Composer autoloader by running the command: composer dump-autoload That's it. The module is now registered and, we can continue creating Handlers, Services, Repositories and whatever is needed for out tutorial. File creation and contents Each file below have a summary description above of what that file does. src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\TimestampsTrait; use Api\\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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\DependencyInjection\\Attribute\\Entity; /** * @extends EntityRepository<object> */ #[Entity(name: Book::class)] 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/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Repository\\BookRepository; interface BookServiceInterface { public function getRepository(): BookRepository; } src/Book/src/Service/BookService.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\DependencyInjection\\Attribute\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { #[Inject(BookRepository::class)] public function __construct(protected BookRepository $bookRepository) { } public function getRepository(): BookRepository { return $this->bookRepository; } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->setRequired($isRequired); $this->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $this->getValidatorChain() ->attachByName(Date::class, [ 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), ], true); } } Now we add all the inputs together in a parent input filter. src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } We split all the inputs just for the purpose of this tutorial and to demonstrate a clean BookInputFiler but you could have all the inputs created directly in the BookInputFilter like this: $nameInput = new Input(); $nameInput->setRequired(true); $nameInput->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); $this->add($nameInput); Now it's time to create the handler. src/Book/src/Handler/BookHandler.php <?php declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\HandlerTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Fig\\Http\\Message\\StatusCodeInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\DependencyInjection\\Attribute\\Inject; class BookHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, BookServiceInterface::class, \"config\" )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService, protected array $config ) { } public function get(ServerRequestInterface $request): ResponseInterface { $book = $this->bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); if (! $book instanceof Book){ return $this->notFoundResponse(); } return $this->createResponse($request, $book); } public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->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(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } $book = $this->bookService->createBook($inputFilter->getValues()); return $this->createResponse($request, $book); } } After we have the handler, we need to register some routes in the RoutesDelegator , the same we created when we registered the module. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $uuid = \\Api\\App\\RoutesDelegator::REGEXP_UUID; $app->get( '/books', BookHandler::class, 'books.list' ); $app->get( '/book/'.$uuid, BookHandler::class, 'book.show' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } We need to configure access to the newly created endpoints, add books.list , book.show and book.create to the authorization rbac array, under the UserRole::ROLE_GUEST key. Make sure you read and understand the rbac documentation. It's time to update the ConfigProvider with all the necessary configuration needed, so the above files to work properly like dependency injection, aliases, doctrine mapping and so on. src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Doctrine\\ORM\\Mapping\\Driver\\AttributeDriver; use Dot\\DependencyInjection\\Factory\\AttributedRepositoryFactory; use Dot\\DependencyInjection\\Factory\\AttributedServiceFactory; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ BookHandler::class => AttributedServiceFactory::class, BookService::class => AttributedServiceFactory::class, BookRepository::class => AttributedRepositoryFactory::class, ], 'aliases' => [ BookServiceInterface::class => BookService::class, ], ]; } private function getDoctrineConfig(): array { return [ 'driver' => [ 'orm_default' => [ 'drivers' => [ 'Api\\Book\\Entity' => 'BookEntities' ], ], 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/Entity', ], ], ]; } private function getHalConfig(): array { return [ AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), AppConfigProvider::getResource(Book::class, 'book.show') ]; } } Migrations We created the Book entity, but we didn't create the associated table for it. You can check the mapping files by running: php bin/doctrine orm:validate-schema 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://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books To retrieve a book use: curl http://0.0.0.0:8080/book/{uuid}","title":"Creating a book module"},{"location":"v5/tutorials/create-book-module/#implementing-a-book-module-in-dotkernel-api","text":"","title":"Implementing a book module in Dotkernel API"},{"location":"v5/tutorials/create-book-module/#folder-and-files-structure","text":"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. . └── src/ └── Book/ └── src/ ├── Collection/ │ └── BookCollection.php ├── Entity/ │ └── Book.php ├── Handler/ │ └── BookHandler.php ├── InputFilter/ │ ├── Input/ │ │ ├── AuthorInput.php │ │ ├── NameInput.php │ │ └── ReleaseDateInput.php │ └── BookInputFilter.php ├── Repository/ │ └── BookRepository.php ├── Service/ │ ├── BookService.php │ └── BookServiceInterface.php ├── ConfigProvider.php └── RoutesDelegator.php 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","title":"Folder and files structure"},{"location":"v5/tutorials/create-book-module/#creating-and-configuring-the-module","text":"Firstly we will need the book module, so we will implement and create the basics for a module to be registered and functional. In src folder we will create the Book folder and in this we will create the src folder. So the final structure will be like this: src/Book/src . In src/Book/src we will create 2 php files: RoutesDelegator.php and ConfigProvider.php . This files will be updated later with all needed configuration. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); return $app; } } src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ ], 'aliases' => [ ], ]; } private function getDoctrineConfig(): array { return [ ]; } private function getHalConfig(): array { return [ ]; } }","title":"Creating and configuring the module"},{"location":"v5/tutorials/create-book-module/#file-creation-and-contents","text":"Each file below have a summary description above of what that file does. src/Book/src/Collection/BookCollection.php <?php declare(strict_types=1); namespace Api\\Book\\Collection; use Api\\App\\Collection\\ResourceCollection; class BookCollection extends ResourceCollection { } src/Book/src/Entity/Book.php To keep things simple in this tutorial our book will have 3 properties: name , author and release date . <?php declare(strict_types=1); namespace Api\\Book\\Entity; use Api\\App\\Entity\\AbstractEntity; use Api\\App\\Entity\\TimestampsTrait; use Api\\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(), ]; } } src/Book/src/Repository/BookRepository.php <?php declare(strict_types=1); namespace Api\\Book\\Repository; use Api\\App\\Helper\\PaginationHelper; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Doctrine\\ORM\\EntityRepository; use Dot\\DependencyInjection\\Attribute\\Entity; /** * @extends EntityRepository<object> */ #[Entity(name: Book::class)] 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/BookServiceInterface.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Repository\\BookRepository; interface BookServiceInterface { public function getRepository(): BookRepository; } src/Book/src/Service/BookService.php <?php declare(strict_types=1); namespace Api\\Book\\Service; use Api\\Book\\Entity\\Book; use Api\\Book\\Repository\\BookRepository; use Dot\\DependencyInjection\\Attribute\\Inject; use DateTimeImmutable; class BookService implements BookServiceInterface { #[Inject(BookRepository::class)] public function __construct(protected BookRepository $bookRepository) { } public function getRepository(): BookRepository { return $this->bookRepository; } public function createBook(array $data): Book { $book = new Book( $data['name'], $data['author'], new DateTimeImmutable($data['releaseDate']) ); return $this->bookRepository->saveBook($book); } public function getBooks(array $filters = []) { return $this->bookRepository->getBooks($filters); } } When creating or updating a book, we will need some validators, so we will create input filters that will be used to validate the data received in the request src/Book/src/InputFilter/Input/AuthorInput.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class AuthorInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\NotEmpty; class NameInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->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 declare(strict_types=1); namespace Api\\Book\\InputFilter\\Input; use Api\\App\\Message; use Laminas\\Filter\\StringTrim; use Laminas\\Filter\\StripTags; use Laminas\\InputFilter\\Input; use Laminas\\Validator\\Date; use Laminas\\Validator\\NotEmpty; class ReleaseDateInput extends Input { public function __construct(?string $name = null, bool $isRequired = true) { parent::__construct($name); $this->setRequired($isRequired); $this->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $this->getValidatorChain() ->attachByName(Date::class, [ 'message' => sprintf(Message::INVALID_VALUE, 'releaseDate'), ], true); } } Now we add all the inputs together in a parent input filter. src/Book/src/InputFilter/BookInputFilter.php <?php declare(strict_types=1); namespace Api\\Book\\InputFilter; use Api\\Book\\InputFilter\\Input\\AuthorInput; use Api\\Book\\InputFilter\\Input\\NameInput; use Api\\Book\\InputFilter\\Input\\ReleaseDateInput; use Laminas\\InputFilter\\InputFilter; class BookInputFilter extends InputFilter { public function __construct() { $this->add(new NameInput('name')); $this->add(new AuthorInput('author')); $this->add(new ReleaseDateInput('releaseDate')); } } We split all the inputs just for the purpose of this tutorial and to demonstrate a clean BookInputFiler but you could have all the inputs created directly in the BookInputFilter like this: $nameInput = new Input(); $nameInput->setRequired(true); $nameInput->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class); $nameInput->getValidatorChain() ->attachByName(NotEmpty::class, [ 'message' => sprintf(Message::VALIDATOR_REQUIRED_FIELD_BY_NAME, 'name'), ], true); $this->add($nameInput); Now it's time to create the handler. src/Book/src/Handler/BookHandler.php <?php declare(strict_types=1); namespace Api\\Book\\Handler; use Api\\App\\Handler\\HandlerTrait; use Api\\Book\\InputFilter\\BookInputFilter; use Api\\Book\\Service\\BookServiceInterface; use Fig\\Http\\Message\\StatusCodeInterface; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use Dot\\DependencyInjection\\Attribute\\Inject; class BookHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, BookServiceInterface::class, \"config\" )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected BookServiceInterface $bookService, protected array $config ) { } public function get(ServerRequestInterface $request): ResponseInterface { $book = $this->bookService->getRepository()->findOneBy(['uuid' => $request->getAttribute('uuid')]); if (! $book instanceof Book){ return $this->notFoundResponse(); } return $this->createResponse($request, $book); } public function getCollection(ServerRequestInterface $request): ResponseInterface { $books = $this->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(), StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY); } $book = $this->bookService->createBook($inputFilter->getValues()); return $this->createResponse($request, $book); } } After we have the handler, we need to register some routes in the RoutesDelegator , the same we created when we registered the module. src/Book/src/RoutesDelegator.php <?php namespace Api\\Book; use Api\\Book\\Handler\\BookHandler; use Mezzio\\Application; use Psr\\Container\\ContainerInterface; class RoutesDelegator { public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { /** @var Application $app */ $app = $callback(); $uuid = \\Api\\App\\RoutesDelegator::REGEXP_UUID; $app->get( '/books', BookHandler::class, 'books.list' ); $app->get( '/book/'.$uuid, BookHandler::class, 'book.show' ); $app->post( '/book', BookHandler::class, 'book.create' ); return $app; } } We need to configure access to the newly created endpoints, add books.list , book.show and book.create to the authorization rbac array, under the UserRole::ROLE_GUEST key. Make sure you read and understand the rbac documentation. It's time to update the ConfigProvider with all the necessary configuration needed, so the above files to work properly like dependency injection, aliases, doctrine mapping and so on. src/Book/src/ConfigProvider.php <?php declare(strict_types=1); namespace Api\\Book; use Api\\Book\\Collection\\BookCollection; use Api\\Book\\Entity\\Book; use Api\\Book\\Handler\\BookHandler; use Api\\Book\\Repository\\BookRepository; use Api\\Book\\Service\\BookService; use Api\\Book\\Service\\BookServiceInterface; use Doctrine\\ORM\\Mapping\\Driver\\AttributeDriver; use Dot\\DependencyInjection\\Factory\\AttributedRepositoryFactory; use Dot\\DependencyInjection\\Factory\\AttributedServiceFactory; use Mezzio\\Application; use Mezzio\\Hal\\Metadata\\MetadataMap; use Api\\App\\ConfigProvider as AppConfigProvider; class ConfigProvider { public function __invoke(): array { return [ 'dependencies' => $this->getDependencies(), 'doctrine' => $this->getDoctrineConfig(), MetadataMap::class => $this->getHalConfig(), ]; } private function getDependencies(): array { return [ 'delegators' => [ Application::class => [ RoutesDelegator::class ] ], 'factories' => [ BookHandler::class => AttributedServiceFactory::class, BookService::class => AttributedServiceFactory::class, BookRepository::class => AttributedRepositoryFactory::class, ], 'aliases' => [ BookServiceInterface::class => BookService::class, ], ]; } private function getDoctrineConfig(): array { return [ 'driver' => [ 'orm_default' => [ 'drivers' => [ 'Api\\Book\\Entity' => 'BookEntities' ], ], 'BookEntities' => [ 'class' => AttributeDriver::class, 'cache' => 'array', 'paths' => __DIR__ . '/Entity', ], ], ]; } private function getHalConfig(): array { return [ AppConfigProvider::getCollection(BookCollection::class, 'books.list', 'books'), AppConfigProvider::getResource(Book::class, 'book.show') ]; } }","title":"File creation and contents"},{"location":"v5/tutorials/create-book-module/#migrations","text":"We created the Book entity, but we didn't create the associated table for it. You can check the mapping files by running: php bin/doctrine orm:validate-schema 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","title":"Migrations"},{"location":"v5/tutorials/create-book-module/#checking-endpoints","text":"If we did everything as planned we can call the http://0.0.0.0:8080/book endpoint and create a new book: curl -X POST http://0.0.0.0:8080/book -H \"Content-Type: application/json\" -d '{\"name\": \"test\", \"author\": \"author name\", \"releaseDate\": \"2023-03-03\"}' To list the books use: curl http://0.0.0.0:8080/books To retrieve a book use: curl http://0.0.0.0:8080/book/{uuid}","title":"Checking endpoints"},{"location":"v5/tutorials/find-user-by-identity/","text":"A practical example: Find user by identity 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 : php ./bin/cli.php route:list This command will list all available endpoints, which looks like this: +--------+---------------------------------+--------------------------------+ | 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/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+ 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. $app->get('/user/' . $uuid, UserHandler::class, 'user.view'); Our route points to get method from UserHandler so let's navigate to that method. public function get(ServerRequestInterface $request): ResponseInterface { $user = $this->userService->findOneBy(['uuid' => $request->getAttribute('uuid')]); return $this->createResponse($request, $user); } As we can see, the method will query the database for the user based on its uuid 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 declare(strict_types=1); namespace Api\\User\\Handler; use Api\\App\\Exception\\BadRequestException; use Api\\App\\Exception\\NotFoundException; use Api\\App\\Handler\\HandlerTrait; use Api\\App\\Message; use Api\\User\\Entity\\User; use Api\\User\\Service\\UserServiceInterface; use Dot\\DependencyInjection\\Attribute\\Inject; use Mezzio\\Hal\\HalResponseFactory; use Mezzio\\Hal\\ResourceGenerator; use Psr\\Http\\Message\\ResponseInterface; use Psr\\Http\\Message\\ServerRequestInterface; use Psr\\Http\\Server\\RequestHandlerInterface; use function sprintf; class IdentityHandler implements RequestHandlerInterface { use HandlerTrait; #[Inject( HalResponseFactory::class, ResourceGenerator::class, UserServiceInterface::class, )] public function __construct( protected HalResponseFactory $responseFactory, protected ResourceGenerator $resourceGenerator, protected UserServiceInterface $userService, ) { } /** * @throws NotFoundException * @throws BadRequestException */ public function get(ServerRequestInterface $request): ResponseInterface { $identity = $request->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 : $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 namespace ApiTest\\Functional; use Api\\App\\Message; class IdentityTest extends AbstractFunctionalTest { public function testEmptyIdentityReturnsNotFound(): void { $response = $this->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.","title":"Find user by identity"},{"location":"v5/tutorials/find-user-by-identity/#a-practical-example-find-user-by-identity","text":"","title":"A practical example: Find user by identity"},{"location":"v5/tutorials/find-user-by-identity/#our-goal","text":"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.","title":"Our goal"},{"location":"v5/tutorials/find-user-by-identity/#what-we-have","text":"Let's print out all available endpoints using : php ./bin/cli.php route:list This command will list all available endpoints, which looks like this: +--------+---------------------------------+--------------------------------+ | 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/{uuid} | | PATCH | user.update | /user/{uuid} | | GET | user.view | /user/{uuid} | +--------+---------------------------------+--------------------------------+","title":"What we have"},{"location":"v5/tutorials/token-authentication/","text":"Token authentication What is token authentication? Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string. How does it work? In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account. Credentials The admin account with role set to both superuser and admin with the following credentials: identity : admin password : dotkernel The user account with role set to both user and guest with the following credentials: identify : test@dotkernel.com password : dotkernel Flow client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource Note The first two steps need to executed only once. Access token should be stored and reused for all upcoming requests. Refresh token should be stored and used to refresh expired access token. For a better overview of the flow, see the below image: Generate admin access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your admin account's identity and <password> with your admin account's password . Both fields come from table admin . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' Generate user access token Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" } Note Replace <identity> with your user account's identity and <password> with your user account's password . Both fields come from table user . Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (modify in: config/autoload/local.php > authentication . access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 400 Bad Request response with the following JSON body: { \"error\": \"Invalid credentials.\", \"error_description\": \"Invalid credentials.\", \"message\": \"Invalid credentials.\" } Refresh admin access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Refresh user access token Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" } Test using curl Execute the below command: curl --location 'https://api.dotkernel.net/security/refresh-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }' Note Make sure you replace <refresh-token> with the refresh token generated with the access token. Response on success You should see a 200 OK response with the following JSON body: { \"token_type\": \"Bearer\", \"expires_in\": 86400, \"access_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.e...wuE39ON1mS5mnTKfA_dSpSWxOmNQdny_AKIbc1qZjMfS24qSUV8HIoOw\", \"refresh_token\": \"def502005a035c8dfe5456d27e85069813a4f8...0b844e843cd62865662a0e723165752dfd7012491502d3d819c2a61d\" } Field description: token_type : token type to be set when sending the Authorization header (example: Authorization: Bearer eyJ0e... ) expires_in : access token lifetime (change here: config/autoload/local.php authentication -> access_token_expire ) access_token : generated access token (store it for later use) refresh_token : generated refresh token (store it for regenerating expired access token) Response on failure You should see a 401 Unauthorized response with the following JSON body: { \"error\": \"invalid_request\", \"error_description\": \"The refresh token is invalid.\", \"hint\": \"Cannot decrypt the refresh token\", \"message\": \"The refresh token is invalid.\" } Test admin authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"admin\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/admin/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body. Test user authentication flow Step 1: Fail to fetch protected API content Try to view your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' You should get a 403 Forbidden JSON response. Step 2: Generate access token Generate admin access token by executing: curl --location 'https://api.dotkernel.net/security/generate-token' \\ --header 'Content-Type: application/json' \\ --data-raw '{ \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"test@dotkernel.com\", \"password\": \"dotkernel\" }' You should get a 200 OK JSON response. Store the value of access_token for later use. Step 3: Successfully fetch protected API content Try again viewing your admin account by executing: curl --location 'https://api.dotkernel.net/user/my-account' \\ --header 'Authorization: Bearer <access_token>' Replace <access_token> with the previously stored access token. You should get a 200 OK JSON response with the requested resource in the body.","title":"Token authentication"},{"location":"v5/tutorials/token-authentication/#token-authentication","text":"","title":"Token authentication"},{"location":"v5/tutorials/token-authentication/#what-is-token-authentication","text":"Token authentication means making a request to an API endpoint while also sending a special header that contains an access token. The access token was previously generated by (usually) the same API as the one you are sending requests to and it consists of an alphanumeric string.","title":"What is token authentication?"},{"location":"v5/tutorials/token-authentication/#how-does-it-work","text":"In order to protect specific resources, clients need to be authenticated with user/admin roles. These roles are identified from the access token sent via the Authorization header. When Dotkernel API receives a request, it tries to read the access token. If it does not find an access token, client has guest role: if the requested endpoint needs no authentication, the requested resource is returned else, a 403 Forbidden response is returned Else, client's account is identified and client has admin / user role (the one assigned in their account) if the requested endpoint is accessible to the client, the requested resource is returned else, a 403 Forbidden response is returned Dotkernel API provides out-of-the-box both an admin and a user account.","title":"How does it work?"},{"location":"v5/tutorials/token-authentication/#flow","text":"client sends API request with credentials API returns a JSON object containing a new access and refresh token client sends API request using Authentication header containing the previously generated access token API returns requested resource","title":"Flow"},{"location":"v5/tutorials/token-authentication/#generate-admin-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate admin access token"},{"location":"v5/tutorials/token-authentication/#generate-user-access-token","text":"Send a POST request to the /security/generate-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"password\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"username\": \"<identity>\", \"password\": \"<password>\" }","title":"Generate user access token"},{"location":"v5/tutorials/token-authentication/#refresh-admin-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"admin\", \"client_secret\": \"admin\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh admin access token"},{"location":"v5/tutorials/token-authentication/#refresh-user-access-token","text":"Send a POST request to the /security/refresh-token endpoint with Content-Type header set to application/json . Set request body to: { \"grant_type\": \"refresh_token\", \"client_id\": \"frontend\", \"client_secret\": \"frontend\", \"scope\": \"api\", \"refresh_token\": \"<refresh-token>\" }","title":"Refresh user access token"},{"location":"v5/tutorials/token-authentication/#test-admin-authentication-flow","text":"","title":"Test admin authentication flow"},{"location":"v5/tutorials/token-authentication/#test-user-authentication-flow","text":"","title":"Test user authentication flow"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 790686d..6861dd4 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,274 +2,274 @@\ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index de1b960..340c417 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/v5/introduction/file-structure/index.html b/v5/introduction/file-structure/index.html index 64db332..40278cc 100644 --- a/v5/introduction/file-structure/index.html +++ b/v5/introduction/file-structure/index.html @@ -769,19 +769,31 @@ https://docs.dotkernel.org/api-documentation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/commands/create-admin-account/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/commands/display-available-endpoints/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/commands/generate-database-migrations/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/commands/generate-tokens/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/core-features/authentication/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/core-features/authorization/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/core-features/content-validation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/core-features/cors/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/core-features/exceptions/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/flow/default-library-flow/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/flow/library-flow-for-email/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/flow/middleware-flow/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/composer/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/configuration-files/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/doctrine-orm/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/faq/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/getting-started/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/installation/test-the-installation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/introduction/file-structure/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/introduction/introduction/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/introduction/packages/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/introduction/server-requirements/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/transition-from-api-tools/api-tools-vs-dotkernel-api/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/transition-from-api-tools/discovery-phase/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/transition-from-api-tools/transition-approach/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/tutorials/create-book-module/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v4/tutorials/token-authentication/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/upgrading/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/commands/create-admin-account/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/commands/display-available-endpoints/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/commands/generate-database-migrations/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/commands/generate-tokens/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/authentication/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/authorization/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/content-validation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/dependency-injection/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/error-reporting/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/core-features/exceptions/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/flow/default-library-flow/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/flow/library-flow-for-email/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/flow/middleware-flow/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/composer/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/configuration-files/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/doctrine-orm/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/faq/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/getting-started/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/installation/test-the-installation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/introduction/file-structure/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/introduction/introduction/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/introduction/packages/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/introduction/psr/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/introduction/server-requirements/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/generate-documentation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/getting-help/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/initialized-components/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/introduction/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/render-documentation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/use-documentation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/openapi/write-documentation/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/transition-from-api-tools/api-tools-vs-dotkernel-api/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/transition-from-api-tools/discovery-phase/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/transition-from-api-tools/transition-approach/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/tutorials/api-evolution/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/tutorials/cors/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/tutorials/create-book-module/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/tutorials/find-user-by-identity/ -2024-11-20 +2024-11-21 https://docs.dotkernel.org/api-documentation/v5/tutorials/token-authentication/ -2024-11-20 +2024-11-21 On this page
It is a good practice to standardize the file structure of projects.
When using Dotkernel API the following structure is installed by default:
-bin
- executable files from CLIconfig
- various configuration filesdata
- should contain project-related data (AVOID storing sensitive data on VCS)documentation
- should contain project-related documentationlog
- storage of log files generated by dot-error-log librarypublic
- publicly visible files. The webserver need to have this folder as www-document root folder.src
- should contain the source code filestest
- should contain the test files.github
- containes workflow files.github
- contains GitHub workflow files.laminas-ci
- contains laminas-ci workflow filesbin
directoryThis directory contents are
+bin
folderThis folder contents are
clear-config-cache.php
which removes the config cache file (data/cache/config-cache.php
- available only when development mode is enabled).clear-config-cache.php
- removes the config cache file (data/cache/config-cache.php
- available only when development mode is enabled).cli.php
used to build console applications based on laminas-clidoctrine
used by the doctrine fixtures to populate the database tablesdoctrine
- used by the doctrine fixtures to populate the database tablesconfig
directoryThis directory contains all application-related config files:
+config
folderThis folder contains all application-related config files:
cli-config.php
: command line interface configuration used by migrations, fixtures, cronsconfig.php
: registers ConfigProviders for installing packagesconfig
directorypipeline.php
: contains a list of middlewares, in the order of their executiontwig-cs-fixer.php
: configuration file for Twig code style checker/fixerconfig/autoload
directoryThis directory contains all service-related local and global config files:
+config/autoload
folderThis folder contains all service-related local and global config files:
authorization.global.php
: configures access per route for user rolescli.global.php
: configures cliconfig/autoload
directoryresponse-header.global.php
: defines headers per routetemplates.global.php
: dotkernel/dot-twigrenderer config filedata
directoryThis directory is a storage for project data files and service caches. +
data
folderThis folder is a storage for project data files and service caches. It contains these folders:
cache
: cache for e.g. Twig filesdata
directorydata/lock
- lock files generated by dotkernel/dot-cli
--AVOID storing sensitive data on VCS.
+AVOID storing sensitive data on the repository!
log
directoryThis directory stores daily log files. +
log
folderThis folder stores daily log files.
When you access the application from the browser, (if not already created) a new log file gets created in the format specified in the config/autoload/error-handling.global.php
config file under the stream
array key.
public
directoryThis directory contains all publicly available assets and serves as the entry point of the application:
+public
folderThis folder contains all publicly available assets and serves as the entry point of the application:
uploads
: a folder that normally contains files uploaded via the application.htaccess
: server configuration file used by Apache web server; it enables the URL rewrite functionalityindex.php
: the application's main entry pointrobots.txt.dist
: a sample robots.txt file that allows/denies bot access to certain areas of your application; activate it by duplicating the file as robots.txt
and comment out the lines that don't match your environmentsrc
directoryThis folder contains a separate folder for each Module. -Each Module folder, in turn, should contain following directories, unless they are empty:
+src
folderThis folder contains a separate folder for each Module.
+These are the modules included by default:
Admin
- contains functionality for managing users with admin
role; note these are users save in the admin
database tableApp
- contains core functionality, from authentication, to rendering, to error reportingUser
- contains functionality for managing regular users--The above example is just some of the directories a project may include, but they should give you an idea about the recommended structure.
-
Other classes in the src
directory may include InputFilter
, EventListener
, Helper
, Command
, Factory
etc.
The src
directory normally also contains these files:
Each Module folder, in turn, should contain the following folders, unless they are empty:
+src/Handler
- Action classes (similar to Controllers but can only perform one action)src/Entity
- Used by database entitiessrc/Service
- Service classessrc/Collection
- Database entities collectionssrc/Repository
- Entity repository folderThe above example is just some of the folders a project may include, but they should give you an idea about the recommended structure.
+Other classes the src
folder may include are InputFilter
, EventListener
, Helper
, Command
, Factory
etc.
The src
folder in each Module folder normally also contains these files:
ConfigProvider.php
- Configuration data for the moduleOpenAPI.php
- Detailed descriptions for each endpoint in the OpenAPI formatRoutesDelegator.php
- Module specific route registrations Module main routes entry fileRoutesDelegator.php
- Module specific route registrationstemplates
directoryThis directory contains the template files, used for example to help render e-mail templates.
+templates
folder in ModulesThis folder contains the template files, used for example to help render e-mail templates.
-diff --git a/v5/introduction/introduction/index.html b/v5/introduction/introduction/index.html index f7c0259..001e869 100644 --- a/v5/introduction/introduction/index.html +++ b/v5/introduction/introduction/index.html @@ -801,22 +801,10 @@Dotkernel API uses twig as Templating Engine. All template files have the extension .html.twig
+Dotkernel API uses twig as Templating Engine. All template files have the extension
.html.twig
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:
-Below is a quick overview of features in Dotkernel API.
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.
@@ -868,23 +834,23 @@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.
+For our API payloads (a value object for describing the API resource, its relational links and any embedded/child resources related to it) we use mezzio/mezzio-hal.
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.0 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 OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using league/oauth2-server package.
+We use mezzio/mezzio-authentication-oauth2 which provides OAuth 2.0 authentication for Mezzio and PSR-7/PSR-15 applications by using the [thephpleague/oauth2-server]https://github.com/thephpleague/oauth2-server package.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.
DotMailServiceMailService
provided by dotkernel/dot-mail you can easily send custom email templates.
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.
+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 that holds configuration files for each category.
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.
For registering new commands first make sure your command class extends Symfony\Component\Console\Command\Command
.
Then you can enable it by registering it in config/autoload/cli.global.php
.
Here you will also find our brand-new file locker configuration, so you can easily turn it on or off (by default: 'enabled' => true
).
Here you will also find our file locker configuration, so you can easily enable and disable it (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.
LoggerInterface
for error loggingResponseInterface
RequestHandlerInterface
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:
+You can find factory-made tests in thetest
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
-vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always
-vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always
+Alternatively, you can run each test category separately with these commands:
+vendor/bin/phpunit --testsuite=UnitTests --testdox --colors=always
+vendor/bin/phpunit --testsuite=FunctionalTests --testdox --colors=always