ドメインを犠牲にすることなくsymfonyフォームを使用する


TLドクター

  • はリッチドメインモデルを使用します:実体はすべての時間
  • で有効な状態になければなりません
  • DATAを使用して、ドメイン
  • からインフラストラクチャ層を分離します
  • 完全にタイプされた善良さ
  • 利益!

  • フーズローバー!
    オクラシウス

    EY Twitterverse、私はDTOSで定義されたAPIを得ました.`` mycommand ``は`` data ''を潜在的に' null ' (または無効)にして構築し、symfony/validatorに渡します.より良いデザイン?
    午後16時38分- 2019年11月20日


    アレックスロック
    ピエロヴァル

    午後12時34分- 2020年5月18日
    あなたのモデルとsymfony形1に対処する方法について、議論はTwitterでポップアップします、後者はあなたのモデルに無効な州を受け入れる必要がある方法で設計しました.このポストでは、コマンドパターンを使用してどのように対処するかを説明します.

    In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time.2


    交響道


    Symfonyの一般的なベストプラクティスは、フォームに直接フォームをマップし、フォームが送信された後にバリデータ3を使用して後者を検証することです.これは、それが検証される前にエンティティの状態を変更し、エンティティが無効な状態を含めることができなければならないことを意味します.フォームからエンティティにデータをマップするには、プロパティアクセス4コンポーネントを使用します.このコンポーネントは、エンティティの状態を変更するには、Getters/settersかpublicプロパティのどちらかに依存します.これらのことの全ては、ドメインロジックと/または他のビジネスルールがどこにでも住んでいる貧血ドメインモデルにつながる.

    逆制御


    我々の貧血ドメインモデルを豊かなものに変えるために、我々はドメイン論理を実体に動かすことによって始めなければなりません.短い答えは:すべてのセッターを削除し、エンティティ自体が独自の状態を担当することを確認する必要があります.これは、フォームを直接エンティティにマップできません.また、我々が我々が我々の実体としてしたのと同じ問題に走らせるためにフォームを写像することになっても、何もしません?

    コマンド紹介


    エンティティをエンティティに直接マッピングする代わりに、代わりにコマンドにマップする必要があります.コマンドは、実行される必要があるすべてを含んでいるDTOの形でメッセージであり、インフラストラクチャ層とドメインの間のギャップを埋めるために使用することができます.実行は、複数の方法で行うことができますが、私はコマンドまたはメッセージバスを使用すると仮定されます.
    実行がスムーズに進むためには、コマンドが有効かどうかを確認する必要があります.これは、我々が我々の実体と同じような問題に遭遇するところです.フォームとデータマッパーに直接適用されるバリデータの制約を使用すると、必要な動作にsymfonyを騙すことができます.フォームが検証される前にまだデータをマップしようとしますが、現在はこのデータをマップする方法を制御しています.これは、データが無効なときにマップしないことを意味します.マッピング手順の後、フォームの妥当性検査がキックインされます.マッパーが適切な結果をもたらさなかった場合、バリデータは直接フォームを検証します.フォームがコマンドのすべての要件をカバーするのに十分な制約がある限り、あなたは有効なコマンドか無効なフォームで終わります.
    Visual Studioの場合は、次のコード例を示します
    <?php declare(strict_types=1);
    
    namespace App;
    
    use Webmozart\Assert\Assert;
    
    final class YourCommand
    {
        private string $foo;
    
        public function __construct(string $foo)
        {
            // Any assertions needed to ensure a valid command.
            Assert::minLength($foo, 3);
            Assert::maxLength($foo, 50);
            $this->foo = $foo;
        }
    
        public function getFoo(): string
        {
            return $this->foo;
        }
    }
    
    <?php declare(strict_types=1);
    
    namespace App;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Component\Validator\Constraints\Length;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Constraints\NotNull;
    
    final class YourCommandType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder
                ->add('foo', TextType::class, [
                    'constraints' => [
                        // Apply any constraints required for this particular property.
                        new NotBlank(),
                        new Length([
                            'min' => 3,
                            'max' => 50,
                        ]),
                    ]
                ])
            ;
    
            // In case your command needs outside information, e.g. the user performing it, you can require it as a form option and pass it to the constructor of the mapper.
            $builder->setDataMapper(new YourCommandMapper());
        }
    
        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults([
                'data_class' => YourCommand::class,
                'empty_data' => null,
                'constraints' => [
                    // This doesn't provide any useful information back to the end user and should not be relied upon. This is only present as a last resort in case a constraint is forgotten.
                    new NotNull([
                        'message' => 'Something went wrong, please check your request and try again.',
                    ]),
                ],
            ]);
        }
    }
    
    <?php declare(strict_types=1);
    
    namespace App;
    
    use Symfony\Component\Form\DataMapperInterface;
    use Throwable;
    use function iterator_to_array;
    
    final class YourCommandMapper implements DataMapperInterface
    {
        public function mapDataToForms($data, $forms)
        {
            // Uni-directional is fine in case of an api or xhr request, otherwise, map the required data to the form here.
        }
    
        public function mapFormsToData($forms, &$data)
        {
            try {
                $forms = iterator_to_array($forms);
    
                $data = new YourCommand($forms['foo']->getData());
            } catch (Throwable $exception) {
                // Nothing to see here... We just need to catch it so symfony continues and eventually starts validating the form.
            }
        }
    }
    
    <?php declare(strict_types=1);
    
    namespace App;
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    
    final class YourController
    {
        public function __invoke(Request $request): Response
        {
            $form = $this->createForm(YourCommandType::class);
            $form->submit($request->request->all());
    
            if ($form->isSubmitted() && $form->isValid()) {
                /** @var YourCommand $command */
                $command = $form->getData();
    
                // Execute the command
                // e.g. $this->commandBus->handle($command);
    
                return new Response(null, Response::HTTP_NO_CONTENT);
            }
    
            // Handle form errors
            $errors = (string) $form->getErrors(true);
    
            return new Response($errors, Response::HTTP_BAD_REQUEST);
        }
    }
    
    https://symfony.com/components/Form

    https://en.wikipedia.org/wiki/Command_pattern