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