cult3

Laravel 4 Eloquent Model Relationships

Jun 10, 2013

Table of contents:

  1. Relations to be made
  2. Creating the Clique model and memberships
  3. Creating polymorphic comments
  4. Writing Model relationship tests
  5. Conclusion
  6. Update

Last week I started looking more in-depth at Model relationships in Laravel 4, including what are the different types of relationship and how each of them work.

I also created the Twitter following model logic that will allow Cribbb users to follow each other.

This week I’m going to finish off creating the Model relationships and then look at writing tests.

Relations to be made

So if you can remember back from last week, I still need to make the Cliques model and memberships and I need to create the polymorphic relationship for comments.

It also occurred to me that Posts should be related to Cliques. The theory is, Users can create Posts in a Clique, or they can post a Status on their own page. I won’t over-complicate things by creating the Status model just yet.

Creating the Clique model and memberships

So a quick reminder of what a Clique is. A Clique is essentially a group on Cribbb. Users can become members of a Clique. A Clique will have many Posts. A Post will belong to a Clique and will be created by a User.

Create the Clique Model

So the first thing to do is to create the Clique model. To make the migration file, I will run the following command:

$ php artisan generate:migration create_cliques_table -fields="name:string"

This will create the following migration file:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateCliquesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create("cliques", function (Blueprint $table) {
            $table->increments("id");
            $table->string("name");
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("cliques");
    }
}

To create the table, run the migrate command from Terminal:

$ php artisan migrate

Next I’ll make a really quick Clique.php model file:

class Clique extends Eloquent
{
}

Create the Clique membership relationship

Next we need to create the pivot table to record the relationship between Users and Cliques. Remember, because this is a many-to-many relationship, we need a separate table to record the relationship.

To create the migration, run the following command from Terminal:

$ php artisan migrate:make create_clique_user_table -create -table=clique_user

Add the two integer columns for user_id and clique_id. Your migration file should look like this:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCliqueUserTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create("clique_user", function (Blueprint $table) {
            $table->increments("id");
            $table->integer("clique_id");
            $table->integer("user_id");
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("clique_user");
    }
}

Again, run the migrate command from Terminal:

$ php artisan migrate

Next open up the User.php and the Clique.php models and add the following methods:

In User.php:

/**
* Clique relationship
*/
public function clique() {
    return $this->belongsToMany('Clique');
}

In Clique.php:

/**
* User relationship
*/
public function users() {
    return $this->belongsToMany('User');
}

And finally, because a Post must belong to a Clique, I need to add a column to the posts table for the clique_id.

Run the follow command to create the migration:

$ php artisan generate:migration add_clique_id_posts_table -fields="clique_id:integer"

Now add the following two methods to Post.php and Clique.php:

In Post.php:

/**
* Clique relationship
*/
public function clique()
{
    return $this->belongsTo('Clique');
}

And in Clique.php:

/**
* Post relationship
*/
public function posts()
{
    return $this->hasMany('Post');
}

Creating polymorphic comments

And finally I need to create a Comments model that will have polymorphic relationships with the Post model and (eventually) the Status Model.

So first, run the following migration to create the table:

$ php artisan generate:migration create_comments_table -fields="body:text, commentable_id:integer, commentable_type:string"

Which will create the following migration:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create("comments", function (Blueprint $table) {
            $table->increments("id");
            $table->text("body");
            $table->integer("commentable_id");
            $table->string("commentable_type");
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop("comments");
    }
}

Once again, run the migrate command to run the migration:

$ php artisan migrate

Next create a Comment.php and copy the following class:

class Comment extends Eloquent
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

And finally add the follow method to the Post.php model:

/**
 * Comment relationship
 */
public function comments()
{
    return $this->morphMany('Comment', 'commentable');
}

Now when I create future Models that also have comments, I can just add the last method to the new Model so that it also has comments.

Writing Model relationship tests

Now to write some tests! Ok, ok so writing the tests once you have already wrote the majority of the code isn’t strictly speaking good Test Driven Development (What is Test Driven Development?) practice, but you get the point. Personally, I think you should be wise to follow best practices but not to blindly follow every rule by the book.

So now I’m going to step through all of the changes that I’ve made and write appropriate tests to ensure everything is working correctly.

A Post must belong to a clique

As I mentioned above, I recently decided that a Post should belong to a Clique. Up until this point a Post only belonged to a User. Because I haven’t added this requirement, all of my Post tests pass correctly.

In order to make a Post require a Clique Id, I simply have to add a new validation rule:

'clique_id' => 'required|numeric'

Now if you run phpunit you should find that two of the Post tests have failed. This is because they now depend on having a clique_id in order to save.

To fix this, all we have to do is to ensure that a clique_id is always set when creating a new Post.

