Creating a REST API with Laravel

In this section, we will build a REST API with Laravel from scratch. This REST API will allow you to manage different clients at your bookstore, not only via the browser, but via the UI as well. You will be able to perform pretty much the same actions as before, that is, listing books, buying them, borrowing for free, and so on.

Once the REST API is done, you should remove all the business logic from the bookstore that you built during the previous chapters. The reason is that you should have one unique place where you can actually manipulate your databases and the REST API, and the rest of the applications, like the web one, should able to communicate with the REST API for managing data. In doing so, you will be able to create other applications for different platforms, like mobile apps, that will use the REST API too, and both the website and the mobile app will always be synchronized, since they will be using the same sources.

As with our previous Laravel example, in order to create a new project, you just need to run the following command:

$ laravel new bookstore_api

Setting OAuth2 authentication

The first thing that we are going to implement is the authentication layer. We will use OAuth2 in order to make our application more secure than basic authentication. Laravel does not provide support for OAuth2 out of the box, but there is a service provider which does that for us.

Installing OAuth2Server

To install OAuth2, add it as a dependency to your project using Composer:

$ composer require "lucadegasperi/oauth2-server-laravel:5.1.*"

This service provider needs quite a few changes. We will go through them without going into too much detail on how things work exactly. If you are more interested in the topic, or if you want to create your own service providers for Laravel, we recommend you to go though the extensive official documentation.

To start with, we need to add the new OAuth2Server service provider to the array of providers in the config/app.php file. Add the following lines at the end of the providers array:

/*
 * OAuth2 Server Service Providers...
 */
        LucaDegasperiOAuth2ServerStorageFluentStorageServiceProvider::class,       LucaDegasperiOAuth2ServerOAuth2ServerServiceProvider::class,

In the same way, you need to add a new alias to the aliases array in the same file:

'Authorizer' => LucaDegasperiOAuth2ServerFacadesAuthorizer::class,

Let's move to the app/Http/Kernel.php file, where we need to make some changes too. Add the following entry to the $middleware array property of the Kernel class:

LucaDegasperiOAuth2ServerMiddlewareOAuthExceptionHandlerMiddleware::class,

Add the following key-value pairs to the $routeMiddleware array property of the same class:

'oauth' => LucaDegasperiOAuth2ServerMiddlewareOAuthMiddleware::class,
'oauth-user' => LucaDegasperiOAuth2ServerMiddlewareOAuthUserOwnerMiddleware::class,
'oauth-client' => LucaDegasperiOAuth2ServerMiddlewareOAuthClientOwnerMiddleware::class,
'check-authorization-params' => LucaDegasperiOAuth2ServerMiddlewareCheckAuthCodeRequestMiddleware::class,
'csrf' => AppHttpMiddlewareVerifyCsrfToken::class,

We added a CSRF token verifier to the $routeMiddleware, so we need to remove the one already defined in $middlewareGroups, since they are incompatible. Use the following line to do so:

AppHttpMiddlewareVerifyCsrfToken::class,

Setting up the database

Let's set up the database now. In this section, we will assume that you already have the bookstore database in your environment. If you do not have it, go back to Chapter 5, Using Databases, to create it in order to proceed with this setup.

The first thing to do is to update the database credentials in the .env file. They should look something similar to the following lines, but with your username and password:

DB_HOST=localhost
DB_DATABASE=bookstore
DB_USERNAME=root
DB_PASSWORD=

In order to prepare the configuration and database migration files from the OAuth2Server service provider, we need to publish it. In Laravel, you do it by executing the following command:

$ php artisan vendor:publish

Now the database/migrations directory contains all the necessary migration files that will create the necessary tables related to OAuth2 in our database. To execute them, we run the following command:

$ php artisan migrate

We need to add at least one client to the oauth_clients table, which is the table that stores the key and secrets for all clients that want to connect to our REST API. This new client will be the one that you will use during the development process in order to test what you have done. We can set a random ID—the key—and the secret as follows:

