Developing a RESTful PHP API on Ubuntu 20.04

Understanding APIs and Their Use Cases

An Application Programming Interface (API) acts as a middleware component, enabling communication between different software systems via a REST (Representational State Transfer) protocol. For example, when designing a mobile app that relies on cloud storage, a PHP-based API can serve as a bridge between an Android client and a remote MySQL database.

Another common scenario involves offering your application’s functionality to external users through public endpoints. Well-known platforms like Google, Twitter, Facebook utilize public APIs for this purpose. Instead of navigating through a graphical user interface (GUI), users can directly interact with your application via specific URLs that handle data retrieval and submission.

Why Building an API Enhances Integration

Incorporating an API into your software enhances flexibility, allowing users to connect with your services through various applications. Moreover, when API responses are formatted in JSON (JavaScript Object Notation), compatibility spans across mobile apps, desktops, tablets, and even IoT devices—without requiring backend modifications.

This guide walks you through creating a RESTful JSON API in PHP for a fictional online shop, running on Ubuntu 20.04. Once completed, you’ll be able to access product data from the database without interacting with a traditional user interface.

Requirements

  • An Ubuntu 20.04 environment
  • A user account with sudo privileges
  • A functioning LAMP stack (either MySQL or MariaDB)

Building the Sample Database

Begin by logging into your server using SSH and launching the MySQL shell:

After entering your root password and seeing the mysql> prompt, execute the following command to create a new database named store_api:

mysql> CREATE DATABASE store_api;

Next, generate a dedicated MySQL user for your API to use during database access:

mysql> CREATE USER 'api_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
mysql> GRANT ALL PRIVILEGES ON store_api.* TO 'api_user'@'localhost';
mysql> FLUSH PRIVILEGES;

For those using MariaDB instead of MySQL, use the following syntax to grant access:

MariaDB> GRANT ALL PRIVILEGES on store_api.* TO 'api_user'@'localhost' identified by 'EXAMPLE_PASSWORD';

Switch the current session to the newly created database:

Defining the Products Table

Now, create a table named products to represent the items available in your online store. This will allow your API to fetch product details, which users would normally only see through a frontend interface.

mysql> CREATE TABLE products (
         product_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
         product_name  VARCHAR(50),
         cost_price  DOUBLE,
         retail_price DOUBLE
         ) ENGINE = InnoDB;

Insert sample entries into the table to simulate a real inventory:

mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('LEATHER JACKET', '89.23', '99.95');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('SILVER COAT', '44.00', '60.00');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('REXI BELT', '14.49', '18.85');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('SUEDE SHOE', '24.00', '36.00');
mysql> INSERT INTO products (product_name, cost_price, retail_price) VALUES ('WOOLEN SWEATER', '14.45', '18.00');

Run a SELECT query to confirm that your entries have been correctly saved:

mysql> SELECT
         product_id,
         product_name,
         cost_price,
         retail_price
         FROM products;

MySQL should return a list similar to this:

+------------+----------------+------------+--------------+
| product_id | product_name   | cost_price | retail_price |
+------------+----------------+------------+--------------+
|          1 | LEATHER JACKET |      89.23 |        99.95 |
|          2 | SILVER COAT    |         44 |           60 |
|          3 | REXI BELT      |      14.49 |        18.85 |
|          4 | SUEDE SHOE     |         24 |           36 |
|          5 | WOOLEN SWEATER |      14.45 |           18 |
+------------+----------------+------------+--------------+
5 rows in set (0.00 sec)

Exit the MySQL session once your setup is complete:

Setting Up Apache ModRewrite for API Routing

To ensure your API endpoints function correctly, it’s essential to configure Apache’s URL rewriting capabilities. Start by activating the mod_rewrite module on your server:

Next, open the main Apache configuration file using nano:

$ sudo nano /etc/apache2/apache2.conf

Locate the directive block associated with /var/www/ and make the following adjustment:

...
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
</Directory>
...

Replace the AllowOverride None line with AllowOverride All so that Apache can respect the settings inside .htaccess files:

...
<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>
...

This change allows the .htaccess files to override the default server behavior. After making this adjustment, restart Apache to apply the changes:

