Passed
Branch main (339f6d)
by Cornelia
17:44 queued 07:47
created

LazyGhostTrait   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 81
eloc 170
dl 0
loc 347
rs 2
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
B __set() 0 32 9
B __isset() 0 29 9
A isLazyObjectInitialized() 0 8 2
A __serialize() 0 30 6
A __clone() 0 8 3
A resetLazyObject() 0 11 3
A initializeLazyObject() 0 11 3
A setLazyObjectAsInitialized() 0 5 3
B createLazyGhost() 0 37 9
A __destruct() 0 10 3
B __unset() 0 32 9
F __get() 0 80 22

How to fix   Complexity   

Complex Class

Complex classes like LazyGhostTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LazyGhostTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\VarExporter;
13
14
use Symfony\Component\Serializer\Attribute\Ignore;
15
use Symfony\Component\VarExporter\Internal\Hydrator;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Symfony\Component\VarExporter\Hydrator. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
use Symfony\Component\VarExporter\Internal\LazyObjectRegistry as Registry;
17
use Symfony\Component\VarExporter\Internal\LazyObjectState;
18
use Symfony\Component\VarExporter\Internal\LazyObjectTrait;
19
20
trait LazyGhostTrait
21
{
22
    use LazyObjectTrait;
23
24
    /**
25
     * Creates a lazy-loading ghost instance.
26
     *
27
     * Skipped properties should be indexed by their array-cast identifier, see
28
     * https://php.net/manual/language.types.array#language.types.array.casting
29
     *
30
     * @param \Closure(static):void    $initializer       The closure should initialize the object it receives as argument
31
     * @param array<string, true>|null $skippedProperties An array indexed by the properties to skip, a.k.a. the ones
32
     *                                                    that the initializer doesn't initialize, if any
33
     * @param static|null              $instance
34
     */
35
    public static function createLazyGhost(\Closure $initializer, ?array $skippedProperties = null, ?object $instance = null): static
36
    {
37
        if (self::class !== $class = $instance ? $instance::class : static::class) {
38
            $skippedProperties["\0".self::class."\0lazyObjectState"] = true;
39
        }
40
41
        if (!isset(Registry::$defaultProperties[$class])) {
42
            Registry::$classReflectors[$class] ??= new \ReflectionClass($class);
43
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
44
            Registry::$defaultProperties[$class] ??= (array) $instance;
45
            Registry::$classResetters[$class] ??= Registry::getClassResetters($class);
46
47
            if (self::class === $class && \defined($class.'::LAZY_OBJECT_PROPERTY_SCOPES')) {
48
                Hydrator::$propertyScopes[$class] ??= $class::LAZY_OBJECT_PROPERTY_SCOPES;
49
            }
50
        } else {
51
            $instance ??= Registry::$classReflectors[$class]->newInstanceWithoutConstructor();
52
        }
53
54
        if (isset($instance->lazyObjectState)) {
55
            $instance->lazyObjectState->initializer = $initializer;
56
            $instance->lazyObjectState->skippedProperties = $skippedProperties ??= [];
57
58
            if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $instance->lazyObjectState->status) {
59
                $instance->lazyObjectState->reset($instance);
60
            }
61
62
            return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type object which includes types incompatible with the type-hinted return Symfony\Component\VarExporter\LazyGhostTrait.
Loading history...
63
        }
64
65
        $instance->lazyObjectState = new LazyObjectState($initializer, $skippedProperties ??= []);
66
67
        foreach (Registry::$classResetters[$class] as $reset) {
68
            $reset($instance, $skippedProperties);
69
        }
70
71
        return $instance;
72
    }
73
74
    /**
75
     * Returns whether the object is initialized.
76
     *
77
     * @param bool $partial Whether partially initialized objects should be considered as initialized
78
     */
79
    #[Ignore]
80
    public function isLazyObjectInitialized(bool $partial = false): bool
0 ignored issues
show
Unused Code introduced by
The parameter $partial 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

80
    public function isLazyObjectInitialized(/** @scrutinizer ignore-unused */ bool $partial = false): bool

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...
81
    {
82
        if (!$state = $this->lazyObjectState ?? null) {
83
            return true;
84
        }
85
86
        return LazyObjectState::STATUS_INITIALIZED_FULL === $state->status;
87
    }
88
89
    /**
90
     * Forces initialization of a lazy object and returns it.
91
     */
92
    public function initializeLazyObject(): static
