DEV Community

Ludovic Fleury
Ludovic Fleury

Posted on • Updated on

Domain Driven Design with PHP and Symfony

Personal take on Domain Driven Design with PHP and Symfony framework (2.x to 5.x)

This article was written in french here

Meaning is everything

DDD is gaining momentum in the Symfony & PHP community. I'm committed into a quest for "meaning" and so I'm a DDD evangelist.

Definition

DDD is a paradigm, an approach, a way.
Take a moment to reflect on the acronym because in order to get the pure value of DDD, you only need to understand these 3 words together: "Domain" "Driven Design" If you need to remember only one thing from DDD, it should be its name.

DDD isn't an software architecture or architectural style. DDD isn't about Entity, Value Object or Aggregate Root. They are convenient, and misleading, technicality in the object-oriented world.

DDD in essence is a shared understanding of the business domain crystallised in a base code by developers.

In practice

If business people would take the time to read a DDD code repository, they should understand 80% of it. Actually, high level language like PHP could be considered like a really poor subset of the english language. With DDD, the PHP code describe business rules, requirements, invariants in an "object-oriented english".

How does my code look like? Business domain processes described with a poor english:

$cashier->checkout($cart);
$cashier->charge($card);
$accountant->write($payment);
$compliance->verify($payment);
Enter fullscreen mode Exit fullscreen mode

How does my classes look like? Business concepts written in a poor english:

namespace Legal\Company\France;

/**
 * French unique registration number for a specific establishments of a company
 * issued by INSEE
 * SIRET: "Système d'Identification du Répertoire des ETablissements"
 * 
 * @see https://www.economie.gouv.fr/entreprises/numeros-siren-siret
 */
Class Siret implements RegistrationNumber
{
    private Siren $siren;
    private Nic $nic;

    public function __construct(string $number)
    {
        try {
            $this->siren = new Siren(mb_substr($number, 0, 9));
            $this->nic = new Nic(mb_substr($number, 9));
        } catch (\Exception $exception) {
            throw new \DomainException('Invalid SIRET number');
        }
    }

    public function getNumber(): string
    {
        return $this->siren->getNumber() . $this->nic->getNumber()
    }
}
Enter fullscreen mode Exit fullscreen mode
namespace Legal\Company\France;

/**
 * Unique registration number for a french company
 * Issued by INSEE, composing the first part of SIRET
 * Système d'Identification du Répertoire des ENtreprises
 *
 * @see https://bpifrance-creation.fr/encyclopedie/formalites-creation-dune-entreprise/formalites-generalites/numeros-didentification
 */
class Siren
{
    private string $number;

    public function __construct(string $number)
    {
         if (preg_match('/^[0-9]{9}$/', $number) !== 1) {
            throw new \DomainException('Invalid SIREN number');
         }

         $this->number = $number;
    }

    public function getNumber(): string
    {
        return $this->number;
    }
}
Enter fullscreen mode Exit fullscreen mode

French developers might be familiar with the concepts examplified. However, if you aren't, it shouldn't take you too long to get a clear idea about the rules governing the french company registration numbers... simply by reading the code.

The direct benefits

More than readable code, we have expressive code translating literraly the business concepts & domain.

The code is also "immutable".
I didn't follow a best practice or a specific pattern, I just followed the french registration number rules. These numbers are immutable, so my code is immutable... and so these classes naturally fit the Value Object definition. It wasn't the goal of the implementation, it's a side effect of it.

Free your mind

Forget about "best practice", "design pattern", "fluent interface", "event dispatching", , MVC, SoC, SRP, SOA, RAD, CRUD, DRY...
Forget tech, Domain drives your design, domain comes first. Use your language (here PHP) to model the domain in the simplest possible way. Like a business expert will try to teach a toddler about a process, the devs are trying to make a machine process it while keeping the meaning explicit.

If you are using Entities with setter & getter, some Value Object in the /ValueObject/ namespace, you are missing the essence of DDD.

Again, DDD is not about respecting a specific way to implement stuff. DDD is about sharing the language between the code and the business domain.

