Passed
Pull Request — master (#1)
by Kevin
02:39
created

Factory::doInstantiate()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 4
eloc 11
c 2
b 0
f 1
nc 5
nop 2
dl 0
loc 27
ccs 12
cts 12
cp 1
crap 4
rs 9.9
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
    private static ?Configuration $configuration = null;
13
14
    private string $class;
15
16
    /** @var callable|null */
17
    private $instantiator;
18
19
    private bool $persist = true;
20
21
    /** @var array<array|callable> */
22
    private array $attributeSet = [];
23
24
    /** @var callable[] */
25
    private array $beforeInstantiate = [];
26
27
    /** @var callable[] */
28
    private array $afterInstantiate = [];
29
30
    /** @var callable[] */
31
    private array $afterPersist = [];
32
33
    /**
34
     * @param array|callable $defaultAttributes
35
     */
36 238
    public function __construct(string $class, $defaultAttributes = [])
37
    {
38 238
        $this->class = $class;
39 238
        $this->attributeSet[] = $defaultAttributes;
40 238
    }
41
42
    /**
43
     * @param array|callable $attributes
44
     *
45
     * @return Proxy|object
46
     */
47 234
    final public function create($attributes = []): Proxy
48
    {
49 234
        $proxy = new Proxy($this->instantiate($attributes));
50
51 230
        if (!$this->persist) {
52 56
            return $proxy;
53
        }
54
55 174
        $proxy->save()->withoutAutoRefresh();
56
57 174
        foreach ($this->afterPersist as $callback) {
58 4
            $this->callAfterPersist($callback, $proxy, $attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes can also be of type callable; however, parameter $attributes of Zenstruck\Foundry\Factory::callAfterPersist() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

58
            $this->callAfterPersist($callback, $proxy, /** @scrutinizer ignore-type */ $attributes);
Loading history...
59
        }
60
61 174
        return $proxy->withAutoRefresh();
62
    }
63
64
    /**
65
     * @param array|callable $attributes
66
     *
67
     * @return Proxy[]|object[]
68
     */
69 56
    final public function createMany(int $number, $attributes = []): array
70
    {
71 56
        return \array_map(fn() => $this->create($attributes), \array_fill(0, $number, null));
72
    }
73
74 64
    public function withoutPersisting(): self
75
    {
76 64
        $cloned = clone $this;
77 64
        $cloned->persist = false;
78
79 64
        return $cloned;
80
    }
81
82
    /**
83
     * @param array|callable $attributes
84
     */
85 182
    final public function withAttributes($attributes = []): self
86
    {
87 182
        $cloned = clone $this;
88 182
        $cloned->attributeSet[] = $attributes;
89
90 182
        return $cloned;
91
    }
92
93
    /**
94
     * @param callable $callback (array $attributes): array
95
     */
96 12
    final public function beforeInstantiate(callable $callback): self
97
    {
98 12
        $cloned = clone $this;
99 12
        $cloned->beforeInstantiate[] = $callback;
100
101 12
        return $cloned;
102
    }
103
104
    /**
105
     * @param callable $callback (object $object, array $attributes): void
106
     */
107 8
    final public function afterInstantiate(callable $callback): self
108
    {
109 8
        $cloned = clone $this;
110 8
        $cloned->afterInstantiate[] = $callback;
111
112 8
        return $cloned;
113
    }
114
115
    /**
116
     * @param callable $callback (object $object, array $attributes, ObjectManager $objectManager): void
117
     */
118 8
    final public function afterPersist(callable $callback): self
119
    {
120 8
        $cloned = clone $this;
121 8
        $cloned->afterPersist[] = $callback;
122
123 8
        return $cloned;
124
    }
125
126
    /**
127
     * @param callable $instantiator (array $attributes, string $class): object
128
     */
129 8
    final public function instantiator(callable $instantiator): self
130
    {
131 8
        $cloned = clone $this;
132 8
        $cloned->instantiator = $instantiator;
133
134 8
        return $cloned;
135
    }
136
137
    /**
138
     * @internal
139
     */
140 266
    final public static function boot(Configuration $configuration): void
141
    {
142 266
        self::$configuration = $configuration;
143 266
    }
144
145
    /**
146
     * @internal
147
     */
148 242
    final public static function configuration(): Configuration
149
    {
150 242
        if (!self::$configuration) {
151
            throw new \RuntimeException('Factory not yet booted.'); // todo
152
        }
153
154 242
        return self::$configuration;
155
    }
156
157 186
    final public static function faker(): Faker\Generator
158
    {
159 186
        return self::configuration()->faker();
160
    }
161
162 4
    private function callAfterPersist(callable $callback, Proxy $proxy, array $attributes): void
163
    {
164 4
        $object = $proxy;
165 4
        $parameters = (new \ReflectionFunction($callback))->getParameters();
166
167 4
        if (isset($parameters[0]) && $parameters[0]->getType() && $this->class === $parameters[0]->getType()->getName()) {
168 4
            $object = $object->object();
169
        }
170
171 4
        $callback($object, $attributes);
172 4
    }
173
174
    /**
175
     * @param array|callable $attributes
176
     */
177 234
    private static function normalizeAttributes($attributes): array
178
    {
179 234
        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...
180
    }
181
182
    /**
183
     * @param array|callable $attributes
184
     */
185 234
    private function instantiate($attributes): object
186
    {
187
        // merge the factory attribute set with the passed attributes
188 234
        $attributeSet = \array_merge($this->attributeSet, [$attributes]);
189
190
        // normalize each attribute set and collapse
191 234
        $attributes = \array_merge(...\array_map([$this, 'normalizeAttributes'], $attributeSet));
192
193 234
        foreach ($this->beforeInstantiate as $callback) {
194 8
            $attributes = $callback($attributes);
195
196 8
            if (!\is_array($attributes)) {
197 4
                throw new \LogicException('Before Instantiate event callback must return an array.');
198
            }
199
        }
200
201
        // filter each attribute to convert proxies and factories to objects
202 230
        $attributes = \array_map(fn($value) => $this->normalizeAttribute($value), $attributes);
203
204
        // instantiate the object with the users instantiator or if not set, the default instantiator
205 230
        $object = ($this->instantiator ?? self::configuration()->instantiator())($attributes, $this->class);
206
207 230
        foreach ($this->afterInstantiate as $callback) {
208 4
            $callback($object, $attributes);
209
        }
210
211 230
        return $object;
212
    }
213
214
    /**
215
     * @param mixed $value
216
     *
217
     * @return mixed
218
     */
219 218
    private function normalizeAttribute($value)
220
    {
221 218
        if ($value instanceof Proxy) {
222 24
            return $value->object();
223
        }
224
225 218
        if (\is_array($value)) {
226
            // possible OneToMany/ManyToMany relationship
227 12
            return \array_map(fn($value) => $this->normalizeAttribute($value), $value);
228
        }
229
230 218
        if (!$value instanceof self) {
231 218
            return $value;
232
        }
233
234 24
        if (!$this->persist) {
235
            // ensure attribute Factory's are also not persisted
236 4
            $value = $value->withoutPersisting();
237
        }
238
239 24
        return $value->create()->object();
240
    }
241
}
242