93
    {
94
        if (!$state = $this->lazyObjectState ?? null) {
95
            return $this;
96
        }
97
98
        if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
99
            $state->initialize($this, '', null);
100
        }
101
102
        return $this;
103
    }
104
105
    /**
106
     * @return bool Returns false when the object cannot be reset, ie when it's not a lazy object
107
     */
108
    public function resetLazyObject(): bool
109
    {
110
        if (!$state = $this->lazyObjectState ?? null) {
111
            return false;
112
        }
113
114
        if (LazyObjectState::STATUS_UNINITIALIZED_FULL !== $state->status) {
115
            $state->reset($this);
116
        }
117
118
        return true;
119
    }
120
121
    public function &__get($name): mixed
122
    {
123
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
124
        $scope = null;
125
        $notByRef = 0;
126
127
        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
128
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);
129
            $state = $this->lazyObjectState ?? null;
130
131
            if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))) {
132
                $notByRef = $access & Hydrator::PROPERTY_NOT_BY_REF;
133
134
                if (LazyObjectState::STATUS_INITIALIZED_FULL === $state->status) {
135
                    // Work around php/php-src#12695
136
                    $property = null === $scope ? $name : "\0$scope\0$name";
137
                    $property = $propertyScopes[$property][4]
138
                        ?? Hydrator::$propertyScopes[$this::class][$property][4] = new \ReflectionProperty($scope ?? $class, $name);
139
                } else {
140
                    $property = null;
141
                }
142
                if (\PHP_VERSION_ID >= 80400 && !$notByRef && ($access >> 2) & \ReflectionProperty::IS_PRIVATE_SET) {
0 ignored issues
show
Bug introduced by
The constant ReflectionProperty::IS_PRIVATE_SET was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
143
                    $scope ??= $writeScope;
144
                }
145
146
                if ($property?->isInitialized($this) ?? LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)) {
147
                    goto get_in_scope;
148
                }
149
            }
150
        }
151
152
        if ($parent = (Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['get']) {
153
            if (2 === $parent) {
154
                return parent::__get($name);
155
            }
156
            $value = parent::__get($name);
157
158
            return $value;
159
        }
160
161
        if (null === $class) {
162
            $frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
163
            trigger_error(\sprintf('Undefined property: %s::$%s in %s on line %s', $this::class, $name, $frame['file'], $frame['line']), \E_USER_NOTICE);
164
        }
165
166
        get_in_scope:
167
168
        try {
169
            if (null === $scope) {
170
                if (!$notByRef) {
171
                    return $this->$name;
172
                }
173
                $value = $this->$name;
174
175
                return $value;
176
            }
177
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
178
179
            return $accessor['get']($this, $name, $notByRef);
180
        } catch (\Error $e) {
181
            if (\Error::class !== $e::class || !str_starts_with($e->getMessage(), 'Cannot access uninitialized non-nullable property')) {
182
                throw $e;
183
            }
184
185
            try {
186
                if (null === $scope) {
187
                    $this->$name = [];
188
189
                    return $this->$name;
190
                }
191
192
                $accessor['set']($this, $name, []);
193
194
                return $accessor['get']($this, $name, $notByRef);
195
            } catch (\Error) {
196
                if (preg_match('/^Cannot access uninitialized non-nullable property ([^ ]++) by reference$/', $e->getMessage(), $matches)) {
197
                    throw new \Error('Typed property '.$matches[1].' must not be accessed before initialization', $e->getCode(), $e->getPrevious());
198
                }
199
200
                throw $e;
201
            }
202
        }
203
    }
204
205
    public function __set($name, $value): void
206
    {
207
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
208
        $scope = null;
209
210
        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
211
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
212
            $state = $this->lazyObjectState ?? null;
213
214
            if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
215
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
216
            ) {
217
                if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
218
                    $state->initialize($this, $name, $writeScope ?? $scope);
219
                }
220
                goto set_in_scope;
221
            }
222
        }
223
224
        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['set']) {
225
            parent::__set($name, $value);
226
227
            return;
228
        }
229
230
        set_in_scope:
231
232
        if (null === $scope) {
233
            $this->$name = $value;
234
        } else {
235
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
236
            $accessor['set']($this, $name, $value);
237
        }
238
    }
239
240
    public function __isset($name): bool
