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>