Thursday, August 29, 2024

Building a Core PHP Single-Page CRUD API

This blog post demonstrates how to create a simple single-page CRUD (Create, Read, Update, Delete) API using only core PHP. The purpose of this project is to provide a clear and straightforward example for beginners who want to understand how to develop a RESTful API in PHP without using frameworks.

System Requirements

To run this project, ensure you have the following installed:

  • PHP: This example uses the latest PHP features and syntax available in version 8.2.
  • MariaDB: The example requires a MariaDB or MySQL instance for the database operations.
  • cURL: A command-line tool for sending HTTP requests. Note that any REST client can be used to test the API endpoints.
  • Composer: The composer.json file is included to provide context for PHP server requirements and dependencies, but it is optional for running this project.

Setup Instructions

  1. Create the api.php file: Place the api.php file on any machine that has PHP configured.
  2. Configure Database Connection: Ensure the database connection settings in the api.php file are correctly set for your database instance. This will allow the API to connect to the database and perform necessary operations.
  3. Run the PHP Server: Start the PHP built-in server in the same folder as api.php to use the example REST calls as-is:
php -S localhost:8080

The Code


<?php

/*
 * CRUD API Example in core PHP.
 *
 * This script provides a basic implementation of a CRUD (Create, Read, Update, Delete) API
 * using PHP, PDO for database interactions, and JSON for data exchange. The API supports
 * creating, reading, updating, and deleting customer records in a MySQL database.
 * It also includes endpoints for setting up and tearing down the customers table, which are
 * available for illustrative purposes and convenience in this example, but should never be 
 * placed in a production environment.
 */

$host = '127.0.0.1';
$dbname = 'testing';
$username = 'username';
$password = 'password';

try {
    /* Establish database connection */
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $exception) {
    die('Database connection failed: ' . $exception->getMessage());
}

/**
 * Validate user input data for creating or updating a customer.
 *
 * @param string[] $data The data to validate
 *
 * @return string[] The array of validation errors
 */
function validateInput(array $data): array
{
    $errors = [];
    if (empty($data['firstName'])) {
        $errors[] = 'First name is required';
    }
    if (empty($data['lastName'])) {
        $errors[] = 'Last name is required';
    }
    if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'Valid email is required';
    }
    return $errors;
}

/* Retrieve HTTP method and path segments */
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$segments = explode('/', trim($path, '/'));

/* Determine the resource and customer ID (if any) from the URL */
$resource = $segments[1] ?? '';
$customerId = $segments[2] ?? '';

