Make sure everything is running as explained in README.md.
Table of contents generated with markdown-toc
src/
Source directory. Not publicjmp/
Application directoryControllers/
Controllers to handle requests and build the responsesMiddleware/
Custom MiddlewaresModels/
Raw data objects with the functionality to convert to an arrayServices/
Business logic and database communicationUtils/
Utility classes
- Private
dependencies.php
Initialize services, controllers, middlewares and add them to the slim containersettings.php
Slim settings and required settings for the dependenciesroutes.php
Register routes with their middlewares here
- Public
index.php
Application entry point
We use JWT for authentication. The token must be submitted in the authorization header using the bearer schema. Example:
Authorization: Bearer <token>
Read more at the api specification.
To adjust the settings, look here:
- In settings.php
- .env and .env docs
The submitted token is decoded by the jwt-middleware. The custom authentication middleware checks the permissions of the enquirer using the sub
entry of the decoded token. If the enquirer has got the right permissions for the route, he can pass. If no token is supplied or if the supplied token is invalid, a 401 is returned. In case the enquirer just hasn't got the required permissions, a 403 is returned.
To receive a new token, the user have to call the login route. If the username and the password are valid, a new token is generated with the following service class Auth.php. The password is hashed using password_hash
with the PASSWORD_DEFAULT
option before it is stored in the database. To verify a password at a login, password_verify
is used.
Only administrators can register new users. To register a new user, the register route has to be called.
Every user input has to be validated before it is processed.
All validations are stored in validation.php and use the RespectValidation validation engine. The stored validations have to be submitted to the validation middleware of the corresponding route in routes.php.
Example:
->add(new Validation($container['validation']['getUser']))
See more at Routes.
In case the automatic generated error messages don't fit your requirements or hold sensible data like a password, you can add translations as described in Translate errors.
Add the translations to validation.php and to the middleware as second parameter. Example:
->add(new Validation(
$this->getContainer()['validation']['login'],
$this->getContainer()['validation']['loginTranslation']
))
It's important, that everyone complies with the following rules.
Improvements: If you recognize code, which doesn't comply with these rules, just correct them.
In this application we use the Optional
very often.
In php it's possible to let a method return mixed
types (e.g. User|bool
) or null able objects, but we decided to not use these possibilities.
When a method doesn't get the expected value, in other words, the execution fails, then we return an Optional
instead of a null
or false
value.
On success:
return Optional::success('the data to return, could be of any type');
On failure:
return Optional::failure();
Handle the returned Optional:
$optional = methodWhichMayFail();
if ($optional->isSuccess()) {
// success
$data = $optional->getData();
} else {
//failure
}
Vice versa:
$optional = methodWhichMayFail();
if ($optional->isFailure()) {
// failure
} else {
// success
$data = $optional->getData();
}
The response object of the slim framework offers a method called withJson
. This method converts an associative array to JSON.
Because the php cast functionality doesn't comply with our requirements to cast model objects to associative arrays, we use the following util and interface:
ArrayConvertable & ArrayConvertableTrait: Every model has to implement this interface and use this trait. It is necessary to properly convert the models into arrays.
Converter: This util is used to convert a model object or a list of model objects properly to an associative array. Use it in the controller as shown in this examples:
return $response->withJson(Converter::convert($modelObject));
or
return $response->withJson(Converter::convertArray($list));
Everywhere it is possible we use object oriented php. So every method signature has to use the php type declarations. If you try to call a method and pass arguments of another type as declared, then php will throw a TypeError.
Also, every method has to be documented with phpdoc. A short summary or description of the method and a documented signature is sufficient.
Example (from UserService.php):
/**
* Select a user by its id
* @param int $userId
* @return Optional containing a User if successfull
*/
public function getUserByUserId(int $userId): Optional
The services handle the business logic as well as the whole database communication. At the moment there isn't a separated Persistence Layer with e.g. Data Access Objects (DAOs).
That the services don't become a mess, all SQL statements have to be coded with the following rules:
- Use the heredoc syntax:
$sql = <<<SQL
# SQL
SQL;
- Write every sql keyword upper-case
- Use the
AS
keyword to change column names from the sql underscore style to the camel case style. E.g.
SELECT user_id AS userId ...
- Use the following for optional parameters (Examples in EventService.php):
... WHERE (:optionalId IS NULL OR id = :optionalId)
- A specific service doesn't query tables to which the service doesn't belong unless it's a join
- Always use prepared statements
- Use
COALESCE
for updating an entry with optional parameters (Example from EventService.php):
UPDATE `jmp`.`event` e
SET e.`title` = COALESCE(:title, e.`title`)
WHERE e.`id` = :id
and use the bindValue
method to set the parameters:
$stmt->bindValue(':title', $params['title'], PDO::PARAM_STR);
This guide will take you through the most important steps to develop a new route. Make sure you've read Code Style before reading the following guide.
First of all the new route has to be documented in the api-specification.
Then you have to create tests for the new route as documented in Create a new test.
Routes return JSON mostly containing one or a list of model objects. So you have to create a new model if the required doesn't exist yet. The model is just a raw data object with the functionality to convert properly to an array.
Then you need a service to communicate with the database and to handle the business logic. Create a new service class if no appropriate exists yet, otherwise add the required method/s to the existing service class.
If no useful service class already exists, you have to create a new one.
The service must
- have a
ContainerInterface
constructor parameter - hold all dependencies as private attributes
- have all dependencies set inside the constructor
- be added to the slim container inside dependencies.php
Check out already existing services as examples. Services
If the required model doesn't already exist, you have to create a new one.
The model must
- have all columns (as in the database) as public attributes
- have a constructor with one array as parameter
- set all attributes (except the ones which are foreign keys in the database) by the values of the array inside the constructor
- implement the ArrayConvertable Interface
- use the ArrayConvertable Trait
Check out already existing model as examples. Models
For each route a specific controller method exists. A controller class itself holds methods handling similar subjects. So if no appropriate controller already exists, a new controller has to be created.
A controller is called by the associated method in the route.php script. The controller is only called after the validation and the authentication passed successful. A controller has to call the required service and has to build the response dependent on the return value of the service. More information about responses and the error responses can be found in the api specification.
If no appropriate controller exists, you have to create a new one.
The controller must
- have a
ContainerInterface
constructor parameter - hold all dependencies as private attributes
- have all dependencies set inside the constructor
Check out already existing controllers as examples. Controllers
Now the new route has to be registered in route.php. It's very important, that the middlewares are added in the right order and with the right configuration.
- ValidationErrorResponseBuilder
- Validation Middleware
- Use the right validation settings. Add them to validation.php
- AuthenticationMiddleware
- Set the right PermissionLevel as noted in the api specification of your route
- JWT Middleware
Example:
$this->get('/users/{id:[0-9]+}', UsersController::class . ':getUser')
->add(new ValidationErrorResponseBuilder())
->add(new Validation($container['validation']['getUser']))
->add(new AuthenticationMiddleware($container, \jmp\Utils\PermissionLevel::ADMIN))
->add($jwtMiddleware);
First run all tests locally with two iterations as described in Running tests and after pushing your changes you should watch the Travis CI Build Status.
We use Postman and newman for integration tests.
The following tests are required to create a new test
Test Data:
Initially test data is inserted into the database during the docker-compose build. The following SQL-Script contains all test data: 03_initData.sql.
Import existing tests and environments:
First import the test collection and environment from the docker/newman/collections directory.
Structure:
- jmp
- category (e.g.
events
)- action (e.g.
create
)- admin (uses the environment variable
admin-token
as authentication-token)- all requests creating events are located here
- one request for each possible scenario
- Successful
- Bad Request
- Not Found
- Forbidden
- etc.
- nonadmin (uses the environment variable
nonadmin-token
as authentication-token)- all requests creating events are located here
- one request for each possible scenario
- Successful
- Bad Request
- Not Found
- Forbidden
- etc.
- admin (uses the environment variable
- action (e.g.
- category (e.g.
Create the missing directory structure needed for your tests and set the authentication tokens for the admin
and nonadmin
directories.
Prerequest scripts:
Use the prerequest scripts to set all query parameters and body-data:
vars = pm.variables;
vars.set('limit', 10);
You can pass the variables using double braces:
http://localhost/api/v1/events?eventType={{eventType}}&group={{group}}&limit={{limit}}&offset={{offset}}&all={{all}}&elapsed={{elapsed}}
Tests:
Postman internally uses Chai as BDD / TDD assertion library.
You should test as much as possible and as precise as possible.
The tests have to:
- be as precise as possible
- test as much as possible
- avoid side-effects (only per admin/nonadmin directory)
- use the variables set in the prerequest scripts:
vars.get('limit');
Use the already existing tests as reference.
Save:
To save the newly created tests export the jmp
-collection and replace the existing in the repository with the new one.
Then you only have to commit and push the changes.
You can run the tests with the postman runner (don't forget to set the jmp-environment) or with newman.
newman:
make test-local dir="$(pwd)"
Everytime you push your changes, a Travis CI job is triggered and all tests are executed.
Build Status:
If you want to use the Apache Web Server isntead of nginx, you have to do some additional configuration.
Note:
Replace example.com by your own domain.
- Create a separate local branch for your changes:
git checkout -b example.com
- Install/Update php dependencies (a running app container or a local php composer installation is required):
docker exec app composer install --classmap-authoritative
docker exec app composer update --classmap-authoritative
- Configure your web hosting as described:
- At least php 7.1
- mysql or mariadb with:
- This scheme: jmp
- A database user with restricted privileges (SELECT, INSERT, UPDATE, DELETE on all tables of the jmp scheme) and a password
- Configure all environment variables as described in dotenv and Get Started.
- Copy api to the webroot of your server
- Test the api
To run the newman test collection you have to do some search/replace with the collection.
The Makefile targets create-test-collection
and test-deployment
will do the work for you.
Use it as described in Create and run customized test script
Makefile
make test-deployment dir="$(pwd)" host="example.com" protocol="https" path="test/api"
This script will make a customized copy in the docker/newman/collections directory with the given host, protocol and path. So you can instantly test your deployment.