241
    {
242
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
243
        $scope = null;
244
245
        if ([$class, , $writeScope] = $propertyScopes[$name] ?? null) {
246
            $scope = Registry::getScopeForRead($propertyScopes, $class, $name);
247
            $state = $this->lazyObjectState ?? null;
248
249
            if ($state && (null === $scope || isset($propertyScopes["\0$scope\0$name"]))
250
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
251
                && LazyObjectState::STATUS_UNINITIALIZED_PARTIAL !== $state->initialize($this, $name, $writeScope ?? $scope)
252
            ) {
253
                goto isset_in_scope;
254
            }
255
        }
256
257
        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['isset']) {
258
            return parent::__isset($name);
259
        }
260
261
        isset_in_scope:
262
263
        if (null === $scope) {
264
            return isset($this->$name);
265
        }
266
        $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
267
268
        return $accessor['isset']($this, $name);
269
    }
270
271
    public function __unset($name): void
272
    {
273
        $propertyScopes = Hydrator::$propertyScopes[$this::class] ??= Hydrator::getPropertyScopes($this::class);
274
        $scope = null;
275
276
        if ([$class, , $writeScope, $access] = $propertyScopes[$name] ?? null) {
277
            $scope = Registry::getScopeForWrite($propertyScopes, $class, $name, $access >> 2);
278
            $state = $this->lazyObjectState ?? null;
279
280
            if ($state && ($writeScope === $scope || isset($propertyScopes["\0$scope\0$name"]))
281
                && LazyObjectState::STATUS_INITIALIZED_FULL !== $state->status
282
            ) {
283
                if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state->status) {
284
                    $state->initialize($this, $name, $writeScope ?? $scope);
285
                }
286
                goto unset_in_scope;
287
            }
288
        }
289
290
        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['unset']) {
291
            parent::__unset($name);
292
293
            return;
294
        }
295
296
        unset_in_scope:
297
298
        if (null === $scope) {
299
            unset($this->$name);
300
        } else {
301
            $accessor = Registry::$classAccessors[$scope] ??= Registry::getClassAccessors($scope);
302
            $accessor['unset']($this, $name);
303
        }
304
    }
305
306
    public function __clone(): void
307
    {
308
        if ($state = $this->lazyObjectState ?? null) {
309
            $this->lazyObjectState = clone $state;
310
        }
311
312
        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['clone']) {
313
            parent::__clone();
314
        }
315
    }
316
317
    public function __serialize(): array
318
    {
319
        $class = self::class;
320
321
        if ((Registry::$parentMethods[$class] ??= Registry::getParentMethods($class))['serialize']) {
322
            $properties = parent::__serialize();
323
        } else {
324
            $this->initializeLazyObject();
325
            $properties = (array) $this;
326
        }
327
        unset($properties["\0$class\0lazyObjectState"]);
328
329
        if (Registry::$parentMethods[$class]['serialize'] || !Registry::$parentMethods[$class]['sleep']) {
330
            return $properties;
331
        }
332
333
        $scope = get_parent_class($class);
334
        $data = [];
335
336
        foreach (parent::__sleep() as $name) {
337
            $value = $properties[$k = $name] ?? $properties[$k = "\0*\0$name"] ?? $properties[$k = "\0$class\0$name"] ?? $properties[$k = "\0$scope\0$name"] ?? $k = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $k is dead and can be removed.
Loading history...
338
339
            if (null === $k) {
340
                trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $name), \E_USER_NOTICE);
341
            } else {
342
                $data[$k] = $value;
343
            }
344
        }
345
346
        return $data;
347
    }
348
349
    public function __destruct()
350
    {
351
        $state = $this->lazyObjectState ?? null;
352
353
        if (LazyObjectState::STATUS_UNINITIALIZED_FULL === $state?->status) {
354
            return;
355
        }
356
357
        if ((Registry::$parentMethods[self::class] ??= Registry::getParentMethods(self::class))['destruct']) {
358
            parent::__destruct();
359
        }
360
    }
361
362
    #[Ignore]
363
    private function setLazyObjectAsInitialized(bool $initialized): void
364
    {
365
        if ($state = $this->lazyObjectState ?? null) {
366
            $state->status = $initialized ? LazyObjectState::STATUS_INITIALIZED_FULL : LazyObjectState::STATUS_UNINITIALIZED_FULL;
0 ignored issues
show
Documentation Bug introduced by
It seems like $initialized ? Symfony\C...ATUS_UNINITIALIZED_FULL of type integer is incompatible with the declared type Symfony\Component\VarExp...nternal\LazyObjectState of property $status.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
367
        }
368
    }
369
}
370