Create fake API responses, using Eloquent Factories
I really enjoy using Laravel Eloquent factories. They provide a one-stop shop for defining a data structure for test data. Wouldn't it be cool though, if we could use them for creating fake API responses? Let's do it.
First, we'll create our factory. This factory will return a fake tweet retrieved from Twitter's REST API. We'll define the tweet attributes (not all of them, for simplicity sake), and use Laravel's Fluent
as our defined $model
. We'll place this factory in a new "App
" namespace, so we don't confuse this factory with a database factory:
1// app/Factories/TweetFactory.php 2 3namespace App\Factories; 4 5use Illuminate\Database\Eloquent\Factories\Factory; 6use Illuminate\Support\Fluent; 7 8class TweetFactory extends Factory 9{10 protected $model = Fluent::class;11 12 public function definition()13 {14 return [15 'id' => $this->faker->randomNumber(),16 'text' => $this->faker->text(280),17 'created_at' => $this->faker->dateTime(),18 ];19 }20}
Now, let's test it:
1use App\Factories\TweetFactory; 2 3$tweet = TweetFactory::new()->make(); 4 5dd($tweet); 6 7// Illuminate\Support\Fluent {#1085 8// #attributes: array:3 [ 9// "id" => 538155710// "text" => "Consequatur ex eius inventore vitae ut..."11// "created_at" => DateTime @1226904414 {#108412// date: 2008-11-17 06:46:54.0 UTC (+00:00)13// }14// ]15// }
Great! It's working, though what happens if we attempt to make multiple?
1TweetFactory::new()->times(5)->make();2 3// TypeError: Illuminate/Database/Eloquent/Factories/Factory::callAfterMaking():4// Argument #1 ($instances) must be of type Illuminate/Support/Collection,5// Illuminate/Support/Fluent given
Dang. This is troublesome, since we can't utilize sequences, or any other handy "times" based methods. Let's try to fix this with some magic of our own.
First, we need to resolve the exception going on above. Laravel's Factory
is expecting a collection to be returned in the make
method after $this->newModel()
, but our Fluent
instance is accepting the newCollection()
method as a dynamic call. Here it is in Laravel's Factory.php
:
1public function make($attributes = [], ?Model $parent = null) 2{ 3 // ... 4 5 $instances = $this->newModel()->newCollection(array_map(function () use ($parent) { 6 return $this->makeInstance($parent); 7 }, range(1, $this->count))); 8 9 $this->callAfterMaking($instances);10 11 return $instances;12}
Let's make a class that will wrap our Fluent
instances with a proxy/decorator that contains the newCollection()
method. We'll call it FactoryCollectionProxy
:
1// app/Factories/FactoryCollectionProxy.php 2 3namespace App\Factories; 4 5class FactoryCollectionProxy 6{ 7 protected $instance; 8 9 public function __construct($instance = null)10 {11 $this->instance = $instance;12 }13 14 public function getInstance()15 {16 return $this->instance;17 }18 19 public function newCollection($items = null)20 {21 return collect($items);22 }23}
Now, let's make a class that extends the Laravel Factory
class so it can properly build multiples of our API data. We'll call it CustomFactory
:
1// app/Factories/CustomFactory.php 2 3namespace App\Factories; 4 5use Illuminate\Database\Eloquent\Factories\Factory; 6use Illuminate\Database\Eloquent\Model; 7use Illuminate\Support\Collection; 8 9abstract class CustomFactory extends Factory10{11 public function newModel(array $attributes = [])12 {13 // Here we will pass the model into a proxy, which contains a "newCollection"14 // method, which is required due to its use in the the base Eloquent factory15 // for creating multiple instances of a model using the "times" method.16 return new FactoryCollectionProxy(17 $this->makeModel($this->modelName(), $attributes)18 );19 }20 21 public function make($attributes = [], ?Model $parent = null)22 {23 $instances = parent::make($attributes, $parent);24 25 if ($instances instanceof Collection) {26 return $instances->map(function ($instance) {27 return $this->resolveFactoryInstance($instance);28 })->toArray();29 }30 31 return $this->resolveFactoryInstance($instances);32 }33 34 protected function makeModel($model, array $attributes = [])35 {36 return new $model($attributes);37 }38 39 protected function resolveFactoryInstance($instance)40 {41 return $instance instanceof FactoryCollectionProxy ? $instance->getInstance() : $instance;42 }43}
Now, let's change our TweetFactory
to extend our brand spanking new CustomFactory
:
1// app/Factories/TweetFactory.php2 3class TweetFactory extends CustomFactory4{5 // ...6}
Ok great, let's test it:
1dd(TweetFactory::new()->times(5)->make()); 2 3// array:5 [ 4// 0 => array:3 [ 5// "id" => 9759 6// "text" => "Qui quaerat sit quo illo iusto..." 7// "created_at" => DateTime @1388443197 {#1087 8// date: 2013-12-30 22:39:57.0 UTC (+00:00) 9// }10// ]11// 1 => array:3 [12// "id" => 22071655113// "text" => "Ex aperiam culpa quia quidem nemo..."14// "created_at" => DateTime @1233202110 {#109015// date: 2009-01-29 04:08:30.0 UTC (+00:00)16// }17// ]18// 2 => array:3 [19// "id" => 6220// "text" => "At repellat aut inventore. In ..."21// "created_at" => DateTime @1382523527 {#109322// date: 2013-10-23 10:18:47.0 UTC (+00:00)23// }24// ]25// 3 => array:3 [26// "id" => 2829527// "text" => "Nemo at vel rerum ab. Voluptatem..."28// "created_at" => DateTime @1177209452 {#109629// date: 2007-04-22 02:37:32.0 UTC (+00:00)30// }31// ]32// 4 => array:3 [33// "id" => 191021834// "text" => "Labore sit aut molestiae quia..."35// "created_at" => DateTime @1291616013 {#109936// date: 2010-12-06 06:13:33.0 UTC (+00:00)37// }38// ]39// ]
Excellent! It works! The only caveat now, is that you cannot use create()
methods (of course). We're not storing database records, after all.