DEV Community

von CARAMEL
von CARAMEL

Posted on

Building a API in PHP

Books API Structure

  1. Create folders
    app/
    app/controllers/
    app/core/
    app/models/
    app/models/DAOs/
    app/models/DTOs/
    app/models/entities/
    app/utils/
    config/
    public/

  2. api/composer.json
    Configure Composer and the PSR-4 autoload so that classes with the namespace App\ are searched inside the app/ folder.
    Key content:
    {
    "name": "user/api",
    "autoload": {
    "psr-4": {
    "App\": "app/"
    }
    }
    }

After creating it, run this command inside the api folder:
composer dump-autoload

  1. api/config/config.php Defines the base URL of the project. The router removes it from REQUEST_URI to keep only routes such as /books/get. <?php

define('BASE_URL', '/proyect/api/public');

  1. api/config/dbconf.json
    MySQL DB connection:
    {
    "host": "localhost",
    "user": "root",
    "password": "",
    "db_name": "books_db"
    }

  2. api/public/.htaccess
    RewriteEngine On

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

RewriteRule ^ index.php [QSA,L]

  1. api/public/index.php This is the entry point. It loads Composer, loads the configuration and calls the router. <?php

use App\Core\Router;

require_once DIR . '/../vendor/autoload.php';
require_once DIR . '/../config/config.php';

(new Router())->dispatch($_SERVER['REQUEST_URI']);

  1. api/app/core/Router.php <?php

namespace App\Core;

class Router
{
protected array $routes = [
'/' => 'HomeController@index',
'/books' => 'BookController@index',
'/books/get' => 'BookController@getAll',
'/books/getById' => 'BookController@getById',
'/books/create' => 'BookController@create',
'/books/update' => 'BookController@update',
'/books/delete' => 'BookController@delete',
];

public function add($route, $params): void
{
    $this->routes[$route] = $params;
}


public function dispatch($uri): void
{
    $uri = parse_url(str_replace(BASE_URL, '', $uri), PHP_URL_PATH);


    if (!isset($this->routes[$uri])) {
        $this->sendNotFound();
        return;
    }


    [$controller, $method] = explode('@', $this->routes[$uri]);
    $controller = 'App\\Controllers\\' . $controller;


    if (!class_exists($controller) || !method_exists($controller, $method)) {
        $this->sendNotFound();
        return;
    }


    (new $controller())->$method();
}


private function sendNotFound(): void
{
    http_response_code(404);
    echo '404 Not Found';
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/core/DatabaseSingleton.php Creates a single PDO connection to MySQL and reuses it. <?php

namespace App\Core;

use PDO;

class DatabaseSingleton
{
private static ?DatabaseSingleton $instance = null;
private PDO $connection;

private function __construct()
{
    $config = json_decode(
        file_get_contents(__DIR__ . '/../../config/dbconf.json'),
        true
    );


    $this->connection = new PDO(
        "mysql:host={$config['host']};dbname={$config['db_name']}",
        $config['user'],
        $config['password']
    );
}


public static function getInstance(): DatabaseSingleton
{
    if (self::$instance === null) {
        self::$instance = new self();
    }


    return self::$instance;
}


public function getConnection(): PDO
{
    return $this->connection;
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/utils/ApiResponse.php Centralizes the JSON response format. <?php

namespace App\Utils;

class ApiResponse
{
private array $response;

public function __construct($status, int $code, string $message, $data = null)
{
    $this->response = [
        'status' => $status,
        'code' => $code,
        'message' => $message,
        'data' => $data
    ];
}


public function getCode(): int
{
    return $this->response['code'];
}


public function toJSON(): string
{
    return json_encode($this->response);
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/models/entities/BookEntity.php Represents a real row from the books table. <?php

namespace App\Models\Entities;

class BookEntity
{
public function __construct(
private $id,
private $author,
private $date,
private $title,
private $description
) {
}

public function getId(): mixed
{
    return $this->id;
}


public function getAuthor(): mixed
{
    return $this->author;
}


public function getDate(): mixed
{
    return $this->date;
}


public function getTitle(): mixed
{
    return $this->title;
}


public function getDescription(): mixed
{
    return $this->description;
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/models/DTOs/BookDTO.php Represents the data returned to the client in JSON. <?php

namespace App\Models\DTOs;

use JsonSerializable;

class BookDTO implements JsonSerializable
{
public function __construct(
private $author,
private $date,
private $title,
private $description
) {
}

public function jsonSerialize(): mixed
{
    return get_object_vars($this);
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/models/DAOs/BookDAO.php Contains the SQL queries for the books table. <?php

namespace App\Models\DAOs;

use App\Core\DatabaseSingleton;
use App\Models\Entities\BookEntity;
use PDO;
use PDOException;

class BookDAO
{
private PDO $connection;

public function __construct()
{
    $this->connection = DatabaseSingleton::getInstance()->getConnection();
}


public function getAll(): array|false
{
    try {
        $stmt = $this->connection->query(
            "SELECT id, author, date, title, description FROM books"
        );


        $books = [];


        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $books[] = $this->rowToEntity($row);
        }


        return $books;
    } catch (PDOException $e) {
        return false;
    }
}


public function getById($id): BookEntity|false
{
    try {
        $stmt = $this->connection->prepare(
            "SELECT id, author, date, title, description FROM books WHERE id = :id"
        );


        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);


        return $row ? $this->rowToEntity($row) : false;
    } catch (PDOException $e) {
        return false;
    }
}


public function save(BookEntity $book): bool
{
    try {
        $stmt = $this->connection->prepare(
            "INSERT INTO books (author, date, title, description)
            VALUES (:author, :date, :title, :description)"
        );


        return $stmt->execute($this->bookParams($book, false));
    } catch (PDOException $e) {
        return false;
    }
}


public function update(BookEntity $book): bool
{
    try {
        $stmt = $this->connection->prepare(
            "UPDATE books
            SET author = :author, date = :date, title = :title, description = :description
            WHERE id = :id"
        );


        $stmt->execute($this->bookParams($book));


        return $stmt->rowCount() > 0;
    } catch (PDOException $e) {
        return false;
    }
}


public function delete($id): bool
{
    try {
        $stmt = $this->connection->prepare(
            "DELETE FROM books WHERE id = :id"
        );


        $stmt->execute(['id' => $id]);


        return $stmt->rowCount() > 0;
    } catch (PDOException $e) {
        return false;
    }
}


private function rowToEntity(array $row): BookEntity
{
    return new BookEntity(
        $row['id'],
        $row['author'],
        $row['date'],
        $row['title'],
        $row['description']
    );
}


private function bookParams(BookEntity $book, bool $withId = true): array
{
    $params = [
        'author' => $book->getAuthor(),
        'date' => $book->getDate(),
        'title' => $book->getTitle(),
        'description' => $book->getDescription()
    ];


    if ($withId) {
        $params['id'] = $book->getId();
    }


    return $params;
}
Enter fullscreen mode Exit fullscreen mode

}

  1. api/app/controllers/BookController.php Receives book requests, calls the DAO and returns JSON. <?php

namespace App\Controllers;

use App\Models\DAOs\BookDAO;
use App\Models\DTOs\BookDTO;
use App\Models\Entities\BookEntity;
use App\Utils\ApiResponse;

class BookController
{
private ?BookDAO $bookDAO = null;

public function index(): void
{
    echo 'Hello from BookController';
}


public function getAll(): void
{
    $books = $this->dao()->getAll();


    if ($books === false) {
        $this->json('not success', 500, 'Error getting data.');
        return;
    }


    $this->json('success', 200, 'Data obtained successfully.', array_map(
        fn ($book) => $this->toDTO($book),
        $books
    ));
}


public function getById(): void
{
    $id = $_GET['id'] ?? null;


    if (!$id) {
        $this->json('not success', 400, 'Book id is missing.');
        return;
    }


    $book = $this->dao()->getById($id);


    if ($book === false) {
        $this->json('not success', 404, 'Book not found.');
        return;
    }


    $this->json('success', 200, 'Data obtained successfully.', $this->toDTO($book));
}


public function create(): void
{
    $book = $this->dataToEntity($this->getJsonData());


    if ($this->dao()->save($book)) {
        $this->json('success', 201, 'Data written successfully.', $this->toDTO($book));
        return;
    }


    $this->json('not success', 500, 'Error writing data.', $book);
}


public function update(): void
{
    $book = $this->dataToEntity($this->getJsonData());


    if ($this->dao()->update($book)) {
        $this->json('success', 200, 'Data updated successfully.', $this->toDTO($book));
        return;
    }


    $this->json('not success', 404, 'Error updating data or book not found.', $book);
}


public function delete(): void
{
    $data = $this->getJsonData();
    $id = $data['id'] ?? $_GET['id'] ?? null;


    if (!$id) {
        $this->json('not success', 400, 'Book id is missing.');
        return;
    }


    if ($this->dao()->delete($id)) {
        $this->json('success', 200, 'Data deleted successfully.');
        return;
    }


    $this->json('not success', 404, 'Error deleting data or book not found.');
}


private function getJsonData(): array
{
    return json_decode(file_get_contents('php://input'), true) ?? [];
}


private function dao(): BookDAO
{
    if ($this->bookDAO === null) {
        $this->bookDAO = new BookDAO();
    }


    return $this->bookDAO;
}


private function dataToEntity(array $data): BookEntity
{
    return new BookEntity(
        $data['id'] ?? null,
        $data['author'] ?? '',
        $data['date'] ?? '',
        $data['title'] ?? '',
        $data['description'] ?? ''
    );
}


private function toDTO(BookEntity $book): BookDTO
{
    return new BookDTO(
        $book->getAuthor(),
        $book->getDate(),
        $book->getTitle(),
        $book->getDescription()
    );
}


private function json($status, int $code, string $message, $data = null): void
{
    $response = new ApiResponse($status, $code, $message, $data);


    header('Content-Type: application/json');
    http_response_code($response->getCode());
    echo $response->toJSON();
}
Enter fullscreen mode Exit fullscreen mode

}

  1. Create the database Database: CREATE DATABASE books_db; USE books_db;

Table:
CREATE TABLE books (
id INT AUTO_INCREMENT PRIMARY KEY,
author VARCHAR(255) NOT NULL,
date DATE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL
);

Example insert:
INSERT INTO books (author, date, title, description)
VALUES
('J. R. R. Tolkien', '1954-07-29', 'The Fellowship of the Ring', 'First book of The Lord of the Rings.'),
('George Orwell', '1949-06-08', '1984', 'Dystopian novel about surveillance and totalitarianism.');

http://localhost/Native/public/books/create

Top comments (0)