DDD in SF3 folder structure DDD in SF3 folder structure php php

DDD in SF3 folder structure


Frameworks & DDD

You make a wrong assumption here, which is "I am going to use the Symfony framework to implement my app in a DDD-ish way".

Don't do this, Frameworks are just an implementation detail and provide one (ore more) delivery methods for your Application. And I mean Application here in the context of the Hexagonal Architecture.

If you look at the following example from one of our contexts you see that our ApiClient context contains three layers (top-level directory structure). Application (contains the use case services), Domain (contains the models and behaviour) and Infrastructure (contains infrastructure concerns like persistence and delivery). I focused on the Symfony integration and the persistence here, since this is what the OP's original question was about:

src/ApiClient├── Application│   ├── ApiClient│   │   ├── CreateApiClient│   │   ├── DisableApiClient│   │   ├── EnableApiClient│   │   ├── GetApiClient│   │   ├── ListApiClient│   │   ├── RemoveApiClient│   │   └── ChangeApiClientDetails│   ├── ClientIpAddress│   │   ├── BlackListClientIpAddress│   │   ├── CreateClientIpAddress│   │   ├── ListByApiClientId│   │   ├── ListClientIpAddresses│   │   └── WhiteListClientIpAddress│   └── InternalContactPerson│       ├── CreateInternalContactPerson│       ├── GetInternalContactPerson│       ├── GetByApiClientId│       ├── ListContacts│       ├── ReassignApiClient│       └── Remove├── Domain│   └── Model│       ├── ApiClient│       ├── ClientIpAddress│       └── InternalContactPerson└── Infrastructure    ├── Delivery    │   └── Http    │       └── SymfonyBundle    │           ├── Controller    │           │   ├── ApiClientController.php    │           │   ├── InternalContactController.php    │           │   └── IpAddressController.php    │           ├── DependencyInjection    │           │   ├── Compiler    │           │   │   ├── EntityManagerPass.php    │           │   │   └── RouterPass.php    │           │   ├── Configuration.php    │           │   ├── MetadataLoader    │           │   │   ├── Adapter    │           │   │   │   ├── HateoasSerializerAdapter.php    │           │   │   │   └── JMSSerializerBuilderAdapter.php    │           │   │   ├── Exception    │           │   │   │   ├── AmbiguousNamespacePathException.php    │           │   │   │   ├── EmptyMetadataDirectoryException.php    │           │   │   │   ├── FileException.php    │           │   │   │   ├── MalformedNamespaceException.php    │           │   │   │   └── MetadataLoadException.php    │           │   │   ├── FileMetadataLoader.php    │           │   │   ├── MetadataAware.php    │           │   │   └── MetadataLoaderInterface.php    │           │   └── MFBApiClientExtension.php    │           ├── DTO    │           │   └── ApiClient    │           │       └── ChangeInternalContact    │           │           ├── ChangeInternalContactRequest.php    │           │           └── ChangeInternalContactResponse.php    │           ├── MFBApiClientBundle.php    │           ├── Resources    │           │   ├── config    │           │   │   ├── domain_services.yml    │           │   │   ├── metadata_loader.yml    │           │   │   ├── routing.yml    │           │   │   └── services.yml    │           │   ├── hateoas    │           │   │   └── ApiClient    │           │   │       ├── Application    │           │   │       │   ├── ApiClient    │           │   │       │   │   ├── CreateApiClient    │           │   │       │   │   │   └── CreateApiClientResponse.yml    │           │   │       │   │   └── ListApiClient    │           │   │       │   │       └── ListApiClientResponse.yml    │           │   │       │   ├── ClientIpAddress    │           │   │       │   │   ├── CreateClientIpAddress    │           │   │       │   │   │   └── CreateClientIpAddressResponse.yml    │           │   │       │   │   ├── ListByApiClientId    │           │   │       │   │   │   └── ListByApiClientIdResponse.yml    │           │   │       │   │   └── ListClientIpAddresses    │           │   │       │   │       └── ListClientIpAddressesResponse.yml    │           │   │       │   └── InternalContactPerson    │           │   │       │       ├── Create    │           │   │       │       │   └── CreateResponse.yml    │           │   │       │       └── List    │           │   │       │           └── ListResponse.yml    │           │   │       └── Domain    │           │   │           ├── ApiClient    │           │   │           │   └── ApiClient.yml    │           │   │           ├── ClientIpAddress    │           │   │           │   └── ClientIpAddress.yml    │           │   │           └── InternalContactPerson    │           │   │               └── InternalContactPerson.yml    │           │   └── serializer    │           │       ├── ApiClient    │           │       │   ├── Application    │           │       │   │   ├── ApiClient    │           │       │   │   │   ├── CreateApiClient    │           │       │   │   │   │   ├── ContactPersonRequest.yml    │           │       │   │   │   │   ├── CreateApiClientRequest.yml    │           │       │   │   │   │   └── CreateApiClientResponse.yml    │           │       │   │   │   └── GetApiClient    │           │       │   │   │       └── GetApiClientResponse.yml    │           │       │   │   ├── ClientIpAddress    │           │       │   │   │   └── CreateClientIpAddress    │           │       │   │   │       ├── CreateClientIpAddressRequest.yml    │           │       │   │   │       └── CreateClientIpAddressResponse.yml    │           │       │   │   └── InternalContactPerson    │           │       │   │       ├── Create    │           │       │   │       │   ├── CreateRequest.yml    │           │       │   │       │   └── CreateResponse.yml    │           │       │   │       ├── Get    │           │       │   │       │   └── GetResponse.yml    │           │       │   │       ├── List    │           │       │   │       │   └── ListResponse.yml    │           │       │   │       └── ReassignApiClient    │           │       │   │           └── ReassignApiClientRequest.yml    │           │       │   └── Domain    │           │       │       ├── ApiClient    │           │       │       │   ├── ApiClient.yml    │           │       │       │   └── ContactPerson.yml    │           │       │       ├── ClientIpAddress    │           │       │       │   └── ClientIpAddress.yml    │           │       │       └── InternalContactPerson    │           │       │           └── InternalContactPerson.yml    │           │       └── Bundle    │           │           └── DTO    │           │               └── ApiClient    │           │                   └── ChangeInternalContact    │           │                       └── ChangeInternalContactRequest.yml    │           └── Service    │               └── Hateoas    │                   └── UrlGenerator.php    └── Persistence        ├── Doctrine        │   ├── ApiClient        │   │   ├── ApiClientRepository.php        │   │   └── mapping        │   │       ├── ApiClientId.orm.yml        │   │       ├── ApiClient.orm.yml        │   │       ├── CompanyName.orm.yml        │   │       ├── ContactEmail.orm.yml        │   │       ├── ContactList.orm.yml        │   │       ├── ContactName.orm.yml        │   │       ├── ContactPerson.orm.yml        │   │       ├── ContactPhone.orm.yml        │   │       └── ContractReference.orm.yml        │   ├── ClientIpAddress        │   │   ├── ClientIpAddressRepository.php        │   │   └── mapping        │   │       ├── ClientIpAddressId.orm.yml        │   │       ├── ClientIpAddress.orm.yml        │   │       └── IpAddress.orm.yml        │   └── InternalContactPerson        │       ├── InternalContactPersonRepository.php        │       └── mapping        │           ├── InternalContactPersonId.orm.yml        │           └── InternalContactPerson.orm.yml        └── InMemory            ├── ApiClient            │   └── ApiClientRepository.php            ├── ClientIpAddress            │   └── ClientIpAddressRepository.php            └── InternalContactPerson                └── InternalContactPersonRepository.php94 directories, 145 files

