Luminova Framework

PHP Luminova: How to Implement a RESTful API with CRUD Operations

Last updated: 2024-10-02 22:05:26

This guide outlines the basic steps to implement a REST API with full CRUD operations using the Luminova Framework.

This guide demonstrates how to build a RESTful API in the Luminova PHP framework, supporting full CRUD (Create, Read, Update, Delete) operations using HTTP methods like GET, POST, PUT, and DELETE.

Prerequisites

Before starting, ensure you have:

  • Luminova PHP Framework installed.
  • A working database (e.g., MySQL).

Step 1: Define API Routes

Routes map client requests to specific controller actions. In Luminova, routes can be defined using method-based or attribute-based routing.

Method-Based Routing

Routes are defined in the /routes/api.php file using method chaining, and linked to corresponding controller methods.

// /routes/api.php
<?php
$router->bind('/v1', function(Router $router) {
    $router->middleware('ANY', '/posts/(:root)', 'PostV1Controller::auth');
    $router->any('/', 'PostV1Controller::error');

    // CRUD routes for posts
    $router->get('/posts', 'PostV1Controller::index');            // Retrieve all posts
    $router->get('/posts/(:int)', 'PostV1Controller::show');      // Retrieve a specific post by ID
    $router->post('/posts', 'PostV1Controller::create');          // Create a new post
    $router->put('/posts/(:int)', 'PostV1Controller::update');    // Update an existing post
    $router->delete('/posts/(:int)', 'PostV1Controller::delete'); // Delete a post by ID
});
  • Middleware: Protects the routes with authentication.
  • Error handling: Ensures that invalid routes trigger the error method.

Attribute-Based Routing

With attribute-based routing, routes are defined directly on controller methods using PHP attributes.

// /app/Controllers/Http/PostV1Controller.php
<?php
namespace App\Controllers\Http;

use Luminova\Base\BaseController;
use Luminova\Attributes\Route;
use Luminova\Attributes\Prefix;
use App\Controllers\Errors\ViewErrors;

#[Prefix(pattern: '/api/v1/(:root)', onError: [ViewErrors::class, 'onRestError'])]
class PostV1Controller extends BaseController
{
    #[Route('/api/v1/posts', methods: ['GET'])]
    public function index(): int {}

    #[Route('/api/v1/posts/(:int)', methods: ['GET'])]
    public function show(int $id): int {}

    #[Route('/api/v1/posts', methods: ['POST'])]
    public function create(): int {}

    #[Route('/api/v1/posts/(:int)', methods: ['PUT'])]
    public function update(int $id): int {}

    #[Route('/api/v1/posts/(:int)', methods: ['DELETE'])]
    public function delete(int $id): int {}

    #[Route('/api/v1/posts/(:root)', middleware: 'before', methods: ['ANY'])]
    public function auth(): int {}

    #[Route('/api/v1/(:root)', error: true, methods: ['ANY'])]
    public function error(): int {}

