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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?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
1 2 3 4 5 6 7 8 |
<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> |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?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
1 |
$user = User::find(1); |
users
where users
.id
= 1 limit 1In 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:
1 2 3 4 5 |
$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.
1 |
$tweets = Tweet::all(); |
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.
1 |
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:
1 2 3 4 5 |
@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:
1 2 3 4 |
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:
1 |
$tweets = User::find(1)->tweets; |
users
where users
.id
= ? limit 1select * from
tweets
where tweets
.twitter_id
= 1278380054You can use the result in your view like so:
1 2 3 4 5 |
@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:
1 2 3 4 |
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:
1 |
$user = Tweet::find(573560672455237632)->user; |
tweets
where tweets
.tweet_id
= 573560672455237632 limit 1select * from
users
where users
.twitter_id
= 1278380054 limit 1This 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.
1 2 3 4 5 |
$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!