mysql> INSERT INTO oauth_clients(id, secret, name)
    -> VALUES('iTh4Mzl0EAPn90sK4EhAmVEXS',
    -> 'PfoWM9yq4Bh6rGbzzJhr8oDDsNZwGlsMIAeVRaPM',
    -> 'Toni');
Query OK, 1 row affected, 1 warning (0.00 sec)

Enabling client-credentials authentication

Since we published the plugins in vendor in the previous step, now we have the configuration files for the OAuth2Server. This plugin allows us different authentication systems (all of them with OAuth2), depending on our necessities. The one that we are interested in for our project is the client_credentials type. To let Laravel know, add the following lines at the end of the array in the config/oauth2.php file:

'grant_types' => [
     'client_credentials' => [
        'class' => 
            'LeagueOAuth2ServerGrantClientCredentialsGrant',
        'access_token_ttl' => 3600
    ]
]

These preceding lines grant access to the client_credentials type, which are managed by the ClientCredentialsGrant class. The access_token_ttl value refers to the time period of the access token, that is, for how long someone can use it. In this case, it is set to 1 hour, that is, 3,600 seconds.

Finally, we need to enable a route so we can post our credentials in exchange for an access token. Add the following route to the routes file in app/Http/routes.php:

Route::post('oauth/access_token', function() {
    return Response::json(Authorizer::issueAccessToken());
});

Requesting an access token

It is time to test what we have done so far. To do so, we need to send a POST request to the /oauth/access_token endpoint that we enabled just now. This request needs the following POST parameters:

  • client_id with the key from the database
  • client_secret with the secret from the database
  • grant_type to specify the type of authentication that we are trying to perform, in this case client_credentials

The request issued using the Advanced REST Client add-on from Chrome looks as follows:

Requesting an access token

The response that you should get should have the same format as this one:

{
    "access_token": "MPCovQda354d10zzUXpZVOFzqe491E7ZHQAhSAax"
    "token_type": "Bearer"
    "expires_in": 3600
}

Note that this is a different way of requesting for an access token than what the Twitter API does, but the idea is still the same: given a key and a secret, the provider gives us an access token that will allow us to use the API for some time.

Preparing the database

Even though we've already done the same in the previous chapter, you might think: "Why do we start by preparing the database?". We could argue that you first need to know the kind of endpoints you want to expose in your REST API, and only then you can start thinking about what your database should look like. But you could also think that, since we are working with an API, each endpoint should manage one resource, so first you need to define the resources you are dealing with. This code first versus database/model first is an ongoing war on the Internet. But whichever way you think is better, the fact is that we already know what the users will need to do with our REST API, since we already built the UI previously; so it does not really matter.

We need to create four tables: books, sales, sales_books, and borrowed_books. Remember that Laravel already provides a users table, which we can use as our customers. Run the following four commands to create the migrations files:

$ php artisan make:migration create_books_table --create=books
$ php artisan make:migration create_sales_table --create=sales
$ php artisan make:migration create_borrowed_books_table 
--create=borrowed_books
$ php artisan make:migration create_sales_books_table 
--create=sales_books

Now we have to go file by file to define what each table should look like. We will try to replicate the data structure from Chapter 5, Using Databases, as much as possible. Remember that the migration files can be found inside the database/migrations directory. The first file that we can edit is the create_books_table.php. Replace the existing empty up method by the following one:

public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->increments('id');
        $table->string('isbn')->unique();
        $table->string('title');
        $table->string('author');
        $table->smallInteger('stock')->unsigned();
        $table->float('price')->unsigned();
    });
}

The next one in the list is create_sales_table.php. Remember that this one has a foreign key pointing to the users table. You can use references(field)->on(tablename) to define this constraint.

public function up()
{
    Schema::create('sales', function (Blueprint $table) {
        $table->increments('id');
        $table->string('user_id')->references('id')->on('users');
        $table->timestamps();
    });
}

The create_sales_books_table.php file contains two foreign keys: one pointing to the ID of the sale, and one to the ID of the book. Replace the existing up method by the following one:

