Passed
Pull Request — master (#216)
by Charly
04:23
created

Factory::disableDoctrineEvents()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 15
ccs 0
cts 0
cp 0
rs 10
cc 4
nc 4
nop 0
crap 20
1
<?php
2
3
namespace Zenstruck\Foundry;
4
5
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Mapping\ClassMetadataInfo;
8
use Faker;
9
10
/**
11
 * @template TObject of object
12
 * @abstract
13
 *
14
 * @author Kevin Bond <[email protected]>
15
 */
16
class Factory
17
{
18
    /** @var Configuration|null */
19
    private static $configuration;
20
21
    /**
22
     * @var string
23
     * @psalm-var class-string<TObject>
24
     */
25
    private $class;
26
27
    /** @var callable|null */
28
    private $instantiator;
29
30
    /** @var bool */
31
    private $persist = true;
32
33
    /** @var bool */
34
    private $doctrineEvents = true;
35
36
    /** @var string|null */
37
    private $doctrineEventTypeName = null;
38
39
    /** @var array */
40
    private $disabledDoctrineEvents = [];
41
42
    /** @var bool */
43
    private $cascadePersist = false;
44
45
    /** @var array<array|callable> */
46 834
    private $attributeSet = [];
47
48 834
    /** @var callable[] */
49 834
    private $beforeInstantiate = [];
50 834
51
    /** @var callable[] */
52
    private $afterInstantiate = [];
53
54
    /** @var callable[] */
55
    private $afterPersist = [];
56
57
    /**
58
     * @param array|callable $defaultAttributes
59 813
     *
60
     * @psalm-param class-string<TObject> $class
61
     */
62 813
    public function __construct(string $class, $defaultAttributes = [])
63
    {
64
        if (self::class === static::class) {
0 ignored issues
show
introduced by
The condition self::class === static::class is always true.
Loading history...
65 813
            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);
66
        }
67 813
68 20
        $this->class = $class;
69
        $this->attributeSet[] = $defaultAttributes;
70 20
    }
71 10
72
    public function __call(string $name, array $arguments)
73
    {
74
        if ('createMany' !== $name) {
75
            throw new \BadMethodCallException(\sprintf('Call to undefined method "%s::%s".', static::class, $name));
76 803
        }
77
78 753
        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);
79 803
80 803
        return $this->many($arguments[0])->create($arguments[1] ?? []);
81
    }
82
83
    /**
84 803
     * @param array|callable $attributes
85
     *
86 803
     * @return Proxy<TObject>&TObject
87 10
     * @psalm-return Proxy<TObject>
88
     */
89
    final public function create($attributes = []): Proxy
90 803
    {
91
        // merge the factory attribute set with the passed attributes
92 803
        $attributeSet = \array_merge($this->attributeSet, [$attributes]);
93 190
94
        // normalize each attribute set and collapse
95
        $attributes = \array_merge(...\array_map([$this, 'normalizeAttributes'], $attributeSet));
96
97 625
        foreach ($this->beforeInstantiate as $callback) {
98 30
            $attributes = $callback($attributes);
99
100 625
            if (!\is_array($attributes)) {
101
                throw new \LogicException('Before Instantiate event callback must return an array.');
102
            }
103
        }
104
105
        // filter each attribute to convert proxies and factories to objects
106
        $attributes = \array_map(
107
            function($value) {
108 240
                return $this->normalizeAttribute($value);
109
            },
110 240
            $attributes
111
        );
112
113
        // instantiate the object with the users instantiator or if not set, the default instantiator
114
        $object = ($this->instantiator ?? self::configuration()->instantiator())($attributes, $this->class);
115
116
        foreach ($this->afterInstantiate as $callback) {
117
            $callback($object, $attributes);
118
        }
119
120 190
        $proxy = new Proxy($object);
121
122 190
        if (!$this->isPersisting() || true === $this->cascadePersist) {
123
            return $proxy;
124
        }
125
126
        if (false === $this->doctrineEvents) {
127
            $this->disableDoctrineEvents();
128 60
        }
129
130 60
        $proxy->save();
131 60
132
        if (false === $this->doctrineEvents) {
133 60
            $this->resetDoctrineEvents();
134
        }
135
136
        return $proxy->withoutAutoRefresh(function(Proxy $proxy) use ($attributes) {
137
            foreach ($this->afterPersist as $callback) {
138
                $proxy->executeCallback($callback, $attributes);
139
            }
140
        });
141 634
    }
