1<?php
2
3namespace Illuminate\Database\Eloquent;
4
5use Faker\Generator as Faker;
6use InvalidArgumentException;
7use Illuminate\Support\Traits\Macroable;
8
9class FactoryBuilder
10{
11    use Macroable;
12
13    /**
14     * The model definitions in the container.
15     *
16     * @var array
17     */
18    protected $definitions;
19
20    /**
21     * The model being built.
22     *
23     * @var string
24     */
25    protected $class;
26
27    /**
28     * The name of the model being built.
29     *
30     * @var string
31     */
32    protected $name = 'default';
33
34    /**
35     * The database connection on which the model instance should be persisted.
36     *
37     * @var string
38     */
39    protected $connection;
40
41    /**
42     * The model states.
43     *
44     * @var array
45     */
46    protected $states;
47
48    /**
49     * The model after making callbacks.
50     *
51     * @var array
52     */
53    protected $afterMaking = [];
54
55    /**
56     * The model after creating callbacks.
57     *
58     * @var array
59     */
60    protected $afterCreating = [];
61
62    /**
63     * The states to apply.
64     *
65     * @var array
66     */
67    protected $activeStates = [];
68
69    /**
70     * The Faker instance for the builder.
71     *
72     * @var \Faker\Generator
73     */
74    protected $faker;
75
76    /**
77     * The number of models to build.
78     *
79     * @var int|null
80     */
81    protected $amount = null;
82
83    /**
84     * Create an new builder instance.
85     *
86     * @param  string  $class
87     * @param  string  $name
88     * @param  array  $definitions
89     * @param  array  $states
90     * @param  array  $afterMaking
91     * @param  array  $afterCreating
92     * @param  \Faker\Generator  $faker
93     * @return void
94     */
95    public function __construct($class, $name, array $definitions, array $states,
96                                array $afterMaking, array $afterCreating, Faker $faker)
97    {
98        $this->name = $name;
99        $this->class = $class;
100        $this->faker = $faker;
101        $this->states = $states;
102        $this->definitions = $definitions;
103        $this->afterMaking = $afterMaking;
104        $this->afterCreating = $afterCreating;
105    }
106
107    /**
108     * Set the amount of models you wish to create / make.
109     *
110     * @param  int  $amount
111     * @return $this
112     */
113    public function times($amount)
114    {
115        $this->amount = $amount;
116
117        return $this;
118    }
119
120    /**
121     * Set the state to be applied to the model.
122     *
123     * @param  string  $state
124     * @return $this
125     */
126    public function state($state)
127    {
128        return $this->states([$state]);
129    }
130
131    /**
132     * Set the states to be applied to the model.
133     *
134     * @param  array|mixed  $states
135     * @return $this
136     */
137    public function states($states)
138    {
139        $this->activeStates = is_array($states) ? $states : func_get_args();
140
141        return $this;
142    }
143
144    /**
145     * Set the database connection on which the model instance should be persisted.
146     *
147     * @param  string  $name
148     * @return $this
149     */
150    public function connection($name)
151    {
152        $this->connection = $name;
153
154        return $this;
155    }
156
157    /**
158     * Create a model and persist it in the database if requested.
159     *
160     * @param  array  $attributes
161     * @return \Closure
162     */
163    public function lazy(array $attributes = [])
164    {
165        return function () use ($attributes) {
166            return $this->create($attributes);
167        };
168    }
169
170    /**
171     * Create a collection of models and persist them to the database.
172     *
173     * @param  array  $attributes
174     * @return mixed
175     */
176    public function create(array $attributes = [])
177    {
178        $results = $this->make($attributes);
179
180        if ($results instanceof Model) {
181            $this->store(collect([$results]));
182
183            $this->callAfterCreating(collect([$results]));
184        } else {
185            $this->store($results);
186
187            $this->callAfterCreating($results);
188        }
189
190        return $results;
191    }
192
193    /**
194     * Set the connection name on the results and store them.
195     *
196     * @param  \Illuminate\Support\Collection  $results
197     * @return void
198     */
199    protected function store($results)
200    {
201        $results->each(function ($model) {
202            if (! isset($this->connection)) {
203                $model->setConnection($model->newQueryWithoutScopes()->getConnection()->getName());
204            }
205
206            $model->save();
207        });
208    }
209
210    /**
211     * Create a collection of models.
212     *
213     * @param  array  $attributes
214     * @return mixed
215     */
216    public function make(array $attributes = [])
217    {
218        if ($this->amount === null) {
219            return tap($this->makeInstance($attributes), function ($instance) {
220                $this->callAfterMaking(collect([$instance]));
221            });
222        }
223
224        if ($this->amount < 1) {
225            return (new $this->class)->newCollection();
226        }
227
228        $instances = (new $this->class)->newCollection(array_map(function () use ($attributes) {
229            return $this->makeInstance($attributes);
230        }, range(1, $this->amount)));
231
232        $this->callAfterMaking($instances);
233
234        return $instances;
235    }
236
237    /**
238     * Create an array of raw attribute arrays.
239     *
240     * @param  array  $attributes
241     * @return mixed
242     */
243    public function raw(array $attributes = [])
244    {
245        if ($this->amount === null) {
246            return $this->getRawAttributes($attributes);
247        }
248
249        if ($this->amount < 1) {
250            return [];
251        }
252
253        return array_map(function () use ($attributes) {
254            return $this->getRawAttributes($attributes);
255        }, range(1, $this->amount));
256    }
257
258    /**
259     * Get a raw attributes array for the model.
260     *
261     * @param  array  $attributes
262     * @return mixed
263     *
264     * @throws \InvalidArgumentException
265     */
266    protected function getRawAttributes(array $attributes = [])
267    {
268        if (! isset($this->definitions[$this->class][$this->name])) {
269            throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}] [{$this->class}].");
270        }
271
272        $definition = call_user_func(
273            $this->definitions[$this->class][$this->name],
274            $this->faker, $attributes
275        );
276
277        return $this->expandAttributes(
278            array_merge($this->applyStates($definition, $attributes), $attributes)
279        );
280    }
281
282    /**
283     * Make an instance of the model with the given attributes.
284     *
285     * @param  array  $attributes
286     * @return \Illuminate\Database\Eloquent\Model
287     */
288    protected function makeInstance(array $attributes = [])
289    {
290        return Model::unguarded(function () use ($attributes) {
291            $instance = new $this->class(
292                $this->getRawAttributes($attributes)
293            );
294
295            if (isset($this->connection)) {
296                $instance->setConnection($this->connection);
297            }
298
299            return $instance;
300        });
301    }
302
303    /**
304     * Apply the active states to the model definition array.
305     *
306     * @param  array  $definition
307     * @param  array  $attributes
308     * @return array
309     *
310     * @throws \InvalidArgumentException
311     */
312    protected function applyStates(array $definition, array $attributes = [])
313    {
314        foreach ($this->activeStates as $state) {
315            if (! isset($this->states[$this->class][$state])) {
316                if ($this->stateHasAfterCallback($state)) {
317                    continue;
318                }
319
320                throw new InvalidArgumentException("Unable to locate [{$state}] state for [{$this->class}].");
321            }
322
323            $definition = array_merge(
324                $definition,
325                $this->stateAttributes($state, $attributes)
326            );
327        }
328
329        return $definition;
330    }
331
332    /**
333     * Get the state attributes.
334     *
335     * @param  string  $state
336     * @param  array  $attributes
337     * @return array
338     */
339    protected function stateAttributes($state, array $attributes)
340    {
341        $stateAttributes = $this->states[$this->class][$state];
342
343        if (! is_callable($stateAttributes)) {
344            return $stateAttributes;
345        }
346
347        return call_user_func(
348            $stateAttributes,
349            $this->faker, $attributes
350        );
351    }
352
353    /**
354     * Expand all attributes to their underlying values.
355     *
356     * @param  array  $attributes
357     * @return array
358     */
359    protected function expandAttributes(array $attributes)
360    {
361        foreach ($attributes as &$attribute) {
362            if (is_callable($attribute) && ! is_string($attribute) && ! is_array($attribute)) {
363                $attribute = $attribute($attributes);
364            }
365
366            if ($attribute instanceof static) {
367                $attribute = $attribute->create()->getKey();
368            }
369
370            if ($attribute instanceof Model) {
371                $attribute = $attribute->getKey();
372            }
373        }
374
375        return $attributes;
376    }
377
378    /**
379     * Run after making callbacks on a collection of models.
380     *
381     * @param  \Illuminate\Support\Collection  $models
382     * @return void
383     */
384    public function callAfterMaking($models)
385    {
386        $this->callAfter($this->afterMaking, $models);
387    }
388
389    /**
390     * Run after creating callbacks on a collection of models.
391     *
392     * @param  \Illuminate\Support\Collection  $models
393     * @return void
394     */
395    public function callAfterCreating($models)
396    {
397        $this->callAfter($this->afterCreating, $models);
398    }
399
400    /**
401     * Call after callbacks for each model and state.
402     *
403     * @param  array  $afterCallbacks
404     * @param  \Illuminate\Support\Collection  $models
405     * @return void
406     */
407    protected function callAfter(array $afterCallbacks, $models)
408    {
409        $states = array_merge([$this->name], $this->activeStates);
410
411        $models->each(function ($model) use ($states, $afterCallbacks) {
412            foreach ($states as $state) {
413                $this->callAfterCallbacks($afterCallbacks, $model, $state);
414            }
415        });
416    }
417
418    /**
419     * Call after callbacks for each model and state.
420     *
421     * @param  array  $afterCallbacks
422     * @param  \Illuminate\Database\Eloquent\Model  $model
423     * @param  string  $state
424     * @return void
425     */
426    protected function callAfterCallbacks(array $afterCallbacks, $model, $state)
427    {
428        if (! isset($afterCallbacks[$this->class][$state])) {
429            return;
430        }
431
432        foreach ($afterCallbacks[$this->class][$state] as $callback) {
433            $callback($model, $this->faker);
434        }
435    }
436
437    /**
438     * Determine if the given state has an "after" callback.
439     *
440     * @param  string  $state
441     * @return bool
442     */
443    protected function stateHasAfterCallback($state)
444    {
445        return isset($this->afterMaking[$this->class][$state]) ||
446               isset($this->afterCreating[$this->class][$state]);
447    }
448}
449