Completed
Push — 4.0 ( 7b064c...ece718 )
by Kiyotaka
05:09 queued 12s
created

EntityProxyService::removeClassExistsBlock()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 12
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.ec-cube.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\DependencyInjection\ContainerInterface;
25
use Symfony\Component\Finder\Finder;
26
use Zend\Code\Reflection\ClassReflection;
27
28
class EntityProxyService
29
{
30
    /**
31
     * @var EntityManagerInterface
32
     */
33
    protected $entityManager;
34
35
    /**
36
     * @var ContainerInterface
37
     */
38
    protected $container;
39 8
40
    /**
41 8
     * EntityProxyService constructor.
42
     *
43
     * @param EntityManagerInterface $entityManager
44
     * @param ContainerInterface $container
45
     */
46
    public function __construct(
47
        EntityManagerInterface $entityManager,
48
        ContainerInterface $container
49
    ) {
50
        $this->entityManager = $entityManager;
51
        $this->container = $container;
52
    }
53
54 2
    /**
55
     * EntityのProxyを生成します。
56 2
     *
57 2
     * @param array $includesDirs Proxyに含めるTraitがあるディレクトリ一覧
58
     * @param array $excludeDirs Proxyから除外するTraitがあるディレクトリ一覧
59
     * @param string $outputDir 出力先
60 2
     * @param OutputInterface $output ログ出力
61
     *
62 2
     * @return array 生成したファイルのリスト
63 2
     */
64
    public function generate($includesDirs, $excludeDirs, $outputDir, OutputInterface $output = null)
65
    {
66 2
        if (is_null($output)) {
67 2
            $output = new ConsoleOutput();
68 2
        }
69
70 2
        $generatedFiles = [];
71
72 2
        list($addTraits, $removeTrails) = $this->scanTraits([$includesDirs, $excludeDirs]);
73
        $targetEntities = array_unique(array_merge(array_keys($addTraits), array_keys($removeTrails)));
74 2
75 1
        // プロキシファイルの生成
76 1
        foreach ($targetEntities as $targetEntity) {
77
            $traits = isset($addTraits[$targetEntity]) ? $addTraits[$targetEntity] : [];
78
            $fileName = $this->originalEntityPath($targetEntity);
79
            $baseName = basename($fileName);
80 2
            $entityTokens = Tokens::fromCode(file_get_contents($fileName));
81 2
82
            if (strpos($fileName, 'app/proxy/entity') === false) {
83
                $this->removeClassExistsBlock($entityTokens); // remove class_exists block
84 2
            } else {
85
                // Remove to duplicate path of /app/proxy/entity
86 2
                $fileName = str_replace('/app/proxy/entity', '', $fileName);
87 2
            }
88 2
89 2
            if (isset($removeTrails[$targetEntity])) {
90
                foreach ($removeTrails[$targetEntity] as $trait) {
91
                    $this->removeTrait($entityTokens, $trait);
92 2
                }
93
            }
94
95
            foreach ($traits as $trait) {
96
                $this->addTrait($entityTokens, $trait);
97
            }
98
            $projectDir = str_replace('\\', '/', $this->container->getParameter('kernel.project_dir'));
99
100
            // baseDir e.g. /src/Eccube/Entity and /app/Plugin/PluginCode/Entity
101
            $baseDir = str_replace($projectDir, '', str_replace($baseName, '', $fileName));
102 2
            if (!file_exists($outputDir.$baseDir)) {
103
                mkdir($outputDir.$baseDir, 0777, true);
104
            }
105 2
106 2
            $file = ltrim(str_replace($projectDir, '', $fileName), '/');
107 2
            $code = $entityTokens->generateCode();
108 2
            $generatedFiles[] = $outputFile = $outputDir.'/'.$file;
109 2
110 2
            file_put_contents($outputFile, $code);
111 2
            $output->writeln('gen -> '.$outputFile);
112 2
        }
113 2
114
        return $generatedFiles;
115 2
    }
116 2
117 2
    private function originalEntityPath(string $entityClassName): string
118
    {
119
        $projectDir = rtrim(str_replace('\\', '/', $this->container->getParameter('kernel.project_dir')), '/');
120 2
        $originalPath = null;
121
122
        if (preg_match('/\AEccube\\\\Entity\\\\(.+)\z/', $entityClassName, $matches)) {
123 2
            $pathToEntity = str_replace('\\', '/', $matches[1]);
124
            $originalPath = sprintf('%s/src/Eccube/Entity/%s.php', $projectDir, $pathToEntity);
125 2
        } elseif (preg_match('/\ACustomize\\\\Entity\\\\(.+)\z/', $entityClassName, $matches)) {
126 2
            $pathToEntity = str_replace('\\', '/', $matches[1]);
127
            $originalPath = sprintf('%s/app/Customize/Entity/%s.php', $projectDir, $pathToEntity);
128
        } elseif (preg_match('/\APlugin\\\\([^\\\\]+)\\\\Entity\\\\(.+)\z/', $entityClassName, $matches)) {
129
            $pathToEntity = str_replace('\\', '/', $matches[2]);
130 2
            $originalPath = sprintf('%s/app/Plugin/%s/Entity/%s.php', $projectDir, $matches[1], $pathToEntity);
131 2
        }
132 2
133 2
        if ($originalPath !== null && file_exists($originalPath)) {
134 2
            return $originalPath;
135 2
        }
136
137
        $rc = new ClassReflection($entityClassName);
138
        return str_replace('\\', '/', $rc->getFileName());
139
    }
140
141 2
    /**
142 2
     * 複数のディレクトリセットをスキャンしてディレクトリセットごとのEntityとTraitのマッピングを返します.
143 2
     *
144 2
     * @param $dirSets array スキャン対象ディレクトリリストの配列
145 2
     *
146 2
     * @return array ディレクトリセットごとのEntityとTraitのマッピング
147 2
     */
148 2
    private function scanTraits($dirSets)
149
    {
150
        // ディレクトリセットごとのファイルをロードしつつ一覧を作成
151 2
        $includedFileSets = [];
152
        foreach ($dirSets as $dirSet) {
153
            $includedFiles = [];
154 2
            $dirs = array_filter($dirSet, 'file_exists');
155
            if (!empty($dirs)) {
156
                $files = Finder::create()
157
                    ->in($dirs)
158
                    ->name('*.php')
159
                    ->files();
160
161
                foreach ($files as $file) {
162
                    require_once $file->getRealPath();
163 6
                    $includedFiles[] = $file->getRealPath();
164
                }
165 6
            }
166
            $includedFileSets[] = $includedFiles;
167
        }
168 6
169
        $declaredTraits = array_map(function ($fqcn) {
170 6
            // FQCNが'\'で始まるように正規化
171 1
            return strpos($fqcn, '\\') === 0 ? $fqcn : '\\'.$fqcn;
172 1
        }, get_declared_traits());
173 1
174 1
        // ディレクトリセットに含まれるTraitの一覧を作成
175 1
        $traitSets = array_map(function () { return []; }, $dirSets);
176 1
        foreach ($declaredTraits as $className) {
177
            $rc = new \ReflectionClass($className);
178
            $sourceFile = $rc->getFileName();
179
            foreach ($includedFileSets as $index => $includedFiles) {
180 5
                if (in_array($sourceFile, $includedFiles)) {
181
                    $traitSets[$index][] = $className;
182 5
                }
183 5
            }
184 5
        }
185
186 5
        // TraitをEntityごとにまとめる
187 5
        $reader = new AnnotationReader();
188
        $proxySets = [];
189
        foreach ($traitSets as $traits) {
190 5
            $proxies = [];
191 5
            foreach ($traits as $trait) {
192 5
                $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...
193
                if ($anno) {
194
                    $proxies[$anno->value][] = $trait;
195
                }
196
            }
197
            $proxySets[] = $proxies;
198
        }
199
200
        return $proxySets;
201
    }
202 4
203
    /**
204 4
     * EntityにTraitを追加.
205 4
     *
206 3
     * @param Tokens $entityTokens Tokens Entityのトークン
207 3
     * @param $trait string 追加するTraitのFQCN
208
     */
209
    private function addTrait($entityTokens, $trait)
210 3
    {
211 3
        $newTraitTokens = $this->convertTraitNameToTokens($trait);
212
213 3
        // Traitのuse句があるかどうか
214 3
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
215
216
        if ($useTraitIndex > 0) {
217 3
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
218 3
            $alreadyUseTrait = $entityTokens->findSequence($newTraitTokens, $useTraitIndex, $useTraitEndIndex);
219 3
            if (is_null($alreadyUseTrait)) {
220
                $entityTokens->insertAt($useTraitEndIndex, array_merge(
221
                    [new Token(','), new Token([T_WHITESPACE, ' '])],
222
                    $newTraitTokens
223
                ));
224 3
            }
225
        } else {
226
            $useTraitTokens = array_merge(
227 3
                [
228 2
                    new Token([T_WHITESPACE, PHP_EOL.'    ']),
229
                    new Token([CT::T_USE_TRAIT, 'use']),
230
                    new Token([T_WHITESPACE, ' ']),
231
                ],
232
                $newTraitTokens,
233
                [new Token(';'), new Token([T_WHITESPACE, PHP_EOL])]);
234
235
            // `class X extends AbstractEntity {`の後にtraitを追加
236
            $classTokens = $entityTokens->findSequence([[T_CLASS], [T_STRING]]);
237
            $classTokenEnd = $entityTokens->getNextTokenOfKind(array_keys($classTokens)[0], ['{']);
238
            $entityTokens->insertAt($classTokenEnd + 1, $useTraitTokens);
239
        }
240
    }
241
242
    /**
243
     * EntityからTraitを削除.
244 6
     *
245
     * @param Tokens $entityTokens Tokens Entityのトークン
246 6
     * @param $trait string 削除するTraitのFQCN
247 6
     */
248 6
    private function removeTrait($entityTokens, $trait)
249
    {
250
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
251 6
        if ($useTraitIndex > 0) {
252
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
253 6
            $traitsTokens = array_slice($entityTokens->toArray(), $useTraitIndex + 1, $useTraitEndIndex - $useTraitIndex - 1);
254 5
255
            // Trait名の配列に変換
256 6
            $traitNames = explode(',', implode(array_map(function ($token) {
257
                return $token->getContent();
258 6
            }, array_filter($traitsTokens, function ($token) {
259
                return $token->getId() != T_WHITESPACE;
260
            }))));
261 6
262
            // 削除対象を取り除く
263
            foreach ($traitNames as $i => $name) {
264
                if ($name === $trait) {
265
                    unset($traitNames[$i]);
266
                }
267
            }
268
269 2
            // use句をすべて削除
270
            $entityTokens->clearRange($useTraitIndex, $useTraitEndIndex + 1);
271 2
272 2
            // traitを追加し直す
273 2
            foreach ($traitNames as $t) {
274 2
                $this->addTrait($entityTokens, $t);
275
            }
276 2
        }
277 2
    }
278
279
    /**
280
     * trait名をトークンに変換する
281
     *
282
     * trait名は以下の2形式で引数に渡される
283
     * - プラグインのTrait -> \Plugin\Xxx\Entity\XxxTrait
284
     * - 本体でuseされているTrait -> PointTrait
285
     *
286
     * @param $name
287
     *
288
     * @return array|Token[]
289
     */
290
    private function convertTraitNameToTokens($name)
291
    {
292
        $result = [];
293
        $i = 0;
294
        foreach (explode('\\', $name) as $part) {
295
            // プラグインのtraitの場合は、0番目は空文字
296
            // 本体でuseされているtraitは0番目にtrait名がくる
297
            if ($part) {
298
                // プラグインのtraitの場合はFQCNにする
299
                if ($i > 0) {
300
                    $result[] = new Token([T_NS_SEPARATOR, '\\']);
301
                }
302
                $result[] = new Token([T_STRING, $part]);
303
            }
304
            $i++;
305
        }
306
307
        return $result;
308
    }
309
310
    /**
311
     * remove block to 'if (!class_exists(<class name>)) { }'
312
     *
313
     * @param Tokens $entityTokens
314
     */
315
    private function removeClassExistsBlock(Tokens $entityTokens)
316
    {
317
        $startIndex = $entityTokens->getNextTokenOfKind(0, [[T_IF]]);
318
        $classIndex = $entityTokens->getNextTokenOfKind(0, [[T_CLASS]]);
319
        if ($startIndex > 0 && $startIndex < $classIndex) { // if statement before class
320
            $blockStartIndex = $entityTokens->getNextTokenOfKind($startIndex, ['{']);
321
            $blockEndIndex = $entityTokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $blockStartIndex);
322
323
            $entityTokens->clearRange($startIndex, $blockStartIndex);
324
            $entityTokens->clearRange($blockEndIndex, $blockEndIndex + 1);
325
        }
326
    }
327
}
328