In this Nettuts+ mini-series, we’ll build a web application from scratch, while diving into a great new PHP framework that’s rapidly picking up steam, called Laravel.
In this lesson, we’ll be working on an integral part of any web application: the Models. Along the way, we’ll learn about Laravel’s amazing ORM implementation: Eloquent.
Review
Welcome back to our Web Applications from Scratch with Laravel series! In the first tutorial of the series, we learned a lot about Laravel and its philosophy:- What Laravel is
- What makes Laravel different from other PHP frameworks
- Where to download Laravel
- How to setup Laravel
- How Laravel’s Routing system works
- Some other features in Laravel’s Routing system
- How to create your first Laravel Controller
- Some additional features with Laravel’s Controllers
- How to create your first Laravel View
- How to use Laravel’s Blade Templating Engine
In this second part of the Laravel series, we’ll be building a crucial part of our test web application, Instapics, which is the Model implementation. Without furhter ado, let’s get started!
What are “Models”?
I’ve already talked a bit about what Models are in one of my previous articles, Zend Framework from Scratch – Models and Integrating Doctrine ORM, so to avoid repeating myself, I’ll write the gist of what I wrote before here. Feel free to refer to the other tutorial and read more about what Models are there.Summary:
- Models are representatives of the Database, and should be where all the business logic of an application resides
- Controllers communicate with Models and ask them to retrieve information they need
- This information is then passed by a Controller to the View and is rendered
- It’s very rare that a Model directly interacts with a View, but sometimes it may happen when necessary
- Models can talk with other Models and aren’t self-contained. They have relationships that intertwine with each other
- These relationships make it easier and quicker for a Controller to get information, since it doesn’t have to interact with different Models – the Models can do that themselves
The Eloquent ORM
An ORM is an object-relational mapper, and Laravel has one that you will absolutely love! It is named “Eloquent,” because it allows you to work with your database objects and relationships using an eloquent and expressive syntax.The Eloquent ORM is Laravel’s built-in ORM implementation. In my opinion, it’s one of the best ORM implementations I’ve seen so far – rivaling even Doctrine ORM. It’s incredibly elegant, making use of industry-standard conventions to lessen configuration.
Conventions
For example, using an Eloquent model assumes that the table the model is representing has anid
field. The id
is the primary key for any record, and is used by most of Eloquent’s methods.Another thing that Eloquent correctly assumes is that your table name is the plural form of your model. For example, your
User
model will reference the users
table. As this might not always be the standard for some, Laravel provides a way to override this: simply use the $table
flag:class User extends Eloquent { public static $table = 'my_users'; }This will instruct Laravel not to use the convention and instead use the specified table.
Lastly, Laravel can also automate the creation and updating of timestamps for us. To do so, add a
created_at
and/or updated_at
column in the table, and set the $timestamp
flag in the model:class User extends Eloquent { public static $timestamps = true; }Eloquent will see the flag, and automatically set the
created_at
field on creation, and update the updated_at
field each time that a record is updated. Pretty cool, huh?Quick Retrieval
Retrieving records is a snap with Eloquent’s retrieval methods. For example, you need to find a specific user record? Just do:$user = User::find($user_id);This returns a
User
model that you can do operations on! Need to use conditionals? Let’s imagine that you want to retrieve a user by email address. To accomplish this task, you might do something like:$user = User::where('email', '=', $email)->first();Alternatively, you could use Laravel’s dynamic methods:
$user = User::where_email($email)->first();
Easy Inserts & Updates
Inserting and updating models using Eloquent can be accomplished in three steps.
Step 1 – Get/Create the model.
$user = new User(); //or get an existing user $user = User::get($user_id);
Step 2 – Set the data
$user->email = 'nikko@instapics.com'; $user->password = 'test1234';
Step 3 – Save
$user->save();
- Done!
And finally, Defining Relationships.
Eloquent makes the process of defining relationships and retrieving related models simple and intuitive.Damn right it does! Eloquent supports three types of relationships:
- One-to-One
- One-to-Many
- Many-to-Many
User
has_one
User_Profile
. You can do that by defining a user_profile
method in the User
model:class User extends Eloquent { public function user_profile() { return $this->has_one('User_Profile'); } }Because
User
is our “dominant” model here (i.e. a user has a profile, and not a profile has a user), we define that a User_Profile
belongs_to
a User
:class User_Profile extends Eloquent { public function user() { return $this->belongs_to('User'); } }Once we’ve defined these relationship, we can then do:
/* Get the User_Profile object of a User This executes two SQL queries: SELECT * FROM `users` WHERE `id` = $user_id SELECT * FROM `user_profiles` WHERE `user_id` = $user_id */ $user = User::find($user_id); $user_profile = $user->user_profile; /* We can also do it the other way around */ $user_profile = User_Profile::where('user_id', '=', $user_id)->first(); $user = $user_profile->user;One thing worth noting here is another convention: Eloquent assumes that the foreign key used in
User_Profile
is the referenced table’s name + _id
. Again, if you want to change this behaviour, you can override it:class User extends Eloquent { public function user_profile() { return $this->has_one('User_Profile', 'user_profile_user_id'); } }Let’s say that we want to define the relationship between a
User
and his Photo
uploads. This is a One-to-Many relationship, unlike the User
-to-User Profile
relationship which was One-to-One. We know that one User
has_many
Photo
uploads, so:class User extends Eloquent { public function photos() { return $this->has_many('Photo'); } } ... ... ... class Photo extends Eloquent { public function user() { return $this->belongs_to('User'); } }The main difference here with
has_one
is that the function we’ll use to retrieve a User
‘s photos will now return an array of Photo
objects. So, if we wanted to fetch all of a User
‘s photos, we could do:$photos = User::find($user_id)->photos; foreach($photos as $photo) { ... ... ... }Nope, referring to
photos
as a property isn’t a typo. Laravel gives us this nice bit of sugar. We could also do:$photos = User::find($user_id)->photos()->get();
Mant-to-Many Relationships
This one is a bit tricky, but once implemented, it makes it easy to handle Many-to-Many relationships between models. Let’s imagine, for example, that you, again, have aUser
model, and each of these Users can have multiple Groups
. A Group
can also have multiple Users
. We’ll use three tables to represent these particular relationships:- Users – table where all our users are
- Groups – table where all our groups are
- Group User – table which lists down all users in a group
- users
- id
- …other columns
- groups
- id
- …other columns
- group_user
- id
- user_id
- group_id
- …other columns
group_user
, is the singular names of the two tables that it is connecting, arranged alphabetically with an underscore. Like always, we’re free to override this.Here’s how the code will look inside each of the models for these three tables:
class User extends Eloquent { public function groups() { //if we wanted to override the default naming convention //for the intermediate table, we can do it like so: //return $this->has_many_and_belongs_to('Group', 'group_listings'); return $this->has_many_and_belongs_to('Group'); } } ... ... ... class Group extends Eloquent { public function users() { //if we wanted to override the default naming convention //for the intermediate table, we can do it like so: //return $this->has_many_and_belongs_to('User', 'group_listings'); return $this->has_many_and_belongs_to('User'); } } ... ... ... class Group_User extends Eloquent { public function group() { return $this->has_one('Group'); } public function user() { return $this->has_one('User'); } }With this in place, we can then take advantage of Eloquent’s relationship functions:
//Get a user's groups $groups = User::find($user_id)->groups; //Get all users in a group $users = Group::find($group_id)->users;
Step 1 - Creating the Instapics Database
Continuing with our web application, Instapics, let’s start off by creating the database of our application. To do so, let’s write down the desired functionalities of the application:- Users can login and register for an account
- Users can follow other users to see photos they have uploaded
- Users can upload their own photo and apply a filter to it
- Users can comment and like photos
users
- id (One-to-One with user_profiles.user_id, Many-to-Many with self using intermediate table relationships.follower_id and followed_id, One-to-Many with photos.user_id and photo_comments.user_id)
- password
- created_at
- updated_at
user_profiles
- id
- user_id (One-to-One with users.id)
- name
- profile_photo
relationships
- id
- follower_id (One-to-One with users.id)
- followed_id (One-to-One with users.id)
- created_at
- updated_at
photos
- id (One-to-Many with photo_comments.user_id)
- user_id (One-to-One with users.id)
- location
- description
- created_at
- updated_at
photo_comments
- id
- user_id (One-to-One with users.id)
- photo_id (One-to-One with photos.id)
- message
- created_at
- updated_at
CREATE DATABASE `instapics`; USE `instapics`; CREATE TABLE `instapics`.`users` ( `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, `email` VARCHAR(100) NOT NULL, `password` VARCHAR(100) NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `Index_email`(`email`) ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE `instapics`.`user_profiles` ( `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INTEGER UNSIGNED NOT NULL, `name` TEXT NOT NULL, `profile_photo` TEXT NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `Index_user_id`(`user_id`), CONSTRAINT `FK_user_profiles_user_id` FOREIGN KEY `FK_user_profiles_user_id` (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE `instapics`.`relationships` ( `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, `follower_id` INTEGER UNSIGNED NOT NULL, `followed_id` INTEGER UNSIGNED NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `Index_follower_id_followed_id`(`follower_id`, `followed_id`), CONSTRAINT `FK_relationships_follower_id` FOREIGN KEY `FK_relationships_follower_id` (`follower_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `FK_relationships_followed_id` FOREIGN KEY `FK_relationships_followed_id` (`followed_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE `instapics`.`photos` ( `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INTEGER UNSIGNED NOT NULL, `location` TEXT NOT NULL, `description` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `FK_photos_user_id` FOREIGN KEY `FK_photos_user_id` (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE `instapics`.`photo_comments` ( `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INTEGER UNSIGNED NOT NULL, `photo_id` INTEGER UNSIGNED NOT NULL, `message` TEXT NOT NULL, `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `FK_photo_comments_user_id` FOREIGN KEY `FK_photo_comments_user_id` (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `FK_photo_comments_photo_id` FOREIGN KEY `FK_photo_comments_photo_id` (`photo_id`) REFERENCES `photos` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;Alternatively, you could use migrations, but we’ll review those in a future lesson.
Step 2 – Setup Laravel’s Database Configuration
Before doing anything with Laravel models, we need to setup our Laravel installation’s database configuration. Open application/config/database.php, to find some of these settings:- profile – setting this to
true
will log all SQL querie times into the Laravel logs. Leave this astrue
for now. - fetch – the type of returned data from PDO. Default value is
PDO::FETCH_CLASS
and should be left like so. - default – this is the name of the connection settings used by the application. The name refers to the index in the
$connections
array just below
connections – an associative array of the possible connections for your application.
- driver – the database server type. This can be
pgsql
,sqlite
,mysql
orsqlsrv
- host – the host name of your database server
- database – the database name
- username – username to use on the database server
- password – password to use on the database server
- charset – charset to use on the database server
- prefix – table prefix on the database, if any
- driver – the database server type. This can be
- redis – if you plan on using Laravel’s
Redis
library, you can set up the server information here.
return array( 'profile' => true, 'fetch' => PDO::FETCH_CLASS, 'default' => 'mysql', 'connections' => array( 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'instapics', 'username' => 'root', 'password' => '(yourpassword)', 'charset' => 'utf8', 'prefix' => '', ), ), 'redis' => array( 'default' => array( 'host' => '127.0.0.1', 'port' => 6379, 'database' => 0 ), ) );
Step 3 - Creating Your First Laravel Model
Begin by creating a Laravel model inside the application/models folder. Create user.php inside, and add the following code:class User extends Eloquent { }Now, based on our review of what the
User
‘s relationships are, we need to code the relationship methods for all of them:class User extends Eloquent { //setting $timestamp to true so Eloquent //will automatically set the created_at //and updated_at values public static $timestamps = true; public function user_profile() { return $this->has_one('User_Profile'); } public function followers() { return $this->has_many_and_belongs_to('User', 'relationships', 'followed_id', 'follower_id'); } public function following() { return $this->has_many_and_belongs_to('User', 'relationships', 'follower_id', 'followed_id'); } public function photos() { return $this->has_many('Photo'); } public function photo_comment() { return $this->has_many('Photo_Comment'); } }Noticeably, we make use of some advanced Many-to-Many functionality here, due to the table structure of our follower model (i.e. the
users
table references the relationships
table which references the users
table again). The has_many_and_belongs_to
function has the following method signature:/** * Get the query for a many-to-many relationship. * * @param string $model * @param string $table * @param string $foreign * @param string $other * @return Relationship */ public function has_many_and_belongs_to($model, $table = null, $foreign = null, $other = null)This actually lets us create a model that has a Many-to-Many relationship with itself (i.e. Users follow other Users). We use
followers
and following
method names on the User
model to enable us to get a User’s followers or get all the users that a single User is following, respectively.Following the
User
model, create the other models. When you finish, you should have:- application/models/photo.php
- application/models/photo_comment.php
- application/models/relationship.php
- application/models/user.php
- application/models/user_profile.php
Step 4 - Create the User Functions for Instapics
Let’s begin using our models by creating some of the user functions we’ll be needing in the application. First up: user registration. From the previous tutorial, we’ve already created a Registration/Login Form on the home page. Right now, it’s not doing anything, but let’s hook it up to aUser
controller, authenticate
action. Create application/controllers/user.php with the following code:class User_Controller extends Base_Controller { public function action_authenticate() { } }Open application/views/home/index.blade.php and look for the login form. Update the form on Line 18 to submit to the
action_authenticate()
method:<form class="well" method="POST" action="user/authenticate">Going back to the
User_Controller
, let’s place some code in action_authenticate()
:class User_Controller extends Base_Controller { public function action_authenticate() { $email = Input::get('email'); $password = Input::get('password'); $new_user = Input::get('new_user', 'off'); if( $new_user == 'on' ) { try { $user = new User(); $user->email = $email; $user->password = Hash::make($password); $user->save(); Auth::login($user); return Redirect::to('dashboard/index'); } catch( Exception $e ) { echo "Faield to create new user!"; } } else { $credentials = array( 'username' => $email, 'password' => $password ); if( Auth::attempt($credentials)) { return Redirect::to('dashboard/index'); } else { echo "Failed to login!"; } } } }Let’s break-down what we’ve done here so far:
- We use the
Input
library to get the inputs from the submitted form - If the
$new_user
flag was checked, we create a newUser
, using theHash
library to generate an encrypted password. - Log in the new
User
with theAuth
library - If the
$new_user
flag was unchecked, we create a$credentials
array, and use it with theAuth
library. - If
Auth::attempt
is true, it means the credentials were correct and we’re logged in - Else, it means the credentials were wrong.
- In both scenarios, redirect to the
Dashboard
controller,index
action when successful, and echo a failure message if not.
The Input library
The basic function of theInput
library is to enable retrieval of form data. For example, in the User
controller, we used Input::get('email');
to get the email value from the form. It’s important to note that the get
method is used for all types of requests and not just the $_GET array.You can read more about the Input library here: http://laravel.com/docs/input#input
The Auth library
Laravel comes with its own authentication mechanism, theAuth
library. It can do the following features with regards to user authentication.Creating a hashed password
We can make use of theHash
library like so:$password = Hash::make($plaintext_password);This method creates a salted and hashed password for a user, using the encryption key we set in the configuration file. To check if a hash is correct, you can use:
if( Hash::check($plaintext_password, $hashed_password) == true ) { echo 'Password is correct.'; }
Logging in a user
For this, we use theAuth
library’s attempt
method. Before that, though, we build a $credentials
array, which is an associative array with a username
and password
indexes:$credentials = array( 'username' => 'yourname@youremail.com', 'password' => 'yourpassword' ); if( Auth::attempt($credentials) == true ) { echo 'User is logged in'; } else { echo 'Credentials failed'; }It’s important to keep in mind that once an attempt is “successful”, the user is automatically logged in.
Loggin a user in/out
Sometimes, we’ll need to login a user without using theattempt
method (such as when logging in a user from a link inside an email, or post-registration). We can do that with the Auth::login
method:Auth::login($user); //where $user is a User object Auth::login(42); //where 42 is the User's IDOn the other hand, we also have an
Auth::logout
method for logging users out:Auth::logout();This will terminate the user’s session.
Retrieving the logged in user’s data
TheAuth::user
method allows us to retrieve the logged in user object, as well as any information attached to it:$email = Auth::user()->email; $created_at = Auth::user()->created_at;
Configuring the Auth
library
The Auth
library has to be configured before using it (though the defaults will work with most projects, like this one). The configuration variables are:- driver – this can be either eloquent or fluent. Developers can write their own drivers by extending the
Driver
class in laravel/auth/drivers. - username – this is the column name of whatever represents your user’s “username” in the database table.
- model – when using Eloquent, this is the model class that the
Auth
library uses - table – when using the Fluent authentication driver, this determines the database table for the users in the application
Going back to the project, try testing out the login/registration features of Instapics! Pretty nifty, huh? You’ll notice though that we don’t have anything in the
Dashboard
controller yet, so let’s work on that next.Step 5 - Create the Instapics Dashboard
The first thing we need to do is create theDashboard
controller, with the index
action. Create the file application/controllers/dashboard.php and put in the following code:class Dashboard_Controller extends Base_Controller { public function action_index() { $photos = Auth::user()->photos()->order_by('created_at', 'desc')->order_by('id', 'desc')->get(); return View::make('dashboard.index', array('photos' => $photos)); } }Now we need to create the Dashboard’s index page. Create application/views/dashboard/index.blade.php and append the following:
@layout('layouts/main') @section('navigation') @parent <li><a href="user/logout">Logout</a></li> @endsection @section('content') <div class="row"> <div class="span3"> <div class="well sidebar-nav"> <ul class="nav nav-list"> <li class="nav-header">Followers</li> </ul> <div style="margin-left: 10px"> @forelse (Auth::user()->followers as $follower) <div style="float: left; width: 30px; margin: 0px 3px 3px 5px;"> <img src="http://d2o0t5hpnwv4c1.cloudfront.net/2069_laravel_2/http://gravatar.com/avatar/{{ md5(strtolower(trim($follower->email))) }}?s=25&d=retro" alt="Follower" title="{{ $follower->email }}" /> </div> @empty <div>You have no followers.</div> @endforelse <div style="clear: both"></div> </div> <ul class="nav nav-list"> <li class="nav-header">Following</li> </ul> <div style="margin-left: 10px"> @forelse (Auth::user()->following as $following) <div style="float: left; width: 30px; margin: 0px 3px 3px 5px;"> <img src="http://d2o0t5hpnwv4c1.cloudfront.net/2069_laravel_2/http://gravatar.com/avatar/{{ md5(strtolower(trim($following->email))) }}?s=25&d=retro" alt="Following" title="{{ $following->email }}" /> </div> @empty <div>You are not following anybody.</div> @endforelse <div style="clear: both"></div> </div> </div> </div> <div class="span9"> <h1>Your Photos</h1> @forelse ($photos as $photo) <div class="well" style="text-align: center"> <img src="http://d2o0t5hpnwv4c1.cloudfront.net/2069_laravel_2/{{ $photo->location }}" alt="{{ $photo->description }}" title="{{ $photo->description }}" /> <p>{{ $photo->description }}</p> </div> @empty <div class="alert alert-info"> <h4 class="alert-heading">Awww!</h4> <p>Seems like you don't have any photos yet. <a href="#">Upload a new one?</a></p> </div> @endforelse </div> </div> @endsectionRefresh the dashboard page, you should see this:
Looking a bit bare? Add this to the
Dashboard
controller, and run it by accessing dashboard/insert_test_data
on your browser:public function action_insert_test_data() { $logged_in_user = Auth::user(); for( $x = 0; $x < 10; $x++ ) { $email = rand().'@gmail.com'; $user = new User(); $user->email = $email; $user->password = Hash::make($email); $user->save(); $logged_in_user->followers()->attach($user->id); if( $x > 5 ) { $logged_in_user->following()->attach($user->id); } } $photos = array( array( 'user_id' => $logged_in_user->id, 'location' => 'http://farm6.staticflickr.com/5044/5319042359_68fb1f91b4.jpg', 'description' => 'Dusty Memories, The Girl in the Black Beret (http://www.flickr.com/photos/cloudy-day/)' ), array( 'user_id' => $logged_in_user->id, 'location' => 'http://farm3.staticflickr.com/2354/2180198946_a7889e3d5c.jpg', 'description' => 'Rascals, Tannenberg (http://www.flickr.com/photos/tannenberg/)' ), array( 'user_id' => $logged_in_user->id, 'location' => 'http://farm7.staticflickr.com/6139/5922361568_85628771cd.jpg', 'description' => 'Sunset, Funset, Nikko Bautista (http://www.flickr.com/photos/nikkobautista/)' ) ); $logged_in_user->photos()->save($photos); }When you refresh the page, you’ll see what it looks like with the sample data inserted:
Conclusion
In the second part of our Laravel series, we learned:- Some background on what “Models” are
- What the Eloquent ORM is
- How to set up Laravel’s database configuration
- How to create your first Laravel Model
- The basic functions of the Auth and Input libraries
- Making use of the Eloquent ORM in a view
Next in our Web Applications from Scratch with Laravel series, we’ll learn more about using Laravel’s filters, Laravel’s Validation library, and how to work with files in Laravel!
What do you think about Laravel’s Eloquent ORM? Is it something that you find useful? Let me know in the comments! And, if you’re a Tuts+ Premium member, stay tuned for our upcoming Laravel Essentials course!
No comments:
Post a Comment