142
143 634
    /**
144 634
     * @see FactoryCollection::__construct()
145
     *
146 634
     * @return FactoryCollection<TObject>
147
     */
148
    final public function many(int $min, ?int $max = null): FactoryCollection
149
    {
150
        return new FactoryCollection($this, $min, $max);
151
    }
152
153
    /**
154 30
     * @return static
155
     */
156 30
    public function withoutPersisting(): self
157 30
    {
158
        $cloned = clone $this;
159 30
        $cloned->persist = false;
160
161
        return $cloned;
162
    }
163
164
    /**
165
     * @return static
166
     */
167 20
    public function withoutDoctrineEvents(?string $eventTypeName = null): self
168
    {
169 20
        $cloned = clone $this;
170 20
        $cloned->doctrineEvents = false;
171
        $cloned->doctrineEventTypeName = $eventTypeName;
172 20
173
        return $cloned;
174
    }
175
176
    /**
177
     * @param array|callable $attributes
178
     *
179
     * @return static
180 20
     */
181
    final public function withAttributes($attributes = []): self
182 20
    {
183 20
        $cloned = clone $this;
184
        $cloned->attributeSet[] = $attributes;
185 20
186
        return $cloned;
187
    }
188
189
    /**
190
     * @param callable $callback (array $attributes): array
191
     *
192
     * @return static
193 20
     */
194
    final public function beforeInstantiate(callable $callback): self
195 20
    {
196 20
        $cloned = clone $this;
197
        $cloned->beforeInstantiate[] = $callback;
198 20
199
        return $cloned;
200
    }
201
202
    /**
203
     * @param callable $callback (object $object, array $attributes): void
204 968
     *
205
     * @return static
206 968
     */
207 968
    final public function afterInstantiate(callable $callback): self
208
    {
209
        $cloned = clone $this;
210
        $cloned->afterInstantiate[] = $callback;
211
212 964
        return $cloned;
213
    }
214 964
215 10
    /**
216
     * @param callable $callback (object|Proxy $object, array $attributes): void
217
     *
218 964
     * @return static
219 964
     */
220 964
    final public function afterPersist(callable $callback): self
221
    {
222
        $cloned = clone $this;
223
        $cloned->afterPersist[] = $callback;
224
225
        return $cloned;
226
    }
227 934
228
    /**
229 934
     * @param callable $instantiator (array $attributes, string $class): object
230
     *
231
     * @return static
232
     */
233 934
    final public function instantiateWith(callable $instantiator): self
234
    {
235
        $cloned = clone $this;
236
        $cloned->instantiator = $instantiator;
237
238
        return $cloned;
239 1016
    }
240
241 1016
    /**
242
     * @internal
243
     */
244 643
    final public static function boot(Configuration $configuration): void
245
    {
246 643
        self::$configuration = $configuration;
247
    }
248
249
    /**
250
     * @internal
251
     */
252 813
    final public static function shutdown(): void
253
    {
254 813
        if (!self::isBooted()) {
255
            return;
256
        }
257
258
        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

258
        self::$configuration->/** @scrutinizer ignore-call */ 
259
                              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...
259
        self::$configuration = null;
260
    }
261
262 753
    /**
263
     * @internal
264 753
     * @psalm-suppress InvalidNullableReturnType
265 80
     * @psalm-suppress NullableReturnStatement
266
     */
267
    final public static function configuration(): Configuration
