Passed
Push — master ( 60e688...e7b848 )
by Kevin
03:21
created

Factory::afterPersist()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
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 310
    public function __construct(string $class, $defaultAttributes = [])
40
    {
41 310
        $this->class = $class;
42 310
        $this->attributeSet[] = $defaultAttributes;
43 310
    }
44
45
    /**
46
     * @param array|callable $attributes
47
     *
48
     * @return Proxy|object
49
     */
50 302
    final public function create($attributes = []): Proxy
51
    {
52
        // merge the factory attribute set with the passed attributes
53 302
        $attributeSet = \array_merge($this->attributeSet, [$attributes]);
54
55
        // normalize each attribute set and collapse
56 302
        $attributes = \array_merge(...\array_map([$this, 'normalizeAttributes'], $attributeSet));
57
58 302
        foreach ($this->beforeInstantiate as $callback) {
59 8
            $attributes = $callback($attributes);
60
61 8
            if (!\is_array($attributes)) {
62 4
                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 298
        $attributes = \array_map(
68
            function($value) {
69 278
                return $this->normalizeAttribute($value);
70 298
            },
71 298
            $attributes
72
        );
73
74
        // instantiate the object with the users instantiator or if not set, the default instantiator
75 298
        $object = ($this->instantiator ?? self::configuration()->instantiator())($attributes, $this->class);
76
77 298
        foreach ($this->afterInstantiate as $callback) {
78 4
            $callback($object, $attributes);
79
        }
80
81 298
        $proxy = new Proxy($object);
82
83 298
        if (!$this->persist) {
84 72
            return $proxy;
85
        }
86
87
        return $proxy->save()->withoutAutoRefresh(function(Proxy $proxy) use ($attributes) {
88 234
            foreach ($this->afterPersist as $callback) {
89 12
                $proxy->executeCallback($callback, $attributes);
90
            }
91 234
        });
92
    }
93
94
    /**
95
     * @see FactoryCollection::__construct()
96
     */
97 88
    final public function many(int $min, ?int $max = null): FactoryCollection
98
    {
99 88
        return new FactoryCollection($this, $min, $max);
100
    }
101
102
    /**
103
     * @param array|callable $attributes
104
     *
105
     * @return Proxy[]|object[]
106
     */
107 68
    final public function createMany(int $number, $attributes = []): array
108
    {
109 68
        return $this->many($number)->create($attributes);
110
    }
111
112 84
    public function withoutPersisting(): self
113
    {
114 84
        $cloned = clone $this;
115 84
        $cloned->persist = false;
116
117 84
        return $cloned;
118
    }
119
120
    /**
121
     * @param array|callable $attributes
122
     */
123 242
    final public function withAttributes($attributes = []): self
124
    {
125 242
        $cloned = clone $this;
126 242
        $cloned->attributeSet[] = $attributes;
127
128 242
        return $cloned;
129
    }
130
131
    /**
132
     * @param callable $callback (array $attributes): array
133
     */
134 12
    final public function beforeInstantiate(callable $callback): self
135
    {
136 12
        $cloned = clone $this;
137 12
        $cloned->beforeInstantiate[] = $callback;
138
139 12
        return $cloned;
140
    }
141
142
    /**
143
     * @param callable $callback (object $object, array $attributes): void
144
     */
145 8
    final public function afterInstantiate(callable $callback): self
146
    {
147 8
        $cloned = clone $this;
148 8
        $cloned->afterInstantiate[] = $callback;
149
150 8
        return $cloned;
151
    }
152
153
    /**
154
     * @param callable $callback (object|Proxy $object, array $attributes): void
155
     */
156 8
    final public function afterPersist(callable $callback): self
157
    {
158 8
        $cloned = clone $this;
159 8
        $cloned->afterPersist[] = $callback;
160
161 8
        return $cloned;
162
    }
163
164
    /**
165
     * @param callable $instantiator (array $attributes, string $class): object
166
     */
167 8
    final public function instantiateWith(callable $instantiator): self
168
    {
169 8
        $cloned = clone $this;
170 8
        $cloned->instantiator = $instantiator;
171
172 8
        return $cloned;
173
    }
174
175
    /**
176
     * @internal
177
     */
178 344
    final public static function boot(Configuration $configuration): void
179
    {
180 344
        self::$configuration = $configuration;
181 344
    }
182
183
    /**
184
     * @internal
185
     */
186 330
    final public static function configuration(): Configuration
187
    {
188 330
        if (!self::isBooted()) {
189
            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?');
190
        }
191
192 330
        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...
193
    }
194
195
    /**
196
     * @internal
197
     */
198 356
    final public static function isBooted(): bool
199
    {
200 356
        return null !== self::$configuration;
201
    }
202
203 246
    final public static function faker(): Faker\Generator
204
    {
205 246
        return self::configuration()->faker();
206
    }
207
208
    /**
209
     * @param array|callable $attributes
210
     */
211 302
    private static function normalizeAttributes($attributes): array
212
    {
213 302
        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...
214
    }
215
216
    /**
217
     * @param mixed $value
218
     *
219
     * @return mixed
220
     */
221 278
    private function normalizeAttribute($value)
222
    {
223 278
        if ($value instanceof Proxy) {
224 32
            return $value->object();
225
        }
226
227 278
        if ($value instanceof FactoryCollection) {
228 28
            $value = $this->normalizeCollection($value);
229
        }
230
231 278
        if (\is_array($value)) {
232
            // possible OneToMany/ManyToMany relationship
233 40
            return \array_map(
234
                function($value) {
235 32
                    return $this->normalizeAttribute($value);
236 40
                },
237 40
                $value
238
            );
239
        }
240
241 278
        if (!$value instanceof self) {
242 278
            return $value;
243
        }
244
245 52
        if (!$this->persist) {
246
            // ensure attribute Factory's are also not persisted
247 12
            $value = $value->withoutPersisting();
248
        }
249
250 52
        return $value->create()->object();
251
    }
252
253 28
    private function normalizeCollection(FactoryCollection $collection): array
254
    {
255 28
        if ($this->persist && $field = $this->inverseRelationshipField($collection->factory())) {
256
            $this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
257 8
                $collection->create([$field => $proxy]);
258 8
                $proxy->refresh();
259 8
            };
260
261
            // creation delegated to afterPersist event - return empty array here
262 8
            return [];
263
        }
264
265 20
        return $collection->all();
266
    }
267
268 20
    private function inverseRelationshipField(self $factory): ?string
269
    {
270 20
        $collectionClass = $factory->class;
271 20
        $collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
272
273 20
        foreach ($collectionMetadata->getAssociationNames() as $field) {
274
            // ensure 1-n and associated class matches
275 20
            if ($collectionMetadata->isSingleValuedAssociation($field) && $collectionMetadata->getAssociationTargetClass($field) === $this->class) {
276 8
                return $field;
277
            }
278
        }
279
280 12
        return null; // no relationship found
281
    }
282
}
283