EC-CUBE4 エンティティ拡張で既存メソッドを書き換える

# 目標

- CustomerエンティティのgetRoles()メソッドで返す値をカスタマイズする。
- 動作確認バージョンは2018/10/18時点で最新の4.0.0。

# 失敗例

http://doc4.ec-cube.net/customize_entity
ドキュメントに従ってエンティティを拡張する。

``` php:app/Customize/Entity/CustomerTrait.php
<?php
namespace Customize\Entity;

use Eccube\Annotation as Eccube;

/**
 * @Eccube\EntityExtension("Eccube\Entity\Customer")
 */
trait CustomerTrait
{
    public function getRoles()
    {
        return ['ROLE_USER', 'ROLE_HOGE'];
    }
}
```

``` sh
$ php bin/console eccube:generate:proxies
gen -> /var/www/html/app/proxy/entity/Customer.php
```

出来上がったエンティティ

``` php:app/proxy/entity/Customer.php
// 前略
    class Customer extends \Eccube\Entity\AbstractEntity implements UserInterface
    {
    use \Customize\Entity\CustomerTrait;
// 中略
        public function getRoles()
        {
            return ['ROLE_USER'];
        }
//後略
```

``` php
var_export((new \Eccube\Entity\Customer())->getRoles());
// array (
//   0 => 'ROLE_CUSTOMER',
// )
```

トレイトで同名のメソッドを定義しても、CustomerクラスのgetRole()で上書きされてしまう。

# そもそも

EC-CUBEはどのようにプロキシファイルを生成しているのか？
https://github.com/EC-CUBE/ec-cube/blob/4.0.0/src/Eccube/Service/EntityProxyService.php

→対象となるトレイトをEntityProxyServiceで字句解析してuse句にセットしてる。
**→だったらDIでサービスを置き換えて字句解析で上書きしちゃえばよくない？**

# 成功例

``` php:app/Customize/Entity/CustomerTrait.php
<?php
// 前略
    public function getTraitRoles() // 関数名を変更
// 後略
```

``` yaml:app/config/eccube/services.yaml
# ファイル末尾
    Customize\Service\EntityProxyService:
        decorates: Eccube\Service\EntityProxyService
        arguments:
        - '@doctrine.orm.default_entity_manager'
        - '@Customize\Service\EntityProxyService.inner'
```

``` php:app/Customize/Service/EntityProxyService.php
<?php
namespace Customize\Service;

use Eccube\Service\EntityProxyService as BaseService;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\Console\Output\OutputInterface;

class EntityProxyService extends BaseService
{
    /**
     * @var BaseService
     */
    private $baseService;

    public function __construct(EntityManagerInterface $entityManager, BaseService $baseService)
    {
        parent::__construct($entityManager);
        $this->baseService = $baseService;
    }

    public function generate($includesDirs, $excludeDirs, $outputDir, OutputInterface $output = null)
    {
        // 生成されたプロキシファイル一覧
        $generatedFiles = $this->baseService->generate($includesDirs, $excludeDirs, $outputDir, $output);

        foreach ($generatedFiles as $generatedFile) {

            // 末尾が/Customer.phpなら
            if (preg_match('#/Customer.php$#', $generatedFile)) {
                $entityTokens = Tokens::fromCode(file_get_contents($generatedFile));
                $functionIndex = 0;
                // T_FUNCTIONを探す
                while(($functionIndex = $entityTokens->getNextTokenOfKind($functionIndex, [[T_FUNCTION]])) > 0) {

                    $nextIndex = $entityTokens->getNextMeaningfulToken($functionIndex);
                    $nextToken = $entityTokens[$nextIndex];

                    // T_FUNCTIONの次の空白以外のトークンがT_STRINGで、getRolesなら
                    if ($nextToken->isGivenKind(T_STRING) && $nextToken->getContent() === 'getRoles') {

                        // 次の'{'から対応する'}'まで
                        $startIndex = $entityTokens->getNextTokenOfKind($nextIndex, ['{']);
                        $endIndex = $entityTokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);

                        // を'{return $this->getTraitRoles();}'で置き換える
                        $newTokens = Tokens::fromCode('{return $this->getTraitRoles();}');
                        $entityTokens->overrideRange($startIndex, $endIndex, $newTokens);
                        $code = $entityTokens->generateCode();
                        file_put_contents($generatedFile, $code);
                        break;
                    }
                }
            }
        }

        return $generatedFiles;
    }
}
```

``` sh
$ php bin/console eccube:generate:proxies
gen -> /var/www/html/app/proxy/entity/Customer.php
```

出来上がったエンティティ

``` php:app/proxy/entity/Customer.php
// 前略
        public function getRoles()
        {return $this->getTraitRoles();}
//後略
```

``` php
var_export((new \Eccube\Entity\Customer())->getRoles());
// array (
//   0 => 'ROLE_CUSTOMER',
//   1 => 'ROLE_HOGE',
// )
```

なんでもできそう。

## 元に戻すときの注意点

一度プロキシファイルを作ると、次回からはプロキシファイルをベースに再生成する。
EntityProxyServiceを元に戻しても、getRolesの中身は戻らない。
かといっていきなりプロキシファイルを消して再生成すると、プロキシファイルがないと言われ怒られる。
必ずキャッシュを消してから再生成すること。

``` sh
$ bin/console cache:clear --no-warmup
$ rm app/proxy/entity/Customer.php
$ php bin/console eccube:generate:proxies
```