Issues (131)

src/Proxy.php (3 issues)

1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
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
    /**
39 857
     * @internal
40
     *
41 857
     * @psalm-param TProxiedObject $object
42 857
     */
43 857
    public function __construct(object $object)
44 857
    {
45
        $this->object = $object;
46 478
        $this->class = \get_class($object);
47
        $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh();
48 478
    }
49
50
    public function __call(string $method, array $arguments)
51 10
    {
52
        return $this->object()->{$method}(...$arguments);
53 10
    }
54
55
    public function __get(string $name)
56 10
    {
57
        return $this->object()->{$name};
58 10
    }
59 10
60
    public function __set(string $name, $value): void
61 10
    {
62
        $this->object()->{$name} = $value;
63 10
    }
64 10
65
    public function __unset(string $name): void
66 10
    {
67
        unset($this->object()->{$name});
68 10
    }
69
70
    public function __isset(string $name): bool
71 20
    {
72
        return isset($this->object()->{$name});
73 20
    }
74 10
75
    public function __toString(): string
76
    {
77
        if (!\method_exists($this->object, '__toString')) {
78 10
            if (\PHP_VERSION_ID < 70400) {
79
                return '(no __toString)';
80
            }
81 10
82
            throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class));
83
        }
84
85
        return $this->object()->__toString();
86
    }
87
88
    /**
89
     * @internal
90
     *
91 170
     * @template TObject of object
92
     * @psalm-param TObject $object
93 170
     * @psalm-return Proxy<TObject>
94 170
     */
95
    public static function createFromPersisted(object $object): self
96 170
    {
97
        $proxy = new self($object);
98
        $proxy->persisted = true;
99 428
100
        return $proxy;
101 428
    }
102
103
    public function isPersisted(): bool
104
    {
105
        return $this->persisted;
106
    }
107 568
108
    /**
109 568
     * @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...
110 40
     */
111
    public function object(): object
112
    {
113 568
        if (!$this->autoRefresh || !$this->persisted) {
114
            return $this->object;
115
        }
116 645
117
        $om = $this->objectManager();
118 645
119 635
        // only check for changes if the object is managed in the current om
120 635
        if (($om instanceof EntityManagerInterface || $om instanceof DocumentManager) && $om->contains($this->object)) {
121
            // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed
122 635
            $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object);
0 ignored issues
show
It seems like $om->getClassMetadata($this->class) can also be of type Doctrine\ORM\Mapping\ClassMetadata; however, parameter $class of Doctrine\ODM\MongoDB\Uni...ork::computeChangeSet() does only seem to accept Doctrine\ODM\MongoDB\Mapping\ClassMetadata, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
            $om->getUnitOfWork()->computeChangeSet(/** @scrutinizer ignore-type */ $om->getClassMetadata($this->class), $this->object);
Loading history...
It seems like $om->getClassMetadata($this->class) can also be of type Doctrine\ODM\MongoDB\Mapping\ClassMetadata; however, parameter $class of Doctrine\ORM\UnitOfWork::computeChangeSet() does only seem to accept Doctrine\ORM\Mapping\ClassMetadata, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
            $om->getUnitOfWork()->computeChangeSet(/** @scrutinizer ignore-type */ $om->getClassMetadata($this->class), $this->object);
Loading history...
123
124
            if (
125 10
                ($om instanceof EntityManagerInterface && !empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) ||
126
                ($om instanceof DocumentManager && !empty($om->getUnitOfWork()->getDocumentChangeSet($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
    /**
229
     * Ensures "autoRefresh" is disabled when executing $callback. Re-enables
230 10
     * "autoRefresh" after executing callback if it was enabled.
231
     *
232 10
     * @param callable $callback (object|Proxy $object): void
233
     *
234 10
     * @psalm-return static
235
     */
236
    public function withoutAutoRefresh(callable $callback): self
237
    {
238
        $original = $this->autoRefresh;
239
        $this->autoRefresh = false;
240 635
241
        $this->executeCallback($callback);
242 635
243 635
        $this->autoRefresh = $original; // set to original value (even if it was false)
244
245 635
        return $this;
246 20
    }
247
248
    public function assertPersisted(string $message = '{entity} is not persisted.'): self
249 635
    {
250 635
        Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]);
251
252
        return $this;
253
    }
254
255 60
    public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self
256
    {
257 60
        Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]);
258
259 60
        return $this;
260
    }
261
262 645
    /**
263
     * @internal
264 645
     */
265
    public function executeCallback(callable $callback, ...$arguments): void
266
    {
267
        Callback::createFor($callback)->invoke(
268
            Parameter::union(
269
                Parameter::untyped($this),
270
                Parameter::typed(self::class, $this),
271
                Parameter::typed($this->class, Parameter::factory(function() { return $this->object(); }))
272
            )->optional(),
273
            ...$arguments
274
        );
275
    }
276
277
    /**
278
     * @psalm-return TProxiedObject|null
279
     */
280
    private function fetchObject(): ?object
281
    {
282
        $objectManager = $this->objectManager();
283
284
        if ($objectManager instanceof DocumentManager) {
285
            $classMetadata = $objectManager->getClassMetadata($this->class);
286
            if (!$classMetadata->isEmbeddedDocument) {
287
                $id = $classMetadata->getIdentifierValue($this->object);
288
            }
289
        } else {
290
            $id = $objectManager->getClassMetadata($this->class)->getIdentifierValues($this->object);
291
        }
292
293
        return empty($id) ? null : $objectManager->find($this->class, $id);
294
    }
295
296
    private function objectManager(): ObjectManager
297
    {
298
        return Factory::configuration()->objectManagerFor($this->class);
299
    }
300
}
301