Issues (126)

Annotations/EdmDirectValueAnnotationsManager.php (11 issues)

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

61
        $transientAnnotations = self::getTransientAnnotations($element, /** @scrutinizer ignore-type */ $annotationsDictionary);
Loading history...
62
63
        if ($immutableAnnotations != null) {
0 ignored issues
show
The condition $immutableAnnotations != null is always false.
Loading history...
64
            /** @var IDirectValueAnnotation $existingAnnotation */
65
            foreach ($immutableAnnotations as $existingAnnotation) {
66
                if (!self::isDead($existingAnnotation->getNamespaceUri(), $existingAnnotation->getName(), $transientAnnotations)) {
67
                    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...
68
                }
69
            }
70
        }
71
        /** @var IDirectValueAnnotation $existingAnnotation */
72
        foreach (self::transientAnnotations($transientAnnotations) as $existingAnnotation) {
73
            yield  $existingAnnotation;
74
        }
75
    }
76
77
    /**
78
     * Sets an annotation value for an EDM element. If the value is null, no annotation is added and an existing
79
     * annotation with the same name is removed.
80
     *
81
     * @param  IEdmElement                    $element       the annotated element
82
     * @param  string                         $namespaceName namespace that the annotation belongs to
83
     * @param  string                         $localName     name of the annotation within the namespace
84
     * @param  mixed                          $value         the value of the annotation
85
     * @return IDirectValueAnnotationsManager self
86
     */
87
    public function setAnnotationValue(IEdmElement $element, string $namespaceName, string $localName, $value): IDirectValueAnnotationsManager
88
    {
89
        $annotationsDictionary         = $this->getAnnotationsDictionary();
90
        $transientAnnotations          = self::getTransientAnnotations($element, $annotationsDictionary);
91
        $transientAnnotationsBeforeSet = null !== $transientAnnotations ? clone $transientAnnotations : new SplObjectStorage();
92
        self::setAnnotation($this->getAttachedAnnotations($element), $transientAnnotations, $namespaceName, $localName, $value);
0 ignored issues
show
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...
93
94
        // There is at least one case (removing an annotation that was not present to begin with) where the transient annotations are not changed,
95
        // so test to see if updating the dictionary is necessary.
96
        if ($transientAnnotations != $transientAnnotationsBeforeSet) {
97
            $annotationsDictionary->offsetSet($element, $transientAnnotations);
98
        }
99
100
        $this->annotationsDictionary = $annotationsDictionary;
101
        return $this;
102
    }
103
104
    /**
105
     * Sets a set of annotation values. If a supplied value is null, no annotation is added and an existing annotation
106
     * with the same name is removed.
107
     * @param  IDirectValueAnnotationBinding[] $annotations The annotations to set
108
     * @return IDirectValueAnnotationsManager  self
109
     */
110
    public function setAnnotationValues(array $annotations): IDirectValueAnnotationsManager
111
    {
112
        /** @var IDirectValueAnnotationBinding $annotation */
113
        foreach ($annotations as $annotation) {
114
            $this->setAnnotationValue(
115
                $annotation->getElement(),
116
                $annotation->getNamespaceUri(),
117
                $annotation->getName(),
118
                $annotation->getValue()
119
            );
120
        }
121
        return $this;
122
    }
123
124
    /**
125
     * @param  IEdmElement $element       the annotated element
126
     * @param  string      $namespaceName namespace that the annotation belongs to
127
     * @param  string      $localName     local name of the annotation
128
     * @return mixed       Returns the annotation value that corresponds to the provided name. Returns null if no
129
     *                                   annotation with the given name exists for the given element.
130
     */
131
    public function getAnnotationValue(IEdmElement $element, string $namespaceName, string $localName)
132
    {
133
        $annotationsDictionary = $this->getAnnotationsDictionary();
134
        $transients            = self::getTransientAnnotations($element, $annotationsDictionary);
135
        $annotation            = self::findTransientAnnotation($transients, $namespaceName, $localName);
136
        if ($annotation != null) {
137
            return $annotation->getValue();
138
        }
139
140
        $immutableAnnotations = $this->getAttachedAnnotations($element);
0 ignored issues
show
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...
141
        if ($immutableAnnotations != null) {
0 ignored issues
show
The condition $immutableAnnotations != null is always false.
Loading history...
142
            /** @var IDirectValueAnnotation $existingAnnotation */
143
            foreach ($immutableAnnotations as $existingAnnotation) {
144
                if ($existingAnnotation->getNamespaceUri() == $namespaceName && $existingAnnotation->getName() == $localName) {
145
                    // No need to check that the immutable annotation isn't Dead, because if it were
146
                    // the tombstone would have been found in the transient annotations.
147
                    return $existingAnnotation->getValue();
148
                }
149
            }
150
        }
151
152
        return null;
153
    }
154
155
    /**
156
     * Retrieves a set of annotation values. For each requested value, returns null if no annotation with the given
157
     * name exists for the given element.
158
     *
159
     * @param  IDirectValueAnnotationBinding[] $annotations The set of requested annotations
160
     * @return array                           Returns values that correspond to the provided annotations. A value is null if no annotation with
161
     *                                                     the given name exists for the given element.
162
     */
163
    public function getAnnotationValues(array $annotations): ?iterable
164
    {
165
        $values = [];
166
167
        $index = 0;
168
        foreach ($annotations as $annotation) {
169
            $values[$index++] = $this->getAnnotationValue($annotation->getElement(), $annotation->getNamespaceUri(), $annotation->getName());
170
        }
171
172
        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...
173
    }
174
175
    /**
176
     * Retrieves the annotations that are directly attached to an element.
177
     *
178
     * @param  IEdmElement   $element the element in question
179
     * @return iterable|null the annotations that are directly attached to an element (outside the control of the manager)
180
     */
181
    protected function getAttachedAnnotations(IEdmElement $element): ?iterable
0 ignored issues
show
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

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