Giter VIP home page Giter VIP logo

symfony-ddd-example's Introduction

DDD, Hexagonal Architecture & CQRS with Symfony and Doctrine

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

Environment Setup

Needed tools

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

Application execution

  1. If not already done, install Docker Compose
  2. Run docker-compose build --pull --no-cache to build fresh images
  3. Run docker-compose up (the logs will be displayed in the current shell)
  4. Open https://localhost in your favorite web browser and accept the auto-generated TLS certificate
  5. Run docker-compose down --remove-orphans to stop the Docker containers.

Project explanation

A very simple application (absolutely not complete) to manage a blog. The goal is to show how to initialize a project with a DDD structure and how to make the different Bounded Contexts communicate.

Structure

$ tree -L 4 src

src
|-- Blog // Company subdomain / Bounded Context: Features related to one of the company business lines / products
|   `-- Post // Some Module inside the Mooc context
|       |-- Application
|       |   |-- Controller // Inside the application layer all is structured by actions
|       |   |   |-- Api
|       |   |   |   |-- GetArticlesController
|       |   |   |   |-- PostArticleController.php
|       |   |   |   |-- PostCommentController.php
|       |   |-- Event 
|       |   |-- EventSubscriber 
|       |   |-- Model // The Data transformer objects for CQRS 
|       |   |   |-- CreateArticleCommand.php 
|       |   |   |-- CreateCommandCommand.php 
|       |   |   |-- FindArticleQuery.php 
|       |   |-- ParamConverter 
|       |   |-- Service (the applications layer services) 
|       |-- Domain
|       |   |-- Entity (The entities and the value objects) 
|       |   |   |-- Article.php // The Aggregate Root of the Bounded Context
|       |   |   |-- ArticleId.php // Value Object   
|       |   |   |-- AuthorId.php // Value Object   
|       |   |   |-- Comment.php // Entity that depends from Aggregate Root
|       |   |   |-- CommentId.php // Value Object 
|       |   |   |-- Email.php // Value Object  
|       |   |-- Event // Domain Events
|       |   |   |-- ArticleCreatedEvent.php
|       |   |   |-- CommentCreatedEvent.php
|       |   |-- Repository 
|       |   |   |-- ArticleRepositoryInterface.php 
|       |   |   |-- CommentRepositoryInterface.php 
|       `-- Infrastructure // The infrastructure layer
|           |-- DoctrineMapping
|           |   |-- Article.orm.xml
|           |   |-- Comment.orm.xml
|           |   |-- Email.orm.xml
|           `-- Repository (the concrete repositories)
|               `--ArticleRepository.php // An implementation of the repository
|               `--CommentRepository.php // An implementation of the repository

Hexagonal Architecture

This projects follows the Hexagonal Architecture pattern.
The application layer can only use domain implementations and the infrastructure layer must implement domain interfaces in order to be completely independent.

For example the repository which is closely related to Doctrine and which is located in the infrastructure layer implements a domain interface.

//App\Blog\Post\Domain\Repository\ArticleRepositoryInterface

interface ArticleRepositoryInterface
{
    public function find($id, $lockMode = null, $lockVersion = null);

    public function save(Article $article): void;
}

CQRS

Inside the Application / Model directory of the Bounded Contexts there are the "Command" or "Query" depending on the action to be performed. These are dispatched via the Symfony message bus which will forward them to the associated service handler.

Example of Command:

//App\Blog\Post\Application\EventSubscriber\OnPublicationApprovedEventSubscriber

$createArticleCommand = new CreateArticleCommand();
$createArticleCommand->setTitle($event->getTitle());
$createArticleCommand->setBody($event->getBody());
$createArticleCommand->setAuthor($event->getAuthor());
$createArticleCommand->setCategory($event->getCategoryId());

$this->messageBus->dispatch($createArticleCommand);

Example of Query

//App\Blog\Post\Application\Controller\Api\GetArticleController

$article = $this->handle(new FindArticleQuery($id));

Repository pattern

Our repositories try to be as simple as possible usually only containing 2 methods save and find.

Bounded Contexts

There are three different Bounded Contexts:

  • Post: It takes care of managing everything related to an article.
  • User: Here you should find everything related to user management.
  • Category: This is where you can create categories, edit or delete them (even if only the creation is implemented).

Aggregates

Each BC has one and only one Aggregate Root.

An Aggregate is a cluster of associated object that we treat as a single unit. Each Aggregate has a single root Entity and a boundary that marks what is inside and what is outside the Aggregate.

A Comment object only really make sense in the context of a Article because we can’t have a Comment without a Article.

The root Entity is the only Entity that is globally accessible in the application. In the example from above, Article would be the root Entity.

Inside the boundary we would have all the associated objects of this Aggregate. For example, we would have the CommentEntity as well as any other Entities or Value Objects that make up the Aggregate.

The Comment object can hold references to each other internally to the Aggregate, but no other external object can hold a reference to any object internally to the Aggregate that is not the root Entity.

The only way to access the Comment Entity is to retrieve the Article root Entity and traverse it’s associated objects.

Communication between Bounded Contexts

The BCs must be independent, this means that they must not know anything about the other BCs. For this reason, communication between BCs should ideally be done with an event-driven system.

In our example, when we want to create an article, the payload contains the category slug and the user id. But how do we get the category id and make sure the user id is correct? This data is managed in other bounded contexts.

Below is the sequence diagram showing the steps to publish an article.

An OnPublicationRequestedEventis dispatched and listened in the user BC which check if the user exists. The UserVerifiedEventevent is then dispatched and listened in the category BC which retrieves the category id by the slug. A new OnPublicationApprovedEventis then dispatched and listened in the Post BC. Now the creation of the article can be done.

How to use

  1. $ docker-compose exec php sh
  2. Create user:
    $ bin/concole app:create-user [email protected] p4$$word ROLE_EDITOR
  3. Create category:
    POST https://localhost/api/categories/ 
     {
         "name": "Sport", 
         "slug": "sport"
     }
    
4) Create Article:
POST https://localhost/api/articles/
    {
        "title": "article",
        "body": "body",
        "author": *author_id*, 
        "categorySlug": "sport"
    }
5) Create Comment:
POST https://localhost/api/comments/
    {
        "article_id": *author_id*, 
        "email": "[email protected]", 
        "message": "test message"
    }
  1. Get Article:
    GET https://localhost/api/articles/*author_id*

About Me

Resources

symfony-ddd-example's People

Contributors

salletti avatar

Watchers

James Cloos avatar

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.