Passed
Pull Request — master (#84)
by Kevin
18:19
created

Factory::boot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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