Symfony + Propel でマルチドメイン対応
modified: 2018-09-24
ひとつの Web アプリを、複数の顧客に対し、ドメインを分けてそれぞれにサービスを提供したい。各顧客はそれぞれでユーザーを登録し、ログインをおこなう。データはドメインごとに完全に独立させたい。
環境
- Symfony 2.8
- Propel 1.5
方針
- 顧客をアクセス時のサブドメインで識別する。(DNS や証明書はワイルドカードで何でもアクセスできるようにしておく。)
- すべてのテーブルに customer_id フィールドを持たせる。
- 読み出すときは検索条件に常に customer_id による絞り込みをおこなう。
- 書き込むときは常に customer_id を設定する。
実装
顧客IDをアクセスドメインから取得する
アクセスドメインから顧客IDを取得するためのサービスを作成する。RequestStack->getMasterRequest()->getHost() でドメインを取得し、先頭の要素を顧客IDとする。
<?php
namespace AppBundle\Library;
use Symfony\Component\HttpFoundation\RequestStack;
class CustomerManager
{
    protected $requestStack;
    function __construct(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }
    function getCurrentCustomerId()
    {
        $request = $this->requestStack->getMasterRequest();
        $h = explode('.', $request->getHost());
        return $h[0];
    }
}
取得した顧客IDをセッションに保存するためのイベントハンドラを作成する。
<?php
namespace AppBundle\Filter\Propel;
use AppBundle\Library\CustomerManager;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class Configurator
{
    protected $customerManager;
    private $session;
    public function __construct(CustomerManager $customerManager, Session $session)
    {
        $this->customerManager = $customerManager;
        $this->session = $session;
    }
    public function onKernelRequest(GetResponseEvent $event)
    {
        $this->session->set('customer_id',
            $this->customerManager->getCurrentCustomerId());
    }
}
これらをサービスに登録する。
services:
    app.customer:
        class: AppBundle\Library\CustomerManager
        arguments: [ "@request_stack" ]
    app.propel.customer_listener:
        class: AppBundle\Filter\Propel\Configurator
        arguments: [ "@app.customer", "@session" ]
        tags:
          - { name: kernel.event_listener, event: kernel.request }
Behavior の作成
以下の処理をおこなう Behavior を作成する。
- すべてのテーブルに customer_id フィールドを持たせる
- DBからの読み出し時、常に customer_id による絞り込みをおこなう
- 保存時に customer_id を設定する
比較対象の顧客IDはセッションに保存しておいたものを使う。
<?php
namespace AppBundle\Filter\Propel;
class CustomerFilterBehavior extends \Behavior
{
    const GET_CUSTOMER_ID = '
global $kernel;
$customerId = $kernel->getContainer()->get(\'session\')->get(\'customer_id\');
';
    public function modifyTable()
    {
        $table = $this->getTable();
        if (!$table->hasColumn('customer_id')) {
            $table->addColumn([
                'name' => 'customer_id',
                'type' => 'VARCHAR',
                'size' => '255',
            ]);
        }
    }
    public function preSelectQuery(\QueryBuilder $builder)
    {
        return self::GET_CUSTOMER_ID .
            '$this->filterByCustomerId($customerId);';
    }
    public function preSelect(\PeerBuilder $builder)
    {
        $column = $this->getTable()->getColumn('customer_id');
        $columnConstant = $builder->getColumnConstant($column);
        return self::GET_CUSTOMER_ID .
            '$criteria->add(' . $columnConstant . ', $customerId);';
    }
    public function preInsert(\PeerBuilder $builder)
    {
        return self::GET_CUSTOMER_ID .
            '$this->setCustomerId($customerId);';
    }
}
この Behavior を登録する。
propel:
    behaviors:
        customer_filter: AppBundle\Filter\Propel\CustomerFilterBehavior
顧客毎に分離する必要があるすべてのテーブルに Behavior を登録する。
<?xml version="1.0" encoding="UTF-8"?>
<database defaultPhpNamingMethod="underscore" heavyIndexing="false" name="propel" defaultIdMethod="native" namespace="AppBundle\Entity\Propel">
    <table skipSql="false" abstract="false" name="users" phpName="User">
        ...
        <behavior name="customer_filter" />
    </table>
</database>