Passed
Pull Request — develop (#279)
by Mikaël
10:56 queued 07:51
created

AbstractModel   F

Complexity

Total Complexity 91

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Test Coverage

Coverage 98.48%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 173
c 1
b 0
f 1
dl 0
loc 452
ccs 194
cts 197
cp 0.9848
rs 2
wmc 91

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getContextualPart() 0 3 1
A getCleanName() 0 3 1
A isAbstract() 0 3 1
A setInheritance() 0 5 1
A setAbstract() 0 5 1
A getMetaValue() 0 5 2
A getOwner() 0 3 1
A nameIsClean() 0 3 2
A getExtendsClassName() 0 11 4
A __construct() 0 4 1
A getInheritance() 0 3 1
A setName() 0 5 1
A getExtends() 0 3 1
A setDocumentation() 0 4 2
A getPackagedName() 0 20 5
A getInheritedModel() 0 3 1
A getName() 0 3 1
A getMetaValueFirstSet() 0 10 3
A getMeta() 0 3 1
A setMeta() 0 5 1
A setOwner() 0 5 1
A purgeUniqueNames() 0 3 1
A replaceReservedMethod() 0 18 5
A getReservedMethodsInstance() 0 3 1
A cleanString() 0 3 1
A getSubDirectory() 0 8 2
A purgePhpReservedKeywords() 0 3 1
A uniqueName() 0 18 4
A replacePhpReservedKeyword() 0 18 5
A getDocSubPackages() 0 3 1
A instanceFromSerializedJson() 0 17 4
A jsonSerialize() 0 8 1
A getNamespace() 0 14 3
A checkSerializedJson() 0 12 4
C addMeta() 0 25 15
B mergeMeta() 0 39 10

How to fix   Complexity   

Complex Class

Complex classes like AbstractModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractModel, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace WsdlToPhp\PackageGenerator\Model;
6
7
use InvalidArgumentException;
8
use JsonSerializable;
9
use WsdlToPhp\PackageGenerator\ConfigurationReader\AbstractReservedWord;
10
use WsdlToPhp\PackageGenerator\ConfigurationReader\GeneratorOptions;
11
use WsdlToPhp\PackageGenerator\ConfigurationReader\PhpReservedKeyword;
12
use WsdlToPhp\PackageGenerator\Generator\AbstractGeneratorAware;
13
use WsdlToPhp\PackageGenerator\Generator\Generator;
14
use WsdlToPhp\PackageGenerator\Generator\Utils as GeneratorUtils;
15
16
/**
17
 * Class AbstractModel defines the basic properties and methods to operations and structs extracted from the WSDL.
18
 */
19
abstract class AbstractModel extends AbstractGeneratorAware implements JsonSerializable
20
{
21
    public const META_DOCUMENTATION = 'documentation';
22
23
    /**
24
     * Original name of the element.
25
     *
26
     * @var mixed
27
     */
28
    protected $name;
29
30
    /**
31
     * Values associated to the operation.
32
     *
33
     * @var string[]
34
     */
35
    protected array $meta = [];
36
37
    /**
38
     * Define the inheritance of a struct by the name of the parent struct or type.
39
     */
40
    protected string $inheritance = '';
41
42
    /**
43
     * Store the object which owns the current model.
44
     */
45
    protected ?AbstractModel $owner = null;
46
47
    /**
48
     * Indicates that the current element is an abstract element.
49
     * It allows to generated an abstract class.
50
     * This will happen for element/complexType that are defined with abstract="true".
51
     */
52
    protected bool $isAbstract = false;
53
54
    /**
55
     * Replaced keywords time in order to generate unique new keyword.
56
     */
57
    protected static array $replacedPhpReservedKeywords = [];
58
59
    /**
60
     * Replaced methods time in order to generate unique new method.
61
     */
62
    protected array $replacedReservedMethods = [];
63
64
    /**
65
     * Unique name generated in order to ensure unique naming (for struct constructor and setters/getters even for different case attribute name with same value).
66
     */
67
    protected static array $uniqueNames = [];
68
69 546
    public function __construct(Generator $generator, $name)
70
    {
71 546
        parent::__construct($generator);
72 546
        $this->setName($name);
73
    }
74
75 172
    public function getExtendsClassName(): string
76
    {
77 172
        $extends = '';
78 172
        if (($model = $this->getInheritedModel()) instanceof Struct && $model->isStruct()) {
79 14
            $extends = $model->getPackagedName($model->isRestriction());
80
        }
81 172
        if (empty($extends)) {
82 162
            $extends = $this->getExtends(true);
83
        }
84
85 172
        return $extends;
86
    }
87
88 404
    public function getInheritance(): string
89
    {
90 404
        return $this->inheritance;
91
    }
92
93 326
    public function setInheritance(string $inheritance = ''): self
94
    {
95 326
        $this->inheritance = $inheritance;
96
97 326
        return $this;
98
    }
99
100 172
    public function getInheritedModel(): ?Struct
101
    {
102 172
        return $this->getGenerator()->getStructByName($this->getInheritance());
103
    }
104
105 244
    public function getMeta(): array
106
    {
107 244
        return $this->meta;
108
    }
109
110 216
    public function setMeta(array $meta = []): self
111
    {
112 216
        $this->meta = $meta;
113
114 216
        return $this;
115
    }
116
117 76
    public function addMeta(string $metaName, $metaValue): self
118
    {
119 76
        if (!is_scalar($metaName) || (!is_scalar($metaValue) && !is_array($metaValue))) {
0 ignored issues
show
introduced by
The condition is_scalar($metaName) is always true.
Loading history...
120 2
            throw new InvalidArgumentException(sprintf('Invalid meta name "%s" or value "%s". Please provide scalar meta name and scalar or array meta value.', gettype($metaName), gettype($metaValue)), __LINE__);
121
        }
122 74
        $metaValue = is_scalar($metaValue) ? ((is_numeric($metaValue) || is_bool($metaValue) ? $metaValue : trim($metaValue))) : $metaValue;
123 74
        if (is_scalar($metaValue) || is_array($metaValue)) {
124 74
            if (!array_key_exists($metaName, $this->meta)) {
125 72
                $this->meta[$metaName] = $metaValue;
126 22
            } elseif (is_array($this->meta[$metaName]) && is_array($metaValue)) {
0 ignored issues
show
introduced by
The condition is_array($metaValue) is always false.
Loading history...
127 10
                $this->meta[$metaName] = array_merge($this->meta[$metaName], $metaValue);
128 12
            } elseif (is_array($this->meta[$metaName])) {
129 8
                array_push($this->meta[$metaName], $metaValue);
130 10
            } elseif (array_key_exists($metaName, $this->meta) && $metaValue !== $this->meta[$metaName]) {
131 6
                $this->meta[$metaName] = [
132 6
                    $this->meta[$metaName],
133 6
                    $metaValue,
134 6
                ];
135
            } else {
136 6
                $this->meta[$metaName] = $metaValue;
137
            }
138 74
            ksort($this->meta);
139
        }
140
141 74
        return $this;
142
    }
143
144 12
    public function setDocumentation(string $documentation): self
145
    {
146 12
        return $this->addMeta(self::META_DOCUMENTATION, is_array($documentation) ? $documentation : [
0 ignored issues
show
introduced by
The condition is_array($documentation) is always false.
Loading history...
147 12
            $documentation,
148 12
        ]);
149
    }
150
151 190
    public function getMetaValue(string $metaName, $fallback = null)
152
    {
153 190
        $meta = $this->getMeta();
154
155 190
        return array_key_exists($metaName, $meta) ? $meta[$metaName] : $fallback;
156
    }
157
158 124
    public function getMetaValueFirstSet(array $names, $fallback = null)
159
    {
160 124
        $meta = $this->getMeta();
161 124
        foreach ($names as $name) {
162 124
            if (array_key_exists($name, $meta)) {
163 90
                return $meta[$name];
164
            }
165
        }
166
167 104
        return $fallback;
168
    }
169
170 542
    public function getName()
171
    {
172 542
        return $this->name;
173
    }
174
175 546
    public function setName($name): self
176
    {
177 546
        $this->name = $name;
178
179 546
        return $this;
180
    }
181
182 422
    public function getCleanName(bool $keepMultipleUnderscores = true): string
183
    {
184 422
        return self::cleanString($this->getName(), $keepMultipleUnderscores);
185
    }
186
187 440
    public function getOwner(): ?AbstractModel
188
    {
189 440
        return $this->owner;
190
    }
191
192 448
    public function setOwner(?AbstractModel $owner = null): self
193
    {
194 448
        $this->owner = $owner;
195
196 448
        return $this;
197
    }
198
199 174
    public function isAbstract(): bool
200
    {
201 174
        return $this->isAbstract;
202
    }
203
204 214
    public function setAbstract(bool $isAbstract): self
205
    {
206 214
        $this->isAbstract = $isAbstract;
207
208 214
        return $this;
209
    }
210
211 112
    public function nameIsClean(): bool
212
    {
213 112
        return '' !== $this->getName() && $this->getName() === $this->getCleanName();
214
    }
215
216 414
    public function getPackagedName(bool $namespaced = false): string
217
    {
218 414
        $nameParts = [];
219 414
        if ($namespaced && !empty($this->getNamespace())) {
220 158
            $nameParts[] = sprintf('\%s\\', $this->getNamespace());
221
        }
222
223 414
        $cleanName = $this->getCleanName();
224 414
        if (!empty($this->getGenerator()->getOptionPrefix())) {
225 284
            $nameParts[] = $this->getGenerator()->getOptionPrefix();
226
        } else {
227 148
            $cleanName = self::replacePhpReservedKeyword($cleanName);
228
        }
229
230 414
        $nameParts[] = ucfirst(self::uniqueName($cleanName, $this->getContextualPart()));
231 414
        if (!empty($this->getGenerator()->getOptionSuffix())) {
232 12
            $nameParts[] = $this->getGenerator()->getOptionSuffix();
233
        }
234
235 414
        return implode('', $nameParts);
236
    }
237
238
    /**
239
     * Allows to define the contextual part of the class name for the package.
240
     */
241 52
    public function getContextualPart(): string
242
    {
243 52
        return '';
244
    }
245
246 22
    public function getExtends(bool $short = false): ?string
247
    {
248 22
        return '';
249
    }
250
251 230
    public function getNamespace(): string
252
    {
253 230
        $namespaces = [];
254 230
        $namespace = $this->getGenerator()->getOptionNamespacePrefix();
255
256 230
        if (!empty($namespace)) {
257 6
            $namespaces[] = $namespace;
258
        }
259
260 230
        if (!empty($this->getSubDirectory())) {
261 214
            $namespaces[] = str_replace('/', '\\', $this->getSubDirectory());
262
        }
263
264 230
        return implode('\\', $namespaces);
265
    }
266
267 242
    public function getSubDirectory(): string
268
    {
269 242
        $subDirectory = '';
270 242
        if (GeneratorOptions::VALUE_CAT === $this->getGenerator()->getOptionCategory()) {
271 240
            $subDirectory = $this->getContextualPart();
272
        }
273
274 242
        return $subDirectory;
275
    }
276
277
    /**
278
     * Returns the sub package name which the model belongs to
279
     * Must be overridden by sub classes.
280
     */
281 2
    public function getDocSubPackages(): array
282
    {
283 2
        return [];
284
    }
285
286 424
    public static function cleanString(string $string, bool $keepMultipleUnderscores = true): string
287
    {
288 424
        return GeneratorUtils::cleanString($string, $keepMultipleUnderscores);
289
    }
290
291 362
    public static function replacePhpReservedKeyword(string $keyword, ?string $context = null): string
292
    {
293 362
        if (PhpReservedKeyword::instance()->is($keyword)) {
294 74
            if (!is_null($context)) {
295 38
                $keywordKey = $keyword.'_'.$context;
296 38
                if (!array_key_exists($keywordKey, self::$replacedPhpReservedKeywords)) {
297 24
                    self::$replacedPhpReservedKeywords[$keywordKey] = 0;
298
                } else {
299 14
                    ++self::$replacedPhpReservedKeywords[$keywordKey];
300
                }
301
302 38
                return '_'.$keyword.(self::$replacedPhpReservedKeywords[$keywordKey] ? '_'.self::$replacedPhpReservedKeywords[$keywordKey] : '');
303
            }
304
305 58
            return '_'.$keyword;
306
        }
307
308 362
        return $keyword;
309
    }
310
311 2
    public function getReservedMethodsInstance(): AbstractReservedWord
312
    {
313 2
        throw new InvalidArgumentException(sprintf('The method %s should be defined in the class %s', __FUNCTION__, get_called_class()));
314
    }
315
316 386
    public function replaceReservedMethod(string $methodName, ?string $context = null): string
317
    {
318 386
        if ($this->getReservedMethodsInstance()->is($methodName)) {
319 12
            if (!is_null($context)) {
320 12
                $methodKey = $methodName.'_'.$context;
321 12
                if (!array_key_exists($methodKey, $this->replacedReservedMethods)) {
322 12
                    $this->replacedReservedMethods[$methodKey] = 0;
323
                } else {
324
                    ++$this->replacedReservedMethods[$methodKey];
325
                }
326
327 12
                return '_'.$methodName.($this->replacedReservedMethods[$methodKey] ? '_'.$this->replacedReservedMethods[$methodKey] : '');
328
            }
329
330 2
            return '_'.$methodName;
331
        }
332
333 378
        return $methodName;
334
    }
335
336
    /**
337
     * Gives the availability for test purpose and multiple package generation to purge unique names.
338
     */
339 416
    public static function purgeUniqueNames()
340
    {
341 416
        self::$uniqueNames = [];
342
    }
343
344
    /**
345
     * Gives the availability for test purpose and multiple package generation to purge reserved keywords usage.
346
     */
347 404
    public static function purgePhpReservedKeywords()
348
    {
349 404
        self::$replacedPhpReservedKeywords = [];
350
    }
351
352 6
    public function jsonSerialize(): array
353
    {
354 6
        return array_merge($this->toJsonSerialize(), [
355 6
            'inheritance' => $this->inheritance,
356 6
            'abstract' => $this->isAbstract,
357 6
            'meta' => $this->meta,
358 6
            'name' => $this->name,
359 6
            '__CLASS__' => get_called_class(),
360 6
        ]);
361
    }
362
363 212
    public static function instanceFromSerializedJson(Generator $generator, array $args): self
364
    {
365 212
        self::checkSerializedJson($args);
366 212
        $class = $args['__CLASS__'];
367 212
        $instance = new $class($generator, $args['name']);
368 212
        unset($args['name'], $args['__CLASS__']);
369 212
        foreach ($args as $arg => $value) {
370 212
            $setFromSerializedJson = sprintf('set%sFromSerializedJson', ucfirst($arg));
371 212
            $set = sprintf('set%s', ucfirst($arg));
372 212
            if (method_exists($instance, $setFromSerializedJson)) {
373 212
                $instance->{$setFromSerializedJson}($value);
374 212
            } elseif (method_exists($instance, $set)) {
375 212
                $instance->{$set}($value);
376
            }
377
        }
378
379 212
        return $instance;
380
    }
381
382
    /**
383
     * Allows to merge meta from different sources and ensure consistency of their order
384
     * Must be passed as less important (at first position) to most important (last position).
385
     */
386 196
    protected function mergeMeta(): array
387
    {
388 196
        $meta = func_get_args();
389 196
        $mergedMeta = [];
390 196
        $metaDocumentation = [];
391
        // gather meta
392 196
        foreach ($meta as $metaItem) {
393 196
            foreach ($metaItem as $metaName => $metaValue) {
394 156
                if (self::META_DOCUMENTATION === $metaName) {
395 42
                    $metaDocumentation = array_merge($metaDocumentation, $metaValue);
396 148
                } elseif (!array_key_exists($metaName, $mergedMeta)) {
397 148
                    $mergedMeta[$metaName] = $metaValue;
398 10
                } elseif (is_array($mergedMeta[$metaName]) && is_array($metaValue)) {
399
                    $mergedMeta[$metaName] = array_merge($mergedMeta[$metaName], $metaValue);
400 10
                } elseif (is_array($mergedMeta[$metaName])) {
401
                    $mergedMeta[$metaName][] = $metaValue;
402
                } else {
403 10
                    $mergedMeta[$metaName] = $metaValue;
404
                }
405
            }
406
        }
407
408
        // sort by key
409 196
        ksort($mergedMeta);
410
411
        // add documentation if any at first position
412 196
        if (!empty($metaDocumentation)) {
413 42
            $definitiveMeta = [
414 42
                self::META_DOCUMENTATION => array_unique(array_reverse($metaDocumentation)),
415 42
            ];
416 42
            foreach ($mergedMeta as $metaName => $metaValue) {
417 34
                $definitiveMeta[$metaName] = $metaValue;
418
            }
419 42
            $mergedMeta = $definitiveMeta;
420 42
            unset($definitiveMeta);
421
        }
422 196
        unset($meta, $metaDocumentation);
423
424 196
        return $mergedMeta;
425
    }
426
427
    /**
428
     * Static method which returns a unique name case sensitively
429
     * Useful to name methods case sensitively distinct, see http://the-echoplex.net/log/php-case-sensitivity.
430
     *
431
     * @param string $name    the original name
432
     * @param string $context the context where the name is needed unique
433
     */
434 418
    protected static function uniqueName(string $name, string $context): string
435
    {
436 418
        $insensitiveKey = mb_strtolower($name.'_'.$context);
437 418
        $sensitiveKey = $name.'_'.$context;
438 418
        if (array_key_exists($sensitiveKey, self::$uniqueNames)) {
439 388
            return self::$uniqueNames[$sensitiveKey];
440
        }
441
442 362
        if (!array_key_exists($insensitiveKey, self::$uniqueNames)) {
443 360
            self::$uniqueNames[$insensitiveKey] = 0;
444
        } else {
445 18
            ++self::$uniqueNames[$insensitiveKey];
446
        }
447
448 362
        $uniqueName = $name.(self::$uniqueNames[$insensitiveKey] ? '_'.self::$uniqueNames[$insensitiveKey] : '');
449 362
        self::$uniqueNames[$sensitiveKey] = $uniqueName;
450
451 362
        return $uniqueName;
452
    }
453
454
    /**
455
     * Must return the properties of the inherited class.
456
     */
457
    abstract protected function toJsonSerialize(): array;
458
459 212
    protected static function checkSerializedJson(array $args): void
460
    {
461 212
        if (!array_key_exists('__CLASS__', $args)) {
462 2
            throw new InvalidArgumentException(sprintf('__CLASS__ key is missing from "%s"', var_export($args, true)));
463
        }
464
465 212
        if (!class_exists($args['__CLASS__'])) {
466 2
            throw new InvalidArgumentException(sprintf('Class "%s" is unknown', $args['__CLASS__']));
467
        }
468
469 212
        if (!array_key_exists('name', $args)) {
470 2
            throw new InvalidArgumentException(sprintf('name key is missing from "%s"', var_export($args, true)));
471
        }
472
    }
473
}
474