Giter Site home page Giter Site logo

codelytv / php-ddd-example Goto Github PK

View Code? Open in Web Editor NEW
2.9K 80.0 1.1K 2.2 MB

🐘🎯 Hexagonal Architecture + DDD + CQRS in PHP using Symfony 6

Home Page: https://pro.codely.tv/library/ddd-en-php

PHP 92.37% Gherkin 1.18% Dockerfile 0.26% Makefile 0.71% Twig 5.48%
cqrs codelytv ddd php symfony behat phpunit doctrine domain-driven-design docker

php-ddd-example's Introduction

Codely logo

🐘🎯 Hexagonal Architecture, DDD & CQRS in PHP

codely.tv CodelyTV Courses Symfony 6 Type Coverage CI pipeline status

Example of a PHP application using Domain-Driven Design (DDD) and Command Query Responsibility Segregation (CQRS) principles keeping the code as simple as possible.

Take a look, play and have fun with this. Stars are welcome 😊

View Demo · Report a bug · Request a feature

🚀 Environment Setup

🐳 Needed tools

  1. Install Docker
  2. Clone this project: git clone https://github.com/CodelyTV/php-ddd-example php-ddd-example
  3. Move to the project folder: cd php-ddd-example

🛠️ Environment configuration

  1. Create a local environment file (cp .env .env.local) if you want to modify any parameter

🔥 Application execution

  1. Install all the dependencies and bring up the project with Docker executing: make build
  2. Then you'll have 3 apps available (2 APIs and 1 Frontend):
    1. Mooc Backend: http://localhost:8030/health-check
    2. Backoffice Backend: http://localhost:8040/health-check
    3. Backoffice Frontend: http://localhost:8041/health-check

✅ Tests execution

  1. Install the dependencies if you haven't done it previously: make deps
  2. Execute PHPUnit and Behat tests: make test

👩‍💻 Project explanation

This project tries to be a MOOC (Massive Open Online Course) platform. It's decoupled from any framework, but it has some Symfony and Laravel implementations.

⛱️ Bounded Contexts

  • Mooc: Place to look in if you wanna see some code 🙂. Massive Open Online Courses public platform with users, videos, notifications, and so on.
  • Backoffice: Here you'll find the use cases needed by the Customer Support department in order to manage users, courses, videos, and so on.

🎯 Hexagonal Architecture

This repository follows the Hexagonal Architecture pattern. Also, it's structured using modules. With this, we can see that the current structure of a Bounded Context is:

$ tree -L 4 src

