Passed
Pull Request — master (#216)
by Charly
03:17 queued 24s
created

Factory::resetDoctrineEvents()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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