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

Factory   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 99.02%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
wmc 55
eloc 122
c 4
b 0
f 1
dl 0
loc 406
ccs 101
cts 102
cp 0.9902
rs 6

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __call() 0 9 2
A remove() 0 7 1
A many() 0 3 1
A __construct() 0 8 2
B create() 0 50 8
A isPersisting() 0 3 2
A withoutPersisting() 0 6 1
A normalizeObject() 0 6 2
A isBooted() 0 3 1
A class() 0 3 1
A configuration() 0 7 2
A normalizeAttributes() 0 3 2
A afterPersist() 0 6 1
B normalizeAttribute() 0 30 8
A afterInstantiate() 0 6 1
A faker() 0 3 1
A save() 0 7 1
A boot() 0 3 1
A shutdown() 0 8 2
A normalizeCollection() 0 13 3
A inverseRelationshipField() 0 13 4
A instantiateWith() 0 6 1
A withProxyGenerator() 0 6 1
A realObject() 0 11 4
A beforeInstantiate() 0 6 1
A withAttributes() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Factory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Factory, and based on these observations, apply Extract Interface, too.

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

261
        self::$configuration->/** @scrutinizer ignore-call */ 
262
                              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...
262 753
        self::$configuration = null;
263
    }
264 753
265 80
    /**
266
     * @internal
267
     * @psalm-suppress InvalidNullableReturnType
268 753
     * @psalm-suppress NullableReturnStatement
269 70
     */
270
    final public static function configuration(): Configuration
271
    {
272 753
        if (!self::isBooted()) {
273
            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?');
274 100
        }
275
276 80
        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...
277 100
    }
278 100
279
    /**
280
     * @internal
281
     */
282 753
    final public static function isBooted(): bool
283 753
    {
284
        return null !== self::$configuration;
285
    }
286 130
287
    final public static function faker(): Faker\Generator
288 30
    {
289
        return self::configuration()->faker();
290
    }
291 130
292
    /**
293
     * Instead of returning an instance of {@see Proxy}, use a generator to create a real proxy for the object.
294 70
     */
295
    public function withProxyGenerator(array $proxyMethods = []): self
296 70
    {
297
        $this->proxyGenerator = new ProxyGenerator(self::configuration());
298 20
        $this->proxyMethods = $proxyMethods;
299 20
300 20
        return $this;
301
    }
302
303 20
    /**
304
     * @internal
305
     *
306 50
     * @psalm-return class-string<TObject>
307
     */
308
    final protected function class(): string
309 50
    {
310
        return $this->class;
311 50
    }
312 50
313
    /**
314 50
     * @param array|callable $attributes
315
     */
316 50
    private static function normalizeAttributes($attributes): array
317 20
    {
318
        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...
319
    }
320
321 30
    /**
322
     * @param mixed $value
323
     *
324 803
     * @return mixed
325
     */
326 803
    private function normalizeAttribute($value)
327
    {
328
        if ($value instanceof Proxy) {
329
            return $value->isPersisted() ? $value->refresh()->object() : $value->object();
330
        }
331
332
        if ($value instanceof FactoryCollection) {
333
            $value = $this->normalizeCollection($value);
334
        }
335
336
        if (\is_array($value)) {
337
            // possible OneToMany/ManyToMany relationship
338
            return \array_map(
339
                function($value) {
340
                    return $this->normalizeAttribute($value);
341
                },
342
                $value
343
            );
344
        }
345
346
        if (!$value instanceof self) {
347
            return \is_object($value) ? self::normalizeObject($value) : $value;
348
        }
349
350
        if (!$this->isPersisting()) {
351
            // ensure attribute Factory's are also not persisted
352
            $value = $value->withoutPersisting();
353
        }
354
355
        return $value->create()->object();
356
    }
357
358
    private static function normalizeObject(object $object): object
359
    {
360
        try {
361
            return Proxy::createFromPersisted($object)->refresh()->object();
362
        } catch (\RuntimeException $e) {
363
            return $object;
364
        }
365
    }
366
367
    private function normalizeCollection(FactoryCollection $collection): array
368
    {
369
        if ($this->isPersisting() && $field = $this->inverseRelationshipField($collection->factory())) {
370
            $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
371
                $collection->create([$field => $proxy]);
372
                $proxy->refresh();
373
            };
374
375
            // creation delegated to afterPersist event - return empty array here
376
            return [];
377
        }
378
379
        return $collection->all();
380
    }
381
382
    private function inverseRelationshipField(self $factory): ?string
383
    {
384
        $collectionClass = $factory->class;
385
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
386
387
        foreach ($collectionMetadata->getAssociationNames() as $field) {
388
            // ensure 1-n and associated class matches
389
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
390
                return $field;
391
            }
392
        }
393
394
        return null; // no relationship found
395
    }
396
397
    private function isPersisting(): bool
398
    {
399
        return self::configuration()->hasManagerRegistry() ? $this->persist : false;
400
    }
401
402
    /**
403
     * @template T of object
404
     * @psalm-param T|Proxy<T>|ValueHolderInterface<T> $proxyObject
405
     * @psalm-return T
406
     *
407
     * @psalm-suppress InvalidReturnType
408
     * @psalm-suppress InvalidReturnStatement
409
     */
410
    private static function realObject(object $proxyObject): object
411
    {
412
        if ($proxyObject instanceof Proxy) {
413
            return $proxyObject->object();
414
        }
415
416
        if ($proxyObject instanceof ValueHolderInterface && $valueHolder = $proxyObject->getWrappedValueHolderValue()) {
417
            return $valueHolder;
418
        }
419
420
        return $proxyObject;
421
    }
422
}
423