梦游者/symfony-micro-service

一个为微服务设计的预配置的 Symfony 项目,包含 docker 文件


README

这是一个骨架项目,预配置了 Symfony 6+ 项目以用作微服务。该项目旨在与以下内容结合使用: 数据服务

设置包括

  • doctrine
  • doctrine-fixtures
  • doctrine-migrations
  • messenger
  • profiler
  • command/query/domain 事件总线
  • 测试助手
  • app 和 redis 容器的 docker 配置
  • docker app 容器配置没有本地挂载
  • bin/ 目录中的 shell 脚本调用 docker 中的库
  • PHP 容器使用 php-pm 作为应用服务器
  • Mutagen 通过 SyncIt 使用默认配置

包括服务设置不同部分的杂项 README 文件

注意:数据服务部分已移至单独的项目以保持微服务范围窄。请参阅 数据服务 了解基本文件。

如果您正在使用微服务,请务必查看: 项目管理器,这是一个 CLI 工具包,使与多个服务一起工作变得更容易。

入门

使用 composer 创建新项目

composer create-project somnambulist/symfony-micro-service <folder> --no-scripts

根据需要自定义基本文件;更改名称(尤其是服务名称)、配置值等。然后: docker-compose up -d 在开发模式下启动 docker 环境。务必阅读 服务发现 以了解 docker 环境的一些设置。

注意:要使用最新版本,在创建项目时将 dev-master 作为最后一个参数。这将检出并使用当前的 master 版本,而不是带标签的版本。

或者,如果使用 Project Manager 与默认模板: spm new:service <service_name> api 或不使用服务名称/模板以使用向导。

推荐的第一步

该项目在所有地方使用 Appexample.dev。您的第一步将是更改基础 PHP 命名空间(如果需要)。对于此操作,非常推荐使用 PhpStorm 的重构/重命名。

域名在几个地方设置,强烈建议更改为更有用的名称。以下文件应更新

  • .env
  • docker-compose*.yml

您应该确保阅读 编译容器

配置的服务

以下 docker 服务预先配置用于开发

  • Redis
  • 运行 php-pm 的 PHP 8.3

测试配置包括所有服务以成功运行测试。

发布/生产仅定义应用,因为它旨在部署到集群。

Docker 服务名称

容器的名称将以前缀形式显示,前缀由 .env 文件中定义的项目名称确定。这是常量 COMPOSE_PROJECT_NAME。如果您删除它,将使用当前文件夹名称。例如:您创建了一个名为 "invoice-service" 的新项目,未设置 COMPOSE 常量,通过 docker-compose 启动的容器将使用 invoice-service_ 前缀。如果您有很多 Docker 项目,它们可能具有相似的文件夹名称,因此使用此常量可以避免冲突。

需要设置的第二个常量是 APP_SERVICE_APP。这是 PHP 应用程序容器的名称。默认情况下,这是 app。强烈建议将其更改为更独特的东西。如果您更改了它,请确保更改 docker-compose*.yml 文件中的容器名称,否则它将不会被使用。此名称由 SyncIt 用于解析应用程序容器,以及由 bin/dc-* 脚本使用。

域名解析

域名和代理已移动到 数据服务

建议的实施方法

注释

  • Twig 在 devtestdocker 环境中启用,在 prod 中禁用
  • Docker ppm 容器需要带有最新构建的 SF 5 ppm 版本

域名

域名代表解决业务问题的方案。它包含实现和解决该问题的所有必要代码;不依赖于第三方或框架代码。它应该是(理想情况下)框架无关的,并且能够转移到其他框架,如果它们证明更适合,则只需对核心领域类进行最小的修改。例如,您不应与框架验证器或服务容器耦合,并避免注入实现,而是使用接口。

领域通常在项目设置期间通过与主要利益相关者和领域专家(真正了解和理解业务运营的人)的讨论来发现。然后使用这些信息来创建软件解决方案。最重要的方面是发现的语言,它允许所有人员有效地沟通并理解特定术语的含义。这种语言不是一成不变的,随着时间的推移而变化,因为知识得到或流程得到改进。保持这些变化是最新的很重要,这包括代码本身。

此项目建议并为领域以下文件夹布局

  • 命令
  • 事件
  • 模型
  • 查询
  • 服务

这些都是建议,您可以根据需要对其进行更改。

模型

此项目围绕领域驱动设计方法进行,Doctrine 为主要领域对象提供持久性。这些模型位于:src/Domain/Models。所有领域模型都应位于此处,包括枚举、值对象和其他以数据为中心的模型。与标准 Symfony 项目不同,模型不应包含 Doctrine 映射注解。将这些添加到 config/mappings 文件夹中的单独文件夹中(默认为 models)。

