Build a RESTful API using PHP and Yii2

April 27, 2021
Written by
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Twilion

Build a RESTful API using PHP and Yii2

Since its introduction, RESTful architecture has redefined the way we think about (and build) software applications by breaking down complex application ecosystems into smaller, more focused applications communicating with each other via RESTful calls.

Client-Server Architecture allows web clients and mobile apps to communicate with the same infrastructure (such as a server-side API) to provide a seamless user experience.

In this tutorial, I will show you how to build a RESTful API using the Yii framework (version 2)- a high-performance, component-based PHP framework.

Prerequisites

A basic understanding of the Yii framework and PHP will be of help in this tutorial. However, I will provide brief explanations and links to the applicable parts of the official documentation throughout the tutorial. If you’re unclear on any concept, you can review the linked material before continuing.

Additionally, you need to have the following installed on your system:

  • PHP version 7 or higher with the PDO extension enabled.
  • Composer globally installed
  • A local database server. While MySQL will be used in this tutorial, you are free to select your preferred database vendor.
  • Postman, or a similar application, to test endpoints. You can also usecURL to test your endpoints.

What we’ll build

In this tutorial, we'll build an API for a library app. This application will handle two major resources, Members and Books. The application will allow basic CRUD operations for both resources. Additionally, members will be able to borrow books. When a book has been borrowed, it won't be possible for another user to borrow the same book.

Getting started

Creating the application

To get started, create a new application called library-api and switch to the project directory using the following commands.

composer create-project --prefer-dist yiisoft/yii2-app-basic library-api
cd library-api

Verify that the application was created successfully using the following command.

php yii serve

By default, the application will be served onhttp://localhost:8080/. Navigate to the URL and confirm that it shows the welcome page, which you can see below.

The default Yii2 home page

Return to the terminal and press Control + C to quit the server.

Setting up the database

Using your preferred database management application, create a new database called library-api. Next, update the database connection parameters to link your application with the newly created database. To do this, open config/db.php and update the returned array to match the one shown below, carefully replacing the placeholders with the username and password for your database.

return [
    'class' => 'yii\\db\\Connection',
    'dsn' => 'mysql:host=localhost;dbname=library_api',
    'username' => '<USERNAME>'
    'password' => '<PASSWORD>',
    'charset' => 'utf8',
];

Create migrations

Next, create the migrations for the database tables. To put the migrations in context, the application will have three entities, namely:

  1. Member: This entity will represent a member of the library. For this article, the member table will contain the name of the members along with the start date of the membership.
  2. Book: This entity will represent a book available in the library. For this article, the book table will contain the name of the book, the author, year of release, and whether the book is available on loan or not.
  3. Loan: This entity will manage the data related to the process of "borrowing" a book. For this article, it will contain the book being borrowed, the member who borrowed it, the date it was borrowed, and the expected return date.

To create a migration, use the yii migrate/create command providing the name of the migration to be created. When asked for confirmation, type yes for the migration to be created. For this article, use the commands below to create the needed migrations.

php yii migrate/create create_member_table
php yii migrate/create create_book_table
php yii migrate/create create_loan_table

By default, migration files are located in the migrations directory. Migration file names are prefixed with the letter "m" and the UTC datetime of its creation. For example m210408_150000_create_member_table.php.

Open migrations/m<YYMMDD_HHMMSS>_create_member_table.php and modify the safeUp function to match the following code.

public function safeUp()
    {
        $this->createTable('member', [
            'id' => $this->primaryKey(),
            'name' => $this->string()->notNull(),
            'started_on' => $this->dateTime()->defaultValue(date('Y-m-d H:i:s'))
        ]);
    }

Edit the safeUp function for migrations/m<YYMMDD_HHMMSS>_create_book_table.php to match the following code.

public function safeUp()
    {
        $this->createTable('book', [
            'id' => $this->primaryKey(),
            'name' => $this->string(),
            'author' => $this->string(),
            'release_year' => $this->smallInteger(),
            'is_available_for_loan' => $this->boolean()->defaultValue(true)
        ]);
    }

