Completed
Push — experimental/sf ( a07b0c...8fb39b )
by Kiyotaka
447:39 queued 422:54
created

EntityProxyService::convertFQCNToTokens()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
ccs 7
cts 7
cp 1
crap 3
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service;
15
16
use Doctrine\Common\Annotations\AnnotationReader;
17
use Doctrine\ORM\EntityManagerInterface;
18
use Eccube\Annotation\EntityExtension;
19
use PhpCsFixer\Tokenizer\CT;
20
use PhpCsFixer\Tokenizer\Token;
21
use PhpCsFixer\Tokenizer\Tokens;
22
use Symfony\Component\Console\Output\ConsoleOutput;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Finder\Finder;
25
use Zend\Code\Reflection\ClassReflection;
26
27
class EntityProxyService
0 ignored issues
show
introduced by
Missing class doc comment
Loading history...
28
{
29
    /**
30
     * @var EntityManagerInterface
31
     */
32
    protected $entityManager;
33
34
    /**
35
     * EntityProxyService constructor.
36
     *
37
     * @param EntityManagerInterface $entityManager
38
     */
39 8
    public function __construct(EntityManagerInterface $entityManager)
40
    {
41 8
        $this->entityManager = $entityManager;
42
    }
43
44
    /**
45
     * EntityのProxyを生成します。
46
     *
47
     * @param array $includesDirs Proxyに含めるTraitがあるディレクトリ一覧
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
48
     * @param array $excludeDirs Proxyから除外するTraitがあるディレクトリ一覧
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
introduced by
Expected 2 spaces after parameter name; 1 found
Loading history...
49
     * @param string $outputDir 出力先
0 ignored issues
show
introduced by
Expected 10 spaces after parameter type; 1 found
Loading history...
introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
50
     * @param OutputInterface $output ログ出力
0 ignored issues
show
introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
51
     *
52
     * @return array 生成したファイルのリスト
53
     */
54 2
    public function generate($includesDirs, $excludeDirs, $outputDir, OutputInterface $output = null)
55
    {
56 2
        if (is_null($output)) {
57 2
            $output = new ConsoleOutput();
58
        }
59
60 2
        $generatedFiles = [];
61
62 2
        list($addTraits, $removeTrails) = $this->scanTraits([$includesDirs, $excludeDirs]);
63 2
        $targetEntities = array_unique(array_merge(array_keys($addTraits), array_keys($removeTrails)));
64
65
        // プロキシファイルの生成
66 2
        foreach ($targetEntities as $targetEntity) {
67 2
            $traits = isset($addTraits[$targetEntity]) ? $addTraits[$targetEntity] : [];
68 2
            $rc = new ClassReflection($targetEntity);
69
70 2
            $entityTokens = Tokens::fromCode(file_get_contents($rc->getFileName()));
71
72 2
            if (isset($removeTrails[$targetEntity])) {
73 1
                foreach ($removeTrails[$targetEntity] as $trait) {
74 1
                    $this->removeTrait($entityTokens, $trait);
75
                }
76
            }
77
78 2
            foreach ($traits as $trait) {
79 2
                $this->addTrait($entityTokens, $trait);
80
            }
81
82 2
            $file = basename($rc->getFileName());
83
84 2
            $code = $entityTokens->generateCode();
85 2
            $generatedFiles[] = $outputFile = $outputDir.'/'.$file;
86 2
            file_put_contents($outputFile, $code);
87 2
            $output->writeln('gen -> '.$outputFile);
88
        }
89
90 2
        return $generatedFiles;
91
    }
92
93
    /**
94
     * 複数のディレクトリセットをスキャンしてディレクトリセットごとのEntityとTraitのマッピングを返します.
95
     *
96
     * @param $dirSets array スキャン対象ディレクトリリストの配列
97
     *
98
     * @return array ディレクトリセットごとのEntityとTraitのマッピング
99
     */
100 2
    private function scanTraits($dirSets)
101
    {
102
        // ディレクトリセットごとのファイルをロードしつつ一覧を作成
103 2
        $includedFileSets = [];
104 2
        foreach ($dirSets as $dirSet) {
105 2
            $includedFiles = [];
106 2
            $dirs = array_filter($dirSet, 'file_exists');
107 2
            if (!empty($dirs)) {
108 2
                $files = Finder::create()
109 2
                    ->in($dirs)
110 2
                    ->name('*.php')
111 2
                    ->files();
112
113 2
                foreach ($files as $file) {
114 2
                    require_once $file->getRealPath();
115 2
                    $includedFiles[] = $file->getRealPath();
116
                }
117
            }
118 2
            $includedFileSets[] = $includedFiles;
119
        }
120
121 2
        $declaredTraits = array_map(function ($fqcn) {
122
            // FQCNが'\'で始まるように正規化
123 2
            return strpos($fqcn, '\\') === 0 ? $fqcn : '\\'.$fqcn;
124 2
        }, get_declared_traits());
125
126
        // ディレクトリセットに含まれるTraitの一覧を作成
127
        $traitSets = array_map(function () { return []; }, $dirSets);
0 ignored issues
show
Coding Style introduced by
Opening brace must be the last content on the line
Loading history...
Coding Style introduced by
It is generally recommended to place each PHP statement on a line by itself.

Let’s take a look at an example:

// Bad
$a = 5; $b = 6; $c = 7;

// Good
$a = 5;
$b = 6;
$c = 7;
Loading history...
128 2
        foreach ($declaredTraits as $className) {
129 2
            $rc = new \ReflectionClass($className);
130 2
            $sourceFile = $rc->getFileName();
131 2
            foreach ($includedFileSets as $index => $includedFiles) {
132 2
                if (in_array($sourceFile, $includedFiles)) {
133 2
                    $traitSets[$index][] = $className;
134
                }
135
            }
136
        }
137
138
        // TraitをEntityごとにまとめる
139 2
        $reader = new AnnotationReader();
140 2
        $proxySets = [];
141 2
        foreach ($traitSets as $traits) {
142 2
            $proxies = [];
143 2
            foreach ($traits as $trait) {
144 2
                $anno = $reader->getClassAnnotation(new \ReflectionClass($trait), EntityExtension::class);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $anno is correct as $reader->getClassAnnotat...EntityExtension::class) (which targets Doctrine\Common\Annotati...r::getClassAnnotation()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
145 2
                if ($anno) {
146 2
                    $proxies[$anno->value][] = $trait;
147
                }
148
            }
149 2
            $proxySets[] = $proxies;
150
        }
151
152 2
        return $proxySets;
153
    }
154
155
    /**
156
     * EntityにTraitを追加.
157
     *
158
     * @param $entityTokens Tokens Entityのトークン
159
     * @param $trait string 追加するTraitのFQCN
160
     */
161 6
    private function addTrait($entityTokens, $trait)
162
    {
163 6
        $newTraitTokens = $this->convertTraitNameToTokens($trait);
164
165
        // Traitのuse句があるかどうか
166 6
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
167
168 6
        if ($useTraitIndex > 0) {
169 1
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
170 1
            $alreadyUseTrait = $entityTokens->findSequence($newTraitTokens, $useTraitIndex, $useTraitEndIndex);
171 1
            if (is_null($alreadyUseTrait)) {
172 1
                $entityTokens->insertAt($useTraitEndIndex, array_merge(
173 1
                    [new Token(','), new Token([T_WHITESPACE, ' '])],
174 1
                    $newTraitTokens
175
                ));
176
            }
177
        } else {
178 5
            $useTraitTokens = array_merge(
179
                [
180 5
                    new Token([T_WHITESPACE, PHP_EOL.'    ']),
181 5
                    new Token([CT::T_USE_TRAIT, 'use']),
182 5
                    new Token([T_WHITESPACE, ' ']),
183
                ],
184 5
                $newTraitTokens,
185 5
                [new Token(';'), new Token([T_WHITESPACE, PHP_EOL])]);
186
187
            // `class X extens AbstractEntity {`の後にtraitを追加
188 5
            $classTokens = $entityTokens->findSequence([[T_CLASS], [T_STRING]]);
189 5
            $classTokenEnd = $entityTokens->getNextTokenOfKind(array_keys($classTokens)[0], ['{']);
190 5
            $entityTokens->insertAt($classTokenEnd + 1, $useTraitTokens);
191
        }
192
    }
193
194
    /**
195
     * EntityからTraitを削除.
196
     *
197
     * @param $entityTokens Tokens Entityのトークン
198
     * @param $trait string 削除するTraitのFQCN
199
     */
200 4
    private function removeTrait($entityTokens, $trait)
201
    {
202 4
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
203 4
        if ($useTraitIndex > 0) {
204 3
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
205 3
            $traitsTokens = array_slice($entityTokens->toArray(), $useTraitIndex + 1, $useTraitEndIndex - $useTraitIndex - 1);
206
207
            // Trait名の配列に変換
208 3
            $traitNames = explode(',', implode(array_map(function ($token) {
209 3
                return $token->getContent();
210
            }, array_filter($traitsTokens, function ($token) {
0 ignored issues
show
Coding Style introduced by
This line of the multi-line function call does not seem to be indented correctly. Expected 16 spaces, but found 12.
Loading history...
211 3
                return $token->getId() != T_WHITESPACE;
212 3
            }))));
213
214
            // 削除対象を取り除く
215 3
            foreach ($traitNames as $i => $name) {
216 3
                if ($name === $trait) {
217 3
                    unset($traitNames[$i]);
218
                }
219
            }
220
221
            // use句をすべて削除
222 3
            $entityTokens->clearRange($useTraitIndex, $useTraitEndIndex + 1);
223
224
            // traitを追加し直す
225 3
            foreach ($traitNames as $t) {
226 2
                $this->addTrait($entityTokens, $t);
227
            }
228
        }
229
    }
230
231
    /**
232
     * trait名をトークンに変換する
233
     *
234
     * trait名は以下の2形式で引数に渡される
235
     * - プラグインのTrait -> \Plugin\Xxx\Entity\XxxTrait
236
     * - 本体でuseされているTrait -> PointTrait
237
     *
238
     * @param $name
239
     *
240
     * @return array|Token[]
241
     */
242 6
    private function convertTraitNameToTokens($name)
243
    {
244 6
        $result = [];
245 6
        $i = 0;
246 6
        foreach (explode('\\', $name) as $part) {
247
            // プラグインのtraitの場合は、0番目は空文字
248
            // 本体でuseされているtraitは0番目にtrait名がくる
249 6
            if ($part) {
250
                // プラグインのtraitの場合はFQCNにする
251 6
                if ($i > 0) {
252 5
                    $result[] = new Token([T_NS_SEPARATOR, '\\']);
253
                }
254 6
                $result[] = new Token([T_STRING, $part]);
255
            }
256 6
            $i++;
257
        }
258
259 6
        return $result;
260
    }
261
}
262