您的模型应关注领域的“状态”以及应如何应用各种操作。这意味着强制有效的状态更改,即:您不需要获取器和设置器。实际上,您应该避免添加这些,因为模型的角色是管理状态,而不是提供查询该状态的 API。本质上,您的模型代表写操作。在许多情况下,这些将使用值对象和可枚举对象以确保始终向领域传递有效数据。当使用简单标量时,应启用严格的类型,并使用所有标量类型提示。

在您的领域模型中,将有一些是关键的并且可以外部访问的。这些很可能是您的聚合根。每个聚合根应该在每次关键状态转换后触发适当的领域事件。一个教义监听器已预先启用,用于监听并将领域事件传播到预先配置的RabbitMQ扇出交换机。聚合根的例子可能包括用户、账户、订单等,但这将取决于您的领域。

一般来说,您的领域模型将遵循业务概念并使用业务熟悉的术语。例如:如果您为销售团队创建一个服务,他们使用“潜在客户”,那么您的领域应该有一个“潜在客户”模型,并应该具有他们认为重要的任何属性。销售团队应该能够查看代码,并至少理解它所表达的名字和概念。

服务

服务应包含与领域交互或为代码领域模型提供额外支持的类,例如:转换或数据类型/格式的翻译。仓库是领域服务的一部分。但关键思想是,领域服务不依赖于框架代码。它们是独立的,封装的——就像模型一样。

例如,货币转换器可以是领域服务;或者一个验证器,它根据领域规则(而不是框架规则)检查一个对象是否可以被另一个对象访问。

仓库

每个聚合根都应该为它定义一个仓库服务。这应该是一个接口,然后接收持久化实现。接口应该尽可能简单,通常

  • find(Uuid $id): Object
  • store(Object $object): bool
  • destroy(Object $object): bool

该接口应该针对特定的对象类型进行编码。在底层,这可能使用Doctrine ObjectManager来持久化和删除对象。

请注意,不需要调用 ->flush(),因为应该使用包含DB事务包装的命令总线。

命令查询职责分离

命令

“命令”是请求对系统进行更改的请求;例如:“创建用户”或“激活某物”。命令通过不返回任何输出的命令总线进行分发。命令应该完全封装,包含执行该请求所需的所有必要数据。这包括任何在之前生成的id,即:使用此系统,您不应该依赖于数据库自动增长或序列(在这种情况下,这些是用于简化数据库建模的代理身份)。相反,您应该只暴露主要对象的UUID,并且仅在必要时暴露内部ID或使用聚合ID生成策略,例如计数器,随着记录的增加而连续增加。

当命令分发时,命令总线处理它以及可能发生的任何错误。这些将作为异常抛出,由自定义JSON异常订阅者收集并转换为API错误消息。这可以通过添加适当的错误处理来覆盖。

命令总线使用以下中间件

  • 验证
  • doctrine_transaction

可以在 config/packages/messenger.yaml 文件中配置其他中间件。

命令只能由一个处理器处理;但处理器可以根据需要抛出更多命令以进行分发。然而:在这种情况下,最好为领域事件编写事件监听器并响应该事件,因为领域事件是在所有Doctrine操作刷新到数据存储之后广播的。

命令处理器可以通过调用领域模型进行必要的更改。这包括创建新对象、加载现有对象、与仓库或其他服务交互。

通常,您的命令将对应于企业实际执行的操作,并且应按此类命名。

查询

查询是从系统中请求信息。查询可能为“通过ID找到X”或“找到所有符合以下标准的商品...”。然后QueryBus执行查询命令并返回结果。查询封装了所有请求的数据,不应包含原始请求对象。使用值对象和基本类型是安全的。已包含几个抽象查询命令,用于基本操作(由somnambulist/domain提供)。

查询命令是不可变的,不应更改;唯一的让步是如果使用includes支持来加载子对象,此时会添加一个with()方法。

查询命令由QueryHandler处理,该处理程序将命令作为魔法__invoke方法的参数接受。如何处理查询完全取决于实现者。可能是纯SQL、API调用、DQL、解析某些文件、返回硬编码的响应等。

例如,一个查询命令可能看起来像

<?php

use Somnambulist\Components\Queries\AbstractQuery;

class FindObjectById extends AbstractQuery
{
    private $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function getId()
    {
        return $this->id;
    }
}