Another value of DDD: the code has value at runtime AND at rest. The code is self-documenting a business process, acting as knowledge center for any human interacting with it.

DDD and Symfony

I started to read & implement DDD around 2014.
It took me a full year reading & implementing before I start to feel comfortable with my tooling (PHP/Symfony/Doctrine) and DDD. As Evans said:

"Don't fight your framework".

-- Domain-driven Design: Tackling Complexity in the Heart of Software

What is my "pragmatic" implementation of DDD with Symfony?
It really depends of the project & the complexity. But when I'm bootstrapping a new project or coaching a new dev, I usually rely on these simple guidelines:

  • I heavily rely on the command pattern, Symfony Messenger is a bliss

  • I do not use CQRS

  • I do not use ES

  • I use Behat & BDD at the Command/Handler level, full coverage.

  • I rarely use unit testing in project, 20-30% code coverage usually.

Why no CQRS & ES?

CQRS & ES are costly. They are extremely powerful solutions but you should use them only when it makes sense cost-wise & business-wise.

ES

Not so many php devs are used to Event Sourcing, and you won't find a lot able to design or use an Event Store properly. If you do not have a good model maturity remember that your events store directly reflects the history of your mistake, which might drastically increase the maintainability cost.

CQRS

CQRS truly shines at scale. This usually brings eventual consistency specificity between the write & read models. Do you really need this complexity and cost?

I implemented both patterns in different projects with PHP/Symfony, sometimes Doctrine, SQL & MongoDB