$ sudo systemctl restart apache2

Creating a .htaccess File for URL Rewriting

Now that mod_rewrite is enabled, create a base folder for your API project. In this example, a versioning approach is used, starting with version 1 as the directory name:

$ sudo mkdir -p /var/www/html/api/v1

Within that folder, create a new .htaccess file:

$ sudo nano /var/www/html/api/v1/.htaccess

Insert the following lines into the file:

RewriteEngine On
RewriteBase /api/v1

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule (.*)$ index.php?request=$1 [QSA,NC,L]

Explanation of the .htaccess Directives

  • RewriteEngine On: This enables Apache’s URL rewriting engine.
  • RewriteBase /api/v1: Defines the base path for all subsequent rewrite rules.
  • RewriteCond %{REQUEST_FILENAME} !-f: Skips rewrite rules if the requested file exists.
  • RewriteCond %{REQUEST_FILENAME} !-d: Skips if the requested path is a directory.
  • RewriteRule (.*)$ index.php?request=$1 [QSA,NC,L]: Redirects any remaining requests to index.php with the request URI passed as a parameter.

For example, a request to http://localhost/api/v1/products/1 will be internally rewritten to http://localhost/api/v1/index.php?request=products/1.

Creating the Main index.php Router

The next step is to set up the main index.php file that will handle all incoming API requests. This file acts as the central router, directing traffic to the appropriate functionality based on the URL and request method.

Use the following command to create and open the file:

$ sudo nano /var/www/html/api/v1/index.php

Insert the following PHP code into the file:

<?php

header("Content-type:application/json");

function load_class($class) {
    require_once $class . '.php';
}

spl_autoload_register('load_class');

$http_verb = $_SERVER['REQUEST_METHOD'];

if ($_SERVER['REQUEST_METHOD'] == 'GET') {
    foreach($_GET as $key => $value) {
        $params[$key] = $value;
    }
}

$request  = explode('/', $_REQUEST['request']);
$resource = $request[0];

if (isset($request[1])) {
    $resource_id = $request[1];
} else {
    $resource_id = '';
}

if ($resource == 'products') {
    $request = new Products;
}

if ($http_verb == 'GET') {

    if (!empty($resource_id)) {
        $response = $request->read($resource_id);
    } else {
        $response = $request->read($resource_id, $params);
    }

    echo $response;
}

Understanding the index.php Code

  • header(“Content-type:application/json”): This line ensures that all output from the API is sent in JSON format, which is ideal for web and mobile clients.
  • load_class function: This custom function simplifies class loading by using PHP’s autoload feature, preventing the need for multiple require statements.
  • spl_autoload_register: Registers the autoloader so that class files are loaded dynamically when needed.
  • Handling GET Requests: If the request method is GET, any incoming GET parameters are collected and stored in the $params array.
  • Parsing the Request: The URL path provided to the API is split into its components. The first part identifies the resource type (e.g., products), and the second, if present, is the specific resource ID.
  • Resource Mapping: When the API identifies a products request, it initializes a new instance of the Products class to handle the logic.
  • GET Execution Flow: If a resource ID is provided, the system retrieves a specific product using the read method. Otherwise, it performs a more general query, potentially using URL filters supplied via GET.

For instance, accessing http://localhost/api/v1/products/1 will instruct the API to fetch the item with ID 1. On the other hand, a call to http://localhost/api/v1/products?type=shoes allows for parameter-based filtering instead.

Building the Products Class for Handling Requests

To respond to API calls targeting the products endpoint—such as http://localhost/api/v1/products or http://localhost/api/v1/products/1—you need to create a class called Products.

Create the file using the following command:

$ sudo nano /var/www/html/api/v1/Products.php

Add this PHP code to define the class:

<?php