public function up()
{
    Schema::create('sales_books', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('sale_id')->references('id')->on('sales');
        $table->integer('book_id')->references('id')->on('books');
        $table->smallInteger('amount')->unsigned();
    });
}

Finally, edit the create_borrowed_books_table.php file, which has the book_id foreign key and the start and end timestamps:

public function up()
{
    Schema::create('borrowed_books', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('book_id')->references('id')->on('books');
        $table->string('user_id')->references('id')->on('users');
        $table->timestamp('start');
        $table->timestamp('end');
    });
}

The migration files are ready so we just need to migrate them in order to create the database tables. Run the following command:

$ php artisan migrate

Also, add some books to the database manually so that you can test later. For example:

mysql> INSERT INTO books (isbn,title,author,stock,price) VALUES
    -> ("9780882339726","1984","George Orwell",12,7.50),
    -> ("9789724621081","1Q84","Haruki Murakami",9,9.75),
    -> ("9780736692427","Animal Farm","George Orwell",8,3.50),
    -> ("9780307350169","Dracula","Bram Stoker",30,10.15),
    -> ("9780753179246","19 minutes","Jodi Picoult",0,10);
Query OK, 5 rows affected (0.01 sec)
Records: 5  Duplicates: 0  Warnings: 0

Setting up the models

The next thing to do on the list is to add the relationships that our data has, that is, to translate the foreign keys from the database to the models. First of all, we need to create those models, and for that we just run the following commands:

$ php artisan make:model Book
$ php artisan make:model Sale
$ php artisan make:model BorrowedBook
$ php artisan make:model SalesBook

Now we have to go model by model, and add the one to one and one to many relationships as we did in the previous chapter. For BookModel, we will only specify that the model does not have timestamps, since they come by default. To do so, add the following highlighted line to your app/Book.php file:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Book extends Model
{
    public $timestamps = false;
}

For the BorrowedBook model, we need to specify that it has one book, and it belongs to a user. We also need to specify the fields we will fill once we need to create the object—in this case, book_id and start. Add the following two methods in app/BorrowedBook.php:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class BorrowedBook extends Model
{
    protected $fillable = ['user_id', 'book_id', 'start'];
    public $timestamps = false;

    public function user() {
        return $this->belongsTo('AppUser');
    }

    public function book() {
        return $this->hasOne('AppBook');
    }
}

Sales can have many "sale books" (we know it might sound a little awkward), and they also belong to just one user. Add the following to your app/Sale.php:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Sale extends Model
{
    protected $fillable = ['user_id'];

    public function books() {
        return $this->hasMany('AppSalesBook');
    }

    public function user() {
        return $this->belongsTo('AppUser');
    }
}

Like borrowed books, sale books can have one book and belong to one sale instead of to one user. The following lines should be added to app/SalesBook.php:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class SaleBook extends Model
{
    public $timestamps = false;
    protected $fillable = ['book_id', 'sale_id', 'amount'];

    public function sale() {
        return $this->belongsTo('AppSale');
    }

    public function books() {
        return $this->hasOne('AppBook');
    }
}

Finally, the last model that we need to update is the User model. We need to add the opposite relationship to the belongs we used earlier in Sale and BorrowedBook. Add these two functions, and leave the rest of the class intact:

<?php

namespace App;

use IlluminateFoundationAuthUser as Authenticatable;

class User extends Authenticatable
{
    //...

    public function sales() {
        return $this->hasMany('AppSale');
    }

    public function borrowedBooks() {
        return $this->hasMany('AppBorrowedBook');
    }
}

Designing endpoints

In this section, we need to come up with the list of endpoints that we want to expose to the REST API clients. Keep in mind the "rules" explained in the Best practices with REST APIs section. In short, keep the following rules in mind:

  • One endpoint interacts with one resource
  • A possible schema could be <API version>/<resource name>/<optional id>/<optional action>
  • Use GET parameters for filtering and pagination

