【问题标题】:DDD in SF3 folder structureSF3 文件夹结构中的 DDD
【发布时间】:2016-08-11 15:26:53
【问题描述】:

我正在考虑一个广告网站,用户可以在其中登录、发布新列表并搜索现有列表。我将把它作为我的第一个完全遵循 DDD 原则的项目。我以前从未在 Symfony 中做过任何 DDD。

以下是我对此的看法。您能否告诉我这是否正确并建议更好的方法?

我可以看到两个域:用户和列表

搜索/显示/发布功能将存在于列表域中。在用户域中实时登录/注销。

SF3目录示例结构是

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

我的主要问题是:

  • 这个结构正确吗?
  • 使用相同名称的不同受保护控制器和公共控制器是个好主意吗?
  • 在网站的用户后端部分显示用户最近发布的列表之类的页面在哪里?这两个领域的界限在哪里?
  • PersistenceBundle 是个好主意还是我应该在 User 和 Listing 捆绑包中拥有持久性?

【问题讨论】:

  • 纯领域驱动设计专注于复杂的领域逻辑。可能对布局您的域业务逻辑可能首先看起来很有用。控制器和视图应该只是次要的实现细节。

标签: php domain-driven-design symfony


【解决方案1】:

框架和 DDD

您在这里做了一个错误的假设,即“我将使用 Symfony 框架以 DDD 式的方式实现我的应用程序”

不要这样做,Frameworks are just an implementation detail 并为您的应用程序提供一种(或多种)交付方式。我指的是Hexagonal Architecture上下文中的应用程序。

如果您从我们的一个上下文中查看以下示例,您会发现我们的ApiClient 上下文包含三层(顶级目录结构)。 应用程序(包含用例服务)、(包含模型和行为)和基础设施(包含基础设施问题,例如持久性和交付)。我在这里专注于 Symfony 集成和持久性,因为这是 OP 最初的问题所在:

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

相当多的文件!

您可以看到我将捆绑包用作应用程序的端口(虽然命名有点,但它不应该是Http交付,因为在严格意义上Hexagonal Architecture 它是一个App-To-App Port)。我强烈建议您阅读DDD in PHP book,其中所有这些概念实际上都通过 PHP 中的富有表现力的示例进行了解释(假设您已经阅读了蓝皮书和红皮书,尽管这本书可以作为独立的书,但仍然可以作为参考)。

【讨论】:

  • 非常感谢您的回答!我喜欢拥有一个端口的想法,从而将应用程序与框架完全分离。我不知道身份验证功能的去向 - 它属于核心域还是应用程序域?
【解决方案2】:

使用 Symfony 构建的 DDD 应用程序的文件夹结构

我第二个 tPl0ch 的回答,但想提出一个文件夹结构的轻微变体,这在我参与的 Symfony 的几个项目中很有用。对于您的特定域,文件夹结构可能如下所示:

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

使用此文件夹结构,您可以分隔onion architecture 的不同层。不同的文件夹清楚地传达了层之间的边界和允许的依赖关系。我在DDD folder structures with Symfony 上写了一篇博文,详细描述了这种方法。


其他资源:

除此之外,我还建议查看以下资源:

  • PHP DDD Cargo Sample:Eric Evans DDD 书中使用的货物样本的 PHP 7 版本
  • Sylius:电子商务 PHP 框架建立在 Symfony 之上,具有基于组件的架构

我从了解 Sylius 代码库中学到了很多东西——它是一个真实世界的项目,而且相当庞大。他们进行了各种测试,并为交付高质量的代码付出了很多努力。

【讨论】:

  • 有了这样的结构,你会把共享服务放在哪里,比如哈希处理程序,或者 JSON 编码器?
猜你喜欢
  • 2012-06-16
  • 1970-01-01
  • 2012-11-17
  • 1970-01-01
  • 1970-01-01
  • 2014-01-20
  • 1970-01-01
  • 2019-11-23
  • 2020-10-08
相关资源
最近更新 更多