Passed
Pull Request — master (#216)
by Charly
04:52 queued 42s
created

Proxy::executeCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 9
rs 10
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\Persistence\ObjectManager;
8
use Zenstruck\Assert;
9
use Zenstruck\Callback;
10
use Zenstruck\Callback\Parameter;
11
12
/**
13
 * @template TProxiedObject of object
14
 * @mixin TProxiedObject
15
 *
16
 * @author Kevin Bond <[email protected]>
17
 */
18
final class Proxy
19
{
20
    /**
21
     * @var object
22
     * @psalm-var TProxiedObject
23
     */
24
    private $object;
25
26
    /**
27
     * @var string
28
     * @psalm-var class-string<TProxiedObject>
29
     */
30
    private $class;
31
32
    /** @var bool */
33
    private $autoRefresh;
34
35
    /** @var bool */
36
    private $persisted = false;
37
38
    /** @var array */
39 857
    private $doctrineEvents = [];
40
41 857
    /**
42 857
     * @internal
43 857
     *
44 857
     * @psalm-param TProxiedObject $object
45
     */
46 478
    public function __construct(object $object)
47
    {
48 478
        $this->object = $object;
49
        $this->class = \get_class($object);
50
        $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh();
51 10
    }
52
53 10
    public function __call(string $method, array $arguments)
54
    {
55
        return $this->object()->{$method}(...$arguments);
56 10
    }
57
58 10
    public function __get(string $name)
59 10
    {
60
        return $this->object()->{$name};
61 10
    }
62
63 10
    public function __set(string $name, $value): void
64 10
    {
65
        $this->object()->{$name} = $value;
66 10
    }
67
68 10
    public function __unset(string $name): void
69
    {
70
        unset($this->object()->{$name});
71 20
    }
72
73 20
    public function __isset(string $name): bool
74 10
    {
75
        return isset($this->object()->{$name});
76
    }
77
78 10
    public function __toString(): string
79
    {
80
        if (!\method_exists($this->object, '__toString')) {
81 10
            if (\PHP_VERSION_ID < 70400) {
82
                return '(no __toString)';
83
            }
84
85
            throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class));
86
        }
87
88
        return $this->object()->__toString();
89
    }
90
91 170
    /**
92
     * @internal
93 170
     *
94 170
     * @template TObject of object
95
     * @psalm-param TObject $object
96 170
     * @psalm-return Proxy<TObject>
97
     */
98
    public static function createFromPersisted(object $object): self
99 428
    {
100
        $proxy = new self($object);
101 428
        $proxy->persisted = true;
102
103
        return $proxy;
104
    }
105
106
    public function isPersisted(): bool
107 568
    {
108
        return $this->persisted;
109 568
    }
110 40
111
    /**
112
     * @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...
113 568
     */
114
    public function object(): object
115
    {
116 645
        if (!$this->autoRefresh || !$this->persisted) {
117
            return $this->object;
118 645
        }
119 635
120 635
        $om = $this->objectManager();
121
122 635
        // only check for changes if the object is managed in the current om
123
        if ($om instanceof EntityManagerInterface && $om->contains($this->object)) {
124
            // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
125 10
            $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object);
126
127 10
            if (!empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) {
128 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));
129 10
            }
130
        }
131 10
132
        $this->refresh();
133
134 90
        return $this->object;
135
    }
136 90
137 10
    /**
138
     * @psalm-return static
139
     */
140 80
    public function save(): self
141 60
    {
142
        $this->objectManager()->persist($this->object);
143 60
        $this->objectManager()->flush();
144
        $this->persisted = true;
145
146 20
        return $this;
147 10
    }
148
149
    public function remove(): self
150 10
    {
151
        $this->objectManager()->remove($this->object);
152 10
        $this->objectManager()->flush();
153
        $this->autoRefresh = $this->persisted = false;
154
155
        return $this;
156
    }
157
158 70
    public function refresh(): self
159
    {
160 70
        if (!$this->persisted) {
161
            throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class));
162
        }
163 80
164
        if ($this->objectManager()->contains($this->object)) {
165 80
            $this->objectManager()->refresh($this->object);
166
167 80
            return $this;
168 80
        }
169
170
        if (!$object = $this->fetchObject()) {
171 80
            throw new \RuntimeException('The object no longer exists.');
172
        }
173
174
        $this->object = $object;
175
176
        return $this;
177 10
    }
178
179 10
    /**
180
     * @param mixed $value
181
     */
