Skip to main content

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>