class Products
{
    public function read($resource_id, $params = '')
    {
        try {

            $db_name     = 'store_api';
            $db_user     = 'api_user';
            $db_password = 'EXAMPLE_PASSWORD';
            $db_host     = 'localhost';

            $pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

            $data = [];

            $sql  = "select * from products";

            if (!empty($resource_id)) {
                $sql .= " where product_id = :product_id";
                $data['product_id'] = $resource_id;
            } else {
                $filter = '';

                if (isset($params['product_name'])) {
                    $filter .= " and product_name = :product_name";
                    $data['product_name'] = $params['product_name'];
                }

                $sql .= " where product_id > 0 $filter";
            }

            $stmt = $pdo->prepare($sql);
            $stmt->execute($data);

            $products = [];

            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $products[] = $row;
            }

            $response = [];
            $response['data'] = $products;

            if (!empty($resource_id)) {
               $response['data'] = array_shift($response['data']);
            }

           return json_encode($response, JSON_PRETTY_PRINT);

        } catch (PDOException $ex) {
            $error = [];
            $error['message'] = $ex->getMessage();

            return $error;
        }
    }
}

Understanding the Products Class Implementation

  • Class Structure: The Products class is invoked from the index.php file whenever a user targets the products endpoint.
  • read() Method: The core logic resides inside the read method, which accepts a resource ID and optional parameters. This method is what executes and responds to incoming GET requests.
  • Database Connection: PDO is used to connect to the MySQL database securely. The database credentials match those previously configured.
  • Query Logic: If a specific product ID is provided, the SQL query fetches only that product. Otherwise, a filter can be applied using query parameters, such as product_name, to return a refined list of results.
  • Safe Execution: Named parameters are used to prevent SQL injection. The query is prepared and executed using $stmt->execute().
  • JSON Response: The output is encoded into a neatly formatted JSON object. If only one item is fetched, it’s returned directly instead of in an array.
  • Error Handling: If the database operation fails, the error message is captured and returned in JSON format.

Testing the PHP API with curl

Once everything is in place, test your API using the curl command to confirm that the endpoint behaves as expected.

View All Products

$ curl localhost/api/v1/products

Sample output:

{
    "data": [
        {
            "product_id": 1,
            "product_name": "LEATHER JACKET",
            "cost_price": 89.23,
            "retail_price": 99.95
        },
        {
            "product_id": 2,
            "product_name": "SILVER COAT",
            "cost_price": 44,
            "retail_price": 60
        },
        {
            "product_id": 3,
            "product_name": "REXI BELT",
            "cost_price": 14.49,
            "retail_price": 18.85
        },
        {
            "product_id": 4,
            "product_name": "SUEDE SHOE",
            "cost_price": 24,
            "retail_price": 36
        },
        {
            "product_id": 5,
            "product_name": "WOOLEN SWEATER",
            "cost_price": 14.45,
            "retail_price": 18
        }
    ]
}

Retrieve a Single Product by ID

Product ID 1:

$ curl localhost/api/v1/products/1

{
    "data": {
        "product_id": 1,
        "product_name": "LEATHER JACKET",
        "cost_price": 89.23,
        "retail_price": 99.95
    }
}

Product ID 2:

$ curl localhost/api/v1/products/2

{
    "data": {
        "product_id": 2,
        "product_name": "SILVER COAT",
        "cost_price": 44,
        "retail_price": 60
    }
}

Filter by Product Name

Retrieve the “SUEDE SHOE” entry:

$ curl localhost/api/v1/products?product_name=SUEDE%20SHOE

{
    "data": [
        {
            "product_id": 4,
            "product_name": "SUEDE SHOE",
            "cost_price": 24,
            "retail_price": 36
        }
    ]
}

Final Thoughts

You’ve now successfully implemented a RESTful API using PHP and MySQL on Ubuntu 20.04. The API delivers data in JSON format and is fully capable of responding to GET requests. You can further expand this setup by adding support for additional HTTP methods like POST, PUT, and DELETE, tailoring the logic to meet your application’s needs.

Source: vultr.com

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in:

Moderne Hosting Services mit Cloud Server, Managed Server und skalierbarem Cloud Hosting für professionelle IT-Infrastrukturen

Real-Time Water Billing with PHP, Redis Pub/Sub & MySQL

MySQL, Tutorial

Linux file permissions with this comprehensive guide. Understand how to utilize chmod and chown commands to assign appropriate access rights, and gain insights into special permission bits like SUID, SGID, and the sticky bit to enhance your system’s security framework.