Passed
Push — master ( 6bd319...2d574a )
by Kevin
04:56
created

Factory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

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