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

Factory::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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

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