AbstractModel   F
last analyzed

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 purgeUniqueNames() 0 3 1
A replaceReservedMethod() 0 18 5
A getReservedMethodsInstance() 0 3 1
A getOwner() 0 3 1
A nameIsClean() 0 3 2
A cleanString() 0 3 1
A getExtendsClassName() 0 11 4
C addMeta() 0 25 15
A getSubDirectory() 0 8 2
A __construct() 0 4 1
A purgePhpReservedKeywords() 0 3 1
A getInheritance() 0 3 1
A uniqueName() 0 18 4
B mergeMeta() 0 39 10
A replacePhpReservedKeyword() 0 18 5
A setName() 0 5 1
A getDocSubPackages() 0 3 1
A instanceFromSerializedJson() 0 17 4
A getExtends() 0 3 1
A jsonSerialize() 0 8 1
A setDocumentation() 0 4 2
A getPackagedName() 0 20 5
A getInheritedModel() 0 3 1
A getName() 0 3 1
A getNamespace() 0 14 3
A getMetaValueFirstSet() 0 10 3
A getMeta() 0 3 1
A setMeta() 0 5 1
A setOwner() 0 5 1
A checkSerializedJson() 0 12 4

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