Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.14% covered (success)
98.14%
158 / 161
94.44% covered (success)
94.44%
34 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractModel
98.14% covered (success)
98.14%
158 / 161
94.44% covered (success)
94.44%
34 / 36
91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getExtendsClassName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getInheritance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInheritance
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getInheritedModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMeta
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMeta
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addMeta
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
15
 setDocumentation
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMetaValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getMetaValueFirstSet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCleanName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOwner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOwner
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAbstract
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAbstract
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 nameIsClean
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getPackagedName
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getContextualPart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtends
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespace
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getSubDirectory
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getDocSubPackages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cleanString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replacePhpReservedKeyword
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getReservedMethodsInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceReservedMethod
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 purgeUniqueNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 purgePhpReservedKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 instanceFromSerializedJson
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 mergeMeta
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
10.05
 uniqueName
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 toJsonSerialize
n/a
0 / 0
n/a
0 / 0
0
 checkSerializedJson
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace WsdlToPhp\PackageGenerator\Model;
6
7use WsdlToPhp\PackageGenerator\ConfigurationReader\AbstractReservedWord;
8use WsdlToPhp\PackageGenerator\ConfigurationReader\GeneratorOptions;
9use WsdlToPhp\PackageGenerator\ConfigurationReader\PhpReservedKeyword;
10use WsdlToPhp\PackageGenerator\Generator\AbstractGeneratorAware;
11use WsdlToPhp\PackageGenerator\Generator\Generator;
12use 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 */
17abstract 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    public function __construct(Generator $generator, $name)
68    {
69        parent::__construct($generator);
70        $this->setName($name);
71    }
72
73    public function getExtendsClassName(): string
74    {
75        $extends = '';
76        if (($model = $this->getInheritedModel()) instanceof Struct && $model->isStruct()) {
77            $extends = $model->getPackagedName($model->isRestriction());
78        }
79        if (empty($extends)) {
80            $extends = $this->getExtends(true);
81        }
82
83        return $extends;
84    }
85
86    public function getInheritance(): string
87    {
88        return $this->inheritance;
89    }
90
91    public function setInheritance(string $inheritance = ''): self
92    {
93        $this->inheritance = $inheritance;
94
95        return $this;
96    }
97
98    public function getInheritedModel(): ?Struct
99    {
100        return $this->getGenerator()->getStructByName($this->getInheritance());
101    }
102
103    public function getMeta(): array
104    {
105        return $this->meta;
106    }
107
108    public function setMeta(array $meta = []): self
109    {
110        $this->meta = $meta;
111
112        return $this;
113    }
114
115    public function addMeta(string $metaName, $metaValue): self
116    {
117        if (!is_scalar($metaName) || (!is_scalar($metaValue) && !is_array($metaValue))) {
118            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        $metaValue = is_scalar($metaValue) ? (is_numeric($metaValue) || is_bool($metaValue) ? $metaValue : trim($metaValue)) : $metaValue;
121        if (is_scalar($metaValue) || is_array($metaValue)) {
122            if (!array_key_exists($metaName, $this->meta)) {
123                $this->meta[$metaName] = $metaValue;
124            } elseif (is_array($this->meta[$metaName]) && is_array($metaValue)) {
125                $this->meta[$metaName] = array_merge($this->meta[$metaName], $metaValue);
126            } elseif (is_array($this->meta[$metaName])) {
127                array_push($this->meta[$metaName], $metaValue);
128            } elseif (array_key_exists($metaName, $this->meta) && $metaValue !== $this->meta[$metaName]) {
129                $this->meta[$metaName] = [
130                    $this->meta[$metaName],
131                    $metaValue,
132                ];
133            } else {
134                $this->meta[$metaName] = $metaValue;
135            }
136            ksort($this->meta);
137        }
138
139        return $this;
140    }
141
142    public function setDocumentation(string $documentation): self
143    {
144        return $this->addMeta(self::META_DOCUMENTATION, is_array($documentation) ? $documentation : [
145            $documentation,
146        ]);
147    }
148
149    public function getMetaValue(string $metaName, $fallback = null)
150    {
151        $meta = $this->getMeta();
152
153        return array_key_exists($metaName, $meta) ? $meta[$metaName] : $fallback;
154    }
155
156    public function getMetaValueFirstSet(array $names, $fallback = null)
157    {
158        $meta = $this->getMeta();
159        foreach ($names as $name) {
160            if (array_key_exists($name, $meta)) {
161                return $meta[$name];
162            }
163        }
164
165        return $fallback;
166    }
167
168    public function getName()
169    {
170        return $this->name;
171    }
172
173    public function setName($name): self
174    {
175        $this->name = $name;
176
177        return $this;
178    }
179
180    public function getCleanName(bool $keepMultipleUnderscores = true): string
181    {
182        return self::cleanString($this->getName(), $keepMultipleUnderscores);
183    }
184
185    public function getOwner(): ?AbstractModel
186    {
187        return $this->owner;
188    }
189
190    public function setOwner(?AbstractModel $owner = null): self
191    {
192        $this->owner = $owner;
193
194        return $this;
195    }
196
197    public function isAbstract(): bool
198    {
199        return $this->isAbstract;
200    }
201
202    public function setAbstract(bool $isAbstract): self
203    {
204        $this->isAbstract = $isAbstract;
205
206        return $this;
207    }
208
209    public function nameIsClean(): bool
210    {
211        return '' !== $this->getName() && $this->getName() === $this->getCleanName();
212    }
213
214    public function getPackagedName(bool $namespaced = false): string
215    {
216        $nameParts = [];
217        if ($namespaced && !empty($this->getNamespace())) {
218            $nameParts[] = sprintf('\%s\\', $this->getNamespace());
219        }
220
221        $cleanName = $this->getCleanName();
222        if (!empty($this->getGenerator()->getOptionPrefix())) {
223            $nameParts[] = $this->getGenerator()->getOptionPrefix();
224        } else {
225            $cleanName = self::replacePhpReservedKeyword($cleanName);
226        }
227
228        $nameParts[] = ucfirst(self::uniqueName($cleanName, $this->getContextualPart()));
229        if (!empty($this->getGenerator()->getOptionSuffix())) {
230            $nameParts[] = $this->getGenerator()->getOptionSuffix();
231        }
232
233        return implode('', $nameParts);
234    }
235
236    /**
237     * Allows to define the contextual part of the class name for the package.
238     */
239    public function getContextualPart(): string
240    {
241        return '';
242    }
243
244    public function getExtends(bool $short = false): ?string
245    {
246        return '';
247    }
248
249    public function getNamespace(): string
250    {
251        $namespaces = [];
252        $namespace = $this->getGenerator()->getOptionNamespacePrefix();
253
254        if (!empty($namespace)) {
255            $namespaces[] = $namespace;
256        }
257
258        if (!empty($this->getSubDirectory())) {
259            $namespaces[] = str_replace('/', '\\', $this->getSubDirectory());
260        }
261
262        return implode('\\', $namespaces);
263    }
264
265    public function getSubDirectory(): string
266    {
267        $subDirectory = '';
268        if (GeneratorOptions::VALUE_CAT === $this->getGenerator()->getOptionCategory()) {
269            $subDirectory = $this->getContextualPart();
270        }
271
272        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    public function getDocSubPackages(): array
280    {
281        return [];
282    }
283
284    public static function cleanString(string $string, bool $keepMultipleUnderscores = true): string
285    {
286        return GeneratorUtils::cleanString($string, $keepMultipleUnderscores);
287    }
288
289    public static function replacePhpReservedKeyword(string $keyword, ?string $context = null): string
290    {
291        if (PhpReservedKeyword::instance()->is($keyword)) {
292            if (!is_null($context)) {
293                $keywordKey = $keyword.'_'.$context;
294                if (!array_key_exists($keywordKey, self::$replacedPhpReservedKeywords)) {
295                    self::$replacedPhpReservedKeywords[$keywordKey] = 0;
296                } else {
297                    ++self::$replacedPhpReservedKeywords[$keywordKey];
298                }
299
300                return '_'.$keyword.(self::$replacedPhpReservedKeywords[$keywordKey] ? '_'.self::$replacedPhpReservedKeywords[$keywordKey] : '');
301            }
302
303            return '_'.$keyword;
304        }
305
306        return $keyword;
307    }
308
309    public function getReservedMethodsInstance(): AbstractReservedWord
310    {
311        throw new \InvalidArgumentException(sprintf('The method %s should be defined in the class %s', __FUNCTION__, static::class));
312    }
313
314    public function replaceReservedMethod(string $methodName, ?string $context = null): string
315    {
316        if ($this->getReservedMethodsInstance()->is($methodName)) {
317            if (!is_null($context)) {
318                $methodKey = $methodName.'_'.$context;
319                if (!array_key_exists($methodKey, $this->replacedReservedMethods)) {
320                    $this->replacedReservedMethods[$methodKey] = 0;
321                } else {
322                    ++$this->replacedReservedMethods[$methodKey];
323                }
324
325                return '_'.$methodName.($this->replacedReservedMethods[$methodKey] ? '_'.$this->replacedReservedMethods[$methodKey] : '');
326            }
327
328            return '_'.$methodName;
329        }
330
331        return $methodName;
332    }
333
334    /**
335     * Gives the availability for test purpose and multiple package generation to purge unique names.
336     */
337    public static function purgeUniqueNames()
338    {
339        self::$uniqueNames = [];
340    }
341
342    /**
343     * Gives the availability for test purpose and multiple package generation to purge reserved keywords usage.
344     */
345    public static function purgePhpReservedKeywords()
346    {
347        self::$replacedPhpReservedKeywords = [];
348    }
349
350    public function jsonSerialize(): array
351    {
352        return array_merge($this->toJsonSerialize(), [
353            'inheritance' => $this->inheritance,
354            'abstract' => $this->isAbstract,
355            'meta' => $this->meta,
356            'name' => $this->name,
357            '__CLASS__' => static::class,
358        ]);
359    }
360
361    public static function instanceFromSerializedJson(Generator $generator, array $args): self
362    {
363        self::checkSerializedJson($args);
364        $class = $args['__CLASS__'];
365        $instance = new $class($generator, $args['name']);
366        unset($args['name'], $args['__CLASS__']);
367        foreach ($args as $arg => $value) {
368            $setFromSerializedJson = sprintf('set%sFromSerializedJson', ucfirst($arg));
369            $set = sprintf('set%s', ucfirst($arg));
370            if (method_exists($instance, $setFromSerializedJson)) {
371                $instance->{$setFromSerializedJson}($value);
372            } elseif (method_exists($instance, $set)) {
373                $instance->{$set}($value);
374            }
375        }
376
377        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    protected function mergeMeta(): array
385    {
386        $meta = func_get_args();
387        $mergedMeta = [];
388        $metaDocumentation = [];
389        // gather meta
390        foreach ($meta as $metaItem) {
391            foreach ($metaItem as $metaName => $metaValue) {
392                if (self::META_DOCUMENTATION === $metaName) {
393                    $metaDocumentation = array_merge($metaDocumentation, $metaValue);
394                } elseif (!array_key_exists($metaName, $mergedMeta)) {
395                    $mergedMeta[$metaName] = $metaValue;
396                } elseif (is_array($mergedMeta[$metaName]) && is_array($metaValue)) {
397                    $mergedMeta[$metaName] = array_merge($mergedMeta[$metaName], $metaValue);
398                } elseif (is_array($mergedMeta[$metaName])) {
399                    $mergedMeta[$metaName][] = $metaValue;
400                } else {
401                    $mergedMeta[$metaName] = $metaValue;
402                }
403            }
404        }
405
406        // sort by key
407        ksort($mergedMeta);
408
409        // add documentation if any at first position
410        if (!empty($metaDocumentation)) {
411            $definitiveMeta = [
412                self::META_DOCUMENTATION => array_unique(array_reverse($metaDocumentation)),
413            ];
414            foreach ($mergedMeta as $metaName => $metaValue) {
415                $definitiveMeta[$metaName] = $metaValue;
416            }
417            $mergedMeta = $definitiveMeta;
418            unset($definitiveMeta);
419        }
420        unset($meta, $metaDocumentation);
421
422        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    protected static function uniqueName(string $name, string $context): string
433    {
434        $insensitiveKey = mb_strtolower($name.'_'.$context);
435        $sensitiveKey = $name.'_'.$context;
436        if (array_key_exists($sensitiveKey, self::$uniqueNames)) {
437            return self::$uniqueNames[$sensitiveKey];
438        }
439
440        if (!array_key_exists($insensitiveKey, self::$uniqueNames)) {
441            self::$uniqueNames[$insensitiveKey] = 0;
442        } else {
443            ++self::$uniqueNames[$insensitiveKey];
444        }
445
446        $uniqueName = $name.(self::$uniqueNames[$insensitiveKey] ? '_'.self::$uniqueNames[$insensitiveKey] : '');
447        self::$uniqueNames[$sensitiveKey] = $uniqueName;
448
449        return $uniqueName;
450    }
451
452    /**
453     * Must return the properties of the inherited class.
454     */
455    abstract protected function toJsonSerialize(): array;
456
457    protected static function checkSerializedJson(array $args): void
458    {
459        if (!array_key_exists('__CLASS__', $args)) {
460            throw new \InvalidArgumentException(sprintf('__CLASS__ key is missing from "%s"', var_export($args, true)));
461        }
462
463        if (!class_exists($args['__CLASS__'])) {
464            throw new \InvalidArgumentException(sprintf('Class "%s" is unknown', $args['__CLASS__']));
465        }
466
467        if (!array_key_exists('name', $args)) {
468            throw new \InvalidArgumentException(sprintf('name key is missing from "%s"', var_export($args, true)));
469        }
470    }
471}