Passed
Pull Request — master (#111)
by Wouter
02:00
created

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

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