PHP Generators - Practical Example
Introduction Generators in PHP enable the construction of iterators without the necessity of creating the whole array. This significantly aids in …
ReadIn Laravel, Factories play a crucial role in generating data for your models.
Suppose we want to create a post with 20 comments. Typically, this would require two lines of code: one to generate the post, and another to create the comments, as shown below:
Here are the models for the user and posts:
1class Post extends Model
2{
3 use HasFactory;
4
5 protected $fillable = [
6 'title',
7 'body',
8 'views',
9 ];
10
11 public function comments()
12 {
13 return $this->hasMany(Comment::class);
14 }
15}
16
17
18class Comment extends Model
19{
20 use HasFactory;
21}
1$post = Post::factory()->create();
2
3Comment::factory(20)->create([
4 'post_id' => $post->id,
5]);
However, Laravel provides a more efficient and streamlined method to achieve the same result, making the process significantly simpler.
1Post::factory()
2 ->hasComments(20)
3 ->create();
This approach will work seamlessly, creating the post and subsequently attaching the 20 comments to that post.
However, if you inspect the PostFactory class, you will notice that the hasComments method isn’t explicitly mentioned.
1class PostFactory extends Factory
2{
3 public function definition(): array
4 {
5 return [
6 'title' => $this->faker->sentence,
7 'content' => $this->faker->paragraph,
8 'user_id' => User::factory(),
9 ];
10 }
11}
So, this naturally raises the question: how does this process actually function?
Before delving into how dynamic calls are made in Laravel, it would be beneficial to understand how they occur in PHP itself. (If you’re already familiar with the magic method __call in PHP, feel free to skip this section.)
In php, any class can define the magic method __call
The __call method is automatically invoked when a non-existent or inaccessible method is called. For instance:
1class Person {
2 public function __call() {
3 return 'Person class';
4 }
5}
6
7
8$person = new Person();
9$person->badMethod() // returns: 'Person class'
Instead of using a simple illustration, let’s design a ValueObject for a better understanding. The main purpose of a ValueObject, as the name suggests, is to hold values. It operates by receiving an array of data and using get{key}
to fetch a specific value from the array.
1$vo = new ValueObject(['name' => 'Ahmed', 'language' => 'php']);
2
3echo $vo->getName(); // Ahmed
4echo $vo->getLanguage(); // php
5echo $vo->getAge(); // null
So, how can we implement this using the __call magic method? Let’s explore.
1class ValueObject {
2 private array $data = [];
3
4 public function __construct(array $data) {
5 $this->data = $data;
6 }
7
8 public function __call($name, $args) {
9 if (str_starts_with($name, 'get')) {
10 $key = lcfirst(substr($name, 3));
11 return $this->data[$key] ?? null;
12 }
13
14 throw new \InvalidArgumentException("Method $name does not exist");
15 }
16}
Inside the __call
method, we check if the $name
starts with get
. For instance, something like getName
would be a match. If it doesn’t, we throw a ‘method not found’ exception.
In a similar vein, Laravel uses this technique to identify any has{relation}
calls.
When inspecting any of Laravel’s factories, you’ll see that they extend the Illuminate\Database\Eloquent\Factories\Factory
class. Let’s delve into this file and search for the __call
method.
1public function __call($method, $parameters)
2{
3 // .... some code ....
4
5 if (! Str::startsWith($method, ['for', 'has'])) {
6 static::throwBadMethodCallException($method);
7 }
8
9 $relationship = Str::camel(Str::substr($method, 3));
10
11 $relatedModel = get_class($this->newModel()->{$relationship}()->getRelated());
12
13 if (method_exists($relatedModel, 'newFactory')) {
14 $factory = $relatedModel::newFactory() ?? static::factoryForModel($relatedModel);
15 } else {
16 $factory = static::factoryForModel($relatedModel);
17 }
18
19 if (str_starts_with($method, 'for')) {
20 return $this->for($factory->state($parameters[0] ?? []), $relationship);
21 } elseif (str_starts_with($method, 'has')) {
22 return $this->has(
23 $factory
24 ->count(is_numeric($parameters[0] ?? null) ? $parameters[0] : 1)
25 ->state((is_callable($parameters[0] ?? null) || is_array($parameters[0] ?? null)) ? $parameters[0] : ($parameters[1] ?? [])),
26 $relationship
27 );
28 }
29}
By examining the first few lines of the __call
method, you can understand how Laravel identifies the relation and creates a factory from it - which, in our case, is the CommentFactory
. Further, if you look at the elseif
statement at line 21, Laravel checks if the string starts with has
. If it does, a new Factory object from PostFactory is initiated, containing a has
collection that refers to the CommentFactory.
1public function has(self $factory, $relationship = null)
2{
3 return $this->newInstance([
4 'has' => $this->has->concat([new Relationship(
5 $factory, $relationship ?? $this->guessRelationship($factory->modelName())
6 )]),
7 ]);
8}
Now that we understand what the has{relation}() function does (it initiates a new Factory with a properly defined relationship based on the has{relation} call), let’s return to the original call chain we discussed earlier.
1Post::factory()
2 ->hasComments(20)
3 ->create();
The hasComments
function returns the PostFactory with a defined comments
relation, and then the create
method is invoked.
Let’s take a closer look at the create
method.
1public function create($attributes = [], ?Model $parent = null)
2{
3 // .... some code ....
4
5 if ($results instanceof Model) {
6 $this->store(collect([$results]));
7
8 $this->callAfterCreating(collect([$results]), $parent);
9 } else {
10 $this->store($results);
11
12 $this->callAfterCreating($results, $parent);
13 }
14
15 return $results;
16}
The key element we’re interested in here is the store
method. Let’s investigate what it does.
1protected function store(Collection $results)
2{
3 $results->each(function ($model) {
4 if (! isset($this->connection)) {
5 $model->setConnection($model->newQueryWithoutScopes()->getConnection()->getName());
6 }
7
8 $model->save();
9
10 foreach ($model->getRelations() as $name => $items) {
11 if ($items instanceof Enumerable && $items->isEmpty()) {
12 $model->unsetRelation($name);
13 }
14 }
15
16 $this->createChildren($model);
17 });
18}
The last line of this method holds the key to our inquiry. The createChildren
method is responsible for creating all defined ‘has’ relations.
Let’s examine this method more closely.
1protected function createChildren(Model $model)
2{
3 Model::unguarded(function () use ($model) {
4 $this->has->each(function ($has) use ($model) {
5 $has->recycle($this->recycle)->createFor($model);
6 });
7 });
8}
Alright, there are some complex calls in here, specifically the lack of type hints that would tell us what $has
and $has->recycle
refer to. For now, let’s set these aside and focus on createFor($model)
. To clarify, the $model
here is an instance of the Post class. This means that $has
is undoubtedly our CommentFactory instance.
The variable $has
is an instance of Illuminate\Database\Eloquent\Factories\Relationship
, which wraps our CommentFactory. If we examine the createFor
method within the Relationship class, we’ll see the following:
1public function createFor(Model $parent)
2{
3 $relationship = $parent->{$this->relationship}();
4
5 if ($relationship instanceof MorphOneOrMany) {
6 $this->factory->state([
7 $relationship->getMorphType() => $relationship->getMorphClass(),
8 $relationship->getForeignKeyName() => $relationship->getParentKey(),
9 ])->create([], $parent);
10 } elseif ($relationship instanceof HasOneOrMany) {
11 $this->factory->state([
12 $relationship->getForeignKeyName() => $relationship->getParentKey(),
13 ])->create([], $parent);
14 } elseif ($relationship instanceof BelongsToMany) {
15 $relationship->attach($this->factory->create([], $parent));
16 }
17}
For our specific case, we are primarily interested in the first else if
clause. This is because a Post hasMany
Comments, which eventually translates to the following:
1Comment::factory()->create(['post_id' => $post->id]);
And that completes the comprehensive overview of how Factory relations function in Laravel!
Introduction Generators in PHP enable the construction of iterators without the necessity of creating the whole array. This significantly aids in …
ReadIntroduction The simples way to use where is just to statically call it on your model as Model::where('name', 'Ahmed')->first() Laravel Query …
Read