Completed
Pull Request — experimental/3.1 (#2561)
by k-yamamura
103:46
created

EntityProxyService::addTrait()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.7536

Importance

Changes 0
Metric Value
cc 3
eloc 20
nc 3
nop 2
dl 0
loc 32
ccs 9
cts 16
cp 0.5625
crap 3.7536
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/*
3
 * This file is part of EC-CUBE
4
 *
5
 * Copyright(c) 2000-2017 LOCKON CO.,LTD. All Rights Reserved.
6
 *
7
 * http://www.lockon.co.jp/
8
 *
9
 * This program is free software; you can redistribute it and/or
10
 * modify it under the terms of the GNU General Public License
11
 * as published by the Free Software Foundation; either version 2
12
 * of the License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program; if not, write to the Free Software
21
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22
 */
23
24
namespace Eccube\Service;
25
26
27
use Doctrine\Common\Annotations\AnnotationReader;
28
use Doctrine\ORM\EntityManager;
29
use Eccube\Annotation\EntityExtension;
30
use Eccube\Annotation\Inject;
31
use Eccube\Annotation\Service;
32
use PhpCsFixer\Tokenizer\CT;
33
use PhpCsFixer\Tokenizer\Token;
34
use PhpCsFixer\Tokenizer\Tokens;
35
use Symfony\Component\Console\Output\ConsoleOutput;
36
use Symfony\Component\Console\Output\OutputInterface;
37
use Symfony\Component\Finder\Finder;
38
use Zend\Code\Reflection\ClassReflection;
39
40
/**
41
 * @Service
42
 */
43
class EntityProxyService
44
{
45
    /**
46
     * @Inject("orm.em")
47
     * @var EntityManager
48
     */
49
    protected $entityManager;
50
51
    /**
52
     * EntityのProxyを生成します。
53
     *
54
     * @param array $includesDirs Proxyに含めるTraitがあるディレクトリ一覧
0 ignored issues
show
introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
55
     * @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...
56
     * @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...
57
     * @param OutputInterface $output ログ出力
0 ignored issues
show
introduced by
Expected 7 spaces after parameter name; 1 found
Loading history...
58
     * @return array 生成したファイルのリスト
59 14
     */
60
    public function generate($includesDirs, $excludeDirs, $outputDir, OutputInterface $output = null)
61 14
    {
62 14
        if (is_null($output)) {
63
            $output = new ConsoleOutput();
64
        }
65 14
66
        $generatedFiles = [];
67 14
68 14
        list($addTraits, $removeTrails) = $this->scanTraits([$includesDirs, $excludeDirs]);
69
        $targetEntities = array_unique(array_merge(array_keys($addTraits), array_keys($removeTrails)));
70
71 14
        // プロキシファイルの生成
72 14
        foreach ($targetEntities as $targetEntity) {
73 14
            $traits = isset($addTraits[$targetEntity]) ? $addTraits[$targetEntity] : [];
74 14
            $rc = new ClassReflection($targetEntity);
75 14
76
            $entityTokens = Tokens::fromCode(file_get_contents($rc->getFileName()));
77 14
78 14
            if (isset($removeTrails[$targetEntity])) {
79
                foreach ($removeTrails[$targetEntity] as $trait) {
80
                    $this->removeTrait($entityTokens, $trait);
81 14
                }
82 4
            }
83 4
84
            foreach ($traits as $trait) {
85
                $this->addTrait($entityTokens, $trait);
86
            }
87 14
88 14
            $file = basename($rc->getFileName());
89 14
90
            $code = $entityTokens->generateCode();
91
            $generatedFiles[] = $outputFile = $outputDir.'/'.$file;
92
            file_put_contents($outputFile, $code);
93 14
            $output->writeln('gen -> '.$outputFile);
94 14
        }
95
96
        return $generatedFiles;
97 14
    }
98 14
99 12
    /**
100
     * 複数のディレクトリセットをスキャンしてディレクトリセットごとのEntityとTraitのマッピングを返します.
101 14
     * @param $dirSets array スキャン対象ディレクトリリストの配列
102
     * @return array ディレクトリセットごとのEntityとTraitのマッピング
103 14
     */
104
    private function scanTraits($dirSets)
105 14
    {
106 14
        // ディレクトリセットごとのファイルをロードしつつ一覧を作成
107 14
        $includedFileSets = [];
108 14
        foreach ($dirSets as $dirSet) {
109
            $includedFiles = [];
110
            $dirs = array_filter($dirSet, 'file_exists');
111 14
            if (!empty($dirs)) {
112
                $files = Finder::create()
113
                    ->in($dirs)
114
                    ->name('*.php')
115
                    ->files();
116
117
                foreach ($files as $file) {
118
                    require_once $file->getRealPath();
119 14
                    $includedFiles[] = $file->getRealPath();
120
                }
121
            }
122 14
            $includedFileSets[] = $includedFiles;
123 14
        }
124 14
125 14
        $declaredTraits = get_declared_traits();
126 14
127 14
        // ディレクトリセットに含まれるTraitの一覧を作成
128 14
        $traitSets = array_map(function() { return []; }, $dirSets);
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
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...
129 14
        foreach ($declaredTraits as $className) {
130 14
            $rc = new \ReflectionClass($className);
131
            $sourceFile = $rc->getFileName();
132 14
            foreach ($includedFileSets as $index=>$includedFiles) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "=>"; 0 found
Loading history...
Coding Style introduced by
Expected 1 space after "=>"; 0 found
Loading history...
133 14
                if (in_array($sourceFile, $includedFiles)) {
134 14
                    $traitSets[$index][] = $className;
135
                }
136
            }
137 14
        }
138
139
        // TraitをEntityごとにまとめる
140 14
        $reader = new AnnotationReader();
141
        $proxySets = [];
142
        foreach ($traitSets as $traits) {
143
            $proxies = [];
144 14
            foreach ($traits as $trait) {
145 14
                $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...
146 14
                if ($anno) {
147 14
                    $proxies[$anno->value][] = $trait;
148 14
                }
149 14
            }
150
            $proxySets[] = $proxies;
151
        }
152
153
        return $proxySets;
154
    }
155 14
156 14
    /**
157 14
     * EntityにTraitを追加.
158 14
     *
159 14
     * @param $entityTokens Tokens Entityのトークン
160 14
     * @param $trait string 追加するTraitのFQCN
161 14
     */
162 14
    private function addTrait($entityTokens, $trait)
163
    {
164
        $newTraitTokens = $this->convertFQCNToTokens($trait);
165 14
166
        // Traitのuse句があるかどうか
167
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
168 14
169
        if ($useTraitIndex > 0) {
170
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
171
            $alreadyUseTrait = $entityTokens->findSequence($newTraitTokens, $useTraitIndex, $useTraitEndIndex);
172
            if (is_null($alreadyUseTrait)) {
173
                $entityTokens->insertAt($useTraitEndIndex, array_merge(
174
                    [new Token(','), new Token([T_WHITESPACE, ' '])],
175 14
                    $newTraitTokens
176
                ));
177 14
            }
178 14
        } else {
179
            $useTraitTokens = array_merge(
180
                [
0 ignored issues
show
introduced by
Add a comma after each item in a multi-line array
Loading history...
181 14
                    new Token([T_WHITESPACE, PHP_EOL.'    ']),
182
                    new Token([CT::T_USE_TRAIT, 'use']),
183
                    new Token([T_WHITESPACE, ' '])
184
                ],
185
                $newTraitTokens,
186
                [new Token(';'), new Token([T_WHITESPACE, PHP_EOL])]);
187
188 14
            // `class X extens AbstractEntity {`の後にtraitを追加
189
            $classTokens = $entityTokens->findSequence([[T_CLASS], [T_STRING]]);
190
            $classTokenEnd = $entityTokens->getNextTokenOfKind(array_keys($classTokens)[0], ['{']);
191 14
            $entityTokens->insertAt($classTokenEnd + 1, $useTraitTokens);
192
        }
193
    }
194
195
    /**
196
     * EntityからTraitを削除.
197
     * @param $entityTokens Tokens Entityのトークン
198
     * @param $trait string 削除するTraitのFQCN
199
     */
200
    private function removeTrait($entityTokens, $trait)
201
    {
202
        $useTraitIndex = $entityTokens->getNextTokenOfKind(0, [[CT::T_USE_TRAIT]]);
203
        if ($useTraitIndex > 0) {
204
            $useTraitEndIndex = $entityTokens->getNextTokenOfKind($useTraitIndex, [';']);
205
            $traitsTokens = array_slice($entityTokens->toArray(), $useTraitIndex + 1, $useTraitEndIndex - $useTraitIndex - 1);
206
207
            // Trait名の配列に変換
208
            $traitNames = explode(',', implode(array_map(function($token) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
209
                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...
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
211
                return $token->getId() != T_WHITESPACE;
212
            }))));
213
214
            // 削除対象を取り除く
215
            array_splice($traitNames, array_search($trait, $traitNames), 1);
216
217
            // use句をすべて削除
218
            $entityTokens->clearRange($useTraitIndex, $useTraitEndIndex + 1);
219
220
            // traitを追加し直す
221
            foreach ($traitNames as $t) {
222
                $this->addTrait($entityTokens, $t);
223
            }
224
        }
225
    }
226
227
    private function convertFQCNToTokens($fqcn)
228
    {
229
        $result = [];
230
        foreach (explode('\\', $fqcn) as $part) {
231
            if ($part) {
232
                $result[] = new Token([T_NS_SEPARATOR, '\\']);
233
                $result[] = new Token([T_STRING, $part]);
234
            }
235
        }
236
        return $result;
0 ignored issues
show
introduced by
Missing blank line before return statement
Loading history...
237
    }
238
}
239