182 10
    public function forceSet(string $property, $value): self
183
    {
184 10
        return $this->forceSetAll([$property => $value]);
185
    }
186
187 40
    public function forceSetAll(array $properties): self
188
    {
189 40
        $object = $this->object();
190
191
        foreach ($properties as $property => $value) {
192
            Instantiator::forceSet($object, $property, $value);
193 40
        }
194
195 40
        return $this;
196
    }
197
198
    /**
199
     * @return mixed
200
     */
201
    public function forceGet(string $property)
202
    {
203
        return Instantiator::forceGet($this->object(), $property);
204
    }
205
206
    public function repository(): RepositoryProxy
207
    {
208
        return Factory::configuration()->repositoryFor($this->class);
209
    }
210
211 635
    public function enableAutoRefresh(): self
212
    {
213 635
        if (!$this->persisted) {
214 635
            throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class));
215
        }
216 635
217
        $this->autoRefresh = true;
218 635
219
        return $this;
220 635
    }
221
222
    public function disableAutoRefresh(): self
223 30
    {
224
        $this->autoRefresh = false;
225 30
226
        return $this;
227 30
    }
228
229
    public function disableDoctrineEvents(?string $eventTypeName = null): self
230 10
    {
231
        /** @var EntityManagerInterface $manager */
232 10
        $manager = $this->objectManager();
233
234 10
        $this->doctrineEvents = $manager->getConnection()->getEventManager()->getListeners($eventTypeName);
235
        foreach ($this->doctrineEvents as $type => $listenersByType) {
236
            foreach ($listenersByType as $listener) {
237
                if ($listener instanceof EventSubscriberInterface) {
238
                    $manager->getConnection()->getEventManager()->removeEventSubscriber($listener);
239
240 635
                    continue;
241
                }
242 635
243 635
                $manager->getConnection()->getEventManager()->removeEventListener($type, $listener);
244
            }
245 635
        }
246 20
247
        return $this;
248
    }
249 635
250 635
    public function resetDoctrineEvents(): self
251
    {
252
        /** @var EntityManagerInterface $manager */
253
        $manager = $this->objectManager();
254
255 60
        foreach ($this->doctrineEvents as $type => $listenersByType) {
256
            foreach ($listenersByType as $listener) {
257 60
                if ($listener instanceof EventSubscriberInterface) {
258
                    $manager->getConnection()->getEventManager()->addEventSubscriber($listener);
259 60
260
                    continue;
261
                }
262 645
263
                $manager->getConnection()->getEventManager()->addEventListener($type, $listener);
264 645
            }
265
        }
266
267
        return $this;
268
    }
269
270
    /**
271
     * Ensures "autoRefresh" is disabled when executing $callback. Re-enables
272
     * "autoRefresh" after executing callback if it was enabled.
273
     *
274
     * @param callable $callback (object|Proxy $object): void
275
     *
276
     * @psalm-return static
277
     */
278
    public function withoutAutoRefresh(callable $callback): self
279
    {
280
        $original = $this->autoRefresh;
281
        $this->autoRefresh = false;
282
283
        $this->executeCallback($callback);
284
285
        $this->autoRefresh = $original; // set to original value (even if it was false)
286
287
        return $this;
288
    }
289
290
    public function assertPersisted(string $message = '{entity} is not persisted.'): self
291
    {
292
        Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]);
293
294
        return $this;
295
    }
296
297
    public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self
298
    {
299
        Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]);
300
301
        return $this;
302
    }
303
304
    /**
305
     * @internal
306
     */
307
    public function executeCallback(callable $callback, ...$arguments): void
308
    {
309
        Callback::createFor($callback)->invoke(
310
            Parameter::union(
311
                Parameter::untyped($this),
312
                Parameter::typed(self::class, $this),
313
                Parameter::typed($this->class, Parameter::factory(function() { return $this->object(); }))
314
            )->optional(),
315
            ...$arguments
316
        );
317
    }
318
319
    /**
320
     * @psalm-return TProxiedObject|null
321
     */
322
    private function fetchObject(): ?object
323
    {
324
        $id = $this->objectManager()->getClassMetadata($this->class)->getIdentifierValues($this->object);
325
326
        return empty($id) ? null : $this->objectManager()->find($this->class, $id);
327
    }
328
329
    private function objectManager(): ObjectManager
330
    {
331
        return Factory::configuration()->objectManagerFor($this->class);
332
    }
333
}
334