Edit the safeUp and safeDown functions for migrations/m<YYMMDD_HHMMSS>_create_loan_table.php to match the following code.

    public function safeUp()
    {
        $this->createTable('loan', [
            'id' => $this->primaryKey(),
            'book_id' => $this->integer(),
            'borrower_id' => $this->integer(),
            'borrowed_on' => $this->dateTime()
                                                                                                                                        ->defaultValue(date('Y-m-d H:i:s')),
            'to_be_returned_on' => $this->dateTime()
        ]);

        // create index for column `book_id`
        $this->createIndex(
            'idx-loan-book_id',
            'loan',
            'book_id'
        );

        // add foreign key for table `post`
        $this->addForeignKey(
            'fk-loan-book_id',
            'loan',
            'book_id',
            'book',
            'id',
            'CASCADE'
        );

        // create index for column `borrower_id`
        $this->createIndex(
            'idx-loan-borrower_id',
            'loan',
            'borrower_id'
        );

        // add foreign key for table `post`
        $this->addForeignKey(
            'fk-loan-borrower_id',
            'loan',
            'borrower_id',
            'member',
            'id',
            'CASCADE'
        );
    }

    public function safeDown()
    {
        $this->dropForeignKey('fk-loan-borrower_id','loan');
        $this->dropIndex('idx-loan-borrower_id', 'loan');
        $this->dropForeignKey('fk-loan-book_id', 'loan');
        $this->dropIndex('idx-loan-book_id', 'loan');
        $this->dropTable('loan');
    }

Seeding the database

To have some data available when the tables are created, we can create migrations to insert fake data into the database. Create two new migrations using the following command:

php yii migrate/create seed_member_table
php yii migrate/create seed_book_table

Open migrations/m<YYMMDD_HHMMSS>_seed_member_table.php and modify the safeUp function and add the insertFakeMembers function after it, as in the code below.

public function safeUp() {
    $this->insertFakeMembers();
}

private function insertFakeMembers() 
{
        $faker = \Faker\Factory::create();

        for ($i = 0; $i < 10; $i++) {
            $this->insert(
                'member',
                [
                    'name' => $faker->name
                ]
            );
        }
    }

Next, open migrations/m<YYMMDD_HHMMSS>_seed_book_table.php and modify the safeUp function and add the insertFakeBooks function after it, as in the code below.

public function safeUp() {

        $this->insertFakeBooks();
    }

private function insertFakeBooks() 
{
        $faker = \Faker\Factory::create();
        for ($i = 0; $i < 50; $i++) {
            $this->insert(
                'book',
                [
                    'name'         => $faker->catchPhrase,
                    'author'       => $faker->name,
                    'release_year' => (int)$faker->year,
                ]
            );
        }
    }

With the changes made, run the migrations to create the tables using the following command:

php yii migrate

When prompted, type “yes” and press “Enter” to run the migrations. Open the library-api database in your preferred application to see the new table structure.

The database&#x27;s table structure

The book table with sample books is shown below.

Listing of the book table

The member table with sample members is shown below.

Listing of the member table

Create the models

Instead of writing raw SQL queries to interact with the database, we will create Active Record classes for our models. These will give us an object-oriented means of accessing and storing data in the database. Create Active Record classes for the Member, Book and, Loan entities using the commands below. When prompted type yes and press Enter to continue.

php yii gii/model --tableName=member --modelClass=Member
php yii gii/model --tableName=book --modelClass=Book
php yii gii/model --tableName=loan --modelClass=Loan

The model classes are saved in the models directory. Interestingly, they help us generate functions to handle intricate relationships in our database structure. For example, in models/Member.php, a getLoans function was created for us, which retrieves all loans associated with a given member. The same is the case for the models/Book.php.

Create the controllers

With the database and models in place, we can create controllers to handle RESTful calls. Yii bases controllers on yii\rest\ActiveController, which provides common RESTful actions. This allows us to create endpoints to handle CRUD actions without the stress of writing the boilerplate code ourselves.

