ただでさえ太りがちな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が必要な場合があります。どうやって返すのか?
またデータベースと言えばトランザクション。これを、どこかに挟み込む必要があります。できれば、汎用で使えるようにしたいのです。
そんなこんなのリファクタリングは、また別途考えることにします。