Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?
like image 327
Vladimir Hraban Avatar asked Aug 11 '16 15:08

Vladimir Hraban


1 Answers

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

like image 143
Thomas Ploch Avatar answered Sep 30 '22 14:09

Thomas Ploch