Passed
Pull Request — master (#50)
by Alex
03:53
created

EdmDirectValueAnnotationsManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
6
namespace AlgoWeb\ODataMetadata\Library\Annotations;
7
8
use AlgoWeb\ODataMetadata\EdmConstants;
9
use AlgoWeb\ODataMetadata\Exception\InvalidOperationException;
10
use AlgoWeb\ODataMetadata\Interfaces\Annotations\IDirectValueAnnotation;
11
use AlgoWeb\ODataMetadata\Interfaces\Annotations\IDirectValueAnnotationBinding;
12
use AlgoWeb\ODataMetadata\Interfaces\Annotations\IDirectValueAnnotationsManager;
13
use AlgoWeb\ODataMetadata\Interfaces\IDocumentation;
14
use AlgoWeb\ODataMetadata\Interfaces\IEdmElement;
15
use AlgoWeb\ODataMetadata\StringConst;
16
use SplObjectStorage;
17
18
/**
19
 * Direct-value annotations manager provides services for setting and getting transient annotations on elements.
20
 *
21
 * @package AlgoWeb\ODataMetadata\Library\Annotations
22
 *
23
 *  An object representing transient annotations is in one of these states:
24
 *    1) Null, if the element has no transient annotations.
25
 *    2) An EdmAnnotation, if the element has exactly one annotation.
26
 *    3) A list of EdmAnnotation, if the element has more than one annotation.
27
 * If the speed of annotation lookup for elements with many annotations becomes a concern, another option
28
 * including a dictionary is possible.
29
 */
30
class EdmDirectValueAnnotationsManager implements IDirectValueAnnotationsManager
31
{
32
    /**
33
     * @var SplObjectStorage|array<IEdmElement, mixed>  keeps track of transient annotations on elements
34
     */
35
    private $annotationsDictionary;
36
37
    /**
38
     * @var array elements for which normal comparison failed to produce a valid result, arbitrarily ordered to enable stable comparisons
39
     */
40
    private $unsortedElements = [];
0 ignored issues
show
introduced by
The private property $unsortedElements is not used, and could be removed.
Loading history...
41
42
    /**
43
     * Initializes a new instance of the EdmDirectValueAnnotationsManager class.
44
     */
45
    public function __construct()
46
    {
47
        $this->annotationsDictionary = new SplObjectStorage();
48
    }
49
50
    /**
51
     * Gets annotations associated with an element.
52
     *
53
     * @param  IEdmElement              $element the annotated element
54
     * @return IDirectValueAnnotation[] The direct value annotations for the element
55
     */
56
    public function getDirectValueAnnotations(IEdmElement $element): iterable
57
    {
58
        // Fetch the annotations dictionary once and only once, because this.annotationsDictionary might get updated by another thread.
59
        $annotationsDictionary = $this->annotationsDictionary;
60
61
        $immutableAnnotations = $this->getAttachedAnnotations($element);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $immutableAnnotations is correct as $this->getAttachedAnnotations($element) targeting AlgoWeb\ODataMetadata\Li...etAttachedAnnotations() 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...
62
        $transientAnnotations = self::getTransientAnnotations($element, $annotationsDictionary);
0 ignored issues
show
Bug introduced by
It seems like $annotationsDictionary can also be of type array<AlgoWeb\ODataMetad...aces\IEdmElement,mixed>; however, parameter $annotationsDictionary of AlgoWeb\ODataMetadata\Li...tTransientAnnotations() does only seem to accept SplObjectStorage, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

62
        $transientAnnotations = self::getTransientAnnotations($element, /** @scrutinizer ignore-type */ $annotationsDictionary);
Loading history...
63
64
        if ($immutableAnnotations != null) {
0 ignored issues
show
introduced by
The condition $immutableAnnotations != null is always false.
Loading history...
65
            /** @var IDirectValueAnnotation $existingAnnotation */
66
            foreach ($immutableAnnotations as $existingAnnotation) {
67
                if (!self::isDead($existingAnnotation->getNamespaceUri(), $existingAnnotation->getName(), $transientAnnotations)) {
68
                    yield $existingAnnotation;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $existingAnnotation returns the type Generator which is incompatible with the documented return type AlgoWeb\ODataMetadata\In...DirectValueAnnotation[].
Loading history...
69
                }
70
            }
71
        }
72
        /** @var IDirectValueAnnotation $existingAnnotation */
73
        foreach (self::transientAnnotations($transientAnnotations) as $existingAnnotation) {
74
            yield  $existingAnnotation;
75
        }
76
    }
77
78
    /**
79
     * Sets an annotation value for an EDM element. If the value is null, no annotation is added and an existing
80
     * annotation with the same name is removed.
81
     *
82
     * @param  IEdmElement                    $element       the annotated element
83
     * @param  string                         $namespaceName namespace that the annotation belongs to
84
     * @param  string                         $localName     name of the annotation within the namespace
85
     * @param  mixed                          $value         the value of the annotation
86
     * @return IDirectValueAnnotationsManager self
87
     */
88
    public function setAnnotationValue(IEdmElement $element, string $namespaceName, string $localName, $value): IDirectValueAnnotationsManager
89
    {
90
        $annotationsDictionary         = $this->getAnnotationsDictionary();
91
        $transientAnnotations          = self::getTransientAnnotations($element, $annotationsDictionary);
92
        $transientAnnotationsBeforeSet = null !== $transientAnnotations ? clone $transientAnnotations : new SplObjectStorage();
93
        self::setAnnotation($this->getAttachedAnnotations($element), $transientAnnotations, $namespaceName, $localName, $value);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getAttachedAnnotations($element) targeting AlgoWeb\ODataMetadata\Li...etAttachedAnnotations() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
94
95
        // There is at least one case (removing an annotation that was not present to begin with) where the transient annotations are not changed,
96
        // so test to see if updating the dictionary is necessary.
97
        if ($transientAnnotations != $transientAnnotationsBeforeSet) {
98
            $annotationsDictionary->offsetSet($element, $transientAnnotations);
99
        }
100
101
        $this->annotationsDictionary = $annotationsDictionary;
102
        return $this;
103
    }
104
105
    /**
106
     * Sets a set of annotation values. If a supplied value is null, no annotation is added and an existing annotation
107
     * with the same name is removed.
108
     * @param  IDirectValueAnnotationBinding[] $annotations The annotations to set
109
     * @return IDirectValueAnnotationsManager  self
110
     */
111
    public function setAnnotationValues(array $annotations): IDirectValueAnnotationsManager
112
    {
113
        /** @var IDirectValueAnnotationBinding $annotation */
114
        foreach ($annotations as $annotation) {
115
            $this->SetAnnotationValue($annotation->getElement(), $annotation->getNamespaceUri(), $annotation->getName(), $annotation->getValue());
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on AlgoWeb\ODataMetadata\In...tValueAnnotationBinding. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
            $this->SetAnnotationValue($annotation->getElement(), $annotation->getNamespaceUri(), $annotation->getName(), $annotation->/** @scrutinizer ignore-call */ getValue());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
116
        }
117
        return $this;
118
    }
119
120
    /**
121
     * @param  IEdmElement $element       the annotated element
122
     * @param  string      $namespaceName namespace that the annotation belongs to
123
     * @param  string      $localName     local name of the annotation
124
     * @return mixed       Returns the annotation value that corresponds to the provided name. Returns null if no
125
     *                                   annotation with the given name exists for the given element.
126
     */
127
    public function getAnnotationValue(IEdmElement $element, string $namespaceName, string $localName)
128
    {
129
        $annotationsDictionary = $this->getAnnotationsDictionary();
130
        $transients            = self::getTransientAnnotations($element, $annotationsDictionary);
131
        $annotation            = self::findTransientAnnotation($transients, $namespaceName, $localName);
132
        if ($annotation != null) {
133
            return $annotation->getValue();
134
        }
135
136
        $immutableAnnotations = $this->getAttachedAnnotations($element);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $immutableAnnotations is correct as $this->getAttachedAnnotations($element) targeting AlgoWeb\ODataMetadata\Li...etAttachedAnnotations() 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...
137
        if ($immutableAnnotations != null) {
0 ignored issues
show
introduced by
The condition $immutableAnnotations != null is always false.
Loading history...
138
            /** @var IDirectValueAnnotation $existingAnnotation */
139
            foreach ($immutableAnnotations as $existingAnnotation) {
140
                if ($existingAnnotation->getNamespaceUri() == $namespaceName && $existingAnnotation->getName() == $localName) {
141
                    // No need to check that the immutable annotation isn't Dead, because if it were
142
                    // the tombstone would have been found in the transient annotations.
143
                    return $existingAnnotation->getValue();
144
                }
145
            }
146
        }
147
148
        return null;
149
    }
150
151
    /**
152
     * Retrieves a set of annotation values. For each requested value, returns null if no annotation with the given
153
     * name exists for the given element.
154
     *
155
     * @param  IDirectValueAnnotationBinding[] $annotations The set of requested annotations
156
     * @return array                           Returns values that correspond to the provided annotations. A value is null if no annotation with
157
     *                                                     the given name exists for the given element.
158
     */
159
    public function getAnnotationValues(array $annotations): ?iterable
160
    {
161
        $values = [];
162
163
        $index = 0;
164
        foreach ($annotations as $annotation) {
165
            $values[$index++] = $this->getAnnotationValue($annotation->getElement(), $annotation->getNamespaceUri(), $annotation->getName());
166
        }
167
168
        return $values;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $values returns the type array which is incompatible with the type-hinted return iterable|null.
Loading history...
169
    }
170
171
    /**
172
     * Retrieves the annotations that are directly attached to an element.
173
     *
174
     * @param  IEdmElement   $element the element in question
175
     * @return iterable|null the annotations that are directly attached to an element (outside the control of the manager)
176
     */
177
    protected function getAttachedAnnotations(IEdmElement $element): ?iterable
0 ignored issues
show
Unused Code introduced by
The parameter $element is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

177
    protected function getAttachedAnnotations(/** @scrutinizer ignore-unused */ IEdmElement $element): ?iterable

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
178
    {
179
        return null;
180
    }
181
182
183
    /**
184
     * Retrieves the transient annotations for an EDM element.
185
     *
186
     * @param  IEdmElement      $element               the annotated element
187
     * @param  SplObjectStorage $annotationsDictionary the dictionary for looking up the element's annotations
188
     * @return mixed|null       The transient annotations for the element, in a form managed by the annotations manager.
189
     *                                                This method is static to guarantee that the annotations dictionary is not fetched more than once per lookup operation.
190
     */
191
    private static function getTransientAnnotations(IEdmElement $element, SplObjectStorage $annotationsDictionary)
192
    {
193
        return $annotationsDictionary->offsetExists($element) ? $annotationsDictionary->offsetGet($element) : null;
194
    }
195
196
    private static function removeTransientAnnotation(&$transientAnnotations, $namespaceName, string $localName)
197
    {
198
        if (null !== $transientAnnotations) {
199
            $singleAnnotation = $transientAnnotations;
200
            if ($singleAnnotation instanceof IDirectValueAnnotation) {
201
                if ($singleAnnotation->getNamespaceUri() == $namespaceName && $singleAnnotation->getName() == $localName) {
202
                    $transientAnnotations = null;
203
                    return;
204
                }
205
            } else {
206
                $annotationsList = $transientAnnotations;
207
                assert(is_array(
208
                    $annotationsList
209
                ));
210
                $numAnnotations = count($annotationsList);
211
                for ($index = 0; $index < $numAnnotations; $index++) {
212
                    $existingAnnotation = $annotationsList[$index];
213
                    assert($existingAnnotation instanceof IDirectValueAnnotation);
214
                    if ($existingAnnotation->getNamespaceUri() == $namespaceName && $existingAnnotation->getName() == $localName) {
215
                        unset($annotationsList[$index]);
216
                        if (count($annotationsList) === 1) {
217
                            $transientAnnotations = array_pop($annotationsList);
218
                        } else {
219
                            $transientAnnotations = $annotationsList;
220
                        }
221
222
                        return;
223
                    }
224
                }
225
            }
226
        }
227
    }
228
    private static function transientAnnotations($transientAnnotations): iterable
229
    {
230
        if ($transientAnnotations == null) {
231
            return [];
232
        }
233
234
        $singleAnnotation = $transientAnnotations;
235
        if ($singleAnnotation instanceof IDirectValueAnnotation) {
236
            if (null !== $singleAnnotation->getValue()) {
237
                yield $singleAnnotation;
238
            }
239
240
            return [];
241
        }
242
243
        $annotationsList = $transientAnnotations;
244
        assert(is_iterable($annotationsList));
245
        /** @var IDirectValueAnnotation $existingAnnotation */
246
        foreach ($annotationsList as $existingAnnotation) {
247
            if (null !== $existingAnnotation->getValue()) {
248
                yield $existingAnnotation;
249
            }
250
        }
251
    }
252
253
    private static function isDead(string $namespaceName, string $localName, $transientAnnotations): bool
254
    {
255
        return self::findTransientAnnotation($transientAnnotations, $namespaceName, $localName) != null;
256
    }
257
258
    private static function setAnnotation(?iterable $immutableAnnotations, &$transientAnnotations, string $namespaceName, string $localName, $value): void
259
    {
260
        $needTombstone = false;
261
        if ($immutableAnnotations != null) {
262
            $filtered = false;
263
            foreach ($immutableAnnotations as $exitingAnnotation) {
264
                if ($exitingAnnotation->getNamespaceUri() == $namespaceName && $exitingAnnotation->getName() == $localName) {
265
                    $filtered = true;
266
                    break;
267
                }
268
            }
269
            if ($filtered) {
270
                $needTombstone = true;
271
            }
272
        }
273
274
        if ($value == null) {
275
            // "Removing" an immutable annotation leaves behind a transient annotation with a null value
276
            // as a tombstone to hide the immutable annotation. The normal logic below makes this happen.
277
            // Removing a transient annotation actually takes the annotation away.
278
            if (!$needTombstone) {
279
                self::removeTransientAnnotation($transientAnnotations, $namespaceName, $localName);
280
                return;
281
            }
282
        }
283
284
        if ($namespaceName == EdmConstants::DocumentationUri && $value != null && !($value instanceof IDocumentation)) {
285
            throw new InvalidOperationException(StringConst::Annotations_DocumentationPun(get_class($value)));
286
        }
287
288
        $newAnnotation = $value != null ?
289
            new EdmDirectValueAnnotation($namespaceName, $localName, $value) :
290
            new EdmDirectValueAnnotation($namespaceName, $localName);
291
292
        if ($transientAnnotations == null) {
293
            $transientAnnotations = $newAnnotation;
294
            return;
295
        }
296
297
        $singleAnnotation = $transientAnnotations;
298
        if ($singleAnnotation instanceof IDirectValueAnnotation) {
299
            if ($singleAnnotation->getNamespaceUri() == $namespaceName && $singleAnnotation->getName() == $localName) {
300
                $transientAnnotations = $newAnnotation;
301
            } else {
302
                $transientAnnotations = [$singleAnnotation, $newAnnotation];
303
            }
304
305
            return;
306
        }
307
308
        $annotationsList = $transientAnnotations;
309
        assert(is_countable($annotationsList));
310
        $numAnnotations = count($annotationsList);
311
        for ($index = 0; $index < $numAnnotations; $index++) {
312
            /** @var IDirectValueAnnotation $existingAnnotation */
313
            $existingAnnotation = $annotationsList[$index];
314
            if ($existingAnnotation->getNamespaceUri() == $namespaceName && $existingAnnotation->getName() == $localName) {
315
                unset($annotationsList[$index]);
316
                break;
317
            }
318
        }
319
        $annotationsList[]    = $newAnnotation;
320
        $transientAnnotations = $annotationsList;
321
    }
322
323
324
    private static function findTransientAnnotation($transientAnnotations, string $namespaceName, string $localName): ?IDirectValueAnnotation
325
    {
326
        if (null !== $transientAnnotations) {
327
            if ($transientAnnotations instanceof IDirectValueAnnotation) {
328
                if ($transientAnnotations->getNamespaceUri() == $namespaceName && $transientAnnotations->getName() == $localName) {
329
                    return $transientAnnotations;
330
                }
331
            } else {
332
                $annotationsList = $transientAnnotations;
333
                assert(is_array($annotationsList));
334
                $filtered = array_filter($annotationsList, function (IDirectValueAnnotation $existingAnnotation) use ($namespaceName, $localName) {
335
                    return $existingAnnotation->getNamespaceUri() == $namespaceName && $existingAnnotation->getName() == $localName;
336
                });
337
                if (count($filtered) === 0) {
338
                    return null;
339
                }
340
                return $annotationsList[0];
341
            }
342
        }
343
344
        return null;
345
    }
346
347
    /**
348
     * @return SplObjectStorage
349
     */
350
    private function getAnnotationsDictionary(): SplObjectStorage
351
    {
352
        return $this->annotationsDictionary;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->annotationsDictionary could return the type array<AlgoWeb\ODataMetad...aces\IEdmElement,mixed> which is incompatible with the type-hinted return SplObjectStorage. Consider adding an additional type-check to rule them out.
Loading history...
353
    }
354
}
355