Failed Conditions
Pull Request — experimental/sf (#3412)
by Kentaro
77:23 queued 14:43
created

EntityProxyService::removeClassExistsBlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 6
rs 9.9
c 0
b 0
f 0
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
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があるディレクトリ一覧
48
     * @param array $excludeDirs Proxyから除外するTraitがあるディレクトリ一覧
49
     * @param string $outputDir 出力先
50
     * @param OutputInterface $output ログ出力
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
            $this->removeClassExistsBlock($entityTokens); // remove class_exists block
73 1
74 1
            if (isset($removeTrails[$targetEntity])) {
75
                foreach ($removeTrails[$targetEntity] as $trait) {
76
                    $this->removeTrait($entityTokens, $trait);
77
                }
78 2
            }
79 2
80
            foreach ($traits as $trait) {
81
                $this->addTrait($entityTokens, $trait);
82 2
            }
83
84 2
            $file = basename($rc->getFileName());
85 2
86 2
            $code = $entityTokens->generateCode();
87 2
            $generatedFiles[] = $outputFile = $outputDir.'/'.$file;
88
            file_put_contents($outputFile, $code);
89
            $output->writeln('gen -> '.$outputFile);
90 2
        }
91
92
        return $generatedFiles;
93
    }
94
95
    /**
96
     * 複数のディレクトリセットをスキャンしてディレクトリセットごとのEntityとTraitのマッピングを返します.
97
     *
98
     * @param $dirSets array スキャン対象ディレクトリリストの配列
99
     *
100 2
     * @return array ディレクトリセットごとのEntityとTraitのマッピング
101
     */
102
    private function scanTraits($dirSets)
103 2
    {
104 2
        // ディレクトリセットごとのファイルをロードしつつ一覧を作成
105 2
        $includedFileSets = [];
106 2
        foreach ($dirSets as $dirSet) {
107 2
            $includedFiles = [];
108 2
            $dirs = array_filter($dirSet, 'file_exists');
109 2
            if (!empty($dirs)) {
110 2
                $files = Finder::create()
111 2
                    ->in($dirs)
112
                    ->name('*.php')
113 2
                    ->files();
114 2
115 2
                foreach ($files as $file) {
116
                    require_once $file->getRealPath();
117
                    $includedFiles[] = $file->getRealPath();
118 2
                }
119
            }
120
            $includedFileSets[] = $includedFiles;
121 2
        }
122
123 2
        $declaredTraits = array_map(function ($fqcn) {
124 2
            // FQCNが'\'で始まるように正規化
125
            return strpos($fqcn, '\\') === 0 ? $fqcn : '\\'.$fqcn;
126
        }, get_declared_traits());
127
128 2
        // ディレクトリセットに含まれるTraitの一覧を作成
129 2
        $traitSets = array_map(function () { return []; }, $dirSets);
130 2
        foreach ($declaredTraits as $className) {
131 2
            $rc = new \ReflectionClass($className);
132 2
            $sourceFile = $rc->getFileName();
133 2
            foreach ($includedFileSets as $index => $includedFiles) {
134
                if (in_array($sourceFile, $includedFiles)) {
135
                    $traitSets[$index][] = $className;
136
                }
137
            }
138
        }
139 2
140 2
        // TraitをEntityごとにまとめる
141 2
        $reader = new AnnotationReader();
142 2
        $proxySets = [];
143 2
        foreach ($traitSets as $traits) {
144 2
            $proxies = [];
145 2
            foreach ($traits as $trait) {
146 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...
147
                if ($anno) {
148
                    $proxies[$anno->value][] = $trait;
149 2
                }
150
            }
151
            $proxySets[] = $proxies;
152 2
        }
153
154
        return $proxySets;
155
    }
156
157
    /**
158
     * EntityにTraitを追加.
159
     *
160
     * @param Tokens $entityTokens Tokens Entityのトークン
161 6
     * @param $trait string 追加するTraitのFQCN
162
     */
163 6
    private function addTrait($entityTokens, $trait)