    private function rules(): void {}
}
  • Attributes: Define routes directly in the controller, specifying HTTP methods and additional configurations like middleware.
  • Prefix Handling: Routes are scoped under /api/v1/*, with a global error handler for non-existent endpoints.

Step 2: Define the Model

The model represents the Post entity and interacts with the database.

// app/Models/Post.php
<?php
namespace App\Models;

use Luminova\Base\BaseModel;

class Post extends BaseModel
{
    protected string $table = 'posts';  // Database table
    protected array $updatable = ['title', 'content'];  // Updatable fields
    protected array $insertable = ['pid', 'title', 'content'];  // Insertable fields
    protected string $primaryKey = 'pid';  // Primary key
    protected bool $cacheable = false;  // Disable caching
}
  • Fields: Define which fields are mass assignable for updates and inserts.
  • Primary Key: Set the primary key for the model.

Step 3: Create Controller Methods

Each controller method in the PostV1Controller class handles a specific CRUD operation, processing incoming API requests and interacting with the Post model.


auth

Middleware Authentication:

public function auth(): int 
{
    $ok = false;
    if(($bearer = $this->request->getAuth()) !== null) {
        $ok = Auth::authorize(
            $bearer, 
            escape($this->request->header->get('user_id'))
        );
    }

    if(!$ok){
        response(401)->send(['message' => 'Invalid credentials']);
    }

    return $ok ? STATUS_SUCCESS : STATUS_ERROR;
}
  • HTTP Method: ANY /api/v1/posts
  • Functionality: This method handles authentication for requests made to the /api/v1/posts endpoint. It checks for a Bearer token from the Authorization header and the user_id from the request headers. If a valid token is found, it calls the Auth::authorize() method to verify the user's identity. The method returns a success (STATUS_SUCCESS) if authentication passes, otherwise it returns an error (STATUS_ERROR). If no Bearer token is provided, it also defaults to returning an error status. This ensures that only authenticated users can access the API.

index

Retrieve All Posts:

public function index(Post $post): int
{
    $data = $post->select();
    if($data){
        return response(200)->json($data);
    }

    return response(404)->json(['message' => 'No post found']);
}
  • HTTP Method: GET /api/v1/posts
  • Functionality: Fetches all posts from the database. Returns a 200 OK status with data if posts exist; otherwise, returns a 204 No Content status if no posts are found.

show

Retrieve a Single Post:

public function show(int $id, Post $post): int
{
    $data = $post->find($id);
    if($data){
        return response(200)->json($data);
    }

    return response(404)->json(['message' => 'No post found']);
}
  • HTTP Method: GET /api/v1/posts/{id}
  • Functionality: Fetches a specific post by its ID. Returns 200 OK with the post data if found, or 204 No Content if the post does not exist.

create

Create a New Post:

public function create(Post $post): int
{
    $response = ['status' => 2011, 'message' => 'Unable to add post.'];
    $this->rules();

    if ($this->validate->validate($this->request->getBody())) {
        $ok = $post->insert([
            'pid' => func()->random(5),
            'title' => escape($this->request->getPost('title')),
            'content' => escape($this->request->getPost('content')),
        ]);

        if ($ok) {
            $response = ['status' => 2001, 'message' => 'Post added successfully.'];
        }
    } else {
        $response['message'] = $this->validate->getErrorLine();
    }

    return response(200)->json($response);
}
  • HTTP Method: POST /api/v1/posts
  • Functionality: Creates a new post using the provided title and content. Returns 200 OK if successful, or an error message if the creation fails.
  • Validation: Ensures that both the title and content are provided and meet the validation criteria before inserting the post.

update

Update an Existing Post:

public function update(int $id, Post $post): int
{
    $response = ['status' => 4011, 'message' => 'Unable to update post.'];
    $this->rules();

    if ($this->validate->validate($this->request->getBody())) {
        $ok = $post->update($id, [
            'title' => escape($this->request->getPost('title')),
            'content' => escape($this->request->getPost('content')),
        ]);

        if ($ok) {
            $response = ['status' => 2001, 'message' => 'Post updated successfully.'];
        }
    } else {
        $response['message'] = $this->validate->getErrorLine();
    }

    return response(200)->json($response);
}
  • HTTP Method: PUT /api/v1/posts/{id}
  • Functionality: Updates an existing post by its ID. Requires a title and content in the request body. Returns 200 OK if the update is successful or an error message if the operation fails.

delete

Delete a Post:

public function delete(int $id, Post $post): int
{
    $response = ['status' => 2001, 'message' => 'Post deleted successfully.'];

    if (!$post->delete($id)) {
        $response['message'] = 'Unable to delete post.';
    }

    return response(200)->json($response);
}
  • HTTP Method: DELETE /api/v1/posts/{id}
  • Functionality: Deletes a post by its ID. Returns 204 No Content if the deletion is successful, or an error message if the operation fails.

error

Invalid Endpoint:

public function error(): int
{
    return response(501)->send(['error' => 'Endpoint not implemented']);
}
  • HTTP Method: Any invalid or unimplemented request to /api/v1/
  • Functionality: Handles requests to invalid or unimplemented API endpoints within the /api/v1/ route. Returns a 501 Not Implemented status code with a JSON response that includes an error message, indicating that the requested endpoint is not available.

rules

Define Validation Rules:

private function rules(): void
{
    $this->validate->rules = [
        'title' => 'required|string|max(255)',
        'content' => 'required|string',
    ];

    $this->validate->messages = [
        'title' => [
            'required' => 'Post title is required.',
            'string' => 'Post title must be a string.',
            'max' => 'Post title cannot exceed 255 characters.',
        ],
        'content' => [
            'required' => 'Post content is required.',
            'string' => 'Post content must be a string.',
        ],
    ];
}
  • Purpose: Defines validation rules for title and content fields used in the create() and update() methods.
  • Validation Rules:
    • title: Must be a required string with a maximum length of 255 characters.
    • content: Must be a required string.