268 753
    {
269 70
        if (!self::isBooted()) {
270
            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?');
271
        }
272 753
273
        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...
274 100
    }
275
276 80
    /**
277 100
     * @internal
278 100
     */
279
    final public static function isBooted(): bool
280
    {
281
        return null !== self::$configuration;
282 753
    }
283 753
284
    final public static function faker(): Faker\Generator
285
    {
286 130
        return self::configuration()->faker();
287
    }
288 30
289
    /**
290
     * @internal
291 130
     *
292
     * @psalm-return class-string<TObject>
293
     */
294 70
    final protected function class(): string
295
    {
296 70
        return $this->class;
297
    }
298 20
299 20
    /**
300 20
     * @param array|callable $attributes
301
     */
302
    private static function normalizeAttributes($attributes): array
303 20
    {
304
        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...
305
    }
306 50
307
    /**
308
     * @param mixed $value
309 50
     *
310
     * @return mixed
311 50
     */
312 50
    private function normalizeAttribute($value)
313
    {
314 50
        if ($value instanceof Proxy) {
315
            return $value->isPersisted() ? $value->refresh()->object() : $value->object();
316 50
        }
317 20
318
        if ($value instanceof FactoryCollection) {
319
            $value = $this->normalizeCollection($value);
320
        }
321 30
322
        if (\is_array($value)) {
323
            // possible OneToMany/ManyToMany relationship
324 803
            return \array_map(
325
                function($value) {
326 803
                    return $this->normalizeAttribute($value);
327
                },
328
                $value
329
            );
330
        }
331
332
        if (!$value instanceof self) {
333
            return \is_object($value) ? self::normalizeObject($value) : $value;
334
        }
335
336
        if (!$this->isPersisting()) {
337
            // ensure attribute Factory's are also not persisted
338
            $value = $value->withoutPersisting();
339
        }
340
341
        // Check if the attribute is cascade persist
342
        if (self::configuration()->hasManagerRegistry()) {
343
            $relationField = $this->relationshipField($value);
344
            $value->cascadePersist = $this->hasCascadePersist($value, $relationField);
345
        }
346
347
        return $value->create()->object();
348
    }
349
350
    private static function normalizeObject(object $object): object
351
    {
352
        try {
353
            return Proxy::createFromPersisted($object)->refresh()->object();
354
        } catch (\RuntimeException $e) {
355
            return $object;
356
        }
357
    }
358
359
    private function normalizeCollection(FactoryCollection $collection): array
360
    {
361
        if ($this->isPersisting()) {
362
            $field = $this->inverseRelationshipField($collection->factory());
363
            $cascadePersist = $this->hasCascadePersist($collection->factory(), $field);
364
365
            if ($field && false === $cascadePersist) {
366
                $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
367
                    $collection->create([$field => $proxy]);
368
                    $proxy->refresh();
369
                };
370
371
                // creation delegated to afterPersist event - return empty array here
372
                return [];
373
            }
374
        }
375
376
        return \array_map(
377
            function(self $factory) {
378
                $factory->cascadePersist = $this->cascadePersist;
379
380
                return $factory;
381
            },
382
            $collection->all()
383
        );
384
    }
385
386
    private function relationshipField(self $factory): ?string
387
    {
388
        $factoryClass = $this->class;
389
        $relationClass = $factory->class;
390
391
        // Check inversedBy side ($this is the owner of the relation)
392
        $factoryClassMetadata = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);
393
394
        foreach ($factoryClassMetadata->getAssociationNames() as $field) {
395
            if (!$factoryClassMetadata->isAssociationInverseSide($field) && $factoryClassMetadata->getAssociationTargetClass($field) === $relationClass) {
396
                return $field;
397
            }
398
        }
399
400
        try {
401
            // Check mappedBy side ($factory is the owner of the relation)
402
            $relationClassMetadata = self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass);
403
        } catch (\RuntimeException $e) {
404
            // relation not managed - could be embeddable
405
            return null;
406
        }