switch ($resource) {
    case 'up':
        if ($method === 'POST') {
            try {
                /* Create 'customers' table if it doesn't exist */
                $sql = 'CREATE TABLE IF NOT EXISTS customers (
                    customerId CHAR(36) DEFAULT uuid() PRIMARY KEY,
                    firstName VARCHAR(255) NOT NULL,
                    lastName VARCHAR(255) NOT NULL,
                    email VARCHAR(255) NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )';
                $pdo->exec($sql);
                echo json_encode(['message' => 'Table created successfully']);
            } catch (PDOException $exception) {
                http_response_code(500);
                echo json_encode(['error' => 'Failed to create table: ' . $exception->getMessage()]);
            }
        } else {
            http_response_code(405);
            echo json_encode(['error' => 'Method not allowed']);
        }
        break;

    case 'down':
        if ($method === 'DELETE') {
            try {
                /* Drop 'customers' table if it exists */
                $sql = 'DROP TABLE IF EXISTS customers';
                $pdo->exec($sql);
                echo json_encode(['message' => 'Table dropped successfully']);
            } catch (PDOException $exception) {
                http_response_code(500);
                echo json_encode(['error' => 'Failed to drop table: ' . $exception->getMessage()]);
            }
        } else {
            http_response_code(405);
            echo json_encode(['error' => 'Method not allowed']);
        }
        break;

    case 'customers':
        switch ($method) {
            case 'GET':
                if ($customerId) {
                    /* Read one customer */
                    $stmt = $pdo->prepare('SELECT * FROM customers WHERE customerId = :customerId');
                    $stmt->execute(['customerId' => $customerId]);
                    $customer = $stmt->fetch(PDO::FETCH_ASSOC);
                    if ($customer) {
                        echo json_encode($customer);
                    } else {
                        http_response_code(404);
                        echo json_encode(['error' => 'Customer not found']);
                    }
                } else {
                    /* Read all customers */
                    $stmt = $pdo->query('SELECT * FROM customers');
                    $customers = $stmt->fetchAll(PDO::FETCH_ASSOC);
                    echo json_encode($customers);
                }
                break;

            case 'POST':
                /* Create a new customer */
                $data = json_decode(file_get_contents('php://input'), true);
                $errors = validateInput($data);
                if (empty($errors)) {
                    $stmt = $pdo->prepare('INSERT INTO customers (firstName, lastName, email) VALUES (:firstName, :lastName, :email)');
                    $stmt->execute([
                        'firstName' => $data['firstName'],
                        'lastName' => $data['lastName'],
                        'email' => $data['email']
                    ]);

                    /* Fetch the customerId of the newly created customer */
                    $stmt = $pdo->query('SELECT customerId FROM customers ORDER BY created_at DESC LIMIT 1');
                    $customerId = $stmt->fetchColumn();

                    echo json_encode(['message' => 'Customer created successfully', 'customerId' => $customerId]);
                } else {
                    http_response_code(400);
                    echo json_encode(['errors' => $errors]);
                }
                break;

            case 'PUT':
                if ($customerId) {
                    /* Update an existing customer */
                    $stmt = $pdo->prepare('SELECT COUNT(*) FROM customers WHERE customerId = :customerId');
                    $stmt->execute(['customerId' => $customerId]);
                    $customerExists = $stmt->fetchColumn();

                    if ($customerExists) {
                        $data = json_decode(file_get_contents('php://input'), true);
                        $errors = validateInput($data);
                        if (empty($errors)) {
                            $stmt = $pdo->prepare('UPDATE customers SET firstName = :firstName, lastName = :lastName, email = :email WHERE customerId = :customerId');
                            $stmt->execute([
                                'firstName' => $data['firstName'],
                                'lastName' => $data['lastName'],
                                'email' => $data['email'],
                                'customerId' => $customerId
                            ]);
                            echo json_encode(['message' => 'Customer updated successfully']);
                        } else {
                            http_response_code(400);
                            echo json_encode(['errors' => $errors]);
                        }
                    } else {
                        http_response_code(404);
                        echo json_encode(['error' => 'Customer not found']);
                    }
                } else {
                    http_response_code(400);
                    echo json_encode(['error' => 'Customer ID is required']);
                }
                break;

            case 'DELETE':
                if ($customerId) {
                    /* Delete an existing customer */
                    $stmt = $pdo->prepare('DELETE FROM customers WHERE customerId = :customerId');
                    $stmt->execute(['customerId' => $customerId]);
                    echo json_encode(['message' => 'Customer deleted successfully']);
                } else {
                    http_response_code(400);
                    echo json_encode(['error' => 'Customer ID is required']);
                }
                break;

            default:
                http_response_code(405);
                echo json_encode(['error' => 'Method not allowed']);
                break;
        }
        break;

    default:
        http_response_code(404);
        echo json_encode(['error' => 'Endpoint not found']);
        break;
}

Operation Sequence Diagram

Below is a sequence diagram that shows the flow of API requests and interactions with the database.

