Completed
Push — master ( 3b9ac7...e8290d )
by Dmitry
03:08
created

Annotation::migrate()   F

Complexity

Conditions 39
Paths > 20000

Size

Total Lines 182

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 182
rs 0
c 0
b 0
f 0
cc 39
nc 969589
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tarantool\Mapper\Plugin;
6
7
use Closure;
8
use Exception;
9
use LogicException;
10
use phpDocumentor\Reflection\DocBlockFactory;
11
use phpDocumentor\Reflection\Types\ContextFactory;
12
use ReflectionClass;
13
use ReflectionMethod;
14
use ReflectionProperty;
15
use Tarantool\Mapper\Entity;
16
use Tarantool\Mapper\Plugin\NestedSet;
17
use Tarantool\Mapper\Repository;
18
use Tarantool\Mapper\Space;
19
20
class Annotation extends UserClasses
21
{
22
    protected $entityClasses = [];
23
    protected $entityPostfix;
24
25
    protected $repositoryClasses = [];
26
    protected $repositoryPostifx;
27
28
    protected $extensions;
29
30
    public function register($class)
31
    {
32
        $isEntity = is_subclass_of($class, Entity::class);
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Tarantool\Mapper\Entity::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
33
        $isRepository = is_subclass_of($class, Repository::class);
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Tarantool\Mapper\Repository::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
34
35
        if (!$isEntity && !$isRepository) {
36
            throw new Exception("Invalid registration for $class");
37
        }
38
39
        if ($isEntity) {
40
            if ($class == Entity::class) {
41
                throw new Exception("Invalid entity registration for $class");
42
            }
43
            $this->entityClasses[] = $class;
44
        }
45
46
        if ($isRepository) {
47
            if ($class == Repository::class) {
48
                throw new Exception("Invalid repository registration for $class");
49
            }
50
            $this->repositoryClasses[] = $class;
51
        }
52
53
        $space = $this->getSpaceName($class);
54
        if ($isEntity) {
55
            $this->mapEntity($space, $class);
56
        } else {
57
            $this->mapRepository($space, $class);
58
        }
59
        return $this;
60
    }
61
62
    public function validateSpace(string $space) : bool
63
    {
64
        foreach ($this->entityClasses as $class) {
65
            if ($this->getSpaceName($class) == $space) {
66
                return true;
67
            }
68
        }
69
70
        foreach ($this->repositoryClasses as $class) {
71
            if ($this->getSpaceName($class) == $space) {
72
                return true;
73
            }
74
        }
75
76
        return parent::validateSpace($space);
77
    }
78
79
    public function getSpace($instance) : string
80
    {
81
        $class = get_class($instance);
82
        $target = $this->isExtension($class) ? $this->getExtensions()[$class] : $class;
83
        return $this->getSpaceName($target);
84
    }
85
86
    public function isExtension(string $class) : bool
87
    {
88
        return array_key_exists($class, $this->getExtensions());
89
    }
90
91
    public function getExtensions() : array
92
    {
93
        if (is_null($this->extensions)) {
94
            $this->extensions = [];
95
            foreach ($this->entityClasses as $entity) {
96
                $reflection = new ReflectionClass($entity);
97
                $parentEntity = $reflection->getParentClass()->getName();
98
                if (in_array($parentEntity, $this->entityClasses)) {
99
                    $this->extensions[$entity] = $parentEntity;
100
                }
101
            }
102
        }
103
        return $this->extensions;
104
    }
105
106
    public function migrate($extensionInstances = true) : self
107
    {
108
        $factory = DocBlockFactory::createInstance();
109
        $contextFactory = new ContextFactory();
110
111
        $schema = $this->mapper->getSchema();
112
113
        $computes = [];
114
        foreach ($this->entityClasses as $entity) {
115
            if ($this->isExtension($entity)) {
116
                continue;
117
            }
118
119
            $spaceName = $this->getSpaceName($entity);
120
121
            $engine = 'memtx';
122
            if (array_key_exists($spaceName, $this->repositoryMapping)) {
123
                $repositoryClass = $this->repositoryMapping[$spaceName];
124
                $repositoryReflection = new ReflectionClass($repositoryClass);
125
                $repositoryProperties = $repositoryReflection->getDefaultProperties();
126
                if (array_key_exists('engine', $repositoryProperties)) {
127
                    $engine = $repositoryProperties['engine'];
128
                }
129
            }
130
131
            if ($schema->hasSpace($spaceName)) {
132
                $space = $schema->getSpace($spaceName);
133
                if ($space->getEngine() != $engine) {
134
                    throw new Exception("Space engine can't be updated");
135
                }
136
            } else {
137
                $space = $schema->createSpace($spaceName, [
138
                    'engine' => $engine,
139
                    'properties' => [],
140
                ]);
141
            }
142
143
            $class = new ReflectionClass($entity);
144
145
            foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
146
                $context = $contextFactory->createFromReflector($property);
147
                $description = $factory->create($property->getDocComment(), $context);
148
                $tags = $description->getTags('var');
149
150
                if (!count($tags)) {
151
                    throw new Exception("No var tag for ".$entity.'::'.$property->getName());
152
                }
153
154
                $byTypes = [];
155
                foreach ($tags as $candidate) {
156
                    $byTypes[$candidate->getName()] = $candidate;
157
                }
158
159
                if (!array_key_exists('var', $byTypes)) {
160
                    throw new Exception("No var tag for ".$entity.'::'.$property->getName());
161
                }
162
163
                $propertyName = $property->getName();
164
                $phpType = $byTypes['var']->getType();
165
166
                if (array_key_exists('type', $byTypes)) {
167
                    $type = (string) $byTypes['type']->getDescription();
168
                } else {
169
                    $type = $this->getTarantoolType((string) $phpType);
170
                }
171
172
                $isNullable = true;
173
174
                if (array_key_exists('required', $byTypes)) {
175
                    $isNullable = false;
176
                }
177
178
                if (!$space->hasProperty($propertyName)) {
179
                    $opts = [
180
                        'is_nullable' => $isNullable,
181
                    ];
182
                    if ($this->isReference((string) $phpType)) {
183
                        $opts['reference'] = $this->getSpaceName((string) $phpType);
184
                    }
185
                    if (array_key_exists('default', $byTypes)) {
186
                        $opts['default'] = $schema->formatValue($type, (string) $byTypes['default']);
187
                    }
188
                    $space->addProperty($propertyName, $type, $opts);
189
                }
190
            }
191
            if ($this->mapper->hasPlugin(NestedSet::class)) {
192
                $nested = $this->mapper->getPlugin(NestedSet::class);
193
                if ($nested->isNested($space)) {
194
                    $nested->addIndexes($space);
195
                }
196
            }
197
198
            if ($class->hasMethod('compute')) {
199
                $computes[] = $spaceName;
200
            }
201
        }
202
203
        foreach ($this->repositoryClasses as $repository) {
204
            $spaceName = $this->getSpaceName($repository);
205
206
            if (!$schema->hasSpace($spaceName)) {
207
                throw new Exception("Repository $spaceName has no entity definition");
208
            }
209
210
            $this->mapRepository($spaceName, $repository);
211
212
            $space = $schema->getSpace($spaceName);
213
214
            $class = new ReflectionClass($repository);
215
            $properties = $class->getDefaultProperties();
216
217
            if (array_key_exists('indexes', $properties)) {
218
                foreach ($properties['indexes'] as $i => $index) {
0 ignored issues
show
Bug introduced by
The expression $properties['indexes'] of type null|integer|double|string|boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
219
                    if (!is_array($index)) {
220
                        $index = (array) $index;
221
                    }
222
                    if (!array_key_exists('fields', $index)) {
223
                        $index = ['fields' => $index];
224
                    }
225
226
                    $index['if_not_exists'] = true;
227
                    try {
228
                        $space->addIndex($index);
229
                    } catch (Exception $e) {
230
                        $presentation = json_encode($properties['indexes'][$i]);
231
                        throw new Exception("Failed to add index $presentation. ". $e->getMessage(), 0, $e);
232
                    }
233
                }
234
            }
235
        }
236
        foreach ($schema->getSpaces() as $space) {
237
            if ($space->isSystem()) {
238
                continue;
239
            }
240
            if (!count($space->getIndexes())) {
241
                if (!$space->hasProperty('id')) {
242
                    throw new Exception("No primary index on ". $space->getName());
243
                }
244
                $space->addIndex(['id']);
245
            }
246
        }
247
248
        foreach ($computes as $spaceName) {
249
            $method = new ReflectionMethod($this->entityMapping[$spaceName], 'compute');
250
            $type = (string) $method->getParameters()[0]->getType();
251
            $sourceSpace = array_search($type, $this->entityMapping);
252
            if (!$sourceSpace) {
253
                throw new Exception("Invalid compute source $type");
254
            }
255
            $compute = Closure::fromCallable([$this->entityMapping[$spaceName], 'compute']);
256
            $this->mapper->getPlugin(Compute::class)->register($sourceSpace, $spaceName, $compute);
257
        }
258
259
        foreach ($this->entityClasses as $entity) {
260
            if ($this->isExtension($entity)) {
261
                continue;
262
            }
263
            if (in_array($entity, $this->extensions)) {
264
                $spaceName = $this->getSpaceName($entity);
265
                $space = $schema->getSpace($spaceName);
266
                if (!$space->hasProperty('class')) {
267
                    throw new Exception("$entity has extensions, but not class property is defined");
268
                }
269
                if ($space->castIndex(['class' => ''], true) === null) {
270
                    $space->addIndex('class', [
0 ignored issues
show
Unused Code introduced by
The call to Space::addIndex() has too many arguments starting with array('unique' => true).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
271
                        'unique' => true,
272
                    ]);
273
                }
274
            }
275
        }
276
277
        if ($extensionInstances) {
278
            foreach ($this->extensions as $class => $target) {
279
                $space = $this->getSpaceName($target);
280
                $this->mapper->findOrCreate($space, [
281
                    'class' => $class,
282
                ]);
283
            }
284
        }
285
286
        return $this;
287
    }
