-
Notifications
You must be signed in to change notification settings - Fork 0
CRUD Operations
Welcome to chapter five in this tutorial on building RESTful applications with Laravel. In the last Request Handling & Responses chapters, I already explained how to get requests, how to parse them, how to validate requests, what to do with the data, and how to send back responses. All that is nice, but a real application will probably do more than that, therefore it's now time to introduce databases or database operations to do more with the data you get than just, well, send it back as a response.
In this chapter you will learn how you can quickly set up databases and configure them with Laravel migrations. We'll also dive into Eloquent, Laravels object relational mapping model, which basically allows you to work with data through your PHP models, so that you have models in your application which are directly and automatically mapped to tables in the database so that you can work with the data without writing any raw SQL queries. One thing we'll do using Laravel's Eloquent is to model relations between our different models. Finally, we'll not only model the relations and set up the database tables, but of course I will also show you how you can work with the data using Laravel Eloquent, which means writing it to the database, fetching it, and more.
So Laravel provides a great support when it comes to working with databases. Specifically it makes it easy to create and configure tables with migrations, to define relations between different models, and to work with the data through the Eloquent model.
But how does this work in detail? Imagine you have your Laravel application, and we might have a blog controller in, well, a blog for example. Now, in this blog we might write several posts, and users might be able to write comments, therefore, we'll probably have a post model and a comment model, and the controller will access both models to, well, create new posts, fetch posts, create comments, and so on. That is very nice, but the blog wouldn't be worth much if the data would not be stored in a database. So, a MySQL database comes into play. Now between those two models we'll probably have relations, for example a comment may belong to a post, on the other hand a post may have multiple comments, so it is a has many relation. Overall, it is a one-to-many relation since one post might have multiple comments, but one comment will only belong to one post. With these relations set up, Eloquent now makes it easy to insert data or work with the data in the database tables. The only thing that is missing is how is the database set up in the first place. Here, Laravel migrations come into play, and with these migrations it's easy to configure database tables and to manage the database overall.
Since this is no beginner tutorial I will only provide brief refreshers on all these topics, and I want to begin with migrations. So how can we create models and migrations with Laravel? Remember, models are the things or the classes we work with in our application, and migrations allow us to create and configure our database tables. If you want to create a model and a migration we can use the php artisan make:model Post -m
make:model command, add the name of this model and the m flag at the end to indicate that we not only want to create this model file in the app folder, but also migration file, which allows us to configure the database table belonging to that model. Because remember, Eloquent allows us to map a model to a database table. There are also times when we don't want a model and a migration, but only a migration. For example, if we have many-to-many relationships, we will need a pivot table, which holds the connections between two tables, and therefore we don't need a separate model. We can create a single migration with the php artisan make:migration PostTableMigration
make:migration command, and then pass the name of this, well, migration.php file basically Laravel will create for us. Those migration files will be stored in the database folder in your project. So it's fine that those migrations are created, or these files are created and stored in the database table, but how exactly is the database configured with them?
<?php
use Illuminate\Support\Facades\Schema;
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->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
If you open a file in this database folder you will see two functions, up
and drop
. Now drop
is just to delete this table, so I will focus on up here, which creates a table. A table configuration, or schema
, is created with the Schema
facade, which has this create method, which as a first argument takes the name of the table which should be created, and as a second argument it takes a closure, which holds the detailed configuration of the table. Now again, I won't dive deep into migrations and Laravel's Eloquent and so on, since you could create an entire tutorial just about that topic, therefore, I will only provide a brief refresher. That being said, attached to this chapter you'll find a document with links leading to detailed explanations about the different configuration options you've got available. For example, you can configure your table to have a field ID, which will auto increment and access the primary key with the increments method. You could set up default time stamps, and this method will create two fields, the created at, and updated at field, which will be automatically filled upon creating an entry or changing it. You might also add, basically in SQL, a varchar, so a string with limited amount of characters, here the field of title would be created. Or if you need more text than just a limited number of characters you could use the text function, and create a field content, for example. So this is how you use migrations to configure tables, and with this up command if you run this migration file, or if you run migrations through the Artisan command-line tool, you would create a table named posts with the field specified in this closure, and the configuration provided with the methods used here. So it's great to have that file, how does it finally get into the database, or converted into a database table?
Manage Migrations
Migrations can be managed with multiple commands. Probably the most important one is
//Create database table
php artisan migrate
the migrate command, which basically takes all your migration files, which haven't been migrated yet, and will create the tables defined there in the database.
//Rollback latest migrations
php artisan migrate:rollback
//Rollback all migrations
php artisan migration:reset
//Rollback & migrate again
php artisan migrate:refresh
You also could use the migrate:rollback command to roll back the latest migrations, or reset to roll back all migrations. If you want to roll back all migrations and then remigrate because you might have changed some configuration in a file or in a table, you might use the refresh command, migrate:refresh to do that.
As always having the theory is nice, but it's time to see this in action. For the application I'm building throughout this tutorial, I will have meetings
and users
. So I will have two models, a meeting
and a user
model. So I'll start with creating those. Now I won't need to create the user model, because the Laravel default installation ships with a user model already created, and it happens to work fine to just use that user model. I'll come back to that once I'm in the editor.
Adarsh:laravel-rest adarshmaurya$ php artisan make:model Meeting -m
Model created successfully.
Created Migration: 2018_12_03_174310_create_meetings_table
So I will create a meeting, and I do this by running php artisan make:model, I will name it Meeting, and by adding -m I make sure that a migration file is created too. So this was created.
Now I also need a pivot table to match meetings with users, basically the registrations. Now, I won't name this table registrations because I want Laravel to be able to infer this pivot table, and I will come back to this later when I talk about relations.
Adarsh:laravel-rest adarshmaurya$ php artisan make:migration create_meeting_user_table
Created Migration: 2018_12_03_174454_create_meeting_user_table
So for now all I will do is I will create a new migration file since I won't need a model, and I will do this running php artisan make:migration, and I want to give this file a name, create_meeting_user_table. Now if I switch to the project in the database, in the migrations folder you should see four files if you're still having the default Laravel installation. Laravel shipped with this create_users and create_password_resets_table. Now for the Laravel RESTful service here I won't need this password_resets_table, but the users_table is fine. The other two files you see here, are the files we just created through this Artisan command. Now if I have a look at the create_users_table, it happens to be just fine the way it is by default configured, name, email, and password are exactly the fields users in our demo project here use, and therefore there are no changes needed here. Now in the other two files, if I open the meetings table, you see there is a basic boilerplate, but that is not all I will need for this project to work, or for this app to work correctly. Besides the ID and the timestamps, I also want to add a dateTime field, which holds the time data of the meeting, and I will copy this two times because I will also need a field type varchar, or added with a string, which should hold the title of the meeting, and a text field, which allows us to hold more characters than just a string field, which should hold the description of a meeting. Now with this, the meeting table is configured too, only the meeting user table to go. Now since I created a pure migration without a model, you can see this file is empty, and therefore I will copy the Schema:create function from the meeting migration file, and I will also copy the drop function. Now of course, this table should not be called meetings, I will call it meeting_user, and I have to copy this in the drop function too. And, by the way, since I didn't mention it, this meetings name here in the meetings table was automatically inferred by Laravel. It takes the model name and then pluralizes it, so it adds an s at the end and makes it lowercase. So, meeting_user will be the table to match meetings and users, and here I only will need two integer fields, one holding the user_id, and the other one holding the meeting_id. With this I'm done configuring the tables, and now I can write those migrations to the database.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating: 2018_12_03_174310_create_meetings_table
Migrated: 2018_12_03_174310_create_meetings_table
Migrating: 2018_12_03_174454_create_meeting_user_table
Migrated: 2018_12_03_174454_create_meeting_user_table
Adarsh:laravel-rest adarshmaurya$
I do this by running php artisan migrate
, and you should see these three success messages here too. With that the database is prepared. If you get some exceptions, with in the providers, AppServiceProvider class add Schema::defaultStringLength(191);
in the boot()
method.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//https://laravel-news.com/laravel-5-4-key-too-long-error
Schema::defaultStringLength(191);
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}
Time to make the next step.
So once the database is in place, it's time to define possible relations an application might have. Laravel out of the box supports the common relations you might have, like one-to-one
, one-to-many
, many-to-many
, and some more. And, as I said, this won't be a deep dive into Laravel Eloquent relations, but I want to discuss the basics about that, or add a brief refresher about that. So how are relations defined?
public function comments(){
return $this->hasMany('App\Comment');
}
In a model file the PHP file created with the make:model command, for example, you basically add a function to set up a relation. So in the post file, in the post model, you might add the comments function, and the name is up to you, but it makes sense to give this function a name which describes the relation, so comments because a post might have multiple comments, and all this function does, or all this method does, is it returns the hasMany method on that model because all the models using Eloquent inherit from the Eloquent base model in Laravel, and this offers all those relations and functions or methods here. So we return this hasMany
method on this model, and as an argument you pass the path, or the namespace, of the model to which you want to set up a relation. So in this example here, the comment model in the app folder, or the app namespace. With this method, this relation is set up, at least in the post model you would have to add the inverse with the belongsTo method in the comment model too. So this is how we define a relation.
Relations are defined as functions in Eloquent models
Assumptions: Laravel makes some assumptions when such relations are set up, don't forget that. For example, it has to assume the foreign key name, or foreign key table field, because if we stick to the comment and post example, once a new comment is added and related to a post, this comment will have a field, post_id for example, and this will be the default Laravel infers, where the idea of the related post will be stored. Now you can override this default assumption of Laravel, which basically is the model name snake_cased, and then suffixed with an id_id. You can override this, but I won't dive into that right now here. Another important assumption of Laravel is when you define many-to-many relationships, where you have a pivot table holding that connection, the pivot table name is also assumed by Laravel. And the name assumed by Laravel will be the snake_case version of the two combined model names, so comment and post for example, where the first model is, well, the one which is first if you order it alphanumerical. So, for comment and post since C comes before P in the alphabet, you would have comment_post as the name Laravel expects to find as a table in your database. And again, this can be overwritten, but I won't go into details about that right now here.
Back in the demo, it's time to set up those relations. And, as I said, this is done in the model files you will find in the app folder. I will start with the User model file, and in this model you see that it already has some content added to it, this is because Laravel shipped with this user model. Note, all the content you see here is absolutely fine for this application. Just to quickly refresh what these two fields do, the fillable property here defines which arguments or which fields of the database you can pass to the constructor of this model when creating a new instance of it, and the hidden field or property defines which fields won't be output if that model is retrieved. And this is very important if we later send back a user retrieved from the database, for example, we certainly don't want to send back the password, and therefore it's good to have that hidden, though with those two fields no relation is set up.
public function meetings(){
return $this->belongsToMany('App\Meeting');
}
So I will do this by adding a function, and I want to set up a relation to the meeting, I will call it meetings since one user might have multiple meetings, and what this will do is it will return the belongsToMany method here since it is a many-to-many relationship, so it's not hasMany because both models may have many connections, or may have many of the other model, so therefore belongsToMany is the right function here, or the right method, and then I pass the name of the method to which I want to create this relation. And of course here I will pass App\Meeting, the namespace to this meeting model.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Meeting extends Model
{
protected $fillable = ['time', 'title', 'description'];
public function users(){
return $this->belongsToMany('App\User');
}
}
In the meeting model, I will define the same. So, basically I can grab this function here, and copy it over. of course I exchange meeting with user. Also I want to add the fillable field here to, so protected fillable will be set up so that we can quickly create a new model by passing all the fields to the constructor. Also make sure to return meetings to users, of course. With that, both models are prepared, the relation is set up, and it's finally time to go to the final step, and not only set everything up, but work with it.
So now that it's time to actually use all those things set up and work with it, the question is how do you interact with data? So here's a quick refresher on this too.
Interact with data
Interacting with data is really simple with Laravel's Eloquent since you can use the model to directly interact with the database tables.
// Single entry
$post = new Post(data);
$post->save();
$post->delete();
For example, if you want to work with a single entry, create a new one for example, you would create a new instance of that model with a new keyword, pass the data with which you want to create this new post, provided the fillable field property is set up accordingly, and then you can simply save that to the database with the save method. On the same hand, if you want to delete that, you call the delete method to easily delete an entry in the database.
// Relation
$post = Post::find(1);
$comment = new Comment(data);
$post->comments()->save($comment);
If you're not working with a single entry, but instead with a relation, you might want to add a new comment to a post, for example. The first step is to find the post with which you want to work, and then the new comment is created, now instead of calling save on the comment, you call save on the relation between post and comment, which you simply create by executing this comments method set up in the post model here. Then, by calling save, the save function is kind of executed on the joint tables, and by passing the comment you want to save as an argument, Laravel automatically will add the post ID of the related post in the corresponding post ID field in the comment, which was created. So this quickly creates this relation in the database, without you writing any raw SQL code.
***Interact with Data (Many-to-many)
// Inserts record into intermediate table
$post = Post::find(1);
$user = new User(data);
$post->users()->attach($user);
$post->users()->detach();
When interacting with many-to-many relationships, it's kind of similar, but here now you're not calling the save method, but instead attach. Attaching makes sure that you're not only storing the relation, the foreign key in one database, but instead a new entry is created in the pivot table in the middle, so the table which holds the connections between two different models. So here again, you pass the user, for example, you might want to attach to a post as an argument, and if you want to detach, in this example here all the users from a post, you want something called the detach method. of course, you may also pass an argument to that method to only detach some users.
With that, it's time to finally add this functionality to our project, and I will do this in the controller, and I want to start with the AuthController
where new users are created. We're already getting all the data.
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
class AuthController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request){
$this->validate($request, [
'name' => 'required',
'email' => 'required|email',
'password' =>'required|min:5'
]);
$name = $request->input('name');
$email = $request->input('email');
$password = $request->input('password');
$user = new User([
'name' => $name,
'email' => $email,
'password' => bcrypt($password)
]);
if($user->save()){
$user->signin = [
'href' => 'api/v1/user/signin',
'method' => 'POST',
'params' => 'email, password'
];
$response =[
'msg' => 'User created',
'user' => $user
];
return response()->json($response,201);
}
$response =[
'msg' => 'An error occurred',
];
return response()->json($response, 404);
}
/**
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @throws \Illuminate\Validation\ValidationException
*/
public function signin(Request $request){
$this->validate($request, [
'email' => 'required|email',
'password' => 'required'
]);
$email = $request->input('email');
$password = $request->input('password');
$user = [
'email' => $email,
'password' => $password,
'signin' => [
'href' => 'api/v1/user/signin',
'method' => 'POST',
'params' => 'email, password'
]
];
$response =[
'msg' => 'Authenticated',
'user' => $user
];
return response()->json($response,200);
//return "It works";
}
}
Now it's really easy to create a new user and save it to the database. It's something done by creating a new user with the new keyword, and make sure to add the import to App\User to the namespace where the user model can be found. And then, since the fillable property is set, you may pass an array with the arguments you want to initialize the user with to that user constructor here. So the array I want to pass has the name field, and of course name was retrieved from the request, the email also retrieved, and the password. However, the password won't be stored directly, I want to encrypt it. So I can use the bcrypt helper method Laravel offers to easily hash this password, because storing unencrypted passwords isn't really a great practice. So here I may pass the password retrieved from the request. Now the user is set up, and next I will create an if block to check if the saving is successful. So I will run user-save here in the if statement, and this will return true if the saving was successful. And in this case, what I want to do is I want to respond with a success message. So I no longer need that user setup down here, but what I will need is I still want to pass that signin link, so I will add a property to my user object, and I can easily do that by accessing signin, which is the name of this property of this route I want to provide, and since this doesn't exist yet, it will be created for me. Now I will set that equal to the signin array created down here. With this, the user object will not only contain the fields it has by the model definition, but this new field I added here. Now that won't be written to the database, it's only there for me to output it with the response I send back to the user. With that I can get rid of this block here. I will copy the response here, and create this, of course referring to this user object still, but now it's the real object as saved to the database, and then I want to return this here. Now, below the if block, we'll get there if we have an error during saving this user, therefore, what I will do here is I will create a new response saying, An error occurred, and you might of course create a more elaborate error message here, and I will just send back this response with a 404 error code.
So let's immediately try it out. In Postman, here with the route where a new user is created.
GET http://192.168.0.5:8000/api/v1/user
{
"name" : "Adarsh",
"email" : "contact@softhinkers.com",
"password" : "test_pw"
}
I'm sending a body with the name, email, and password field, and as you can see I'm getting back this User created message with the signin route too, so this looks great.
{
"msg": "User created",
"user": {
"name": "Adarsh",
"email": "contact@softhinkers.com",
"updated_at": "2018-12-03 19:25:40",
"created_at": "2018-12-03 19:25:40",
"id": 1,
"signin": {
"href": "api/v1/user/signin",
"method": "POST",
"params": "email, password"
}
}
}
Also recognize that you're not seeing the password field here. This is because password was specified in the hidden property in the model file.
Now I won't set up the signin route yet, because this will be added in the last chapter when authentication comes into place. So now it's time to move on to the MeetingController
.
public function index()
{
$meeting = [
'title' => 'Title',
'description' => 'Description',
'time' => 'Time',
'user_id' => 'User Id',
'view_meeting' => [
'href' => 'api/v1/meeting/1',
'method' => 'GET'
]
];
Here I'll start with the top where all meetings are fetched, and since I'm returning an array of meetings here, I will delete this dummy meeting object here, instead create this meetings objects where I use the Meeting model as a facade to directly get all the meetings in the database.
public function index()
{
$meeting = Meeting::all();
And again if you're not familiar with this command, definitely check out the detailed documentation about how Laravel Eloquent works. This basically is an easy access to the database, which allows me to fetch all meetings stored in the database through that Meeting model here.
public function index()
{
$meetings = Meeting::all();
foreach($meetings as $meeting){
$meeting->view_meeting = [
'href' => 'api/v1/meeting/'. $meeting->id,
'method' =>'GET'
];
}
$response =[
'msg' => 'List of all Meetings',
'meetings' => [ $meetings
]
];
return response()->json($response, 200);
//return "It works";
}
Now I want to loop through all the meetings I got back, because I want to add the link to each meeting, this link to the individual meetings. I do this by simply copying the code I had before, but now I will just do this the same way I did it when creating a new user. I use the meeting of the current iteration, and add or access this view_meeting property, which of course doesn't exist, therefore it will be added by Laravel. I then set this equal to this array, which holds the information I want to pass, or this link I want to pass. And with that, each meeting in my array of meetings will get this link attached. Next, in the response I no longer need to add those individual meetings, but instead meetings will simply refer to the meetings object. Now in order to make this work, also add the import here at the top to the meeting namespace, however one important thing to change here in the index function is, of course, I'm no longer pointing to the dummy index of 1, but instead I'm adding the meeting ID. Now with that in place, I'm going to go ahead and add all the functionalities for all the other actions, but as in the last chapter, I will pause and come back to the interesting parts to make sure that nothing is left out here.
$meeting = new Meeting([
'time' => Carbon::createFromFormat('YmdHie', $time),
'title' => $title,
'description' => $description
]);
For example, here when creating a meeting, notice that I use Carbon, which is a built-in third-party package Laravel ships with, which makes working with dates really easy. And here the date format I introduced in the last chapter comes into play. I pass this expected format to Carbon to let it know how this should be transformed into a default time I can store in the database. And with that I make sure that I'm always storing the same data format in the database.
if($meeting->save()){
$meeting->users()->attach($user_id);
$meeting->view_meeting = [
'href' => 'api/v1/meeting' . $meeting->id,
'method' => 'GET'
];
$message =[
'msg' => 'Meeting created',
'meeting' => $meeting
];
return response()->json($message, 201);
}
Besides that, it's pretty similar to creating a new user, but notice that I'm also using the attach
method here with the users relation to add this entry in the pivot table, to add the connection between meetings and users.
public function show($id)
{
$meeting = Meeting::with('users')->where('id',$id)->firstOrFail();
$meeting->view_meeting =[
'href' => 'api/v1/meeting',
'method'=>'GET'
];
$response =[
'msg' => 'Meeting information',
'meeting' => $meeting
];
return response()->json($response,200);
//return "It works";
}
When getting a single meeting, I get all meetings with the users
, which basically means that Laravel should either load the user model data with that meeting, so not only the data contained in this meeting table, but also of the related user table since I want to output both meeting and user data, and then I use the firstOrFail
method to make sure that if no fitting meeting is found, Laravel will automatically send back a 404 error to the user.
So we can test this here, since I haven't created a meeting yet, if I get a single meeting with the ID of 1, I get back a 404 error telling me that no meeting was found.
GET http://192.168.0.5:8000/api/v1/meeting/1
{
"message": "No query results for model [App\\Meeting].",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 200,
"trace": [
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 176,
"function": "prepareException",
"class": "Illuminate\\Foundation\\Exceptions\\Handler",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/app/Exceptions/Handler.php",
"line": 49,
"function": "render",
"class": "Illuminate\\Foundation\\Exceptions\\Handler",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 83,
"function": "render",
"class": "App\\Exceptions\\Handler",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 32,
"function": "handleException",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php",
"line": 41,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Routing\\Middleware\\SubstituteBindings",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php",
"line": 58,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 104,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 684,
"function": "then",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 659,
"function": "runRouteWithinStack",
"class": "Illuminate\\Routing\\Router",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 625,
"function": "runRoute",
"class": "Illuminate\\Routing\\Router",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"line": 614,
"function": "dispatchToRoute",
"class": "Illuminate\\Routing\\Router",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 176,
"function": "dispatch",
"class": "Illuminate\\Routing\\Router",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 30,
"function": "Illuminate\\Foundation\\Http\\{closure}",
"class": "Illuminate\\Foundation\\Http\\Kernel",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/fideloper/proxy/src/TrustProxies.php",
"line": 57,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Fideloper\\Proxy\\TrustProxies",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
"line": 31,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
"line": 31,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php",
"line": 27,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php",
"line": 62,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 151,
"function": "handle",
"class": "Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
"line": 53,
"function": "Illuminate\\Pipeline\\{closure}",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"line": 104,
"function": "Illuminate\\Routing\\{closure}",
"class": "Illuminate\\Routing\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 151,
"function": "then",
"class": "Illuminate\\Pipeline\\Pipeline",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
"line": 116,
"function": "sendRequestThroughRouter",
"class": "Illuminate\\Foundation\\Http\\Kernel",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/public/index.php",
"line": 55,
"function": "handle",
"class": "Illuminate\\Foundation\\Http\\Kernel",
"type": "->"
},
{
"file": "/Users/adarshmaurya/Playground/play-by-play-php-laravel-restful-web-services-started/laravel-rest/server.php",
"line": 21,
"function": "require_once"
}
]
}
Now of course the fact that we're seeing detailed error messages here can be changed in the .env file, where the environment can be sent to production and debug messages can be turned off.
Now if I create a meeting, and I'm already passing the data I need here,
{
"time" : "201801301330IST",
"title" : "Test Meeting 2",
"description" : "Test",
"user_id": "1"
}
user_id should be let's say 1 since that is the ID of the user I created, a meeting was indeed created,
{
"msg": "Meeting created",
"meeting": {
"time": {
"date": "2018-01-30 13:30:00.000000",
"timezone_type": 2,
"timezone": "IST"
},
"title": "Test Meeting 2",
"description": "Test",
"updated_at": "2018-12-03 19:59:24",
"created_at": "2018-12-03 19:59:24",
"id": 1,
"view_meeting": {
"href": "api/v1/meeting1",
"method": "GET"
}
}
}
and if I now get this meeting data, and I need the ID 1 since I already tested this with a couple of meetings here in my production, you see that I get the meeting data, and no longer the 404 error.
{
"msg": "Meeting information",
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 19:59:24",
"time": "2018-01-30 13:30:00",
"title": "Test Meeting 2",
"description": "Test",
"view_meeting": {
"href": "api/v1/meeting",
"method": "GET"
},
"users": [
{
"id": 1,
"name": "Adarsh",
"email": "contact@softhinkers.com",
"email_verified_at": null,
"created_at": "2018-12-03 19:25:40",
"updated_at": "2018-12-03 19:25:40",
"pivot": {
"meeting_id": 1,
"user_id": 1
}
}
]
}
}
When updating a meeting I first fetch the meeting with the findOrFail method to return error if no meeting with the passed ID exists, and then I also check if the user ID passed to this action here, or with the request, is actually an ID registered for this meeting.
public function update(Request $request, $id)
{
$this->validate($request, [
'title' => 'required',
'description' => 'required',
'time' => 'required|date_format:YmdHie',
'user_id' => 'required'
]);
$title = $request->input('title');
$description = $request->input('description');
$time= $request->input('time');
$user_id = $request->input('user_id');
$meeting = [
'title' => $title,
'description' => $description,
'time' => $time,
'user_id' => $user_id,
'view_meeting' => [
'href' => 'api/v1/meeting/1',
'method' => 'GET'
]
];
$meeting = Meeting::with('users')->findOrFail($id);
if(!$meeting->users()->where('users.id', $user_id)->first()){
return response()->json([
'msg' => 'user not registered for meeting, update not successful'
], 401);
};
$meeting->time = Carbon::createFromFormat('YmdHie', $time);
$meeting->title = $title;
$meeting->description = $description;
if(!$meeting->update()){
return response()->json([
'msg' => 'Error during updating'
], 401);
}
$meeting->view_meeting =[
'href' => 'api/v1/meeting/' . $meeting->id,
'method' => 'GET'
];
$response =[
'msg' => 'Meeting updated',
'meeting' => $meeting
];
return response()->json($response,200);
//return "It works";
}
This is just an optional check I have here to make sure that only registered users are able to edit or update this meeting. If this is not the case, an error message is sent back. Next, I do pretty much the same as when creating a meeting, just storing the data, however on that fetch meeting object from the database, and therefore I'm not calling save, but update to update the data in the database. If it fails I'm returning error, otherwise I'm attaching the link, and finally I return a success response.
PATCH http://192.168.0.5:8000/api/v1/meeting/1
{
"time": "201812040146UTC",
"timezone_code": "UTC",
"title": "TitleX",
"description": "DescriptionY",
"user_id": "5"
}
If I try this, and I think it's more interesting to try this out with, let's say a meeting ID which exists, but a user ID which doesn't exist, so if I add user_id 5 for example, and 1, we get back the error that we're not able to do this because we're not registered.
{
"msg": "user not registered for meeting, update not successful"
}
{
"time": "201812040146UTC",
"timezone_code": "UTC",
"title": "TitleX",
"description": "DescriptionY",
"user_id": "1"
}
PATCH http://192.168.0.5:8000/api/v1/meeting/1
And if I use user_id 1, which is the user in my database, I get back that the meeting was updated successfully.
{
"msg": "Meeting updated",
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 20:19:39",
"time": {
"date": "2018-12-04 01:46:00.000000",
"timezone_type": 3,
"timezone": "UTC"
},
"title": "TitleX",
"description": "DescriptionY",
"view_meeting": {
"href": "api/v1/meeting/1",
"method": "GET"
},
"users": [
{
"id": 1,
"name": "Adarsh",
"email": "contact@softhinkers.com",
"email_verified_at": null,
"created_at": "2018-12-03 19:25:40",
"updated_at": "2018-12-03 19:25:40",
"pivot": {
"meeting_id": 1,
"user_id": 1
}
}
]
}
}
Now when deleting a meeting, I first also find the meeting, or fail if it doesn't exist, then I fetch all the users attached to the meeting, and I do this because, next step, I detach all users, and then I try to delete it.
public function destroy($id)
{
$meeting = Meeting::findOrFail($id);
$users = $meeting->users;
$meeting->users()->detach();
if(!$meeting->delete()){
foreach($users as $user){
$meeting->users()->attach($user);
}
return response()->json(['msg' => 'deletion failed'], 404);
}
$response =[
'msg' => 'Meeting deleted',
'create' => [
'href' => 'api/v1/meeting',
'method' => 'POST',
'params' => 'title, description, time'
]
];
return response()->json($response, 200);
//return "It works";
}
}
Now if deleting fails, I will loop through all the users I fetched and reattach them to the meeting, just to make sure that I'm not losing that data in case I am not able to delete the meeting. Then I'm either returning an error response, or the response that the meeting was successfully deleted.
Now you might wonder if it does make since to also check if a user is registered for a meeting before deleting it so that only registered users are able to delete, yes, but this is something I will add once authentication is added since this will give me a clever and easy way to retrieve the user trying to do that.
Now in the RegistrationController
when creating a new registration, I fetch meeting and user with the findOrFail methods, then I create a dummy message, which I will output as a default saying that the user is already registered for a meeting, and this message is sent if this check here is successful.
public function store(Request $request)
{
$this->validate($request, [
'meeting_id' => 'required',
'user_id' => 'required'
]);
$meeting_id = $request->input('meeting_id');
$user_id = $request->input('meeting_id');
$meeting = Meeting::findOrFail($meeting_id);
$user = User::findOrFail($user_id);
//dummy message
$message = [
'msg' => 'User is already registered for meeting',
'user' => $user,
'meeting' => $meeting,
'unregister' => [
'href' => 'api/v1/meeting/registration/' . $meeting->id,
'method' => 'DELETE'
]
];
if($meeting->users()->where('users.id', $user->id)->first()){
return response()->json($message, 404);
}
$user->meetings()->attach($meeting);
$response =[
'msg' => 'List of all Meetings',
'meeting' => $meeting,
'user' => $user,
'unregister' => [
'href' => 'api/v1/meeting/registration/1',
'method' => 'DELETE'
]
];
return response()->json($response, 200);
// return "It works";
}
Here I just check if a user with that ID already exists and/or registered for this meeting, or connected to the meeting I fetched here. If this is not the case, so the user is not already registered for a meeting, then I attach the user to the meeting with the attach method so that this entry in the pivot table is created, and response is sent back. Also notice of course that the unregistered link has the meeting id of the meeting used here attached to it, or added to it at the end.
{
"msg": "User is already registered for meeting",
"user": {
"id": 1,
"name": "Adarsh",
"email": "contact@softhinkers.com",
"email_verified_at": null,
"created_at": "2018-12-03 19:25:40",
"updated_at": "2018-12-03 19:25:40"
},
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 20:19:39",
"time": "2018-12-04 01:46:00",
"title": "TitleX",
"description": "DescriptionY"
},
"unregister": {
"href": "api/v1/meeting/registration/1",
"method": "DELETE"
}
}
If I try this out, first with the meeting or the user who created this meeting, I should get an error telling me that this user is already registered for the meeting, and this is the case. Also I get the meeting and the user data, as well as the unregistration link.
{
"user_id": "2",
"meeting_id":"1"
}
And if I use another user, and I created one behind the scenes here, if I create a user who's not registered,
{
"msg": "User registered for meeting",
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 20:19:39",
"time": "2018-12-04 01:46:00",
"title": "TitleX",
"description": "DescriptionY"
},
"user": {
"id": 2,
"name": "Test2",
"email": "contact2@softhinkers.com",
"email_verified_at": null,
"created_at": "2018-12-03 20:49:14",
"updated_at": "2018-12-03 20:49:14"
},
"unregister": {
"href": "api/v1/meeting/registration/1",
"method": "DELETE"
}
}
I will get back a message telling me that the user was successfully registered for a meeting. of course, also with the detailed message about user and meeting.
Now when unregistering a user, this will get a little bit bigger once we add authentication to this application, which allows us to fetch the user trying to unregister. For now, I will just detach all users, and I simply do this by using the detach method on the, well, users connection, or the users relation on this meeting.
public function destroy($id)
{
$meeting = Meeting::findOrFail($id);
$meeting->users()->detach();
$response =[
'msg' => 'User unregister for meeting',
'meeting' => $meeting,
'user' => 'tbd',
'unregister' => [
'href' => 'api/v1/meeting/registration',
'method' => 'POST',
'params' => 'user_id, meeting_id'
]
];
return response()->json($response, 200);
//return "It works";
}
If I try this out, and I registered two users if you remember that, if I unregister from meeting ID 1 in my case here, I get the message that users were unregistered, and also data about the meeting, and of course user here is to be done since this will be coming in the last chapter of this tutorial.
{
"msg": "User unregister for meeting",
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 20:19:39",
"time": "2018-12-04 01:46:00",
"title": "TitleX",
"description": "DescriptionY"
},
"user": "tbd",
"unregister": {
"href": "api/v1/meeting/registration",
"method": "POST",
"params": "user_id, meeting_id"
}
}
If I now get information about that meeting, you see that it has no users attached to it.
GET http://192.168.0.5:8000/api/v1/meeting/1
{
"msg": "Meeting information",
"meeting": {
"id": 1,
"created_at": "2018-12-03 19:59:24",
"updated_at": "2018-12-03 20:19:39",
"time": "2018-12-04 01:46:00",
"title": "TitleX",
"description": "DescriptionY",
"view_meeting": {
"href": "api/v1/meeting",
"method": "GET"
},
"users": []
}
}
Now, of course as I said, this will change in the last chapter where only one user will be unregistered, but for now that's great, and all the functionality is now implemented into this application. You are able to create meetings, to update them, to sign up users, and the only things that are missing are the authentication and some user related things, which will come in the last chapter of this tutorial.
So again, a lot has been covered in this chapter. You learned :
-
How to utilize migrations to quickly set up database tables, and why this is so helpful and important when creating RESTful services.
-
You took advantage of Eloquent to model relations and to interact with data, and that of course is a key part of probably any RESTful application you're going to create.
In the next chapter, I will conclude this application, or this guide on creating RESTful applications, with an introduction to authentication in RESTful applications, and how to implement such authentication in a Laravel app.