Passed
Pull Request — master (#84)
by Kevin
02:40
created

Proxy::__toString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 11
rs 10
ccs 3
cts 4
cp 0.75
cc 3
nc 3
nop 0
crap 3.1406
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\Persistence\ObjectManager;
7
use PHPUnit\Framework\Assert;
0 ignored issues
show
Bug introduced by
The type PHPUnit\Framework\Assert 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...
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
    /**
38
     * @internal
39 857
     *
40
     * @psalm-param TProxiedObject $object
41 857
     */
42 857
    public function __construct(object $object)
43 857
    {
44 857
        $this->object = $object;
45
        $this->class = \get_class($object);
46 478
        $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh();
47
    }
48 478
49
    public function __call(string $method, array $arguments)
50
    {
51 10
        return $this->object()->{$method}(...$arguments);
52
    }
53 10
54
    public function __get(string $name)
55
    {
56 10
        return $this->object()->{$name};
57
    }
58 10
59 10
    public function __set(string $name, $value): void
60
    {
61 10
        $this->object()->{$name} = $value;
62
    }
63 10
64 10
    public function __unset(string $name): void
65
    {
66 10
        unset($this->object()->{$name});
67
    }
68 10
69
    public function __isset(string $name): bool
70
    {
71 20
        return isset($this->object()->{$name});
72
    }
73 20
74 10
    public function __toString(): string
75
    {
76
        if (!\method_exists($this->object, '__toString')) {
77
            if (\PHP_VERSION_ID < 70400) {
78 10
                return '(no __toString)';
79
            }
80
81 10
            throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class));
82
        }
83
84
        return $this->object()->__toString();
85
    }
86
87
    /**
88
     * @internal
89
     *
90
     * @template TObject as object
91 170
     * @psalm-param TObject $object
92
     * @psalm-return Proxy<TObject>
93 170
     */
94 170
    public static function createFromPersisted(object $object): self
95
    {
96 170
        $proxy = new self($object);
97
        $proxy->persisted = true;
98
99 428
        return $proxy;
100
    }
101 428
102
    public function isPersisted(): bool
103
    {
104
        return $this->persisted;
105
    }
106
107 568
    /**
108
     * @psalm-return TProxiedObject
109 568
     */
110 40
    public function object(): object
111
    {
112
        if (!$this->autoRefresh || !$this->persisted || !Factory::configuration()->isFlushingEnabled()) {
113 568
            return $this->object;
114
        }
115
116 645
        $om = $this->objectManager();
117
118 645
        // only check for changes if the object is managed in the current om
119 635
        if ($om instanceof EntityManagerInterface && $om->contains($this->object)) {
120 635
            // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
121
            $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object);
122 635
123
            if (!empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) {
124
                throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://github.com/zenstruck/foundry#auto-refresh for details).', $this->class));
125 10
            }
126
        }
127 10
128 10
        $this->refresh();
129 10
130
        return $this->object;
131 10
    }
132
133
    /**
134 90
     * @psalm-return static
135
     */
136 90
    public function save(): self
137 10
    {
138
        $this->objectManager()->persist($this->object);
139
140 80
        if (Factory::configuration()->isFlushingEnabled()) {
141 60
            $this->objectManager()->flush();
142
        }
143 60
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
    /**
230 10
     * Ensures "autoRefresh" is disabled when executing $callback. Re-enables
231
     * "autoRefresh" after executing callback if it was enabled.
232 10
     *
233
     * @param callable $callback (object|Proxy $object): void
234 10
     *
235
     * @psalm-return static
236
     */
237
    public function withoutAutoRefresh(callable $callback): self
238
    {
239
        $original = $this->autoRefresh;
240 635
        $this->autoRefresh = false;
241
242 635
        $this->executeCallback($callback);
243 635
244
        $this->autoRefresh = $original; // set to original value (even if it was false)
245 635
246 20
        return $this;
247
    }
248
249 635
    public function assertPersisted(string $message = 'The object is not persisted.'): self
250 635
    {
251
        Assert::assertNotNull($this->fetchObject(), $message);
252
253
        return $this;
254
    }
255 60
256
    public function assertNotPersisted(string $message = 'The object is persisted but it should not be.'): self
257 60
    {
258
        Assert::assertNull($this->fetchObject(), $message);
259 60
260
        return $this;
261
    }
262 645
263
    /**
264 645
     * @internal
265
     */
266
    public function executeCallback(callable $callback, ...$arguments): void
267
    {
268
        Callback::createFor($callback)->invoke(
269
            Parameter::union(
270
                Parameter::untyped($this),
271
                Parameter::typed(self::class, $this),
272
                Parameter::typed($this->class, Parameter::factory(function() { return $this->object(); }))
273
            )->optional(),
274
            ...$arguments
275
        );
276
    }
277
278
    /**
279
     * @psalm-return TProxiedObject|null
280
     */
281
    private function fetchObject(): ?object
282
    {
283
        $id = $this->objectManager()->getClassMetadata($this->class)->getIdentifierValues($this->object);
284
285
        return empty($id) ? null : $this->objectManager()->find($this->class, $id);
286
    }
287
288
    private function objectManager(): ObjectManager
289
    {
290
        return Factory::configuration()->objectManagerFor($this->class);
291
    }
292
}
293