Passed
Pull Request — master (#84)
by Kevin
02:40
created

Factory::delayFlush()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

222
        self::$configuration->/** @scrutinizer ignore-call */ 
223
                              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...
223
        self::$configuration = null;
224
    }
225
226
    /**
227 934
     * @internal
228
     * @psalm-suppress InvalidNullableReturnType
229 934
     * @psalm-suppress NullableReturnStatement
230
     */
231
    final public static function configuration(): Configuration
232
    {
233 934
        if (!self::isBooted()) {
234
            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?');
235
        }
236
237
        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...
238
    }
239 1016
240
    /**
241 1016
     * @internal
242
     */
243
    final public static function isBooted(): bool
244 643
    {
245
        return null !== self::$configuration;
246 643
    }
247
248
    final public static function faker(): Faker\Generator
249
    {
250
        return self::configuration()->faker();
251
    }
252 813
253
    final public static function delayFlush(callable $callback): void
254 813
    {
255
        self::configuration()->delayFlush($callback);
256
    }
257
258
    /**
259
     * @internal
260
     *
261
     * @psalm-return class-string<TObject>
262 753
     */
263
    final protected function class(): string
264 753
    {
265 80
        return $this->class;
266
    }
267
268 753
    /**
269 70
     * @param array|callable $attributes
270
     */
271
    private static function normalizeAttributes($attributes): array
272 753
    {
273
        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...
274 100
    }
275
276 80
    /**
277 100
     * @param mixed $value
278 100
     *
279
     * @return mixed
280
     */
281
    private function normalizeAttribute($value)
282 753
    {
283 753
        if ($value instanceof Proxy) {
284
            return $value->isPersisted() ? $value->refresh()->object() : $value->object();
285
        }
286 130
287
        if ($value instanceof FactoryCollection) {
288 30
            $value = $this->normalizeCollection($value);
289
        }
290
291 130
        if (\is_array($value)) {
292
            // possible OneToMany/ManyToMany relationship
293
            return \array_map(
294 70
                function($value) {
295
                    return $this->normalizeAttribute($value);
296 70
                },
297
                $value
298 20
            );
299 20
        }
300 20
301
        if (!$value instanceof self) {
302
            return \is_object($value) ? self::normalizeObject($value) : $value;
303 20
        }
304
305
        if (!$this->isPersisting()) {
306 50
            // ensure attribute Factory's are also not persisted
307
            $value = $value->withoutPersisting();
308
        }
309 50
310
        return $value->create()->object();
311 50
    }
312 50
313
    private static function normalizeObject(object $object): object
314 50
    {
315
        try {
316 50
            return Proxy::createFromPersisted($object)->refresh()->object();
317 20
        } catch (\RuntimeException $e) {
318
            return $object;
319
        }
320
    }
321 30
322
    private function normalizeCollection(FactoryCollection $collection): array
323
    {
324 803
        if ($this->isPersisting() && $field = $this->inverseRelationshipField($collection->factory())) {
325
            $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
326 803
                $collection->create([$field => $proxy]);
327
                $proxy->refresh();
328
            };
329
330
            // creation delegated to afterPersist event - return empty array here
331
            return [];
332
        }
333
334
        return $collection->all();
335
    }
336
337
    private function inverseRelationshipField(self $factory): ?string
338
    {
339
        $collectionClass = $factory->class;
340
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
341
342
        foreach ($collectionMetadata->getAssociationNames() as $field) {
343
            // ensure 1-n and associated class matches
344
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
345
                return $field;
346
            }
347
        }
348
349
        return null; // no relationship found
350
    }
351
352
    private function isPersisting(): bool
353
    {
354
        return self::configuration()->hasManagerRegistry() ? $this->persist : false;
355
    }
356
}
357