Vladimir Hraban Vladimir Hraban - 3 months ago 22
PHP Question

DDD in SF3 folder structure

I am thinking through an ad website where Users can login, post new Listings and search existing ones. I am going to make this my first project following DDD principles completely. I have never done any DDD in Symfony before.

Below are my thoughts about this. Could you please tell me if this is correct and advice on better ways?

I can see two domains: User and Listing

Searching/displaying/posting functionality will live in Listing domain. Logging in/out live in a User domain.

SF3 directory example structure is

app/
ListingBundle/
src/
Listing.php
SearchService.php
ListingRepositoryInterface.php
Controller/
public/
ListingController.php
protected/
ListingController.php
Resource/
view/
public/
detail.twig.html
protected/
edit.twig.html

UserBundle/
src/
User.php
AuthService.php
UserRepositoryInterface.php
Controller/
public/
UserController.php
protected/
UserController.php
Resource/
view/
public/
login.twig.html
protected/
dashboard.twig.html

PersistenceBundle
src/
UserRepository.php
ListingRepository.php


My main questions are:


  • Is this structure correct?

  • Is it a good idea to have separate protected and public controllers with the same name?

  • Where do things go like a page on the User backend part of the website showing recent Listings posted by the User? Where is the boundary between those two domains?

  • Is PersistenceBundle a good idea or should I have persistence live inside User and Listing bundle?


Answer

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.php

94 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).

Comments