Books API Structure
Create folders
app/
app/controllers/
app/core/
app/models/
app/models/DAOs/
app/models/DTOs/
app/models/entities/
app/utils/
config/
public/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
- 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');
api/config/dbconf.json
MySQL DB connection:
{
"host": "localhost",
"user": "root",
"password": "",
"db_name": "books_db"
}api/public/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
- 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']);
- 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';
}
}
- 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;
}
}
- 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);
}
}
- 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;
}
}
- 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);
}
}
- 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;
}
}
- 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();
}
}
- 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.');
Top comments (0)