164
    {
165
        $newTraitTokens = $this->convertTraitNameToTokens($trait);
166 6
167
        // Traitのuse句があるかどうか
168 6
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
169 1
170 1
        if ($useTraitIndex > 0) {
171 1
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
172 1
            $alreadyUseTrait = $entityTokens->findSequence($newTraitTokens, $useTraitIndex, $useTraitEndIndex);
173 1
            if (is_null($alreadyUseTrait)) {
174 1
                $entityTokens->insertAt($useTraitEndIndex, array_merge(
175
                    [new Token(','), new Token([T_WHITESPACE, ' '])],
176
                    $newTraitTokens
177
                ));
178 5
            }
179
        } else {
180 5
            $useTraitTokens = array_merge(
181 5
                [
182 5
                    new Token([T_WHITESPACE, PHP_EOL.'    ']),
183
                    new Token([CT::T_USE_TRAIT, 'use']),
184 5
                    new Token([T_WHITESPACE, ' ']),
185 5
                ],
186
                $newTraitTokens,
187
                [new Token(';'), new Token([T_WHITESPACE, PHP_EOL])]);
188 5
189 5
            // `class X extens AbstractEntity {`の後にtraitを追加
190 5
            $classTokens = $entityTokens->findSequence([[T_CLASS], [T_STRING]]);
191
            $classTokenEnd = $entityTokens->getNextTokenOfKind(array_keys($classTokens)[0], ['{']);
192
            $entityTokens->insertAt($classTokenEnd + 1, $useTraitTokens);
193
        }
194
    }
195
196
    /**
197
     * EntityからTraitを削除.
198
     *
199
     * @param Tokens $entityTokens Tokens Entityのトークン
200 4
     * @param $trait string 削除するTraitのFQCN
201
     */
202 4
    private function removeTrait($entityTokens, $trait)
203 4
    {
204 3
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
205 3
        if ($useTraitIndex > 0) {
206
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
207
            $traitsTokens = array_slice($entityTokens->toArray(), $useTraitIndex + 1, $useTraitEndIndex - $useTraitIndex - 1);
208 3
209 3
            // Trait名の配列に変換
210
            $traitNames = explode(',', implode(array_map(function ($token) {
211 3
                return $token->getContent();
212 3
            }, array_filter($traitsTokens, function ($token) {
213
                return $token->getId() != T_WHITESPACE;
214
            }))));
215 3
216 3
            // 削除対象を取り除く
217 3
            foreach ($traitNames as $i => $name) {
218
                if ($name === $trait) {
219
                    unset($traitNames[$i]);
220
                }
221
            }
222 3
223
            // use句をすべて削除
224
            $entityTokens->clearRange($useTraitIndex, $useTraitEndIndex + 1);
225 3
226 2
            // traitを追加し直す
227
            foreach ($traitNames as $t) {
228
                $this->addTrait($entityTokens, $t);
229
            }
230
        }
231
    }
232
233
    /**
234
     * trait名をトークンに変換する
235
     *
236
     * trait名は以下の2形式で引数に渡される
237
     * - プラグインのTrait -> \Plugin\Xxx\Entity\XxxTrait
238
     * - 本体でuseされているTrait -> PointTrait
239
     *
240
     * @param $name
241
     *
242 6
     * @return array|Token[]
243
     */
244 6
    private function convertTraitNameToTokens($name)
245 6
    {
246 6
        $result = [];
247
        $i = 0;
248
        foreach (explode('\\', $name) as $part) {
249 6
            // プラグインのtraitの場合は、0番目は空文字
250
            // 本体でuseされているtraitは0番目にtrait名がくる
251 6
            if ($part) {
252 5
                // プラグインのtraitの場合はFQCNにする
253
                if ($i > 0) {
254 6
                    $result[] = new Token([T_NS_SEPARATOR, '\\']);
255
                }
256 6
                $result[] = new Token([T_STRING, $part]);
257
            }
258
            $i++;
259 6
        }
260
261
        return $result;
262
    }
263
264
    /**
265
     * remove block to 'if (class_exists(<class name>)) { }'
266
     *
267
     * @params Tokens $entityTokens
268
     */
269
    private function removeClassExistsBlock(Tokens $entityTokens)
270
    {
271
        $startIndex = $entityTokens->getNextTokenOfKind(0, [[T_IF]]);
272
        if ($startIndex > 0) {
273
            $blockStartIndex = $entityTokens->getNextTokenOfKind($startIndex, ['{']);
274
            $blockEndIndex = $entityTokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $blockStartIndex);
275
276
            $entityTokens->clearRange($startIndex, $blockStartIndex);
277
            $entityTokens->clearRange($blockEndIndex, $blockEndIndex + 1);
278
        }
279
    }
280
}
281