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" => 5381557
10// "text" => "Consequatur ex eius inventore vitae ut..."
11// "created_at" => DateTime @1226904414 {#1084
12// 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 Factory
10{
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 factory
15 // 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.php
2 
3class TweetFactory extends CustomFactory
4{
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" => 220716551
13// "text" => "Ex aperiam culpa quia quidem nemo..."
14// "created_at" => DateTime @1233202110 {#1090
15// date: 2009-01-29 04:08:30.0 UTC (+00:00)
16// }
17// ]
18// 2 => array:3 [
19// "id" => 62
20// "text" => "At repellat aut inventore. In ..."
21// "created_at" => DateTime @1382523527 {#1093
22// date: 2013-10-23 10:18:47.0 UTC (+00:00)
23// }
24// ]
25// 3 => array:3 [
26// "id" => 28295
27// "text" => "Nemo at vel rerum ab. Voluptatem..."
28// "created_at" => DateTime @1177209452 {#1096
29// date: 2007-04-22 02:37:32.0 UTC (+00:00)
30// }
31// ]
32// 4 => array:3 [
33// "id" => 1910218
34// "text" => "Labore sit aut molestiae quia..."
35// "created_at" => DateTime @1291616013 {#1099
36// 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.