src structure

  • src/Controller/: Symfony controller
  • src/Command/: Symfony CLI commands

  • src/Action/: Command + CommandHandler

  • src/Domain/: AR, Entity, VO, Repository Interface

  • src/*: Infrastructure, other stuff.

Having something called /Domain/ sucks. But it also works.
Usually I replace "Domain" by the actual Domain we are tackling like /Ecommerce/.

Multiple domains issue

The problem starts when you have multiple domains. Opinions are quite strong about Core & supporting domain. Me? I don't care. I put everything in /Domain/ and then split if needed without qualifying as "Core" or "Supporting":

  • /Domain/Payment
  • /Domain/Compliance
  • /Domain/Delivery

It's simple & it's consistent.

What's in the "Domain“ ?

Inside the src/Domain/, I put every class that implement a business domain concern/concept. Some are persisted classes, others aren't, we also have the domain service there.

Since src/Entity doesn't exist anymore, Doctrine configuration is customised to scan src/Domain. Annotations are used to configure the persistence mapping, because "locality" is great.

Obviously when the project gets bigger, I do reorganize the /Domain/ in order to reflect the business domain:

/Domain/Legal/Company/France/RegistrationNumber/Siret.php
/Domain/Legal/Company/France/RegistrationNumber/Siren.php
/Domain/Legal/Company/France/RegistrationNumber/Nic.php
Enter fullscreen mode Exit fullscreen mode

Command to conquer

The good

I love the command pattern:

  • command handlers play well with hexagonal architecture

  • command handlers bring consistency to manipulate my model layer, they are the unique entrypoints.

  • command handlers bring consistency to the application service layer, as they are the application service

  • command handlers define the transactionnal boundaries of a business process/action

  • command handlers help to deal & design with Aggregate Root

The bad and the ugly

I do abuse command handlers in some circumstances:

  • When facing unknown specs, lack of understanding, lack of available knowledge, when I'm lazy or in a rush: I implement in the command handler code that would be moved later into domain services or domain objects.

  • When I need to synchronize several business processes together, I use handler instead of the SAGA hell

  • When an AR implementation is really tricky or costly with Doctrine/Symfony. I do leak the implementation in the handler (it's bad, don't do this at home)

It sounds probably terrible for purist. I'm really sorry and really okay with that as it worked for me.

Naming the command

Commands are named against the process, the business action or behavior they
implement. I do adapt to reflect the specific business needs:

src/Action/Checkout.php
src/Action/CheckoutHandler.php

src/Action/FraudulentPaymentDeclaration.php
src/Action/FraudulentPaymentDeclarationHandler.php

src/Action/Compliance/Registration.php
src/Action/Compliance/RegistrationHandler.php
src/Action/Compliance/FrenchEstablishmentRegistration.php
src/Action/Compliance/FrenchEstablishmentRegistrationHandler.php
Enter fullscreen mode Exit fullscreen mode

Maybe I would have used an imperative form for the Command: Register instead of Registration

-- Florian Klein

Business process, domain process or domain action are usually called by a unique name. It is important to tak the time to identify exactly the noun used to qualify this process. It's not always possible.

A verb might be a symptom of a weak domain understanding, example:

  • MarkAsRead
  • ValidateStatus

It's an advice, a best effort. In reality, in my repo I do have a lot of command phrased with a verb.

Implementation details

Commands are expression of (domain) intents. They are designed as simple DTO accepting only primitives

easing hexagonal architecture & [port/adapter](port/adapter implementation. Commands are the boundary between primitives and domain concepts in my application:

namespace Action\Compliance;

use Domain\Geo\Address;
use Domain\Geo\Country;

class Registration
{
  private string $userId;
  private string $name;
  private string $addressLine1;
  private string $addressLine2;
  private string $addressZip;
  private string $addressLocality;
  private string $addressCountry;

  public function __construct(
    string $userId,
    string $name, 
    string $addressLine1, 
    string $addressLine2,
    string $addressZip,
    string $addressLocality,
    string $addressCountry
  ) {
    $this->userId = $userId
    // ... boring stuff
  }

  public function getUserId(): Uuid
  {
    return new Uuid($this->userId);
  }

  public function getName(): string
  {
    return $this->name;
  }

  public function getAddress(): Address
  {
    return new Address(
      $this->addressLine1,
      $this->addressLine2,
      $this->addressZip,
      $this->addressLocality,
      new Country($this->addressCountry)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

I do add Symfony validation with annotation to my command in order to pre-validate and dispatch to the user any input errors. Yet when it comes to enforce business rules: my domain concept (mostly Value Object) carry the implementation and any violation raise exception.

Implementing commands this way, ease hexagonal:

  • commands can be used as a Symfony/Form model you know: the "data-class".

  • commands can be composed from arguments of a Symfony/Console CLI command.

  • commands can be composed by a RabbitMQ worker/consumer from a unserialized message

  • command can be composed by an event processed from a Symfony/EvenetListener

  • from web socket, etc...

Once my command is passed to its handler, we enter the domain kingdom. Only dmain object are used over there (VO, Entity). A domain object provides strong guarantees:

  • its state is consistent, always valid (according to the business rules)

  • its state is mostly immutable

  • its public API allows only expected states change, following the domain specification

Using these domain object is a delight and a relief: the cognitive load is really low and the confidence in the system is very high.

Few tips:

  • I do not use typed ID for each AR/Entity (UserId, BookId). It's a pain to maintain with Doctrine.

  • When to use a VO (domain object) over a primitive? If the concept is important/relevant for the domain, if the concept follows rules from the domain: it's a VO. Otherwise, primitives are good enough. We're still in PHP, not in Java.

Command handlers ?

Command handlers coordinate the domain concept altogether (VO, Entity, AR).

It usually relies on a repository to fetch the concerned AR.

The handler then calls methods on the domain object (AR, Entity). Since Doctrine is following data-mapping, the handler manage the persistence cycle. Somtimes it coordinate other infrastructure details, usually anything related to IO required to compose a correct state in order to perform a business action.

Symfony Messenger does the heavy lifting. You just need to dispatch the command into the bus. from your CLI-command, HTTP controller, RabbitMQ consumer and voila, your handler will be invoked with the command.

Since I do not use CQRS, most of my commands are synchronous over HTTP. Using Symfony/Messenger to get the last result of a command execution, I just need to add some serialization to get a REST representation for the HTTP response.

Bounded Contexts (BC)

I consider one git repo as one bounded context. My understanding might & will change later. I could split BC's later.

A lot of BC's might mean:

  • domain misunderstanding
  • unknown about the domain
  • high complexity

Organizing BC's

Microservice are a pain to orchestrate. If you've got a devops army good for you. If not, locality works again:

src/BC1/Controller/
src/BC1/Command/
src/BC1/Domain/

src/BC2/Controller/
src/BC2/Command/
src/BC2/Domain/
Enter fullscreen mode Exit fullscreen mode

Why? Because splitting BC is risky. Synchronising BC is costly. Your understanding will develop and will require a lot of refactoring & iteration: moving code from "BC's" and "domains" back and forth. Once you reach model maturity, split your repo, do your microservice show.

Conclusion

These are tricks or compromises that I found efficient to standardise a DDD approach with PHP, Symfony & Doctrine. These are not what you should focus on.

These are what you might wanna use to never have to think about the "how" and just focus about the "what" & "why".

Bonus track

Can I share/re-use concepts over BC ?

First: Never ever share Entities across BC.
Second: VO sharing is tough, especially with Doctrine, you would need to add your domain via composer/vendor and add mapping via yaml.
Last: Domain sharing is so-so. Like you can share a Geo domain (ISO Country, ISO Phone, ISO Currency)... but that would be very generic supporting domain.

Why "core" domain sharing is bad? Because from 2 different BC's if a domain is exactly seen & envisioned with the same properties, rules, shapes... Well you have only one BC. Careful, it's not because 2 BC shares domain names that they mean the same thing.

Example: For HR dept(BC) and Tech management(BC), "Employee" (Domain) means something very different even if they share some similitude.

BC collaboration, synchronization?

This is where things get hairy. First of all, BC should be isolated, you shouldn't have any direct coupling.

Data-wise, either you go for 2 storages or 2 schemas if you're in SQL.
if you are cheap and share the same schema... do not make relation between your BC's tables.

The command pattern is really helpful there. Let say I have a checkout process with a BC about payment and another BC about compliance.

Lazy: I can call my 2 commands PaymentCharge & PaymentFraudAnalyze at the same level (HTTP controller, CLI-command). Pro: It guarantee the loose coupling. Cons: your controller/cli-command carry some app/service/business sync.

Best: Go for event. Use the Symfony dispatcher, don't build your own stuff. Explicitly dispatch an event from your Command Handler. Create a specific EventListener in BC2. DON'T be lazy, do not go for "command sourcing pattern". Your BC1 is emitting an event ("Mom I'm Done"), your BC2 is listening, then transforming to its own command.

You can also add static analysis to enforce coupling rules (ensure BC aren't mixed). If you keep BC isolated (Data storage) & sync BC's with event, you are building scalable BC's respecting microservice requirements. Meaning, you could quickly move and split your BCs across repo and your infra.

Top comments (13)

Collapse
 
noverkill profile image
noverkill • Edited

Can you point me to one or more of your github repo(s) where I could see the above concepts implemented in a full / partial application(s)? A simple demo app would be very useful implemented particularly to showcase above concepts. I think share more actual code examples next to the concepts would have been also useful to clarify things. Without actual real life code examples I think this article is too theoretical and only mostly makes sense to people that already very familiar with the concepts you are explaining. One other thing I do not really get, is what happens to the Service layer in DDD, or that's what's replaced with commands?

Collapse
 
ludofleury profile image
Ludovic Fleury • Edited

thank you very much for your constructive feedback. You are totally right. I'll be trying to iterate on them, I don't know if I would edit or post a second one.

The service layer, It is common to discriminate: application service from domain service. It was confusing me when I started. I was used to SOA with a unique service layer.

Basically: Domain service are usually "pure service", like "pure function". Meaning they externalise complexe cross entity logic but they are usually stateless.

For instance: Shipping cost estimation working with an "order", a "shipping address" and a "shipping method". This could be a good domain service. The good part: domain service are easily unit testable. And we like to have domain logic covered by them.

Application (service) layer: these are the handlers in a command pattern approach, and it saved me from so much headaches trying to organise my code.

To be honest, I'm not even quite sure how I would manage a plain DDD project without the command pattern. I would have to implement application service layer to manipulate the persistence, name them (the hardest part) and their methods. Would probably be really messy.

Collapse
 
juanma1331 profile image
juanma1331

Hi im new to backend development and im trying to implement the concepts explained in the article. But how can i catch exceptions thrown by the handlers on my controllers?. I can get returned data from handlers using envelopes, so i was thinking about returning data and error from handlers. What do you think about this? Any suggestion? Plz could you provide with some basic implementation? any repo? Thanks

Collapse
 
ludofleury profile image
Ludovic Fleury

Hi, welcome to backend dev ! I'll hope you will have a rich journey there :)

What you are mentioning is actually a pattern: notification.
Martin Fowler wrote about it there: martinfowler.com/articles/replaceT...

When dealing with a sync command (no rabbitmq etc) I favor intercepting exception directly in the controller, I don't have access to code right now, but something like this, should work:



class MyController
{
  public function xxx() 
  {
    try {
      $this->bus->dispatch($command);
    } catch ( exception )
      // rethrow with appropriate HTTP error code
   }
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
juanma1331 profile image
juanma1331

Thanks for your help. ;)

Collapse
 
ferrius profile image
Serge

Hi, thank you for a nice post. Also you can find my implementation of DDD/CQRS there
github.com/ferrius/ddd-cqrs-example

Collapse
 
mohammedabdoh profile image
Mohammed Abdoh

I like your implementation. I have one concern to share with you. Don't you think that the UserInterface for instance is violating the idea of CQRS in common? Because in CQRS you are splitting your writes from your reads and in this case, your repository might only have to have one read method to load the aggregate by its ID while it case persist the aggregate

Collapse
 
ludofleury profile image
Ludovic Fleury

I checked, and it looks clean, here's a DDD / CQRS / ES with Symfony/Doctrine & SQL as event store

github.com/ludofleury/blackflag

Collapse
 
qdequippe profile image
Quentin Dequippe

For you, is stuff related to emails, translation, url generation has to be in Domain? If yes how to handle it?

Collapse
 
ludofleury profile image
Ludovic Fleury

Hi Quentin, as usual « it depends » :)
We always need the context to try to address it.

If you are into translation or email marketing business and e-mail is one of your product, well it’s probably in your core domain, following many rules, invariant and logic.

If you are into ad with url proxy or url shortener with stats or even SEO optimisation business, maybe url generation will be in your core domain.

If not, it might be considered infrastructure. What’s in the domain depends strongly on business requirement about the concept (email, url). Does it needs to « mutate » following an advanced state logic ? DDD might be worth the upfront cost and complexity.

Otherwise simple CRUD does the job.

Collapse
 
yagami271 profile image
Abdallah ismail

can we add setter on command ?

Collapse
 
ludofleury profile image
Ludovic Fleury • Edited

You can do whatever you want! its your implem :)

usually we do not use setter in the "domain" because we do not "set state", a specific process/feature change the state of something.

Yet in command, you can do whatever you want. Its a DTO: en.wikipedia.org/wiki/Data_transfe...

So its just structured data. In my implementation, I use command as a frontier/boundary between user input (weak) & model layer (strong).

So:

  • you always pass primitive to the command (creation).
  • you always get (value) object from the command (usage/execution).

Example: I don't give a DateTime object to my command, I give a date string "2020/01/01" and I get a DateTimeImmutable object from my command.

Why ? because, if you pass it from HTTP or CLI, the user input is a string.

Which brings 2 validations concerns: the input validation and model validation:

  1. I create the command with the date string
  2. I ensure the string looks like a valide date (input validation)
  3. I get the DateTimeImmutable object from my command in my handler (and validate that the date is logic with the requirement of my application)

OR

  1. I get a "Birthday" value object which encapsulate a DateTimeImmutable object (if needed) and in the constructor of this VO, I check that the person is not 350 years old, or at least 18 (according to the requirement of my domain)
Collapse
 
remytheroux profile image
Rémy THEROUX

Great and pragmatic implementation, that's what i like. The bonus track are precious advises, especially on micro service part.