Passed
Push — master ( 468e80...0416dc )
by Kevin
02:52
created

src/Proxy.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\Persistence\ObjectManager;
7
use PHPUnit\Framework\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
    /**
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 of 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
     * @return TProxiedObject
0 ignored issues
show
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...
109 568
     */
110 40
    public function object(): object
111
    {
112
        if (!$this->autoRefresh || !$this->persisted) {
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
        $this->objectManager()->flush();
140 80
        $this->persisted = true;
141 60
142
        return $this;
143 60
    }
144
145
    public function remove(): self
146 20
    {
147 10
        $this->objectManager()->remove($this->object);
148
        $this->objectManager()->flush();
149
        $this->autoRefresh = $this->persisted = false;
150 10
151
        return $this;
152 10
    }
153
154
    public function refresh(): self
155
    {
156
        if (!$this->persisted) {
157
            throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class));
158 70
        }
159
160 70
        if ($this->objectManager()->contains($this->object)) {
161
            $this->objectManager()->refresh($this->object);
162
163 80
            return $this;
164
        }
165 80
166
        if (!$object = $this->fetchObject()) {
167 80
            throw new \RuntimeException('The object no longer exists.');
168 80
        }
169
170
        $this->object = $object;
171 80
172
        return $this;
173
    }
174
175
    /**
176
     * @param mixed $value
177 10
     */
178
    public function forceSet(string $property, $value): self
179 10
    {
180
        return $this->forceSetAll([$property => $value]);
181
    }
182 10
183
    public function forceSetAll(array $properties): self
184 10
    {
185
        $object = $this->object();
186
187 40
        foreach ($properties as $property => $value) {
188
            Instantiator::forceSet($object, $property, $value);
189 40
        }
190
191
        return $this;
192
    }
193 40
194
    /**
195 40
     * @return mixed
196
     */
197
    public function forceGet(string $property)
198
    {
199
        return Instantiator::forceGet($this->object(), $property);
200
    }
201
202
    public function repository(): RepositoryProxy
203
    {
204
        return Factory::configuration()->repositoryFor($this->class);
205
    }
206
207
    public function enableAutoRefresh(): self
208
    {
209
        if (!$this->persisted) {
210
            throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class));
211 635
        }
212
213 635
        $this->autoRefresh = true;
214 635
215
        return $this;
216 635
    }
217
218 635
    public function disableAutoRefresh(): self
219
    {
220 635
        $this->autoRefresh = false;
221
222
        return $this;
223 30
    }
224
225 30
    /**
226
     * Ensures "autoRefresh" is disabled when executing $callback. Re-enables
227 30
     * "autoRefresh" after executing callback if it was enabled.
228
     *
229
     * @param callable $callback (object|Proxy $object): void
230 10
     *
231
     * @psalm-return static
232 10
     */
233
    public function withoutAutoRefresh(callable $callback): self
234 10
    {
235
        $original = $this->autoRefresh;
236
        $this->autoRefresh = false;
237
238
        $this->executeCallback($callback);
239
240 635
        $this->autoRefresh = $original; // set to original value (even if it was false)
241
242 635
        return $this;
243 635
    }
244
245 635
    public function assertPersisted(string $message = 'The object is not persisted.'): self
246 20
    {
247
        Assert::assertNotNull($this->fetchObject(), $message);
248
249 635
        return $this;
250 635
    }
251
252
    public function assertNotPersisted(string $message = 'The object is persisted but it should not be.'): self
253
    {
254
        Assert::assertNull($this->fetchObject(), $message);
255 60
256
        return $this;
257 60
    }
258
259 60
    /**
260
     * @internal
261
     */
262 645
    public function executeCallback(callable $callback, ...$arguments): void
263
    {
264 645
        Callback::createFor($callback)->invoke(
265
            Parameter::union(
266
                Parameter::untyped($this),
267
                Parameter::typed(self::class, $this),
268
                Parameter::typed($this->class, Parameter::factory(function() { return $this->object(); }))
269
            )->optional(),
270
            ...$arguments
271
        );
272
    }
273
274
    /**
275
     * @psalm-return TProxiedObject|null
276
     */
277
    private function fetchObject(): ?object
278
    {
279
        $id = $this->objectManager()->getClassMetadata($this->class)->getIdentifierValues($this->object);
280
281
        return empty($id) ? null : $this->objectManager()->find($this->class, $id);
282
    }
283
284
    private function objectManager(): ObjectManager
285
    {
286
        return Factory::configuration()->objectManagerFor($this->class);
287
    }
288
}
289