Quite a lot of files!

You can see that I am using the bundle as a Port of the Application (the naming is a little bit of though, it should not be Http delivery, since in the strict sense of the Hexagonal Architecture it is an App-To-App Port). I strongly advise you to read the DDD in PHP book where all these concepts are actually explained with expressive examples in PHP (assuming you have read the blue book and the red book already, although this book works as a standalone while still making references).


A folder structure for DDD applications built with Symfony

I second tPl0ch's answer, but would like to propose a slight variant of the folder structure which has been useful in a couple of projects with Symfony where I was involved in. For your specific domain the folder structure could look as follows:

app    Listing        Domain            Model                Listing.php            Repository                ListingRepository.php            Service                SearchService.php        Infrastructure            Repository                DoctrineListingRepository.php   // or some other implementation            Resources                // symfony & doctrine config etc.            Service                ElasticSearchService.php        // or some other implementation            ListingInfrastructureBundle.php        Presentation            Controller                ViewListingController.php       // assuming this is the "public" part                EditListingController.php       // assuming this is the "protected" part            Forms                ListingForm.php            Resources                // symfony config & views etc.            ListingPresentationBundle.php    User        // ...        Infrastructure            Service                AuthService.php        // ...

With this folder structure, you separate the different layers of the onion architecture. The different folders clearly communicate the boundaries and allowed dependencies between the layers. I have written a blog post on DDD folder structures with Symfony which describes the approach in detail.


Additional Resources:

Apart from that, I also recommend to have a look at the following resources:

  • PHP DDD Cargo Sample: PHP 7 Version of the cargo sample used in Eric Evans DDD book
  • Sylius: eCommerce PHP framework built on top of Symfony with component-based architecture

I have learned a lot from understanding the Sylius code base - it is a real world project and is fairly huge. They have all kinds of tests and have put a lot of effort into shipping high quality code.