Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/einenlum/chal
https://github.com/einenlum/chal
Last synced: 8 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/einenlum/chal
- Owner: Einenlum
- License: mit
- Created: 2019-02-19T12:48:29.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2020-02-12T19:56:11.000Z (almost 5 years ago)
- Last Synced: 2024-12-24T00:15:29.531Z (17 days ago)
- Language: PHP
- Size: 294 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Coding challenge
## Install
The project uses docker and docker-compose.
`make install`
## Run
`make up`
The api documentation should be available on `http://localhost/doc` (or `http://localhost/doc.json` if you want the OpenAPI Json version).
## Tests
The project is statically checked thanks to [phpstan](https://github.com/phpstan/phpstan) and tested with [phpspec](https://github.com/phpspec/phpspec) and [Behat](https://github.com/Behat/Behat).
`make test`
## Architecture
The project follows an _Hexagonal architecture_: it is divided between:
- The `domain` part (all the core knowledge of the domain: here all the core bricks of the app, all the rules concerning places, events, etc.)
- The `application` part (the parts that will contain the DTOs and some handlers which can apply some actions on the model)
- The `infrastructure` part: all the third party layers (the adapters) that are implementation details (framework, IO…).The bricks of the domain know nothing about the infrastructure. The `Domain` should only know about itself. The `Application` can know about itself and the `Domain`. The `Infrastructure` can know about itself, the `Application` and the `Domain`.
This architecture helps building solid and maintainable code: **the core of the app is not dependant in any way from the third party tools**. This means that the project can be focused first on the domain knowledge (which can be unit tested) and the implementation choices (which message broker or database to use…) can be done later with a better understanding of the requirements.
## Some personal choices in this project
I decided to do almost everything in a customized way, because it's fun. I would use more third party services otherwise (FOSRestBundle, for example).
### Entities
I decided to use annotations instead of yaml to define my schema. I would prefer to decouple my models from my entities, but Yaml is deprecated in Doctrine and [will be dropped in Doctrine 3](https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/yaml-mapping.html). Therefore (since I'm not really an XML fan), annotations are used here.
The id is not autogenerated on the database side, but on the backend side thanks to UUIDs. Some advantages:
- Avoid coupling to the database (the app is the master orchester).
- The object is in a valid state after the creation (on the contrary, it is invalid while it is not persisted with autogenerated ids)
- The id are not incremental, which means that exposing them to the user exposes less information about the internal data.### Validation
Since the DTO are defined in the `Application` part, they should in theory know nothing about the infrastructure (cf. _supra_). This is why the validation part is defined in `Yaml` in `config/validation.yaml`.
### Request lifecycle
Because decoding the request and validating it is a repetitive and tedious task, I decided to implement some event subscribers dedicated to these tasks.
The first one (`App\Infrastructure\Symfony\Request\Subscriber\DecodeJson`) is taking the request JSON content and tries to decode it. If it fails, then we can already throw a `BadRequestHttpException` (400), because it means the JSON is malformed.
The second one (`App\Infrastructure\Symfony\Request\Subscriber\InjectDTOIfNeeded`) runs after the first one. If the Controller has a custom `@InjectDTO` annotation on the method, it will transform the decoded data from the `Request` to the requested DTO by denormalizing it, and it will inject it into the `Request`'s attributes so that it is available in the controller arguments.
Also, if there is a `mapping` argument, it will take an attribute from the request (like a resolved url parameter for example) and inject it into the data to denormalize.Others (`App\Infrastructure\Symfony\Request\Subscriber\TransformUuids`) transform some simple parameters to handle them more easily, like transforming a string into a real `Uuid` object.
It's maybe a bit overkill, but it's always fun to play with listeners.
Also `EventSubscriber`s are preferred to `EventListener`s because we know right away when reading the the classes which events they're listening at. Plus, the use of constants avoid typo mistakes.### Proxies for serializing
To avoid coupling between the models and the serialized objects (we're only sending _representations_ of our models through the network), I use a system of proxies. These are generated by the appropriate factory called by the Symfony normalizer.
### Controller responses
The controller responses are custom Responses (`OKResponse`, `CreatedResponse`…) which are then serialized and transformed to HTTPResponse in their appropriate subscribers (cf. [src/Infrastructure/Symfony/Response/Subscriber/Success](src/Infrastructure/Symfony/Response/Subscriber/Success) and [src/Infrastructure/Symfony/Response/Subscriber/FailureResponseSubscriber](src/Infrastructure/Symfony/Response/Subscriber/FailureResponseSubscriber.php)).
### DRY and encapsulation
The project is absolutely overkill for such simple needs: but I tried to encapsulate things that could be repeated and are then error prone.
A bit of an extreme example is the class [JsonResponse](src/Infrastructure/Symfony/Response/Http/JsonResponse.php): It extends the base Symfony `JsonResponse` because the fourth argument is always set up to true.
Same goes for the [JsonSerializer](Symfony\Component\Serializer\SerializerInterface), hiding the second parameter (`json`) which is always called the same way.If this is definitely too much for a project of this side, this could help productivity on big projects, after a bit more time of boilerplate, since everything is after based on common conventions.
## Some failures
- I could not manage to implement the geolocation part. I thought I could manage to create the DQL functions to use the ST_Distance_Sphere but 1/ I discovered very late that mariadb still does not propose it, 2/ even with MySQL, it seems my formula is wrong somehow.
- The documentation is not very elegant… I have to admit I'm really not happy with the actual ecosystem around the API documentation in PHP. I used here nelmio api doc because it's pretty easy to use (even if the documentation is really bad, which is kind of a joke) but one drawback is that it uses the OpenAPI spec version 2.0 and still not the 3.0.
- No HATEOAS here cause I did not have enough time. Also because I am not a huge fan of it (I don't agree totally with [this angry developer](https://jeffknupp.com/blog/2014/06/03/why-i-hate-hateoas/), but at least partly).
- PS: Sorry for the last minute cs fixing. It messes up the commit history.