288
289
    public function getEntityClass(Space $space, array $data) : ?string
290
    {
291
        $class = parent::getEntityClass($space, $data);
292
        if (in_array($class, $this->getExtensions())) {
293
            if (!array_key_exists('class', $data) || !$data['class']) {
294
                throw new LogicException("Extension without class defined");
295
            }
296
            return $data['class'];
297
        }
298
        return $class;
299
    }
300
301
    public function setEntityPostfix(?string $postfix) : self
302
    {
303
        $this->entityPostfix = $postfix;
304
        return $this;
305
    }
306
307
    public function setRepositoryPostfix(?string $postfix) : self
308
    {
309
        $this->repositoryPostifx = $postfix;
310
        return $this;
311
    }
312
313
    private $spaceNames = [];
314
315
    public function getRepositorySpaceName($class) : string
316
    {
317
        return array_search($class, $this->repositoryMapping);
318
    }
319
320
    public function getSpaceName(string $class) : string
321
    {
322
        if (!array_key_exists($class, $this->spaceNames)) {
323
            $reflection = new ReflectionClass($class);
324
            $className = $reflection->getShortName();
325
326
            if ($reflection->isSubclassOf(Repository::class)) {
327
                if ($this->repositoryPostifx) {
328
                    $className = substr($className, 0, strlen($className) - strlen($this->repositoryPostifx));
329
                }
330
            }
331
332
            if ($reflection->isSubclassOf(Entity::class)) {
333
                if ($this->entityPostfix) {
334
                    $className = substr($className, 0, strlen($className) - strlen($this->entityPostfix));
335
                }
336
            }
337
338
            $this->spaceNames[$class] = $this->mapper->getSchema()->toUnderscore($className);
339
        }
340
341
        return $this->spaceNames[$class];
342
    }
343
344
    private $tarantoolTypes = [];
345
346
    private function isReference(string $type) : bool
347
    {
348
        return $type[0] == '\\';
349
    }
350
351
    private function getTarantoolType(string $type) : string
352
    {
353
        static $map;
354
        if (!$map) {
355
            $map = [
356
                'mixed' => '*',
357
                'array' => '*',
358
                'float' => 'number',
359
                'int' => 'unsigned',
360
            ];
361
        }
362
363
        if (array_key_exists($type, $map)) {
364
            return $map[$type];
365
        }
366
367
        return $this->isReference($type) ? 'unsigned' : 'string';
368
    }
369
}
370