To start off, create controllers for the Member and Book entities using the commands below. Type “yes” and press “Enter” when prompted.

php yii gii/controller --controllerClass=app\\controllers\\MemberController --baseClass=yii\\rest\\ActiveController

php yii gii/controller --controllerClass=app\\controllers\\BookController --baseClass=yii\\rest\\ActiveController

The controllerClass argument specifies the name of the controller to be created. You are expected to provide a Fully Qualified Namespaced class.

Note: The double backspaces (\\) used in specifying the namespace are necessary to escape the single backspace in the fully-qualified class name.

The controller classes are stored in the controller directory. Open controllers/BookController.php and edit the content to match the following.

<?php

namespace app\controllers;

class BookController extends \yii\rest\ActiveController {

    public $modelClass = 'app\models\Book';

}

In the same way, open controllers/MemberController.php and edit the content to match the following.

<?php

namespace app\\controllers;

class MemberController extends \yii\rest\ActiveController
{
    public $modelClass = 'app\models\Member';

}

Next, modify the urlManager component in your application configuration, which you will find in config/web.php.

In config/web.php, an array, stored in $config, is returned. This array contains a components element In that element, replace the commented out version of the urlManager element with the following:

'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' => true,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 'controller' => 'member'],
                ['class' => 'yii\rest\UrlRule', 'controller' => 'book'],
            ],
        ],

This change adds a URL rule for the Member and Book controllers so that associated data can be accessed and manipulated with pretty URLs and meaningful HTTP verbs.

The components element also contains a request element which holds the configuration for the request Application Component. To enable the API to parse JSON input, add the following to the request element.

'parsers' => [
        'application/json' => 'yii\web\JsonParser',
    ],

At this stage, we have an application that can handle the following requests:

Method

Route

Description

GET

/{members, books}

List all members or books page by page;

POST

/{members, books}

Create a new member or book;

GET

/{members, books}/123

Return the details of the member or book 123;

PATCH and PUT

/{members, books}/123

Update the member or book 123;

DELETE

/{members, books}/123

Delete the member or book 123;

Serve the application by running php yii serve and test your newly created endpoints. 

For example, to retrieve the lists of books from the database, send a GET request to http://localhost:8080/books. To do that, launch Postman if you don't have it open already.

If this is your first time using Postman, you need to enter the API endpoint where it says ‘Enter request URL’ and select the method (the action type) on the left of that field. The default method is GET and this is suitable for our use case. Next press the Send button, which is located to the right of the request URL field. This process is depicted by the image below:

Testing the books post endpoint in Postman

If successful, you will see a JSON response containing the lists of books from the database and a status code of200 Ok.

Next, create a new member by sending a POST request to http://localhost:8080/members and input the  appropriate details as shown in the screenshot below

Creating a new member using the members endpoint in Postman

Note that your details might not be similar to this, but here is what the raw JSON requests should look like:

{
    "name": "olususi oluyemi",
    "started_on": "2020-03-24 12:30:10"
}

If the request is successful, you will receive a JSON response containing a 201 Created status code.

The last controller we need is the LoanController to manage loan-related requests. With regards to loans, the API should only permit requests to create a new loan i.e., borrow a book or view all the loans on the system. Create the LoanController using the following command. Type yes and press Enter when prompted.

php yii gii/controller --controllerClass=app\\controllers\\LoanController

Note: Because we do not need all the RESTful actions made available by the ActiveController class, we did not specify a base class. The generated controller will extend the yii/web/Controller class by default.

Before we create actions to handle requests, we need to add the new routing rules for the new API endpoints. In config/web.php, add the following to the end of the rules element’s array in the urlManager element.

'GET loans' => 'loan/index',
'POST loans' => 'loan/borrow'

When completed, the urlManager should look like the code below:

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'enableStrictParsing' => true,
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => 'member'],
        ['class' => 'yii\rest\UrlRule', 'controller' => 'book'],
        'GET loans' => 'loan/index',
        'POST loans' => 'loan/borrow'
    ],
],

