Code examples and good practices using Domain Drive Development, Hexagonal Architecture, CQRS,
Symfony 6, PHP8 and anything else I can think of...
Report Bug
·
Request Feature
Table of Contents
I have created this project to have a guide of code examples and good practices as a future reference for me and for anyone who may be interested.
I will be adding more examples that I think are interesting and that provide an extra for anyone who wants to get started in the technologies mentioned bellow.
Features
- PHP8
- Symfony 6
- DDD guidelines
- Hexagonal Architecture
- SOLID
- Docker
- Doctrine ORM & DB migrations
- Albums module with CQRS pattern (command and query bus)
- Static code analysis: PHPCS, Rector, Psalm
- Unit and integration tests: PHPUnit
- Acceptance tests: Behat
- Basic Authorization, with mandatory token to http POST endpoints
- Basic JWT Authorization, with mandatory token to http PUT endpoints
- NoSql: Redis examples
- Frontend examples (React, Redux, Webpack, Babel, etc.): on this repo
- Elastic stack (Elasticsearch, Logstash, Kibana, Filebeat)
Upcoming Features
- Aggregates organization
- Using native PHP amqp extension to publish events (instead of Symfony/Messenger)
- RabbitMQ configuration wizard (queues and exchanges, retry, dead-letter and bindings)
- Supervisor configuration wizard (file .ini per queue)
I will add new features and examples, this project is constantly evolving! You can see unreleased code at here
- docker: How to install docker? Please click here
- make
Clone repo, download deps and create docker services:
git clone https://github.com/masfernandez/symfony-ddd-hexarch-cqrs.git
cd symfony-ddd-hexarch-cqrs
make composer-install
make up
Execute at root path:
make prod-start
In order to create a new Album
is mandatory to include a valid Token
in request's Authorization header. So first, let's create a new User:
make create-demo-user
# Credentials for demo user: test@email.com 1234567890
Now, it's time to get a valid token:
curl -i -X POST 'http://api.musiclabel.127.0.0.1.nip.io/authentication' \
-H 'Content-Type: application/json' \
--data-raw '{
"email": "test@email.com",
"password": "1234567890"
}'
You can find the Token in response's Location header:
Server: nginx/1.19.5
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.0
Location: 4ac71eeda13c8fe7f0e4c017412bd9f2d886288cb8c88331007f2a9c7652385b
Cache-Control: no-cache, private
Date: Fri, 15 Jan 2021 11:23:45 GMT
X-Robots-Tag: noindex
Strict-Transport-Security: max-age=31536000
{}
We can publish new Albums now: (replace the value of the token here with the one you got before... obviously)
curl -i -X POST 'http://api.musiclabel.127.0.0.1.nip.io/albums' \
-H 'Authorization: Bearer 4ac71eeda13c8fe7f0e4c017412bd9f2d886288cb8c88331007f2a9c7652385b' \
-H 'Content-Type: application/json' \
--data-raw '{
"id": "0da69030-3ed7-42b5-8aa5-25fb61dab1b2",
"title": "Abbey Road",
"release_date": "1969-09-26 00:00:00"
}'
Verifying the Album created:
curl -X GET 'http://api.musiclabel.127.0.0.1.nip.io/albums?page[number]=1&page[size]=1&sort=title&fields[albums]=id,title,release_date'
We need a JWToken to make PUT operations on Albums, so let's get one:
curl -i -X POST 'http://api.musiclabel.127.0.0.1.nip.io/authentication/jwt' \
-H 'Content-Type: application/json' \
--data-raw '{
"email": "test@email.com",
"password": "1234567890"
}'
You can find the JWToken (header.payload.signature) in response's headers:
- header+payload in Location header
- signature in set-cookie header
HTTP/2 201
server: nginx/1.19.8
content-type: application/json
location: header+payload:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwOi8vZXhhbXBsZS5vcmciLCJqdGkiOiJlWFZocHBTR0JwZllTeHNZIiwiaWF0IjoxNjE3MzU1NTMyLjQyNzU3MSwibmJmIjoxNjE3MzU1NTMzLjQyNzU3MSwiZXhwIjoxNjE3MzU5MTMyLjQyNzU3MSwidWlkIjoiMGY4MzNjMjItZmVmZC00ZmFmLWE3YzItNGEwNzlhMjJjMzdjIn0
x-powered-by: PHP/8.0.3
cache-control: no-cache, private
date: Fri, 02 Apr 2021 09:25:32 GMT
x-robots-tag: noindex
set-cookie: signature=GFZiEgVkKIbv5YszK_5wKmhLpqlkhYUUS1N1nCLLavs; path=/; secure; httponly; samesite=none
strict-transport-security: max-age=31536000
You may be asking why sending the JWToken like this... well, I'm to lazy to write hundred of words when there is a lot of information already on there. Just few tips:
- Main reason is security (XSS and CSRF). This is an evolved example of the following medium article:
- https://medium.com/@ryanchenkie_40935/react-authentication-how-to-store-jwt-in-a-cookie-346519310e81
Recommend read:
Don't forget the purpose of this repo: just to show some examples, crazy dev ideas and my opinionated vision on how to approach some scenarios ;)
Let's replace Album created before:
The client (React, Vue, Curl, Postman... whatever) should know how to re-construct the JWToken to make a request (remember, header + payload in Authorization header and signature in cookie)
Note: replace the values of the token here with the one you got before... obviously
curl -i -X PUT 'http://api.musiclabel.127.0.0.1.nip.io/albums/0da69030-3ed7-42b5-8aa5-25fb61dab1b2' \
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwOi8vZXhhbXBsZS5vcmciLCJqdGkiOiJlWFZocHBTR0JwZllTeHNZIiwiaWF0IjoxNjE3MzU1NTMyLjQyNzU3MSwibmJmIjoxNjE3MzU1NTMzLjQyNzU3MSwiZXhwIjoxNjE3MzU5MTMyLjQyNzU3MSwidWlkIjoiMGY4MzNjMjItZmVmZC00ZmFmLWE3YzItNGEwNzlhMjJjMzdjIn0' \
-H 'Content-Type: application/json' \
-H 'Cookie: signature=GFZiEgVkKIbv5YszK_5wKmhLpqlkhYUUS1N1nCLLavs' \
--data-raw '{
"title": "New album value here",
"release_date": "2021-04-02 00:00:00"
}'
Response:
HTTP/2 204
server: nginx/1.19.8
x-powered-by: PHP/8.0.3
cache-control: no-cache, private
date: Fri, 02 Apr 2021 09:58:22 GMT
x-robots-tag: noindex
strict-transport-security: max-age=31536000
Verifying the Album updated:
curl -i -X GET 'http://api.musiclabel.127.0.0.1.nip.io/albums?page[number]=1&page[size]=1&sort=title&fields[albums]=id,title,release_date'
Response:
HTTP/2 200
server: nginx/1.19.8
content-type: application/json
x-powered-by: PHP/8.0.3
cache-control: no-cache, private
date: Fri, 02 Apr 2021 09:59:36 GMT
x-robots-tag: noindex
strict-transport-security: max-age=31536000
{
"data": [
{
"id": "0da69030-3ed7-42b5-8aa5-25fb61dab1b2",
"title": "New album value here",
"release_date": "2021-04-02 00:00:00"
}
],
"links": {
"self": "\/albums?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"first": "\/albums?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"prev": "\/albums?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"next": "\/albums?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"last": "\/albums?page%5Bnumber%5D=1&page%5Bsize%5D=1"
},
"meta": {
"total_pages": 1
}
}
Several docker services will be available and ready for use when the app starts:
RabbitMQ
http://localhost:15672
user: rabbit_user
password: rabbit_pass
Kibana
Kibana Nginx logs configuration:
-
Execute below command after all ELK services are started:
make filebeat-dashboards
-
Go to http://localhost:5601/app/dashboards and search for "Nginx"
Kibana Symfony logs configuration:
-
Create an index pattern at http://localhost:5601/app/management/kibana/indexPatterns/create
- Step 1. Write "logstash*" as index pattern and press "Next" button
- Step 2. Select @timestamp for Time field and press "Create index" button
-
Configure logs
- Step 3. Go to http://localhost:5601/app/logs/settings and include "logs-,filebeat-,logstash*" in `Log indices´ field and confirm pressing Apply button at bottom page.
Logs can be visualized now at http://localhost:5601/app/logs/stream
make dev-start
make test
There are several services in the Docker stack for this project. All services are built from official docker images except:
- Nginx: Custom docker image. I will optimize some parameters soon but at this moment is just a wrapper of the official docker image. More info here
- PHP-FPM: Custom docker image. It has 2 main targets, for production and development environment. Each env has some deps that you can check at here. This is also a repo I'm working on.
See the open issues for a list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Distributed under the MIT License. See LICENSE.txt
for more information.
Miguel Ángel Sánchez Fernández - mangel.sanfer@gmail.com
(linkedin hiden profile - require login)
Project Link: https://github.com/masfernandez/symfony-ddd-hexarch-cqrs
- README template based on: https://github.com/othneildrew/Best-README-Template
- CHANGELOG template based on: https://keepachangelog.com/en/1.0.0/