So what will the user need to do? We already have a good idea about that, since we created the UI. A brief summary would be as follows:

  • List all the available books with some filtering (by title and author), and paginated when necessary. Also retrieve the information on a specific book, given the ID.
  • Allow the user to borrow a specific book if available. In the same way, the user should be able to return books, and list the history of borrowed books too (filtered by date and paginated).
  • Allow the user to buy a list of books. This could be improved, but for now let's force the user to buy books with just one request, including the full list of books in the body. Also, list the sales of the user following the same rules as that with borrowed books.

We will start straightaway with our list of endpoints, specifying the path, the HTTP method, and the optional parameters. It will also give you an idea on how to document your REST APIs.

  • GET /books
    • title: Optional and filters by title
    • author: Optional and filters by author
    • page: Optional, default is 1, and specifies the page to return
    • page-size: Optional, default is 50, and specifies the page size to return
  • GET /books/<book id>
  • POST /borrowed-books
    • book-id: Mandatory and specifies the ID of the book to borrow
  • GET /borrowed-books
    • from: Optional and returns borrowed books from the specified date
    • page: Optional, default is 1, and specifies the page to return
    • page-size: Optional, default is 50, and specifies the number of borrowed books per page
  • PUT /borrowed-books/<borrowed book id>/return
  • POST /sales
    • books: Mandatory and it is an array listing the book IDs to buy and their amounts, that is, {"book-id-1": amount, "book-id-2": amount, ...}
  • GET /sales
    • from: Optional and returns borrowed books from the specified date
    • page: Optional, default is 1, and specifies the page to return
    • page-size: Optional, default is 50, and specifies the number of sales per page
  • GET /sales/<sales id>

We use POST requests when creating sales and borrowed books, since we do not know the ID of the resource that we want to create a priori, and posting the same request will create multiple resources. On the other hand, when returning a book, we do know the ID of the borrowed book, and sending the same request multiple times will leave the database in the same state. Let's translate these endpoints to routes in app/Http/routes.php:

/*
 * Books endpoints.
 */
Route::get('books', ['middleware' => 'oauth',
    'uses' => 'BookController@getAll']);
Route::get('books/{id}', ['middleware' => 'oauth',
    'uses' => 'BookController@get']);
/*
 * Borrowed books endpoints.
 */
Route::post('borrowed-books', ['middleware' => 'oauth',
    'uses' => 'BorrowedBookController@borrow']);
Route::get('borrowed-books', ['middleware' => 'oauth',
    'uses' => 'BorrowedBookController@get']);
Route::put('borrowed-books/{id}/return', ['middleware' => 'oauth',
    'uses' => 'BorrowedBookController@returnBook']);
/*
 * Sales endpoints.
 */
