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

transientAnnotations()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 21
rs 9.2222
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\Library\EdmDirectValueAnnotation;
16
use AlgoWeb\ODataMetadata\StringConst;
17
use SplObjectStorage;
18
19
/**
20
 * Direct-value annotations manager provides services for setting and getting transient annotations on elements.
21
 *
22
 * @package AlgoWeb\ODataMetadata\Library\Annotations
23
 *
24
 *  An object representing transient annotations is in one of these states:
25
 *    1) Null, if the element has no transient annotations.
26
 *    2) An EdmAnnotation, if the element has exactly one annotation.
27
 *    3) A list of EdmAnnotation, if the element has more than one annotation.
28
 * If the speed of annotation lookup for elements with many annotations becomes a concern, another option
29
 * including a dictionary is possible.
30
 */
31
class EdmDirectValueAnnotationsManager implements IDirectValueAnnotationsManager
32
{
33
    /**
34
     * @var SplObjectStorage|array<IEdmElement, mixed>  keeps track of transient annotations on elements
35
     */
36
    private $annotationsDictionary;
37
38
    /**
39
     * @var array elements for which normal comparison failed to produce a valid result, arbitrarily ordered to enable stable comparisons
40
     */
41
    private $unsortedElements = [];
0 ignored issues
show
introduced by
The private property $unsortedElements is not used, and could be removed.
Loading history...
42
43
    /**
44
     * Initializes a new instance of the EdmDirectValueAnnotationsManager class.
45
     */
46
    public function __construct()
47
    {
48
        $this->annotationsDictionary = new SplObjectStorage();
49
    }
50
51
    /**
52
     * Gets annotations associated with an element.
53
     *
54
     * @param  IEdmElement              $element the annotated element
55
     * @return IDirectValueAnnotation[] The direct value annotations for the element
56
     */
57
    public function getDirectValueAnnotations(IEdmElement $element): iterable
58
    {
59
        // Fetch the annotations dictionary once and only once, because this.annotationsDictionary might get updated by another thread.
60
        $annotationsDictionary = $this->annotationsDictionary;
61
62
        $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...
63
        $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

63
        $transientAnnotations = self::getTransientAnnotations($element, /** @scrutinizer ignore-type */ $annotationsDictionary);
Loading history...
64
65
        if ($immutableAnnotations != null) {
0 ignored issues
show
introduced by
The condition $immutableAnnotations != null is always false.
Loading history...
66
            /** @var IDirectValueAnnotation $existingAnnotation */
67
            foreach ($immutableAnnotations as $existingAnnotation) {
68
                if (!self::isDead($existingAnnotation->getNamespaceUri(), $existingAnnotation->getName(), $transientAnnotations)) {
69
                    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...
70
                }
71
            }
72
        }
73
        /** @var IDirectValueAnnotation $existingAnnotation */
74
        foreach (self::transientAnnotations($transientAnnotations) as $existingAnnotation) {
75
            yield  $existingAnnotation;
76
        }
77
    }
78
79
    /**
80
     * Sets an annotation value for an EDM element. If the value is null, no annotation is added and an existing
81
     * annotation with the same name is removed.
82
     *
83
     * @param  IEdmElement                    $element       the annotated element
84
     * @param  string                         $namespaceName namespace that the annotation belongs to
85
     * @param  string                         $localName     name of the annotation within the namespace
86
     * @param  mixed                          $value         the value of the annotation
87
     * @return IDirectValueAnnotationsManager self
88
     */
89
    public function setAnnotationValue(IEdmElement $element, string $namespaceName, string $localName, $value): IDirectValueAnnotationsManager
90
    {
91
        $annotationsDictionary         = $this->getAnnotationsDictionary();
92
        $transientAnnotations          = self::getTransientAnnotations($element, $annotationsDictionary);
93
        $transientAnnotationsBeforeSet = null !== $transientAnnotations ? clone $transientAnnotations : new SplObjectStorage();
94
        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...
95
96
        // There is at least one case (removing an annotation that was not present to begin with) where the transient annotations are not changed,
97
        // so test to see if updating the dictionary is necessary.
98
        if ($transientAnnotations != $transientAnnotationsBeforeSet) {
99
            $annotationsDictionary->offsetSet($element, $transientAnnotations);
100
        }
101
102
        $this->annotationsDictionary = $annotationsDictionary;
103
        return $this;
104
    }
105
106
    /**
107
     * Sets a set of annotation values. If a supplied value is null, no annotation is added and an existing annotation
108
     * with the same name is removed.
109
     * @param  IDirectValueAnnotationBinding[] $annotations The annotations to set
110
     * @return IDirectValueAnnotationsManager  self
111
     */
112
    public function setAnnotationValues(array $annotations): IDirectValueAnnotationsManager
113
    {
114
        /** @var IDirectValueAnnotationBinding $annotation */
115
        foreach ($annotations as $annotation) {
116
            $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

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

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