Passed
Pull Request — master (#216)
by Charly
05:48
created

Proxy   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Test Coverage

Coverage 94.79%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 46
eloc 98
c 2
b 0
f 0
dl 0
loc 314
rs 8.72
ccs 91
cts 96
cp 0.9479

27 Methods

Rating   Name   Duplication   Size   Complexity  
A save() 0 7 1
A disableAutoRefresh() 0 5 1
A forceGet() 0 3 1
A remove() 0 7 1
A forceSetAll() 0 9 2
A repository() 0 3 1
A refresh() 0 19 4
A enableAutoRefresh() 0 9 2
A forceSet() 0 3 1
A __get() 0 3 1
A __set() 0 3 1
A disableDoctrineEVents() 0 19 4
A isPersisted() 0 3 1
A resetDoctrineVents() 0 18 4
A withoutAutoRefresh() 0 10 1
A __construct() 0 5 1
A assertNotPersisted() 0 5 1
A __call() 0 3 1
A __unset() 0 3 1
A createFromPersisted() 0 6 1
A fetchObject() 0 5 2
A assertPersisted() 0 5 1
A object() 0 21 6
A __isset() 0 3 1
A __toString() 0 11 3
A objectManager() 0 3 1
A executeCallback() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like Proxy 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 Proxy, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\Persistence\ObjectManager;
7
use Zenstruck\Assert;
8
use Zenstruck\Callback;
9
use Zenstruck\Callback\Parameter;
10
11
/**
12
 * @template TProxiedObject of object
13
 * @mixin TProxiedObject
14
 *
15
 * @author Kevin Bond <[email protected]>
16
 */
17
final class Proxy
18
{
19
    /**
20
     * @var object
21
     * @psalm-var TProxiedObject
22
     */
23
    private $object;
24
25
    /**
26
     * @var string
27
     * @psalm-var class-string<TProxiedObject>
28
     */
29
    private $class;
30
31
    /** @var bool */
32
    private $autoRefresh;
33
34
    /** @var bool */
35
    private $persisted = false;
36
37
    /** @var array */
38
    private $doctrineEvents = [];
39 857
40
    /**
41 857
     * @internal
42 857
     *
43 857
     * @psalm-param TProxiedObject $object
44 857
     */
45
    public function __construct(object $object)
46 478
    {
47
        $this->object = $object;
48 478
        $this->class = \get_class($object);
49
        $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh();
50
    }
51 10
52
    public function __call(string $method, array $arguments)
53 10
    {
54
        return $this->object()->{$method}(...$arguments);
55
    }
56 10
57
    public function __get(string $name)
58 10
    {
59 10
        return $this->object()->{$name};
60
    }
61 10
62
    public function __set(string $name, $value): void
63 10
    {
64 10
        $this->object()->{$name} = $value;
65
    }
66 10
67
    public function __unset(string $name): void
68 10
    {
69
        unset($this->object()->{$name});
70
    }
71 20
72
    public function __isset(string $name): bool
73 20
    {
74 10
        return isset($this->object()->{$name});
75
    }
76
77
    public function __toString(): string
78 10
    {
79
        if (!\method_exists($this->object, '__toString')) {
80
            if (\PHP_VERSION_ID < 70400) {
81 10
                return '(no __toString)';
82
            }
83
84
            throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class));
85
        }
86
87
        return $this->object()->__toString();
88
    }
89
90
    /**
91 170
     * @internal
92
     *
93 170
     * @template TObject of object
94 170
     * @psalm-param TObject $object
95
     * @psalm-return Proxy<TObject>
96 170
     */
97
    public static function createFromPersisted(object $object): self
98
    {
99 428
        $proxy = new self($object);
100
        $proxy->persisted = true;
101 428
102
        return $proxy;
103
    }
104
105
    public function isPersisted(): bool
106
    {
107 568
        return $this->persisted;
108
    }
109 568
110 40
    /**
111
     * @return TProxiedObject
0 ignored issues
show
Bug introduced by
The type Zenstruck\Foundry\TProxiedObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
112
     */
113 568
    public function object(): object
114
    {
115
        if (!$this->autoRefresh || !$this->persisted) {
116 645
            return $this->object;
117
        }
118 645
119 635
        $om = $this->objectManager();
120 635
121
        // only check for changes if the object is managed in the current om
122 635
        if ($om instanceof EntityManagerInterface && $om->contains($this->object)) {
123
            // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
124
            $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object);
125 10
126
            if (!empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) {
127 10
                throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details).', $this->class));
128 10
            }
129 10
        }
130
131 10
        $this->refresh();
132
133
        return $this->object;
134 90
    }
135
136 90
    /**
137 10
     * @psalm-return static
138
     */
139
    public function save(): self
140 80
    {
141 60
        $this->objectManager()->persist($this->object);
142
        $this->objectManager()->flush();
143 60
        $this->persisted = true;
144
145
        return $this;
146 20
    }
147 10
148
    public function remove(): self
149
    {
150 10
        $this->objectManager()->remove($this->object);
151
        $this->objectManager()->flush();
152 10
        $this->autoRefresh = $this->persisted = false;
153
154
        return $this;
155
    }
