Completed
Push — master ( 228895...6e1f3b )
by Kevin
16s queued 14s
created

Factory::class()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 0
crap 2
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
    /**
254 813
     * @internal
255
     *
256
     * @psalm-return class-string<TObject>
257
     */
258
    final protected function class(): string
259
    {
260
        return $this->class;
261
    }
262 753
263
    /**
264 753
     * @param array|callable $attributes
265 80
     */
266
    private static function normalizeAttributes($attributes): array
267
    {
268 753
        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...
269 70
    }
270
271
    /**
272 753
     * @param mixed $value
273
     *
274 100
     * @return mixed
275
     */
276 80
    private function normalizeAttribute($value)
277 100
    {
278 100
        if ($value instanceof Proxy) {
279
            return $value->isPersisted() ? $value->refresh()->object() : $value->object();
280
        }
281
282 753
        if ($value instanceof FactoryCollection) {
283 753
            $value = $this->normalizeCollection($value);
284
        }
285
286 130
        if (\is_array($value)) {
287
            // possible OneToMany/ManyToMany relationship
288 30
            return \array_map(
289
                function($value) {
290
                    return $this->normalizeAttribute($value);
291 130
                },
292
                $value
293
            );
294 70
        }
295
296 70
        if (!$value instanceof self) {
297
            return \is_object($value) ? self::normalizeObject($value) : $value;
298 20
        }
299 20
300 20
        if (!$this->isPersisting()) {
301
            // ensure attribute Factory's are also not persisted
302
            $value = $value->withoutPersisting();
303 20
        }
304
305
        return $value->create()->object();
306 50
    }
307
308
    private static function normalizeObject(object $object): object
309 50
    {
310
        try {
311 50
            return Proxy::createFromPersisted($object)->refresh()->object();
312 50
        } catch (\RuntimeException $e) {
313
            return $object;
314 50
        }
315
    }
316 50
317 20
    private function normalizeCollection(FactoryCollection $collection): array
318
    {
319
        if ($this->isPersisting() && $field = $this->inverseRelationshipField($collection->factory())) {
320
            $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
321 30
                $collection->create([$field => $proxy]);
322
                $proxy->refresh();
323
            };
324 803
325
            // creation delegated to afterPersist event - return empty array here
326 803
            return [];
327
        }
328
329
        return $collection->all();
330
    }
331
332
    private function inverseRelationshipField(self $factory): ?string
333
    {
334
        $collectionClass = $factory->class;
335
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
336
337
        foreach ($collectionMetadata->getAssociationNames() as $field) {
338
            // ensure 1-n and associated class matches
339
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
340
                return $field;
341
            }
342
        }
343
344
        return null; // no relationship found
345
    }
346
347
    private function isPersisting(): bool
348
    {
349
        return self::configuration()->hasManagerRegistry() ? $this->persist : false;
350
    }
351
}
352