Completed
Pull Request — experimental/sf (#3412)
by Kentaro
272:45 queued 265:41
created

EntityProxyService::removeClassExistsBlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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