这将被一个具有以下签名的QueryHandler执行

<?php

class FindObjectByIdQueryHandler
{
    
    public function __invoke(FindObjectById $query)
    {
        // do some operations to find the thing
        return $object;
    }
}

使用QueryBus允许在任何时候通过用另一个实现替换查询处理程序来更改查询处理。例如:我们从一个大型服务开始,该服务需要拆分,拆分部分的查询不需要更改,只需更新处理程序以进行API调用即可,并且仍然可以返回之前的相同对象。无需更改控制器。

这种方法的缺点是文件很多;然而,每个文件都可以独立进行测试。

交付

Delivery文件夹用于将生成系统响应的任何输出机制。这是任何API或Web控制器所在的地方,控制台命令等。ViewModel将位于系统的这一部分。

每个主要输出类型应保留在其自己的命名空间中,以避免污染,例如,API响应不要与Web响应混淆。

默认情况下,提供了ApiConsole,并且已在services.yaml文件中映射为服务。

API从一开始就打算完全版本化,以确保向后兼容性。版本化应在控制器、表单和转换器级别进行。每个版本应有自己的控制器、表单请求和转换器。如果某个版本未更改特定输出,则在需要的情况下可以重用先前版本。

FormRequests是Laravel中的一个概念,您可以通过类型提示验证请求对象,以确保请求包含规则中定义的数据。它为Symfony Form库提供了一种更干净的设置,该库处理起来可能相当复杂。使用此库是可选的。有关更多信息,请参阅Form Request Bundle

对于控制器,最好围绕聚合根进行分组,例如,有一个用户聚合,因此在src/Delivery/Api/V1文件夹中会有一个Users文件夹。在此文件夹内,您可以按照FormsTransformers文件夹或包含特定的ViewModels进行组织。

对于控制器,最好遵循每个动作一个控制器的方法,例如:而不是一个包含创建、更新、删除、查看、列表等方法的控制器;这些现在是独立的控制器:CreateControllerListControllerViewController等。命名策略由您决定。它们也可以被命名为:DisplayUserAsJson而不是ViewController等。无论使用哪种命名策略,都应该保持一致。

为了帮助处理控制器的一些典型请求/响应周期,包含了一个辅助库(somnambulist/api-bundle)。该库通过类似于DingoAPI的系统集成了Fractal响应转换器。当与命令和查询总线一起使用时,这可以使控制器非常薄和轻量;将大部分业务逻辑保留在命令和查询处理器中。

视图模型,又称演示者

为了查询系统,例如获取API响应,创建一个视图模型而不是使用主要领域模型。这允许使用自定义表示,包括演示逻辑,而不会在领域模型中填充演示逻辑。包含了一个包:somnambulist/read-models,通过活动记录方法提供此功能,然而也可以使用纯SQL / PDO。

有关使用库的更多详细信息,请参阅read-models文档。

数据库迁移

数据库迁移由Doctrine迁移处理。在编写迁移时,强烈建议您不要使用实体管理器来持久化记录。如果您需要更改模式或结构,这可能会使管理旧迁移变得非常困难。

然而:如果您确实需要实体管理器,您必须在doctrine_migrations.yaml文件中添加一个工厂覆盖,并具有以下内容

<?php declare(strict_types=1);

namespace App\Resources\Factories;

use Doctrine\Migrations\AbstractMigration;
use Doctrine\Migrations\Version\DbalMigrationFactory;
use Doctrine\Migrations\Version\MigrationFactory;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class MigrationFactoryDecorator
 *
 * From: https://symfony.com.cn/doc/master/bundles/DoctrineMigrationsBundle/index.html#migration-dependencies
 *
 * @package    App\Resources\Factories
 * @subpackage App\Resources\Factories\MigrationFactoryDecorator
 */
class MigrationFactoryDecorator implements MigrationFactory
{
    private MigrationFactory $factory;
    private ContainerInterface $container;

    public function __construct(DbalMigrationFactory $migrationFactory, ContainerInterface $container)
    {
        $this->factory   = $migrationFactory;
        $this->container = $container;
    }

    public function createVersion(string $migrationClassName): AbstractMigration
    {
        $instance = $this->factory->createVersion($migrationClassName);

        if ($instance instanceof ContainerAwareInterface) {
            $instance->setContainer($this->container);
        }

        return $instance;
    }
}

这同样适用于您希望将其注入到迁移中的任何其他服务。

贡献

欢迎贡献!发现错误、想要额外的文档或更好的解释?那么在项目上创建一个工单,或者打开一个PR。