Symfony2でビジネスロジックを大量にコントローラに書かない構成

気がつくとコントローラに大量のビジネスロジックが含まれるようなプロジェクトがよくあるけれども、複数の場所に同じロジックが存在していたりコントローラの見通しが悪くなったりするので望ましい状況ではない。そんな状況に陥らないようにするための構成。

EntityRepositoryを使う

以下のようなEntityクラスを仮定する。

<?php
// Entityクラス
namespace Foo\BarBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Foo\BarBundle\Repository\UserRepository;

/**
 * @ORM\Entity
 * @ORM\Table(name="user_table")
 * @ORM\Entity(repositoryClass="Foo\BarBundle\Repository\UserRepository")
 */
class User{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="boolean")
     */
    private $locked = false;
    
    /**
     * @ORM\Column(type="boolean")
     */
    private $enabled = true;
    
    // ゲッタ、セッタ面倒なので書くの省きます。以下にあると思ってください。
    // ...
}

有効なユーザ(lockedがfalse, enabledがtrue)を探すには、以下のようなコードになる。

$dql = <<<DQL
SELECT
    u
FROM
    FooBarBundle:User u
WHERE
    u.locked = false
    AND u.enabled = true
ORDER BY u.id
DQL;
$users = $em->createQuery($dql)
  ->getResult();

// もしくは
$users = $em->getRepository('FooBarBundle:User u')
  ->findBy(array(
    'locked' => false,
    'enabled' => true,
  ));

これぐらいだとコントローラに書いてしまってもいいかな、という程度の条件だけど、もっと条件が複雑になってきたときにいろんなところに毎回同じ処理を書きたくはないし、コントローラに複雑な処理を書くのはいかがなものかと思う。そこで、共通の処理をくくりだせるようになっているのがRepositoryクラス。

上記のEntityクラスに含まれるアノテーション@ORM\Entity(repositoryClass="Foo\BarBundle\Repository\UserRepository")がRepositoryクラスの指定。

下記のような感じでRepositoryクラスを定義。

<?php
// Repositoryクラス
namespace Foo\BarBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Foo\BarBundle\Entity\User;

class UserRepository extends EntityRepository
{
    public function createSearchEnabledUsersQueryBuilder()
    {
        $dql = <<<DQL
SELECT
    u
FROM
    FooBarBundle:User u
WHERE
    u.locked = false
    AND u.enabled = true
ORDER BY u.id
DQL;
        return $this->getEntityManager()
            ->createQuery($dql);
    }
    
    public function searchEnabledUsers()
    {
        return $this->createSearchEnabledUsersQueryBuilder()
            ->getResult();
    }
}

これで、以下のようにsearchEnabledUsersを呼び出すことができるようになる。

$users = $em->getRepository('FooBarBundle:User u')
  ->searchEnabledUsers();

ビジネスロジックを書くためのサービスを定義する

たとえば複数のテーブルからデータを取得して、それを使ってinsertしたり外部サービスに送信したり、などというような複雑な処理があるとして、それを書くための場所は標準構成では特に定められていないけれど、Modelを定義するための場所を独自に作って、そこに書いてしまうのがいいのかな、と思ってる。たとえば、Foo\BarBundle\Manager以下にUserManagerを定義してみる。

<?php

namespace Foo\BarBundle\Manager;
use Foo\BarBundle\Entity\User;

class UserManager
{
    private $encoderFactory;
    private $entityManager;
    
    public function setEncoder($encoderFactory)
    {
        $this->encoderFactory = $encoderFactory;
    }
    
    public function setEntityManager($entityManager)
    {
        $this->entityManager = $entityManager;
    }
    
    public static function get($encoderFactory, $entityManager)
    {
        $obj = new UserManager();
        $obj->setEncoderFactory($encoderFactory);
        $obj->setEntityManager($entityManager);
        return $obj;
    }
    
    public function registerUser($info)
    {
        $user = new User();
        
        // なんかいろいろする
        $user->setSalt(md5(rand(10000,99999).$info->username));
        $encoder = $this->encoderFactory->getUser($user);
        $user->setPassword($encoder->encodePassword($info->password, $user->getSalt()));
        
        // ...
        
        $this->em->persist($user);
        $this->em->flush();
        
        return $user;
    }
}

これを使えるようにするためには、Resources/config/services.ymlにサービス登録の設定を追加する必要がある。

parameters:
    foo_bar.user_manager.class:  Foo\BarBundle\Manager\UserManager

services:
    foo_bar.user_manager:
        class:  %foo_bar.user_manager.class%
        factory_class:  %foo_bar.user_manager.class%
        factory_method: get
        arguments:
            encoder: @security.encoder_factory
            entityManager: @doctrine.orm.entity_manager

コントローラから使うときは以下のような感じで。

// ユーザ登録
$userManager = $this->get('foo_bar.user_manager');
$user = $userManager->registerUser($data);