Completed
Push — master ( 852076...2b713c )
by Alex
23s queued 14s
created

getAnnotationsDictionary()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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