Route::post('sales', ['middleware' => 'oauth',
    'uses' => 'SalesController@buy]);
Route::get('sales', ['middleware' => 'oauth',
    'uses' => 'SalesController@getAll']);
Route::get('sales/{id}', ['middleware' => 'oauth',
    'uses' => 'SalesController@get']);

In the preceding code, note how we added the middleware oauth to all the endpoints. This will require the user to provide a valid access token in order to access them.

Adding the controllers

From the previous section, you can imagine that we need to create three controllers: BookController, BorrowedBookController, and SalesController. Let's start with the easiest one: returning the information of a book given the ID. Create the file app/Http/Controllers/BookController.php, and add the following code:

<?php

namespace AppHttpControllers;

use AppBook;
use IlluminateHttpJsonResponse;
use IlluminateHttpResponse;

class BookController extends Controller {

    public function get(string $id): JsonResponse {
        $book = Book::find($id);
    
        if (empty($book)) {
            return new JsonResponse (
                null,
                JsonResponse::HTTP_NOT_FOUND
            );
        }
    
        return response()->json(['book' => $book]);
    }
}

Even though this preceding example is quite easy, it contains most of what we will need for the rest of the endpoints. We try to fetch a book given the ID from the URL, and when not found, we reply with a 404 (not found) empty response—the constant Response::HTTP_NOT_FOUND is 404. In case we have the book, we return it as JSON with response->json(). Note how we add the seemingly unnecessary key book; it is true that we do not return anything else and, since we ask for the book, the user will know what we are talking about, but as it does not really hurt, it is good to be as explicit as possible.

Let's test it! You already know how to get an access token—check the Requesting an access token section. So get one, and try to access the following URLs:

  • http://localhost/books/0?access_token=12345
  • http://localhost/books/1?access_token=12345

Assuming that 12345 is your access token, that you have a book in the database with ID 1, and you do not have a book with ID 0, the first URL should return a 404 response, and the second one, a response something similar to the following:

{
    "book": {
        "id": 1
        "isbn": "9780882339726"
        "title": "1984"
        "author": "George Orwell"
        "stock": 12
        "price": 7.5
    }
}

Let's now add the method to get all the books with filters and pagination. It looks quite verbose, but the logic that we use is quite simple:

public function getAll(Request $request): JsonResponse {
    $title = $request->get('title', '');
    $author = $request->get('author', '');
    $page = $request->get('page', 1);
    $pageSize = $request->get('page-size', 50);

    $books = Book::where('title', 'like', "%$title%")
        ->where('author', 'like', "%$author%")
        ->take($pageSize)
        ->skip(($page - 1) * $pageSize)
        ->get();

    return response()->json(['books' => $books]);
}

We get all the parameters that can come from the request, and set the default values of each one in case the user does not include them (since they are optional). Then, we use the Eloquent ORM to filter by title and author using where(), and limiting the results with take()->skip(). We return the JSON in the same way we did with the previous method. In this one though, we do not need any extra check; if the query does not return any book, it is not really a problem.

You can now play with your REST API, sending different requests with different filters. The following are some examples:

  • http://localhost/books?access_token=12345
  • http://localhost/books?access_token=12345&title=19&page-size=1
  • http://localhost/books?access_token=12345&page=2

The next controller in the list is BorrowedBookController. We need to add three methods: borrow, get, and returnBook. As you already know how to work with requests, responses, status codes, and the Eloquent ORM, we will write the entire class straightaway:

<?php

namespace AppHttpControllers;

use AppBook;
use AppBorrowedBook;
use IlluminateHttpJsonResponse;
use IlluminateHttpRequest;
use LucaDegasperiOAuth2ServerFacadesAuthorizer;

class BorrowedBookController extends Controller {

    public function get(): JsonResponse {
        $borrowedBooks = BorrowedBook::where(
            'user_id', '=', Authorizer::getResourceOwnerId()
        )->get();

        return response()->json(
            ['borrowed-books' => $borrowedBooks]
        );
    }

    public function borrow(Request $request): JsonResponse {
        $id = $request->get('book-id');

        if (empty($id)) {
            return new JsonResponse(
                ['error' => 'Expecting book-id parameter.'],
                JsonResponse::HTTP_BAD_REQUEST
            );
        }

        $book = Book::find($id);

        if (empty($book)) {
            return new JsonResponse(
                ['error' => 'Book not found.'],
                JsonResponse::HTTP_BAD_REQUEST
            );
        } else if ($book->stock < 1) {
            return new JsonResponse(
                ['error' => 'Not enough stock.'],
                JsonResponse::HTTP_BAD_REQUEST
            );
        }

        $book->stock--;
        $book->save();

        $borrowedBook = BorrowedBook::create(
            [
                'book_id' => $book->id,
                'start' => date('Y-m-d H:i:s'),
                'user_id' => Authorizer::getResourceOwnerId()
            ]
        );

        return response()->json(['borrowed-book' => $borrowedBook]);
    }

    public function returnBook(string $id): JsonResponse {
        $borrowedBook = BorrowedBook::find($id);

        if (empty($borrowedBook)) {
            return new JsonResponse(
                ['error' => 'Borrowed book not found.'],
                JsonResponse::HTTP_BAD_REQUEST
            );
        }

        $book = Book::find($borrowedBook->book_id);
        $book->stock++;
        $book->save();

        $borrowedBook->end = date('Y-m-d H:m:s');
        $borrowedBook->save();

        return response()->json(['borrowed-book' => $borrowedBook]);
    }
}

The only thing to note in the preceding code is how we also update the stock of the book by increasing or decreasing the stock, and invoke the save method to save the changes in the database. We also return the borrowed book object as the response when borrowing a book so that the user can know the borrowed book ID, and use it when querying or returning the book.

You can test how this set of endpoints works with the following use cases:

  • Borrow a book. Check that you get a valid response.
  • Get the list of borrowed books. The one that you just created should be there with a valid starting date and an empty end date.
  • Get the information of the book you borrowed. The stock should be one less.
  • Return the book. Fetch the list of borrowed books to check the end date and the returned book to check the stock.

Of course, you can always try to trick the API and ask for books without stock, non-existing borrowed books, and the like. All these edge cases should respond with the correct status codes and error messages.

We finish this section, and the REST API, by creating the SalesController. This controller is the one that contains more logic, since creating a sale implies adding entries to the sales books table, prior to checking for enough stock for each one. Add the following code to app/Html/SalesController.php:

<?php

namespace AppHttpControllers;

use AppBook;
use AppSale;
use AppSalesBook;
use IlluminateHttpJsonResponse;
use IlluminateHttpRequest;
use LucaDegasperiOAuth2ServerFacadesAuthorizer;

class SalesController extends Controller {

    public function get(string $id): JsonResponse {
        $sale = Sale::find($id);

        if (empty($sale)) {
            return new JsonResponse(
                null,
                JsonResponse::HTTP_NOT_FOUND
            );
        }

        $sale->books = $sale->books()->getResults();
        return response()->json(['sale' => $sale]);
    }

    public function buy(Request $request): JsonResponse {
        $books = json_decode($request->get('books'), true);

        if (empty($books) || !is_array($books)) {
            return new JsonResponse(
                ['error' => 'Books array is malformed.'],
                JsonResponse::HTTP_BAD_REQUEST
            );
        }

        $saleBooks = [];
        $bookObjects = [];
        foreach ($books as $bookId => $amount) {
            $book = Book::find($bookId);
            if (empty($book) || $book->stock < $amount) {
                return new JsonResponse(
                    ['error' => "Book $bookId not valid."],
                    JsonResponse::HTTP_BAD_REQUEST
                );
            }

            $bookObjects[] = $book;
            $saleBooks[] = [
                'book_id' => $bookId,
                'amount' => $amount
            ];
        }

        $sale = Sale::create(
            ['user_id' => Authorizer::getResourceOwnerId()]
        );
        foreach ($bookObjects as $key => $book) {
            $book->stock -= $saleBooks[$key]['amount'];

            $saleBooks[$key]['sale_id'] = $sale->id;
            SalesBook::create($saleBooks[$key]);
        }

        $sale->books = $sale->books()->getResults();
        return response()->json(['sale' => $sale]);
    }

    public function getAll(Request $request): JsonResponse {
        $page = $request->get('page', 1);
        $pageSize = $request->get('page-size', 50);

        $sales = Sale::where(
                'user_id', '=', Authorizer::getResourceOwnerId()
             )
            ->take($pageSize)
            ->skip(($page - 1) * $pageSize)
            ->get();

        foreach ($sales as $sale) {
            $sale->books = $sale->books()->getResults();
        }

        return response()->json(['sales' => $sales]);
    }
}

In the preceding code, note how we first check the availability of all the books before creating the sales entry. This way, we make sure that we do not leave any unfinished sale in the database when returning an error to the user. You could change this, and use transactions instead, and if a book is not valid, just roll back the transaction.

In order to test this, we can follow similar steps as we did with borrowed books. Just remember that the books parameter, when posting a sale, is a JSON map; for example, {"1": 2, "4": 1} means that I am trying to buy two books with ID 1 and one book with ID 4.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.183.10