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

Factory::withoutPersisting()   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 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
ccs 4
cts 4
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 callable|null */
13
    private static $defaultInstantiator;
14
    private static ?Faker\Generator $faker = null;
15
16
    private string $class;
17
18
    /** @var callable|null */
19
    private $instantiator;
20
21
    private bool $persist = true;
22
23
    /** @var array<array|callable> */
24
    private array $attributeSet = [];
25
26
    /** @var callable[] */
27
    private array $beforeInstantiate = [];
28
29
    /** @var callable[] */
30
    private array $afterInstantiate = [];
31
32
    /** @var callable[] */
33
    private array $afterPersist = [];
34
35
    /**
36
     * @param array|callable $defaultAttributes
37
     */
38 118
    public function __construct(string $class, $defaultAttributes = [])
39
    {
40 118
        $this->class = $class;
41 118
        $this->attributeSet[] = $defaultAttributes;
42 118
    }
43
44
    /**
45
     * @param array|callable $attributes
46
     *
47
     * @return Proxy|object
48
     */
49 116
    final public function create($attributes = []): Proxy
50
    {
51 116
        $proxy = new Proxy($this->instantiate($attributes));
52
53 114
        if (!$this->persist) {
54 28
            return $proxy;
55
        }
56
57 86
        $proxy->save()->withoutAutoRefresh();
58
59 86
        foreach ($this->afterPersist as $callback) {
60 2
            $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

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