UserUserPHP API ServerPHP API ServerMariaDB DatabaseMariaDB DatabaseBring Application UpPOST /api.php/upCreate `customers` tableTable createdstatusCreate New Customerloop[Create Multiple Customers]POST /api.php/customersInsert new customer recordCustomer createdcustomerIdUpdate Existing CustomerPUT /api.php/customers/{customerId}Update customer record by IDCustomer updatedstatusRetrieve Customer by IDGET /api.php/customers/{customerId}Select customer record by IDCustomer record detailscustomer dataRetrieve All CustomersGET /api.php/customers/Select all customer recordsList of customer recordscustomer database arrayDelete Customer by IDDELETE /api.php/customers/{customerId}Delete customer record by IDCustomer deletedstatusBring Application DownDELETE /api.php/downDrop `customers` tableTable droppedstatus

API Endpoints

This API provides the following endpoints:

1. Bring the Application Up

  • URL: /api.php/up
  • Method: POST
  • Description: Initializes the application and creates the customers table in the database.
curl --request POST \
  --url http://localhost:8080/api.php/up \
  --header 'Content-Type: application/json'

Example Output:

{
  "message": "Table created successfully"
}

2. Create a New Customer

  • URL: /api.php/customers
  • Method: POST
  • Description: Creates a new customer record.
curl --request POST \
  --url http://localhost:8080/api.php/customers \
  --header 'Content-Type: application/json' \
  --data '{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john.doe@example.com"
}'

Example Output:

{
  "message": "Customer created successfully",
  "customerId": "2090a31b-64aa-11ef-9cca-0242ac120002"
}

3. Update an Existing Customer

  • URL: /api.php/customers/{customerId}
  • Method: PUT
  • Description: Updates an existing customer record by ID.
curl --request PUT \
  --url http://localhost:8080/api.php/customers/2090a31b-64aa-11ef-9cca-0242ac120002 \
  --header 'Content-Type: application/json' \
  --data '{
  "firstName": "Janet",
  "lastName": "Smith",
  "email": "jane.smith@example.com"
}'

Example Output:

{
  "message": "Customer updated successfully"
}

4. Retrieve a Customer by ID

  • URL: /api.php/customers/{customerId}
  • Method: GET
  • Description: Retrieves a customer record by ID.
curl --request GET \
  --url http://localhost:8080/api.php/customers/2090a31b-64aa-11ef-9cca-0242ac120002

Example Output:

{
  "customerId": "2090a31b-64aa-11ef-9cca-0242ac120002",
  "firstName": "Janet",
  "lastName": "Smith",
  "email": "jane.smith@example.com",
  "created_at": "2024-08-27 19:25:32"
}

5. Retrieve All Customers

  • URL: /api.php/customers
  • Method: GET
  • Description: Retrieves all customer records.
curl --request GET \
  --url http://localhost:8080/api.php/customers/

Example Output:

[
  {
    "customerId": "2090a31b-64aa-11ef-9cca-0242ac120002",
    "firstName": "Janet",
    "lastName": "Smith",
    "email": "jane.smith@example.com",
    "created_at": "2024-08-27 19:25:32"
  },
  {
    "customerId": "9b9a1019-65c0-11ef-87f6-0242ac140002",
    "firstName": "John",
    "lastName": "Marsh",
    "email": "john.marsh@example.com",
    "created_at": "2024-08-29 04:38:59"
  }
]

6. Delete a Customer

  • URL: /api.php/customers/{customerId}
  • Method: DELETE
  • Description: Deletes a customer record by ID.
curl --request DELETE \
  --url http://localhost:8080/api.php/customers/2090a31b-64aa-11ef-9cca-0242ac120002

Example Output:

{
  "message": "Customer deleted successfully"
}

7. Bring the Application Down

  • URL: /api.php/down
  • Method: DELETE
  • Description: Cleans up and destroys the customers table in the database.
curl --request DELETE \
  --url http://localhost:8080/api.php/down \
  --header 'Content-Type: application/json'

Example Output:

{
  "message": "Table dropped successfully"
}

Conclusion

This example demonstrates a fundamental RESTful API using core PHP and MariaDB. It is an excellent starting point for beginners looking to understand the basics of API development without requiring a framework. You can use this code to build upon and expand your knowledge of RESTful services in PHP.

No comments: