Laravel hasMany and belongsTo Tutorial

Laravel hasMany and belongsTo Tutorial

The Laravel hasMany and belongsTo relationships are a fantastic feature of the framework. Relationships, and specifically Eloquent Relationships, are a really popular feature of Laravel. The many to many relationships tutorial here at Vegibit is always the number one or two most popular post as you can see on the right hand side of this page. In this episode, we’re going to show some love to the hasMany and belongsTo relationships that Eloquent provides. Let’s get crackin.


The Relationships To Model

There are a ton of different ways to explore these relationships. A blog post having many comments. A comment that belongs to a user. A blog post that has many categories. You get the idea. For this example, we’ll explore the concept of a User and his or her Tweets. As you can imagine, we’ll explore the concept of a User that hasMany tweets. In addition, we’ll have a Tweet model where we can see that an individual tweet belongsTo a User. It should be a fun example.


Set Up The Database

First off, we need a database to hold our users and tweets. Dealing with users is quite easy, as it is just a single table. The tweets however are more granular. We’ll have 6 tables in total which include users, tweet_urls, tweet_tags, tweet_retweets, tweet_mentions, and tweets. Here are the migrations to make it happen. Of course we are creating this on the most excellent Laravel Homestead which we covered in this episode.

create_users_table.php source

<?php

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

class CreateUsersTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->bigInteger('twitter_id')->unique('twitter_id');
            $table->string('name', 150);
            $table->string('screen_name', 150);
            $table->string('location', 150)->nullable();
            $table->string('description')->nullable();
            $table->string('expanded_url')->nullable();
            $table->integer('followers_count')->nullable();
            $table->integer('friends_count')->nullable();
            $table->integer('listed_count');
            $table->string('twitter_created_at', 10);
            $table->integer('favourites_count');
            $table->integer('statuses_count');
            $table->string('status', 150);
            $table->text('profile_image_url', 16777215)->nullable();
            $table->enum('user_status', array('Active', 'Deactive'))->default('Active');
            $table->string('oauth_token', 60)->nullable();
            $table->string('oauth_token_secret', 150)->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

}

create_tweets_table.php source

<?php

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

class CreateTweetsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweets', function(Blueprint $table)
		{
			$table->bigInteger('tweet_id')->unsigned()->primary();
			$table->string('tweet_text', 160);
			$table->dateTime('twitter_created_at')->index('twitter_created_at');
			$table->bigInteger('twitter_id')->unsigned()->index('twitter_id');
			$table->boolean('is_rt');
			$table->integer('retweet_count')->index('retweet_count');
		});
	}


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

}

create_tweet_mentions_table.php source

<?php

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

class CreateTweetMentionsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweet_mentions', function(Blueprint $table)
		{
			$table->bigInteger('tweet_id')->unsigned()->index('tweet_id');
			$table->dateTime('twitter_created_at')->index('twitter_created_at');
			$table->bigInteger('source_user_id')->unsigned()->index('source_user_id');
			$table->bigInteger('target_user_id')->unsigned()->index('target_user_id');
		});
	}


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

}

create_tweet_retweets_table.php source

<?php

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

class CreateTweetRetweetsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweet_retweets', function(Blueprint $table)
		{
			$table->bigInteger('tweet_id')->unsigned()->index('tweet_id');
			$table->dateTime('twitter_created_at')->index('twitter_created_at');
			$table->bigInteger('source_user_id')->unsigned()->index('source_user_id');
			$table->bigInteger('target_user_id')->unsigned()->index('target_user_id');
		});
	}


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

}

create_tweet_tags_table.php source

<?php

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

class CreateTweetTagsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweet_tags', function(Blueprint $table)
		{
			$table->bigInteger('tweet_id')->unsigned()->index('tweet_id');
			$table->bigInteger('twitter_id')->unsigned()->index('twitter_id');
			$table->string('tag', 100)->index('tag');
			$table->dateTime('twitter_created_at')->index('twitter_created_at');
		});
	}


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

}

create_tweet_urls_table.php source

<?php

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

class CreateTweetUrlsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('tweet_urls', function(Blueprint $table)
		{
			$table->bigInteger('tweet_id')->unsigned()->index('tweet_id');
			$table->bigInteger('twitter_id')->unsigned()->index('twitter_id');
			$table->string('url', 100)->index('url');
			$table->dateTime('twitter_created_at')->index('twitter_created_at');
		});
	}


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

}

We Need Some Users and Tweeets

If we’re going to model the relationship between Users and Tweets, then we’re going to need some to work with! For this, we will use the most excellent package provided by thujohn. If you would like to follow along, you’ll need to configure an application in the developer tools at Twitter to do so. Rather than go into the details on how to set that up, check out the prior link, or just google around, there are a ton of examples of how to set up your application. Now, we’ll explore how to set up the Routes, Controllers, and Models to meet our goals. These following code samples may or may not contain “best practices”, it’s just a quick first stab at getting some tweets to work with!


The Twitter Login Button

We’re going to need a way to log in to Twitter. In the efforts of quickness, we’ll just hack the shipped welcome view to add a link to the login route of our application. Note that we will be sure to leave the call to an inspiring quote, so as to stay inspired at all times.

The Welcome View source

	<body>
		<div class="container">
			<div class="content">
				<div class="title"><a href="http://homestead.app/twitter/login">Log In To Twitter</a></div>
				<div class="quote">{{ Inspiring::quote() }}</div>
			</div>
		</div>
	</body>

Log In To Twitter


The Routes To Use

Here is the routes file to use for this example. As you can see it is just a slightly modified version of the route file that ships with Laravel. We set up the routes for the login and callback from twitter, as well as a route that will get some tweets to work with. We’ll just put all these methods in one Twitter Controller, and call it a day.

routes.php source

<?php

Route::get('/', 'WelcomeController@index');

Route::get('home', 'HomeController@index');

Route::controllers([
    'auth' => 'Auth\AuthController',
    'password' => 'Auth\PasswordController',
]);

Route::get('/twitter/login', 'TwitterController@twitterlogin');

Route::get('/twitter/callback', 'TwitterController@twittercallback');

Route::get('gettweets', 'TwitterController@gettweets');

The Methods of The Twitter Controller

Here is the source of our Twitter controller. If you follow it along, it should be pretty straightforward. We login to twitter, the callback is processed, Eloquent creates a new user, the user is authenticated, and finally we just redirect to the gettweets method to fetch a few tweets to work with. Read the comments if any of this is a little foreign to you. For reference, we use Eloquent to create a new User, and Laravel’s Query Builder to handle inserting all the tweets.

TwitterController.php source

<?php namespace App\Http\Controllers;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Twitter;
use Session;
use Redirect;
use Input;
use App\User;
use App\Tweet;
use Auth;
use DB;

class TwitterController extends Controller
{
    public function __construct()
    {
        // protects every single method except twitterlogin, twittercallback
        $this->middleware('auth', ['except' => ['twitterlogin', 'twittercallback']]);

    }

    public function twitterlogin()
    {
        // your SIGN IN WITH TWITTER  button should point to this route
        $sign_in_twitter = TRUE;
        $force_login = FALSE;
        $callback_url = 'http://' . $_SERVER['HTTP_HOST'] . '/twitter/callback';

        // Make sure we make this request w/o tokens, overwrite the default values in case of login.
        Twitter::set_new_config(['token' => '', 'secret' => '']);

        // Request user tokens from Twitter
        $token = Twitter::getRequestToken($callback_url);

        if (isset($token['oauth_token_secret'])) {

            // build the authorization url
            $url = Twitter::getAuthorizeURL($token, $sign_in_twitter, $force_login);

            // set user tokens into session
            Session::put('oauth_state', 'start');
            Session::put('oauth_request_token', $token['oauth_token']);
            Session::put('oauth_request_token_secret', $token['oauth_token_secret']);

            // redirect to authorization url
            return Redirect::to($url);
        }

        return Redirect::to('twitter/error');
    }

    public function twittercallback()
    {
        if (Session::has('oauth_request_token')) {
            $request_token = [
                'token' => Session::get('oauth_request_token'),
                'secret' => Session::get('oauth_request_token_secret'),
            ];

            // $request_token holds the TEMPORARY credentials, only used for initial login!
            // merge app keys and user tokens into one variable
            Twitter::set_new_config($request_token);

            $oauth_verifier = FALSE;
            if (Input::has('oauth_verifier')) {
                $oauth_verifier = Input::get('oauth_verifier');
            }

            // getAccessToken() will reset the $request_token for you
            $token = Twitter::getAccessToken($oauth_verifier);
            if (!isset($token['oauth_token_secret'])) {
                return Redirect::to('sorry')->with('error', 'We could not log you in on Twitter.');
            }

            // at this point we have NEW tokens, these are the ones to save into the database
            // These tokens can be used to make api calls at a later time
            // $user->oauth_token = $token['oauth_token'];
            // $user->oauth_token_secret = $token['oauth_token_secret'];

            $credentials = Twitter::query('account/verify_credentials');
            if (is_object($credentials) && !isset($credentials->error)) {
                // $credentials contains the Twitter user object with all the info about the user.
                // Add here your own user logic, store profiles, create new users on your tables...you name it!
                // Typically you'll want to store at least, user id, name and access tokens
                // if you want to be able to call the API on behalf of your users
                // This is also the moment to log in your users if you're using Laravel's Auth class
                //  Auth::login($user); should do it

                // create a new user with Eloquent and log them in
                $user = new User;
                $user->twitter_id = $credentials->id;
                $user->name = $credentials->name;
                $user->screen_name = $credentials->screen_name;
                $user->location = $credentials->location;
                $user->description = $credentials->description;
                $user->expanded_url = $credentials->entities->url->urls[0]->expanded_url;
                $user->followers_count = $credentials->followers_count;
                $user->friends_count = $credentials->friends_count;
                $user->listed_count = $credentials->listed_count;
                $user->twitter_created_at = $credentials->created_at;
                $user->favourites_count = $credentials->favourites_count;
                $user->statuses_count = $credentials->statuses_count;
                $user->status = $credentials->status->text;
                $user->profile_image_url = $credentials->profile_image_url;
                $user->oauth_token = $token['oauth_token'];
                $user->oauth_token_secret = $token['oauth_token_secret'];
                $user->save();
                Auth::login($user);

                Session::flash('flash_message', '<b>Howdy Partner!</b> You made it.');
                Session::flash('flash_type', 'alert-success');
                return Redirect::to('gettweets');
            }
            Session::flash('flash_message', '<b>Ouch!</b> Something went wrong, try again later.');
            Session::flash('flash_type', 'alert-danger');
            return Redirect::to('/');
        }
    }

    
    public function gettweets()
    {

        // grab the token and secret from our database
        // since we are authenticated, we can fetch this info with Auth::user()
        $request_token = [
            'token' => Auth::user()->oauth_token,
            'secret' => Auth::user()->oauth_token_secret,
        ];

        // merge app keys and user tokens into one variable
        Twitter::set_new_config($request_token);

        // you are now ready to make api calls on behalf of the user
        // fetch the twitter_id of the user from our database first

        $tweets = Twitter::getUserTimeline([
            'user_id' => Auth::user()->twitter_id,
            'include_entities' => 'true',
            'include_rts' => 'true',
            'exclude_replies' => 'false',
            'trim_user' => 'true',
            'count' => 100
        ]);


        // We’ll use Laravel’s Query Builder to insert the tweets

        foreach ($tweets as $tweet) {
            $tweet_id = $tweet->id;
            $tweet_text = $tweet->text;
            $twitter_created_at = date('Y-m-d H:i:s', strtotime($tweet->created_at));
            $retweet_count = $tweet->retweet_count;
            $twitter_id = $tweet->user->id;

            if (isset($tweet->retweeted_status)) {
                $is_rt = 1;
                $tweet_text = $tweet->retweeted_status->text;
                $retweet_count = 0;
                $retweet_user_id = $tweet->retweeted_status->user->id;
                $entities = $tweet->retweeted_status->entities;
            } else {
                $is_rt = 0;
                $entities = $tweet->entities;
            }

            DB::table('tweets')->insert(
                [
                    'tweet_id' => $tweet_id,
                    'tweet_text' => $tweet_text,
                    'twitter_created_at' => $twitter_created_at,
                    'twitter_id' => $twitter_id,
                    'is_rt' => $is_rt,
                    'retweet_count' => $retweet_count
                ]
            );

            if ($is_rt) {
                DB::table('tweet_retweets')->insert(
                    [
                        'tweet_id' => $tweet_id,
                        'twitter_created_at' => $twitter_created_at,
                        'source_user_id' => $twitter_id,
                        'target_user_id' => $retweet_user_id
                    ]
                );
            }

            if ($entities->hashtags) {
                foreach ($entities->hashtags as $hashtag) {
                    $tag = $hashtag->text;
                    DB::table('tweet_tags')->insert(
                        [
                            'tweet_id' => $tweet_id,
                            'twitter_id' => $twitter_id,
                            'twitter_created_at' => $twitter_created_at,
                            'tag' => $tag
                        ]
                    );
                }
            }

            if ($entities->user_mentions) {
                foreach ($entities->user_mentions as $user_mention) {
                    $target_user_id = $user_mention->id;
                    DB::table('tweet_mentions')->insert(
                        [
                            'tweet_id' => $tweet_id,
                            'twitter_created_at' => $twitter_created_at,
                            'source_user_id' => $twitter_id,
                            'target_user_id' => $target_user_id
                        ]
                    );
                }
            }

            if ($entities->urls) {
                foreach ($entities->urls as $url) {
                    $url = $url->expanded_url;
                    DB::table('tweet_urls')->insert(
                        [
                            'tweet_id' => $tweet_id,
                            'twitter_created_at' => $twitter_created_at,
                            'twitter_id' => $twitter_id,
                            'url' => $url
                        ]
                    );
                }
            }
        }

    }

}

