Symfony + Propel でマルチドメイン対応

ひとつの Web アプリを、複数の顧客に対し、ドメインを分けてそれぞれにサービスを提供したい。各顧客はそれぞれでユーザーを登録し、ログインをおこなう。データはドメインごとに完全に独立させたい。

## 環境

- Symfony 2.8
- Propel 1.5

## 方針

- 顧客をアクセス時のサブドメインで識別する。(DNS や証明書はワイルドカードで何でもアクセスできるようにしておく。)
- すべてのテーブルに customer_id フィールドを持たせる。
- 読み出すときは検索条件に常に customer_id による絞り込みをおこなう。
- 書き込むときは常に customer_id を設定する。

## 実装

### 顧客IDをアクセスドメインから取得する

アクセスドメインから顧客IDを取得するためのサービスを作成する。RequestStack->getMasterRequest()->getHost() でドメインを取得し、先頭の要素を顧客IDとする。

```php
<?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
<?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());
    }
}
```

これらをサービスに登録する。

```yml:app/config/services.yml
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
<?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 を登録する。

```yml:app/config/config.yml
propel:
    behaviors:
        customer_filter: AppBundle\Filter\Propel\CustomerFilterBehavior
```

顧客毎に分離する必要があるすべてのテーブルに Behavior を登録する。

```src/AppBundle/Resources/config/schema.xml
<?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>
```