使用 Symfony 寻找或创建替代方案或最佳实践

问题描述 投票:0回答:1

我正在从想要存储在数据库中的玩家 API 获取数据。仅仅因为该玩家列表可能会更新,我使用一个命令将获取的数据转换为 PlayerModel 并将它们传递给 PlayerService,然后在检查玩家是否存在时,使用工厂创建一个新玩家或更新它。玩家实体与国家/地区实体具有多对一关系。有些国家可能不存在,所以我想创建它们。我在 PlayerFactory 中做到了这一点,我关心的是这是否是一个好的实践,如果不是,我很想知道如何改进这部分代码。

命令:

public function execute(InputInterface $input, OutputInterface $output): int
{
    $players = $this->client->getPlayers();

    $playerModels = [];
    foreach ($players as $player) {
        try {
            $playerModels[] = $this->playerTransformer->transform($player);
        } catch (MissingMandatoryParametersException $exception) {
        }
    }

    $this->playerImportService->importPlayers($playerModels);

    return Command::SUCCESS;
}

进口服务:

/**
 * @param PlayerModel[] $players
 */
public function importPlayers(array $players): void
{
    foreach ($players as $playerModel) {
        $player = $this->playerRepository->findOneBy([
            'code' => $playerModel->code,
        ]);

        if ($player === null) {
            $player = $this->playerFactory->createFromModel($playerModel);
        } else {
            $player = $this->playerFactory->updateFromModel($player, $playerModel);
        }

        $this->entityManager->persist($player);
    }

    $this->entityManager->flush();
}

玩家工厂:

public function createFromModel(PlayerModel $playerModel): Player
{
    $country = $this->countryRepository->findOneBy([
        'code' => $playerModel->getNationality()->getCode(),
    ]);

    if ($country === null) {
        $country = $this->countryFactory->createFromModel($playerModel->getNationality());
    }

    return (new Player())
        ->setCode($playerModel->getCode())
        ->setFirstName($playerModel->getFirstName())
        ->setLastName($playerModel->getLastName())
        ->setBirthdate($playerModel->getBirthDate())
        ->setNationality($country)
        ->setHeight($playerModel->getHeight())
        ->setWeight($playerModel->getWeight())
    ;
}
php symfony standards
1个回答
0
投票

其实,你建造的不是工厂,而是变压器;您正在将一种数据模型转换为另一种数据模型。工厂的目的是从一组继承公共接口或对象的类中创建一个对象。

例如

public function create(string $someParameter): PlayerInterface
{
    return match ($someParameter) {
        'a' => new PlayerA(),
        'b' => new PlayerB(),
        default => throw new \InvalidArgumentException(sprintf('%s not implemented', $someParameter)),
    };
}

您可以在这里找到此模式(以及其他模式)的指南:重构大师

但是,就您而言,我认为您可以通过几个步骤改进您的代码:

改进命名

例如,PlayerModel 不是一个好名字,因为它太通用了。相反,更喜欢更具体的术语,例如 PlayerDTO(DTO = 数据传输对象),这使得代码更容易理解。

处理并记录错误

确保您的程序始终捕获潜在错误并记录它们,以帮助调试,以防生产中出现阻塞错误。

避免贫血域模型

在我看来,为所有属性配备一个 setter 并不是什么大问题。更喜欢专门的方法。

让我们以实体 Player 为例。与其使用像

$player->setPoint($player->getPoint()++)
这样的setter,不如定义像
$player->wonPoint()
这样的方法。在您的上下文中,我们可以想象一个方法 updateFromDTO:
$player->updateFromDTO($dto);

另一个用例可能是类的实例化。您可以执行 Player::createFromDTO($dto) 或 Player::createTennisPlayer([required data])、Player::createPremiumPlayer([required data]) 等。这允许您根据您的环境创建不同类型的玩家。

来源:维基百科博客票

避免表示层逻辑

命令和控制器是表示层,换句话说,它是客户端和应用程序之间的桥梁,因此,为了从最大的可扩展性和可靠性中受益,表示层应该只处理客户端输入(请求控制器的主体和命令的参数)并将其传递到您的域层(您的业务逻辑)。

奖励:实现 ImportService 的策略设计模式

它将允许您根据所需的导入方法传递不同的策略,我们可以稍后根据文件或其他内容对导入进行映像,并且它尊重 SOLID 原则中的 OCP(开放关闭原则),开放扩展但关闭修改,另一方面,要更改 ImportService 的行为,您只需创建一个新策略。

以下是这些更改的一个小示例:

DTO:

class PlayerDTO {
    public function __construct(
        public readonly string $name,
        ...etc
    ) {}

    public static function fromApiRequest(array $data): self
    {
        return new self($data['name'], ...other data);
    }
}

命令:

public function execute(InputInterface $input, OutputInterface $output): int
{
    try {
        $this->playerImportService->importPlayers($this->apiImportStrategy);
    catch(ImportException $e) {
        $this->logger->error($e->getMessage(), $e->getTrace());

        return Command::FAIL;
    }

    return Command::SUCCESS;
}

进口服务:

/**
 * @throw ImportException Can be thrown by the strategy if an error occurs durring the api call for example
 */
public function importPlayers(PlayerImportStrategyInterface $playerImportStrategy): void
{
    $players = $playerImportStrategy->findPlayers();

    foreach ($players as $playerData) {
        /** @var PlayerDTO $normalizedPlayerData */
        $normalizedPlayerData = $playerImportStrategy->normalizeData($playerData);

        $country = $this->findOrCreateCountry();

        $player = $this->playerRepository->findOneBy([
            'code' => $normalizedPlayerData->code,
        ]);

        if ($player) {
            $player->updateFromDTO($normalizedPlayerData, $country); // Entity already tracked by doctrine does not need to be persisted when updates are made

            continue;
        }

        $player = Player::createFromDTO($normalizedPlayerData, $country);

        $this->entityManager->persist($player);
    }

    $this->entityManager->flush();
}

玩家:

public static function createFromDTO(PlayerDTO $playerDTO, Country $country): self
{
    return new self(
        code: $playerDTO->code,
        firstName: $playerDTO->firstName,
        lastName: $playerDTO->lastName,
        birthdate: $playerDTO->birthDate,
        nationality: $country,
        height: $playerDTO->height,
        weight: $playerDTO->weight
    );
}

API策略:

class ApiImportStrategy implements PlayerImportStrategyInterface
{
    public function __construct(
        private readonly SomeApiService $apiService
    ) {}

    public function findPlayers(): array
    {
        return $this->apiService->getPlayers();
    }

    public function normalizeData(array $playerData): PlayerDTO
    {
        return PlayerDTO::fromApiRequest($playerData);
    }
}

希望有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.