src
|-- Mooc // Company subdomain / Bounded Context: Features related to one of the company business lines / products
|   `-- Videos // Some Module inside the Mooc context
|       |-- Application
|       |   |-- Create // Inside the application layer all is structured by actions
|       |   |   |-- CreateVideoCommand.php
|       |   |   |-- CreateVideoCommandHandler.php
|       |   |   `-- VideoCreator.php
|       |   |-- Find
|       |   |-- Trim
|       |   `-- Update
|       |-- Domain
|       |   |-- Video.php // The Aggregate of the Module
|       |   |-- VideoCreatedDomainEvent.php // A Domain Event
|       |   |-- VideoFinder.php
|       |   |-- VideoId.php
|       |   |-- VideoNotFound.php
|       |   |-- VideoRepository.php // The `Interface` of the repository is inside Domain
|       |   |-- VideoTitle.php
|       |   |-- VideoType.php
|       |   |-- VideoUrl.php
|       |   `-- Videos.php // A collection of our Aggregate
|       `-- Infrastructure // The infrastructure of our module
|           |-- DependencyInjection
|           `-- Persistence
|               `--MySqlVideoRepository.php // An implementation of the repository
`-- Shared // Shared Kernel: Common infrastructure and domain shared between the different Bounded Contexts
    |-- Domain
    `-- Infrastructure

Repository pattern

Our repositories try to be as simple as possible usually only containing 2 methods search and save. If we need some query with more filters we use the Specification pattern also known as Criteria pattern. So we add a searchByCriteria method.

You can see an example here and its implementation here.

Aggregates

You can see an example of an aggregate here. All aggregates should extend the AggregateRoot.

Command Bus

There is 1 implementations of the command bus.

  1. Sync using the Symfony Message Bus.

Query Bus

The Query Bus uses the Symfony Message Bus.

Event Bus

The Event Bus uses the Symfony Message Bus. The MySql Bus uses a MySql+Pulling as a bus. The RabbitMQ Bus uses RabbitMQ C extension.

📱 Monitoring

Every time a domain event is published it's exported to Prometheus. You can access to the Prometheus panel here.

🤔 Contributing

There are some things missing (add swagger, improve documentation...), feel free to add this if you want! If you want some guidelines feel free to contact us :)

🤩 Extra

This code was shown in the From framework coupled code to #microservices through #DDD talk and doubts where answered in the DDD y CQRS: Preguntas Frecuentes video.

🎥 Used in the CodelyTV Pro courses:

🌐 remember to visit our courses

php-ddd-example's People

Contributors

amfolgar avatar bertuz avatar d4nicoder avatar danielramosacosta avatar dependabot[bot] avatar gabrielfs7 avatar garciasdos avatar javiercane avatar jdreesen avatar joanhey avatar jonijnm avatar jotavaldivia avatar jpina avatar juanpalacios08 avatar keldrox avatar lauraferrandof avatar mangelsnc avatar micperr avatar mikados avatar mikybars avatar neveldo avatar pana1990 avatar patrick-mota avatar protectedhector avatar quentinrogeret34 avatar rgomezcasas avatar santakadev avatar sdecandelario avatar sultanicq avatar testica avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

php-ddd-example's Issues

ReadMe: Add use cases and patterns implemented section

Add a new top level section to the README.md file with the following contents:

Use cases and patterns implemented

Important: We should reference a piece of code illustrating each one of the use cases or patterns. Something very similar to what we've done in the Scala API repo with the Libraries and implementation examples README.md section

Undefined method getException() on RequestEvent class

I came across this error while trying to access http://api.codelytv.localhost:8030/

Attempted to call an undefined method named "getException" of class "Symfony\Component\HttpKernel\Event\ExceptionEvent".

To make it work, I changed a few things in this class:

  • I used the ExceptionEvent class instead of RequestEvent in the method signature of onException() and changed the getException() call by getThrowable().
  • I used \Throwable in the method signature of exceptionCodeFor() instead of Exception.
  • I replaced class_basename() by get_class inside this same method

which results in the following:

final class ApiExceptionListener
{
    private ApiExceptionsHttpStatusCodeMapping $exceptionHandler;

    public function __construct(ApiExceptionsHttpStatusCodeMapping $exceptionHandler)
    {
        $this->exceptionHandler = $exceptionHandler;
    }

    public function onException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        $event->setResponse(
            new JsonResponse(
                [
                    'code'    => $this->exceptionCodeFor($exception),
                    'message' => $exception->getMessage(),
                ],
                $this->exceptionHandler->statusCodeFor(get_class($exception))
            )
        );
    }

    private function exceptionCodeFor(\Throwable $error): string
    {
        $domainErrorClass = DomainError::class;

        return $error instanceof $domainErrorClass ? $error->errorCode() : Utils::toSnakeCase(get_class($error));
    }
}

These are not optimal changes by any means, but they allowed me to display a fully functionnal error message on the homepage.

No one publish events from SymfonySyncDomainEventPublisher to RabbitMQ

Yes, I see the listeners like CreateNotificationOnVideoCreated and event handlers executor ConsumeDomainEventsCommand but seems no one publish Domain Events like VideoCreatedDomainEvent to the RabbitMQ or SymfonySyncEventBus using notify method. Isn't it? Please help me, I am confused. Thanks.

UPD: yes, out the box we use SymfonySyncDomainEventPublisher to publish events synchronously, but anyways no one executes the handlers of them.

Crear grupo de discord o slack

Estaría super bien crear un grupo de slack o discord en que los alumnos y algunos tutores podamos charlar de los distintos temas de la plataforma (ddd, tdd, php, symfony, docker...)

Sorry de antemano si este no es el mejor lugar para comentar esto.

Un abrazo chicos.

Working on windows 10 docker integration

I tried to use it on windows 10 but if I do make build powershell gives some errors

process_begin: CreateProcess(NULL, pwd, ...) failed. Do not run Composer as root/super user! See https://getcomposer.org/root for details Composer could not find a composer.json file in /app To initialize a project, please create a composer.json file as described in the https://getcomposer.org/ "Getting Started" section make: *** [composer-install] Erreur 1

I fixed that just changing pwd and making it cross system compliant from $(shell pwd) to ${PWD}

I've removed $(id -u):$(id -g) too since such thing doesn't exist in docker for windows but we could create a volume with those arguments.

Or we could simply have two different make files one for windows and one for linux.

But even if I try to do all the changes to make it works I still getting some errors...
I changed
composer-install: @docker run --rm --interactive --tty --volume $(shell pwd):/app --user $(id -u):$(id -g) \ gsingh1/prestissimo install \ --ignore-platform-reqs \ --no-ansi \ --no-interaction
to
composer-install: @docker run --rm --interactive --tty --volume ${PWD}:/app gsingh1/prestissimo install --ignore-platform-reqs --no-ansi --no-interaction

Still getting this error:
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Composer could not find a composer.json file in /app
To initialize a project, please create a composer.json file as described in the https://getcomposer.org/ "Getting Started" section
make: *** [composer-install] Erreur 1

I don't know why because if I execute the command directly in powershell all seems ok:
docker run --rm --interactive --tty --volume ${PWD}:/app gsingh1/prestissimo install --ignore-platform-reqs --no-ansi --no-interaction

Maybe it's related to make but I don't really know much about it...

Where would you hash the passwords?

Hi,

I just started learning DDD and Hexagonal architecture. The code here is very interesting and I'm learning from it a lot, so thanks for that :)

I just have one question. Where would you hash passwords?

I noticed the code for comparing password is inside CodelyTv\Backoffice\Auth\Domain\AuthPassword class. Is this the right place to hash the passwords before they would be persisted?

Error when goes to -> 2. Go to the API health check page

When goes to url http://api.codelytv.localhost:8030/health-check to the API health check page, show a warning:

Warning: file_put_contents(/app/apps/mooc/backend/var/cache/test/CodelyTv_Apps_Mooc_Backend_MoocBackendKernelTestDebugContainerDeprecations.log): failed to open stream: No such file or directory

The problem is with permissions when execute make build i need sud and create fies with root.
When solve this return: {"mooc-backend":"ok","rand":1}

Hostname not working

After installing if you try to go to
http://api.codelytv.dev:8030/status

It doesn't work probably because .dev is now a valid domain name (by Google).

You should change this domain, I switched in my hosts (in Windows 10) from this to
127.0.0.1 api.codelytv.local

And now going to http://api.codelytv.local:8030/status works as intended giving me status 200 ok.

I suggest you to use traefik to avoid any futur problems.
It's a reverse proxy so you don't need to configure your hosts anymore since traefik will handle this.
https://github.com/containous/traefik

I can integrate it in your docker-compose.yml if you want.
@rgomezcasas @JavierCane Just tell me and I'll do a pull request with this.

The good thing with traefik is if you want after to go "live" it's easier too since it supports automatic certificate generation (let's encrypt) and you just have to change the domain name (in your env or wherever you want).

Typo with a namespace

There is a typo with the namespace CodelyTv\Mooc\Courses\Infraestructure\Persistence

Infraestructure should be Infrastructure ?

Add Git pre-push hook

Follow the same principles as in the Scala API repo

You can copy the very same pre-push script just modifying the command to execute. That is, instead of calling to sbt prep, we should call to a command defined in the composer.json in order to run PhpStan, CodeSniffer, acceptance tests, and unit tests.

This issue includes the addition to the README.md of this pre-push script as in the Scala API Repo 🙂

[Duda] ddd & doctrine

Buenas chicos, antes de nada gran curso.

Tengo una duda de concepto con respecto a ddd y como se manejaría con doctrine:

Imaginémonos que tenemos una clase User (que todos sabemos que no para de crecer y crecer sin darnos cuenta) y queremos dividirla en contextos para evitar que la clase tenga toda la lógica centralizada de todos los casos de uso que se puedan dar, como lo resolveríais y como manejaríais el mapeo con doctrine?

Por ejemplo imaginémonos una red social, user tiene cierta lógica que se aplica en la autenticación (registrar, login...) y luego otra lógica que se da en la parte de amigos (agregar amigo, borrar amigo...)

Usaríais una clase abstracta BaseUser con las propiedades básicas del usuario (userId y username) y extenderíais de ella en función del contexto?, es posible manejar esto con doctrine?

Gracias de antemano chicos ;)

How can a dependency be added to the project?

I do not have composer installed on my local computer and I see that it is not installed in the php docker container, is there a way to install a dependency in the project that I am not seeing? or do I have to install composer on my local computer?

Question: Databases creation procedure.

static::generateDatabaseIfNotExists($parameters, $schemaFile);

Hello, and first, thanks you to sharing this project, you made a great job. I'm trying to figure out why a raw sql mapping is used instead of directly using the mapping of entities (*.orm.xml). Could you explain it to me please. It make sense for performance, for the tests (initialisation time) ? or for more granular definition of database ? I'm trying now to use the xml mapping first (at place of EntityManagerFactory::generateDatabaseIfNotExists) but it seems to be a headache mission.

Video/demo presentation in English

Hey guys,

I found this repository today and it looks very interesting.

Can I find somewhere your video presentation in English?

Cheers!

Question: a commandBus into a commandHandler = bug

Hola, i need to split operations, so i tried to add a command bus into a command handler with the usage of InMemorySymfonyCommandBus. I have this issue:

Error {#855
  #message: "Maximum function nesting level of '256' reached, aborting!"
  #code: 0
  #file: "./var/cache/dev/ContainerUxFsxIU/App_KernelDevDebugContainer.php"
  #line: 1800
  trace: {
    ./var/cache/dev/ContainerUxFsxIU/App_KernelDevDebugContainer.php:1800 {
      ContainerUxFsxIU\App_KernelDevDebugContainer->ContainerUxFsxIU\{closure}^
      › 
      › return $this->privates['App\\Shared\\Infrastructure\\Bus\\Command\\InMemorySymfonyCommandBus'] = new \App\Shared\Infrastructure\Bus\Command\InMemorySymfonyCommandBus(new RewindableGenerator(function () {
      ›     yield 0 => ...

A idea ?

Question about validation

Hey, thanks a lot for the repo.

I've tried to play with the repository and I wanted to see where would you do validation?
I've created a video with a non existent course.
I wonder how would you do if you wanted to validate that the course exists ?

Thanks in advance :)

Create Request/Response DTOs for Application Services

What

Instead of instantiating the Value Objects directly from the controller, we should instantiate a DTO such as SignUpUserRequest and pass it to the Application Service.

The Application Services intended for querying data, we should return also a DTO such as UserResponse.

Why

This way we:

  • Avoid coupling our entry points to the internal details on how we model out our domain (UserId value objects and so on).
  • Leave the code on the Hexagonal Architecture approach closer to what we'll do while evolving to an architecture applying CQRS concepts. We will only need to:
    • Replace the XxxRequest by their corresponding XxxCommand,
    • Send the commands to the Command Bus instead of directly calling the Application Service
    • Implement the Command Handler in order to extract each one of the scalar values from the Command, convert them to Value Object instances, and call to the Application Service
    • Modify the Application Service in order to directly receive the Value Objects instead of the XxxRequest

health check error before make build

file_put_contents(/app/apps/mooc/backend/var/cache/test/CodelyTv_Apps_Mooc_Backend_MoocBackendKernelTestDebugContainerDeprecations.log): failed to open stream: No such file or directory
Unable to create the cache directory (/app/apps/mooc/backend/var/cache/test)

This errors show when I go to health check route after make build.

I don't know how to resolve. Any help?

Optimize PHP Dockerfile with multistage builds (builder pattern)

We've already added Docker Compose to this repository thanks to the @sdecandelario PR: #56

However, we had an interesting discussion about how we should implement the etc/infrastructure/php/Dockerfile for the PHP container specifically regarding the provision (composer install) phase.

Provisioning in an early stage of the Dockerfile would allow us to perform some optimizations 😬

One of these optimizations would be applying the builder pattern or Multistage builds. You can read more information about them in the "Docker: De 0 a deploy" CodelyTV Pro course by @fiunchinho (here you have the course repository with some examples), and a blog post about this technic and its benefits.

However, we've tried to apply this strategy and we've miserably failed (🤣). The reason why we haven't been able to do so is because the PHP Dockerfile is in a subdirectory and it can't copy the composer.json file (it is inside the parent folder).

We don't want to move the PHP Dockerfile to the project root folder because it would add too much noise, but we don't know the best way to have the needed files available. Any suggestions?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.