Symfonyでタスク管理アプリ作ってみた(サービス編)

  • 0
    Like
  • 0
    Comment

    ただでさえ太りがちなControllerですが、これにアノテーションが入ってくると、さらに情報の集約が進みます。ここにフォームの処理などをガッツリと書きこむと見通しが悪くなります。

    フレームワークの勉強のため、Symfony3でタスク管理アプリ作ってみたのパート4です。

    Controllerをスリムにするため、サービスオブジェクトを導入します。

    サービスの作成

    フォームの生成からエンティティの処理まで、コントローラ内で行ってた処理を別クラスに移動して、タスクの新規登録サービスTaskCreateを作ります。

    ディレクトリ構造

    最初に、クラスをどこに置くか考えます。コントローラから使うサービスなので、アプリケーション層に属するサービスとなると思います。そこでAppServiceディレクトリに置くことにしました。

    AppBundle
    ├── AppService
    │   └── TaskCrud
    │       ├── TaskCreate.php
    │       └── TaskDTO.php
    └── Controller
        └── TaskController.php
    

    ここにサービスやDTOクラスを置きます。

    サービスの登録

    新しくクラスを作っても、サービスとしてフレームワークに登録しないと使えません。Symfonyで新たにサービスを登録する方法をもとに設定を加えます。

    まず最初に、/src/AppBundle/DependencyInjection/AppExtension.phpというクラスを作成します。

    <?php
    namespace AppBundle\DependencyInjection;
    
    use Symfony\Component\Config\FileLocator;
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
    use Symfony\Component\HttpKernel\DependencyInjection\Extension;
    
    class AppExtension extends Extension
    {
        /**
         * Loads a specific configuration.
         *
         * @param array            $configs   An array of configuration values
         * @param ContainerBuilder $container A ContainerBuilder instance
         *
         * @throws \InvalidArgumentException When provided tag is not defined in this extension
         */
        public function load(array $configs, ContainerBuilder $container)
        {
            $loader = new YamlFileLoader(
                $container,
                new FileLocator(__DIR__ . '/../Resources/config')
            );
            $loader->load('services.yml');
        }
    
        public function getAlias()
        {
            return 'app';
        }
    }
    

    次に、/src/AppBundle/Resources/config/services.ymlを作成して、サービスの定義を書き込みます。

    parameters:
    
    services:
        app.task-create:
            class: AppBundle\AppService\TaskCrud\TaskCreate
            arguments:  ["@doctrine.orm.default_entity_manager", "@form.factory"]
    

    @doctrine.orm.default_entity_managerがDoctrine2のエンティティマネージャー、@form.factoryがフォームビルダーでした。これ探すのが結構面倒だった。

    Controllerからサービスを呼び出すには、$this->getを使います。

        public function someAction() {
            $service = $this->get('app.task-create');
        }
    

    サービスの設計

    そもそも、Taskの新規登録用のサービスとして、どんな機能が必要なのでしょう?

    サービスの機能について

    これから追加してゆくCRUD用のサービスについて考えてみます。先に作ったコントローラからメソッドを抜き出して、新しいクラスに作り変えます。まずは、インターフェースを考えてみます。

    新規登録には、2つの機能が必要となります。

    • getCreateForm:formオブジェクトの生成
    • create:リクエストからTaskを新規登録する。

    getCreateFormの機能は、新しいフォームオブジェクトを作ること。コントローラから分離したのは、エラーの場合のフォーム再描画でも必要とされるからと、時々全く別のコントローラでもフォームを利用する場合があるからです。

    一方、createについては設計に悩んだところ。

    方向としては、コントローラからサービス中の処理を出来るだけ隠すことにしました。必要な情報を全部与えて、結果だけ返します。シグネチャーとしては、次のような感じ。

    namespace AppBundle\AppService\TaskCrud;
    
    class TaskCreate {
        /**
         * @param Request $request
         * @return mixed
         */
        public function create(Request $request) {...}
    }
    

    ひとまず、createメソッドの引数としてはRequest $requestのみに。これを元に、フォームから入力値を取り出して、エンティティを保存できます。もし、他に必要な情報があれば、適宜引数を増やすか、$requestのアトリビュートに設定することになります。

    悩んだのは返り値です。

    返り値を元に、エラーがあったかどうか、エラーの場合はフォームを再描画する必要があります。で、$formを返してもらえばOKと気が付きました。内部にはエラーかどうか、さらにはエラーメッセージも入っているので便利なオブジェクトです。

    コントローラのリファクタリング

    ではTaskCreateサービスを使った場合、コントローラがどのくらい簡単になるかみてみます。

    フォームの描画

    フォームを描画するcreateActionメソッドです。

        /**
         * @Config\Route("/projects/create", name="project-create")
         * @Config\Method({"GET"})
         * @return Response
         */
        public function createAction()
        {
            $crud = $this->get('app.task-create');
            $form = $crud->getCreateForm();
    
            return $this->viewCreateForm($form);
        }
    

    app.project-crudという名前でTaskCreateサービスを呼び出してます。getCreateFormでフォームオブジェクトを取得して、フォームを描画します。

    描画自体は、viewCreateFormメソッドにあります。

        /**
         * @param FormInterface $form
         * @return Response
         */
        private function viewCreateForm($form): Response
        {
            return $this->render('task/project/create.html.twig', [
                'form' => $form->createView(),
            ]);
        }
    

    エラーがある場合にフォームを再描画するので、別メソッドとして切り出してあります。

    新規登録の処理

    次に、フォームの入力を受け取って、データを登録するinsertAction処理です。

        public function insertAction(Request $request)
        {
            $dto  = $crud->create($project, $group, $request);
            if (!$dto->isValid()) {
                $form = $dto->getForm();
                return $this->viewCreateForm($form);
            }
    
            $this->addFlash('message', 'created a new task!');
            return $this->redirectToRoute('task-detail', ['id' => $id]);
        }
    

    app.project-crudでサービスを受け取って、すぐに実行。結果をチェックして、エラーがあれば先のviewCreateFormでフォームを再描画。というシンプルな処理になってます。

    このコードをだと、サービスを入れ替えるだけで他エンティティの新規登録にも使えそうですね。まったく同じとはならないかもしれませんが、悩まずコーディングできそうです。

    今後のリファクタリング

    肝心のTaskCreateクラスです。

    Taskの場合は$formを返せば十分でしたが、新規エンティティのIDが必要な場合があります。どうやって返すのか?

    またデータベースと言えばトランザクション。これを、どこかに挟み込む必要があります。できれば、汎用で使えるようにしたいのです。

    そんなこんなのリファクタリングは、また別途考えることにします。