407
408
        foreach ($relationClassMetadata->getAssociationNames() as $field) {
409
            if (($relationClassMetadata->isSingleValuedAssociation($field) || $relationClassMetadata->isCollectionValuedAssociation($field)) && $relationClassMetadata->getAssociationTargetClass($field) === $factoryClass) {
410
                return $field;
411
            }
412
        }
413
414
        return null; // no relationship found
415
    }
416
417
    private function inverseRelationshipField(self $factory): ?string
418
    {
419
        $collectionClass = $factory->class;
420
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
421
422
        foreach ($collectionMetadata->getAssociationNames() as $field) {
423
            // ensure 1-n and associated class matches
424
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
425
                return $field;
426
            }
427
        }
428
429
        return null; // no relationship found
430
    }
431
432
    private function hasCascadePersist(self $factory, ?string $field): bool
433
    {
434
        if (null === $field) {
435
            return false;
436
        }
437
438
        $factoryClass = $this->class;
439
        $relationClass = $factory->class;
440
        $classMetadataFactory = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);
441
        $relationClassMetadata = self::configuration()->objectManagerFor($relationClass)->getClassMetadata($relationClass);
442
443
        if (!$relationClassMetadata instanceof ClassMetadataInfo || !$classMetadataFactory instanceof ClassMetadataInfo) {
444
            return false;
445
        }
446
447
        if ($relationClassMetadata->hasAssociation($field)) {
448
            $inversedBy = $relationClassMetadata->getAssociationMapping($field)['inversedBy'];
449
            if (null === $inversedBy) {
450
                return false;
451
            }
452
453
            $cascadeMetadata = $classMetadataFactory->getAssociationMapping($inversedBy)['cascade'] ?? [];
454
        } else {
455
            $cascadeMetadata = $classMetadataFactory->getAssociationMapping($field)['cascade'] ?? [];
456
        }
457
458
        return \in_array('persist', $cascadeMetadata, true);
459
    }
460
461
    private function isPersisting(): bool
462
    {
463
        if (!$this->persist || !self::configuration()->hasManagerRegistry()) {
464
            return false;
465
        }
466
467
        try {
468
            self::configuration()->objectManagerFor($this->class);
469
470
            return true;
471
        } catch (\RuntimeException $e) {
472
            // entity not managed (perhaps Embeddable)
473
            return false;
474
        }
475
    }
476
477
    private function resetDoctrineEvents(): void
478
    {
479
        $this->doctrineEvents = true;
480
        $this->doctrineEventTypeName = null;
481
482
        /** @var EntityManagerInterface $manager */
483
        $manager = self::configuration()->objectManagerFor($this->class);
484
485
        foreach ($this->disabledDoctrineEvents as $type => $listenersByType) {
486
            foreach ($listenersByType as $listener) {
487
                if ($listener instanceof EventSubscriberInterface) {
488
                    $manager->getConnection()->getEventManager()->addEventSubscriber($listener);
489
490
                    continue;
491
                }
492
493
                $manager->getConnection()->getEventManager()->addEventListener($type, $listener);
494
            }
495
        }
496
    }
497
498
    private function disableDoctrineEvents(): void
499
    {
500
        /** @var EntityManagerInterface $manager */
501
        $manager = self::configuration()->objectManagerFor($this->class);
502
503
        $this->disabledDoctrineEvents = $manager->getConnection()->getEventManager()->getListeners($this->doctrineEventTypeName);
504
        foreach ($this->disabledDoctrineEvents as $type => $listenersByType) {
505
            foreach ($listenersByType as $listener) {
506
                if ($listener instanceof EventSubscriberInterface) {
507
                    $manager->getConnection()->getEventManager()->removeEventSubscriber($listener);
508
509
                    continue;
510
                }
511
512
                $manager->getConnection()->getEventManager()->removeEventListener($type, $listener);
513
            }
514
        }
515
    }
516
}
517