156
157
    public function refresh(): self
158 70
    {
159
        if (!$this->persisted) {
160 70
            throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class));
161
        }
162
163 80
        if ($this->objectManager()->contains($this->object)) {
164
            $this->objectManager()->refresh($this->object);
165 80
166
            return $this;
167 80
        }
168 80
169
        if (!$object = $this->fetchObject()) {
170
            throw new \RuntimeException('The object no longer exists.');
171 80
        }
172
173
        $this->object = $object;
174
175
        return $this;
176
    }
177 10
178
    /**
179 10
     * @param mixed $value
180
     */
181
    public function forceSet(string $property, $value): self
182 10
    {
183
        return $this->forceSetAll([$property => $value]);
184 10
    }
185
186
    public function forceSetAll(array $properties): self
187 40
    {
188
        $object = $this->object();
189 40
190
        foreach ($properties as $property => $value) {
191
            Instantiator::forceSet($object, $property, $value);
192
        }
193 40
194
        return $this;
195 40
    }
196
197
    /**
198
     * @return mixed
199
     */
200
    public function forceGet(string $property)
201
    {
202
        return Instantiator::forceGet($this->object(), $property);
203
    }
204
205
    public function repository(): RepositoryProxy
206
    {
207
        return Factory::configuration()->repositoryFor($this->class);
208
    }
209
210
    public function enableAutoRefresh(): self
211 635
    {
212
        if (!$this->persisted) {
213 635
            throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class));
214 635
        }
215
216 635
        $this->autoRefresh = true;
217
218 635
        return $this;
219
    }
220 635
221
    public function disableAutoRefresh(): self
222
    {
223 30
        $this->autoRefresh = false;
224
225 30
        return $this;
226
    }
227 30
228
    public function disableDoctrineEVents(?string $eventTypeName = null): self
229
    {
230 10
        /** @var EntityManagerInterface $manager */
231
        $manager = $this->objectManager();
232 10
233
        $this->doctrineEvents = $manager->getConnection()->getEventManager()->getListeners($eventTypeName);
234 10
        foreach ($this->doctrineEvents as $type => $listenersByType) {
235
            foreach ($listenersByType as $listener) {
236
                if ($listener instanceof EventSubscriberInterface) {
0 ignored issues
show
Bug introduced by
The type Zenstruck\Foundry\EventSubscriberInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
237
                    $manager->getConnection()->getEventManager()->removeEventSubscriber($listener);
238
239
                    continue;
240 635
                }
241
242 635
                $manager->getConnection()->getEventManager()->removeEventListener($type, $listener);
243 635
            }
244
        }
245 635
246 20
        return $this;
247
    }
248
249 635
    public function resetDoctrineVents(): self
250 635
    {
251
        /** @var EntityManagerInterface $manager */
252
        $manager = $this->objectManager();
253
254
        foreach ($this->doctrineEvents as $type => $listenersByType) {
255 60
            foreach ($listenersByType as $listener) {
256
                if ($listener instanceof EventSubscriberInterface) {
257 60
                    $manager->getConnection()->getEventManager()->addEventSubscriber($listener);
258
259 60
                    continue;
260
                }
261
262 645
                $manager->getConnection()->getEventManager()->addEventListener($type, $listener);
263
            }
264 645
        }
265
266
        return $this;
267
    }
268
269
    /**
270
     * Ensures "autoRefresh" is disabled when executing $callback. Re-enables
271
     * "autoRefresh" after executing callback if it was enabled.
272
     *
273
     * @param callable $callback (object|Proxy $object): void
274
     *
275
     * @psalm-return static
276
     */
277
    public function withoutAutoRefresh(callable $callback): self
278
    {
279
        $original = $this->autoRefresh;
280
        $this->autoRefresh = false;
281
282
        $this->executeCallback($callback);
283
284
        $this->autoRefresh = $original; // set to original value (even if it was false)
285
286
        return $this;
287
    }
288
289
    public function assertPersisted(string $message = '{entity} is not persisted.'): self
290
    {
291
        Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]);
292
293
        return $this;
294
    }
295
296
    public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self
297
    {
298
        Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]);
299
300
        return $this;
301
    }
302
303
    /**
304
     * @internal
305
     */
306
    public function executeCallback(callable $callback, ...$arguments): void
307
    {
308
        Callback::createFor($callback)->invoke(
309
            Parameter::union(
310
                Parameter::untyped($this),
311
                Parameter::typed(self::class, $this),
312
                Parameter::typed($this->class, Parameter::factory(function() { return $this->object(); }))
313
            )->optional(),
314
            ...$arguments
315
        );
316
    }
317
318
    /**
319
     * @psalm-return TProxiedObject|null
320
     */
321
    private function fetchObject(): ?object
322
    {
323
        $id = $this->objectManager()->getClassMetadata($this->class)->getIdentifierValues($this->object);
324
325
        return empty($id) ? null : $this->objectManager()->find($this->class, $id);
326
    }
327
328
    private function objectManager(): ObjectManager
329
    {
330
        return Factory::configuration()->objectManagerFor($this->class);
331
    }
332
}
333