Passed
Pull Request — master (#127)
by Wouter
02:48
created

Factory::withoutAutoRefresh()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 19
ccs 8
cts 8
cp 1
rs 9.6111
cc 5
nc 7
nop 2
crap 5
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Faker;
6
use ProxyManager\Proxy\ValueHolderInterface;
7
8
/**
9
 * @template TObject as object
10
 * @abstract
11
 *
12
 * @author Kevin Bond <[email protected]>
13
 */
14
class Factory
15
{
16
    /** @var Configuration|null */
17
    private static $configuration;
18
19
    /**
20
     * @var string
21
     * @psalm-var class-string<TObject>
22
     */
23
    private $class;
24
25
    /** @var callable|null */
26
    private $instantiator;
27
28
    /** @var bool */
29
    private $persist = true;
30
31
    /** @var array<array|callable> */
32
    private $attributeSet = [];
33
34
    /** @var callable[] */
35
    private $beforeInstantiate = [];
36
37
    /** @var callable[] */
38
    private $afterInstantiate = [];
39
40
    /** @var callable[] */
41
    private $afterPersist = [];
42
43
    /** @var ProxyGenerator|null */
44
    private $proxyGenerator;
45
46 834
    /** @var array */
47
    private $proxyMethods = [];
48 834
49 834
    /**
50 834
     * @param array|callable $defaultAttributes
51
     *
52
     * @psalm-param class-string<TObject> $class
53
     */
54
    public function __construct(string $class, $defaultAttributes = [])
55
    {
56
        if (self::class === static::class) {
0 ignored issues
show
introduced by
The condition self::class === static::class is always true.
Loading history...
57
            trigger_deprecation('zenstruck/foundry', '1.9', 'Instantiating "%s" directly is deprecated and this class will be abstract in 2.0, use "%s" instead.', self::class, AnonymousFactory::class);
58
        }
59 813
60
        $this->class = $class;
61
        $this->attributeSet[] = $defaultAttributes;
62 813
    }
63
64
    public function __call(string $name, array $arguments)
65 813
    {
66
        if ('createMany' !== $name) {
67 813
            throw new \BadMethodCallException(\sprintf('Call to undefined method "%s::%s".', static::class, $name));
68 20
        }
69
70 20
        trigger_deprecation('zenstruck/foundry', '1.7', 'Calling instance method "%1$s::createMany()" is deprecated and will be removed in 2.0, use the static "%1$s:createMany()" method instead.', static::class);
71 10
72
        return $this->many($arguments[0])->create($arguments[1] ?? []);
73
    }
74
75
    /**
76 803
     * @param array|callable $attributes
77
     *
78 753
     * @return Proxy|object
79 803
     *
80 803
     * @psalm-return Proxy<TObject>|TObject
81
     */
82
    final public function create($attributes = []): object
83
    {
84 803
        // merge the factory attribute set with the passed attributes
85
        $attributeSet = \array_merge($this->attributeSet, [$attributes]);
86 803
87 10
        // normalize each attribute set and collapse
88
        $attributes = \array_merge(...\array_map([$this, 'normalizeAttributes'], $attributeSet));
89
90 803
        foreach ($this->beforeInstantiate as $callback) {
91
            $attributes = $callback($attributes);
92 803
93 190
            if (!\is_array($attributes)) {
94
                throw new \LogicException('Before Instantiate event callback must return an array.');
95
            }
96
        }
97 625
98 30
        // filter each attribute to convert proxies and factories to objects
99
        $attributes = \array_map(
100 625
            function($value) {
101
                return $this->normalizeAttribute($value);
102
            },
103
            $attributes
104
        );
105
106
        // instantiate the object with the users instantiator or if not set, the default instantiator
107
        $object = ($this->instantiator ?? self::configuration()->instantiator())($attributes, $this->class);
108 240
109
        foreach ($this->afterInstantiate as $callback) {
110 240
            $callback($object, $attributes);
111
        }
112
113
        if ($this->proxyGenerator) {
114
            $object = $this->proxyGenerator->generate($object, $this->proxyMethods);
115
116
            if ($this->isPersisting()) {
117
                self::persist($object);
118
            }
119
120 190
            return $object;
121
        }
122 190
123
        $proxy = new Proxy($object);
124
125
        if (!$this->isPersisting()) {
126
            return $proxy;
127
        }
128 60
129
        return $proxy->save()->withoutAutoRefresh(function(Proxy $proxy) use ($attributes) {
130 60
            foreach ($this->afterPersist as $callback) {
131 60
                $proxy->executeCallback($callback, $attributes);
132
            }
133 60
        });
134
    }
135
136
    /**
137
     * @psalm-param object|Proxy<object> $object
138
     */
139
    final public static function persist(object $object): void
140
    {
141 634
        $object = self::realObject($object);
142
143 634
        $objectManager = self::configuration()->objectManagerFor($object);
144 634
        $objectManager->persist($object);
145
        $objectManager->flush();
146 634
    }
147
148
    public static function remove(object $object): void
149
    {
150
        $object = self::realObject($object);
151
152
        $objectManager = self::configuration()->objectManagerFor($object);
153
        $objectManager->remove($object);
154 30
        $objectManager->flush();
155
    }
156 30
157 30
    /**
158
     * @see FactoryCollection::__construct()
159 30
     *
160
     * @psalm-return FactoryCollection<TObject>
161
     */
162
    final public function many(int $min, ?int $max = null): FactoryCollection
163
    {
164
        return new FactoryCollection($this, $min, $max);
165
    }
166
167 20
    final public static function disableAutoRefresh(object $model): void
168
    {
169 20
        if ($model instanceof Proxy) {
170 20
            $model->disableAutoRefresh();
171
172 20
            return;
173
        }
174
175
        if ($model instanceof ValueHolderInterface) {
176
            $model->getWrappedValueHolderValue()->_foundry_autoRefresh = false;
177
        }
178
    }
179
180 20
    final public static function withoutAutoRefresh(object $model, callable $callback): void
181
    {
182 20
        if ($model instanceof Proxy) {
183 20
            $model->withoutAutoRefresh($callback);
184
185 20
            return;
186
        }
187
188
        if ($model instanceof ValueHolderInterface) {
189
            $realModel = self::realObject($model);
190
            $original = \property_exists($realModel, '_foundry_autoRefresh') ? $realModel->_foundry_autoRefresh : true;
191
192
            $realModel->_foundry_autoRefresh = false;
193 20
        }
194
195 20
        ($callback)($model);
196 20
197
        if ($model instanceof ValueHolderInterface) {
198 20
            $realModel->_foundry_autoRefresh = $original;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $realModel does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $original does not seem to be defined for all execution paths leading up to this point.
Loading history...
199
        }
200
    }
201
202
    /**
203
     * @return static
204 968
     */
205
    public function withoutPersisting(): self
206 968
    {
207 968
        $cloned = clone $this;
208
        $cloned->persist = false;
209
210
        return $cloned;
211
    }
212 964
213
    /**
214 964
     * @param array|callable $attributes
215 10
     *
216
     * @return static
217
     */
218 964
    final public function withAttributes($attributes = []): self
219 964
    {
220 964
        $cloned = clone $this;
221
        $cloned->attributeSet[] = $attributes;
222
223
        return $cloned;
224
    }
225
226
    /**
227 934
     * @param callable $callback (array $attributes): array
228
     *
229 934
     * @return static
230
     */
231
    final public function beforeInstantiate(callable $callback): self
232
    {
233 934
        $cloned = clone $this;
234
        $cloned->beforeInstantiate[] = $callback;
235
236
        return $cloned;
237
    }
238
239 1016
    /**
240
     * @param callable $callback (object $object, array $attributes): void
241 1016
     *
242
     * @return static
243
     */
244 643
    final public function afterInstantiate(callable $callback): self
245
    {
246 643
        $cloned = clone $this;
247
        $cloned->afterInstantiate[] = $callback;
248
249
        return $cloned;
250
    }
251
252 813
    /**
253
     * @param callable $callback (object|Proxy $object, array $attributes): void
254 813
     *
255
     * @return static
256
     */
257
    final public function afterPersist(callable $callback): self
258
    {
259
        $cloned = clone $this;
260
        $cloned->afterPersist[] = $callback;
261
262 753
        return $cloned;
263
    }
264 753
265 80
    /**
266
     * @param callable $instantiator (array $attributes, string $class): object
267
     *
268 753
     * @return static
269 70
     */
270
    final public function instantiateWith(callable $instantiator): self
271
    {
272 753
        $cloned = clone $this;
273
        $cloned->instantiator = $instantiator;
274 100
275
        return $cloned;
276 80
    }
277 100
278 100
    /**
279
     * @internal
280
     */
281
    final public static function boot(Configuration $configuration): void
282 753
    {
283 753
        self::$configuration = $configuration;
284
    }
285
286 130
    /**
287
     * @internal
288 30
     */
289
    final public static function shutdown(): void
290
    {
291 130
        if (!self::isBooted()) {
292
            return;
293
        }
294 70
295
        self::$configuration->faker()->unique(true); // reset unique
0 ignored issues
show
Bug introduced by
The method faker() does not exist on null. ( Ignorable by Annotation )

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

295
        self::$configuration->/** @scrutinizer ignore-call */ 
296
                              faker()->unique(true); // reset unique

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
296 70
        self::$configuration = null;
297
    }
298 20
299 20
    /**
300 20
     * @internal
301
     * @psalm-suppress InvalidNullableReturnType
302
     * @psalm-suppress NullableReturnStatement
303 20
     */
304
    final public static function configuration(): Configuration
305
    {
306 50
        if (!self::isBooted()) {
307
            throw new \RuntimeException('Foundry is not yet booted. Using in a test: is your Test case using the Factories trait? Using in a fixture: is ZenstruckFoundryBundle enabled for this environment?');
308
        }
309 50
310
        return self::$configuration;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::configuration could return the type null which is incompatible with the type-hinted return Zenstruck\Foundry\Configuration. Consider adding an additional type-check to rule them out.
Loading history...
311 50
    }
312 50
313
    /**
314 50
     * @internal
315
     */
316 50
    final public static function isBooted(): bool
317 20
    {
318
        return null !== self::$configuration;
319
    }
320
321 30
    final public static function faker(): Faker\Generator
322
    {
323
        return self::configuration()->faker();
324 803
    }
325
326 803
    /**
327
     * Instead of returning an instance of {@see Proxy}, use a generator to create a real proxy for the object.
328
     */
329
    public function withProxyGenerator(array $proxyMethods = []): self
330
    {
331
        $this->proxyGenerator = new ProxyGenerator(self::configuration());
332
        $this->proxyMethods = $proxyMethods;
333
334
        return $this;
335
    }
336
337
    /**
338
     * @internal
339
     *
340
     * @psalm-return class-string<TObject>
341
     */
342
    final protected function class(): string
343
    {
344
        return $this->class;
345
    }
346
347
    /**
348
     * @param array|callable $attributes
349
     */
350
    private static function normalizeAttributes($attributes): array
351
    {
352
        return \is_callable($attributes) ? $attributes(self::faker()) : $attributes;
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_callable($attr...:faker()) : $attributes could return the type callable which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
353
    }
354
355
    /**
356
     * @param mixed $value
357
     *
358
     * @return mixed
359
     */
360
    private function normalizeAttribute($value)
361
    {
362
        if ($value instanceof Proxy) {
363
            return $value->isPersisted() ? $value->refresh()->object() : $value->object();
364
        }
365
366
        if ($value instanceof FactoryCollection) {
367
            $value = $this->normalizeCollection($value);
368
        }
369
370
        if (\is_array($value)) {
371
            // possible OneToMany/ManyToMany relationship
372
            return \array_map(
373
                function($value) {
374
                    return $this->normalizeAttribute($value);
375
                },
376
                $value
377
            );
378
        }
379
380
        if (!$value instanceof self) {
381
            return \is_object($value) ? self::normalizeObject($value) : $value;
382
        }
383
384
        if (!$this->isPersisting()) {
385
            // ensure attribute Factory's are also not persisted
386
            $value = $value->withoutPersisting();
387
        }
388
389
        return $value->create()->object();
390
    }
391
392
    private static function normalizeObject(object $object): object
393
    {
394
        try {
395
            return Proxy::createFromPersisted($object)->refresh()->object();
396
        } catch (\RuntimeException $e) {
397
            return $object;
398
        }
399
    }
400
401
    private function normalizeCollection(FactoryCollection $collection): array
402
    {
403
        if ($this->isPersisting() && $field = $this->inverseRelationshipField($collection->factory())) {
404
            $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
405
                $collection->create([$field => $proxy]);
406
                $proxy->refresh();
407
            };
408
409
            // creation delegated to afterPersist event - return empty array here
410
            return [];
411
        }
412
413
        return $collection->all();
414
    }
415
416
    private function inverseRelationshipField(self $factory): ?string
417
    {
418
        $collectionClass = $factory->class;
419
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
420
421
        foreach ($collectionMetadata->getAssociationNames() as $field) {
422
            // ensure 1-n and associated class matches
423
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
424
                return $field;
425
            }
426
        }
427
428
        return null; // no relationship found
429
    }
430
431
    private function isPersisting(): bool
432
    {
433
        return self::configuration()->hasManagerRegistry() ? $this->persist : false;
434
    }
435
436
    /**
437
     * @template T of object
438
     * @psalm-param T|Proxy<T>|ValueHolderInterface<T> $proxyObject
439
     * @psalm-return T
440
     *
441
     * @psalm-suppress InvalidReturnType
442
     * @psalm-suppress InvalidReturnStatement
443
     */
444
    private static function realObject(object $proxyObject): object
445
    {
446
        if ($proxyObject instanceof Proxy) {
447
            return $proxyObject->object();
448
        }
449
450
        if ($proxyObject instanceof ValueHolderInterface && $valueHolder = $proxyObject->getWrappedValueHolderValue()) {
451
            return $valueHolder;
452
        }
453
454
        return $proxyObject;
455
    }
456
}
457