Using the shorthand notation provided by Yii, we can specify the route for a given URL. The URL is provided as the key with the route to the appropriate action as the value. By prefixing the URL with an HTTP verb, the application can handle URLs with the same pattern (for different actions) accordingly. You can read more about this here.

To allow the API to handle POST requests, CSRF validation will be disabled in the LoanController. To do that, in controllers/LoanController.php, add the following member variable.

public $enableCsrfValidation = false;

Note: Disabling CSRF without additional checks in place may expose your application to security threats. Read more about thishere.

Next, update LoanController's actionIndex method to handle the request to get all loans by replacing the method's existing code with the code below.

public function actionIndex() 
{
        $loans = Loan::find()->all();
        return $this->asJson($loans);
}

NOTE: Don't forget to import the loan model

use app\models\Loan;

Before creating the action to handle the creation of a new loan, add a helper function to return an error response from the API, by adding the code below to the end of LoanController.

private function errorResponse($message) {
                                
    // set response code to 400
    Yii::$app->response->statusCode = 400;

    return $this->asJson(['error' => $message]);
}

NOTE: Don't forget to import Yii.

use Yii;

With the changes made, before creating a new loan, the application will run the following checks:

  1. Ensure that the book_id provided matches an existing book in the database.
  2. Ensure that the selected book can be borrowed i.e., is_available_for_loan is set to true.
  3. Ensure that the member_id provided matches an existing member in the database.

If any of the above conditions are not satisfied, the errorResponse function should be called, returning an appropriate response message.

Before creating the action, add a function to the Book model which allows the book to be marked as borrowed. To do that, in models/Book.php, add the following code.

public function markAsBorrowed() 
{
        $this->is_available_for_loan = (int)false;
        $this->save();
    }

Next, create an action to handle the request to borrow a book by adding the following code to controllers/LoanController.php.

public function actionBorrow() 
{
        $request = Yii::$app->request;

        $bookId = $request->post('book_id');
        $book = Book::findOne($bookId);

        if (is_null($book)) {
            return $this->errorResponse('Could not find book with provided ID');
        }

        if (!$book->is_available_for_loan) {
            return $this->errorResponse('This book is not available for loan');
        }

        $borrowerId = $request->post('member_id');

        if (is_null(Member::findOne($borrowerId))) {
            return $this->errorResponse('Could not find member with provided ID');
        }

        $loan = new Loan();
        $returnDate = strtotime('+ 1 month');
        $loan->attributes =
            [
                'book_id'           => $bookId,
                'borrower_id'       => $borrowerId,
                'borrowed_on'       => date('Y-m-d H:i:s'),
                'to_be_returned_on' => date('Y-m-d H:i:s', $returnDate)
            ];

        $book->markAsBorrowed();
        $loan->save();

        return $this->asJson(
            $loan
        );
    }

NOTE: Don't forget to import the Book and Member models

use app\models\Member;
use app\models\Book;

Test the new routes

You can test your new routes to see them in action. To borrow a book, you will send a POST HTTP request to this endpoint http://localhost:8080/loans. Enter the API endpoint in the URL field and select POST from the action dropdown field. Next, select Body on the next line and use the following  code as an example of a raw JSON request:

{
    "book_id": 9,
    "member_id" : 10
}

Loaning a book using the loan endpoint in Postman

If the request is successful, you will get a JSON response similar to the response below, along with an HTTP 200 OK status code:

{
    "book_id": 9,
    "member_id" : 10,
    "borrowed_on": "2021-03-25 21:39:32",
    "to_be_returned_on": "2021-04-25 21:39:32",
    "id": 6
}

Conclusion

In this article, we built a RESTful API for a small library application with the Yii2 framework. During the process, we saw some advantages of developing with the Yii2 framework, such as generation of features (models, controllers, etc.) with minimal code/boilerplate, its secure-by-default features to protect applications from SQL injection, and CSRF attacks, to name but a few.

Additionally, the underlying philosophy in how the code was structured allows us to take advantage of best practices and OOP patterns, thus resulting in code that is easier to maintain.

The entire codebase for this tutorial is available on GitHub. Feel free to explore further. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.