The User and Tweet Models

Let’s now investigate the User and Tweet models to see how we set up the relationships for our hasMany and belongsTo scenarios. It’s so slick how this works.

User.php source

<?php namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{

    // override the id primary key
    // protected $primaryKey = 'twitter_id';

    use Authenticatable, CanResetPassword;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = ['remember_token'];

    public function tweets()
    {
        return $this->hasMany('App\Tweet', 'twitter_id', 'twitter_id');
    }

}

Tweet.php source

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class Tweet extends Model
{

    // override the id primary key

    protected $primaryKey = 'tweet_id';

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'tweets';

    public $timestamps = false;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [];

    public function user()
    {
        return $this->belongsTo('App\User', 'twitter_id', 'twitter_id');
    }

}

Testing Our Eloquent Relationships

With all of the leg work out of the way, we are now ready to start testing Eloquent and the relationships we had set up in our Models. This is the fun part! We have the users table and the tweets table for which we can test out all kinds of queries on. First, we’ll practice queries using each model without using any relationships. After all, it never hurts to practice. Once we have become familiar with accessing any field in the table we want, we’ll dive into using our hasMany and belongsTo relationships. Since we have two models to work with, first we’ll practice standard eloquent queries without relationships, then we will investigate the others.


Collections Vs Objects

Before we start firing off our queries, we need to make note of a quick item. Eloquent queries are going to either return a collection, or an object. This will change how you can access the data within the resulting variable this result is assigned to. Keep this in mind when working with Eloquent. Assuming we have an object to work with however, we can easily understand how the convention works. We’ll start with the ‘Hello World’ of Eloquent, finding a user by id. We’ll also list the actual queries Eloquent produces for us.

Object Example


$user = User::find(1);
select * from `users` where `users`.`id` = 1 limit 1

In this instance, we get an object assigned to $user. This is where the magic of an object relational mapper comes in. We can now access the table’s column or field names as if they were attributes of that object. The format looks a bit like this.

