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