First I added a factory array to Clique.php:

/**
 * Factory
 */
public static $factory = array(
    'name' => 'string'
);

Next I added a new key / value pair to the Post factory:

'clique_id' => 'factory|Clique'

And now I can create a new Clique in my tests and assign it to the Post like this:

// Create a new Clique
$clique = FactoryMuff::create("Clique");

// Set the clique_id
$post->clique_id = $clique->id;

Users can follow each other

Next I will write tests to ensure that the follower model is working correctly.

This is what I wrote for testing follower and following relationships. It should be pretty self explanatory, I’m just creating four users and then making different combinations of relationships:

/**
 * Test a user can follower other users
 */
public function testUserCanFollowerUsers()
{
    // Create users
    $philip = FactoryMuff::create('User');
    $jack = FactoryMuff::create('User');
    $ev = FactoryMuff::create('User');
    $biz = FactoryMuff::create('User');

    // First set
    $philip->follow()->save($jack);

    // First tests
    $this->assertCount(1, $philip->follow);
    $this->assertCount(0, $philip->followers);

    // Second set
    $jack->follow()->save($ev);
    $jack->follow()->save($biz);

    // Second tests
    $this->assertCount(2, $jack->follow);
    $this->assertCount(1, $jack->followers);

    // Third set
    $ev->follow()->save($jack);
    $ev->follow()->save($philip);
    $ev->follow()->save($biz);

    // Third tests
    $this->assertCount(3, $ev->follow);
    $this->assertCount(1, $ev->followers);

    // Fourth set
    $biz->follow()->save($jack);
    $biz->follow()->save($ev);

    // Fourth tests
    $this->assertCount(2, $biz->follow);
    $this->assertCount(2, $biz->followers);
}

The eagle eyed amongst you will notice that at the minute you could actually follow yourself. This is a bit of a weird situation, but it can be easily addressed by writing our own methods to create the following relationships. I’ll address this in the future with a more specific article on that problem.

Creating a Clique

Next I will write some basic tests for the Clique model. This is really basic stuff at the minute, but in the future I will add a lot more of the required functionality.

For now though I will just set the name of the Clique to be required, and I will write a test to ensure that it works correctly.

First set the $fillable and the $rules array property in the model:

/**
 * Properties that can be mass assigned
 *
 * @var array
 */
protected $fillable = array('name');

/**
 * Ardent validation rules
 */
public static $rules = array(
    'name' => 'required',
);

Also ensure that the model extends Ardent rather than Eloquent.

Next the test to ensure that the name is required is simply:

public function testNameIsRequired()
{
    // Create a new Clique
    $clique = new Clique;

    // Post should not save
    $this->assertFalse($clique->save());

    // Save the errors
    $errors = $clique->errors()->all();

    // There should be 1 error
    $this->assertCount(1, $errors);

    // The error should be set
    $this->assertEquals($errors[0], "The name field is required.");
}

Becoming a member of a Clique

Next I will test becoming a member of a Clique. This is pretty straight forward as you should recognise that it’s basically the same process as following another user:

public function testCliqueUserRelationship()
{
    // Create a new Clique
    $clique = FactoryMuff::create('Clique');

    // Create two Users
    $user1 = FactoryMuff::create('User');
    $user2 = FactoryMuff::create('User');

    // Save Users to the Clique
    $clique->users()->save($user1);
    $clique->users()->save($user2);

    // Count number of Users
    $this->assertCount(2, $clique->users);
}

Adding a comment to a post

Finally, adding a comment to a post is very similar to all of the other Model relationship tests so far:

/**
 * Test adding new comment
 */
public function testAddingNewComment()
{
    // Create a new Post
    $post = FactoryMuff::create('Post');

    // Create a new Comment
    $comment = new Comment(array('body' => 'A new comment.'));

    // Save the Comment to the Post
    $post->comments()->save($comment);

    // This Post should have one comment
    $this->assertCount(1, $post->comments);
}

Conclusion

So hopefully as you can see, creating Model relationships is relatively straight forward when you have a firm understanding of each of the different types of relationships and when they should be applied. Understanding these very simple relationship types will enable you to build very powerful applications.

As you can probably tell, much of this functionality and the associated tests are still very basic. I don’t think it is a good use of time to really focus on building every eventuality at this stage, as there is more value in just fleshing stuff out.

Update

After thinking about these relationships some more, I decided that it was too complicated. I really want to keep this as simple as it can possibly be. I decided to completely get rid of the concept of “Cliques”. Having this as an additional object in the application was just too messy.

Don’t be afraid to cut stuff out that doesn’t feel right. It’s always better to remove stuff that isn’t right rather than have an overcomplicated product.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.