$table->column_name

Since we know the names of the columns in our table, it is so easy to access that data now. In this case we see that here:

$user->screen_name;       // vegibit
$user->favourites_count;  // 553
$user->statuses_count;    // 1135
$user->expanded_url:      // https://vegibit.com
$user->status;            // #Linux Files and Directories http://t.co/ZM9BJOmpAA

All we have to do is simply reference the column names, it’s fantastic.

Collection Example

When you perform queries that return many results, they will come back as an Eloquent Collection of objects. Working with collections is a bit different, and is worthy of an entire episode to itself. The takeaway however is that many times you will be iterating over the collection to get data. Just like we saw an example of one User above, how about an example of a collection of tweets? That’s easy.


$tweets = Tweet::all();
select * from `tweets`

The above code looks so simple and innocent doesn’t it? Behold! That one tiny little variable $tweets is a large Eloquent Collection of objects! Since it is a collection, you now have access to tons and tons of helper methods available to you. I wonder just how many tweets that collection contains? Easy.

echo $tweets->count();  // 100

The takeaway is that in an instance of a collection like this, you will usually pass the result to a view, and then loop over it like so:

@foreach ($tweets as $tweet)

    {{ $tweet->tweet_text  }}
    
@endforeach

This of course would echo out all 100 tweets to the browser. Now that we have the basic idea of working with an object instance and an eloquent collection when working with the database, let’s now test out our hasMany and belongsTo relationships.


User hasMany Tweets Queries

Recall that in our User Model, we had set up a relationship. We did this with a simple function like so:

    public function tweets()
    {
        return $this->hasMany('App\Tweet', 'twitter_id', 'twitter_id');
    }

Note: We did make use of the optional second and third parameters to the hasMany method. These define the foreign and local key to use for the relationship between the two tables.

The convention to figure out how to represent these things is:

This Class Name relationship Models.

Following this convention, we can say, “This User hasMany Tweets.” Easy. We can now find a user’s tweets like so:


$tweets = User::find(1)->tweets;
select * from `users` where `users`.`id` = ? limit 1
select * from `tweets` where `tweets`.`twitter_id` = 1278380054

You can use the result in your view like so:


@foreach ($tweets as $tweet)

    {{ $tweet->tweet_text  }}
    
@endforeach

Since we defined that tweets() function in our User model, we have that super slick access to finding a users tweets by simply adding the name of the function to the database call, i.e., User::find(1)->tweets; Also note that you have access to every single column in the remote table. So when we do this query, not only do we have access to $tweet->tweet_text like we show above, but also $tweet->id, $tweet->twitter_created_at, $tweet->twitter_id, $tweet->is_rt, and $tweet->retweet_count. So slick!


Tweet belongsTo User Queries

Recall that in our Tweet Model, we had also set up a relationship. We did this with a simple function like so:

    public function user()
    {
        return $this->belongsTo('App\User', 'twitter_id', 'twitter_id');
    }

This Class Name relationship Models.

Again following our convention, we can say that, “This Tweet belongsTo a User.” Let’s find the user of a tweet like so now:


$user = Tweet::find(573560672455237632)->user;
select * from `tweets` where `tweets`.`tweet_id` = 573560672455237632 limit 1
select * from `users` where `users`.`twitter_id` = 1278380054 limit 1

This is so slick. In this query, we provide the id of a tweet, and find the user who created it. Since we are getting one single result, a user, this comes back as an object, not a collection, so we can access values right away with no need to iterate.

$user->name;              // vegibit
$user->description:       // Random Bits of Awesomeness!
$user->favourites_count;  // 553
$user->statuses_count;    // 1135
$user->expanded_url:      // https://vegibit.com

Laravel hasMany and belongsTo Summary

This was a great episode where we built a basic twitter application we could log into and then store some tweets. By using the power of eloquent, we were able to see how to set up relationships based on the tweets we collected. Of course we know that a user may have many tweets, and a tweet always is created by a user, but now we know how to express this in code using Laravel. Thanks for reading!

Leave a Reply