PHP Luminova: How to Implement a RESTful API with CRUD Operations
This guide outlines the basic steps to implement a REST API with full CRUD operations using the Luminova Framework.
This guide walks you through building a RESTful API using the Luminova PHP framework. It covers full CRUD (Create, Read, Update, Delete) functionality, utilizing HTTP methods such as GET
, POST
, PUT
, and DELETE
.
For a working implementation and source code, check out the Luminova HTTP and CLI REST API Examples on GitHub.
Requirements
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.
Attribute-Based Routing
With attribute-based routing, routes are defined directly on controller methods using PHP attributes.
// /app/Controllers/Http/RestController.php
<?php
namespace App\Controllers\Http;
use Luminova\Base\BaseController;
use Luminova\Attributes\Prefix;
use Luminova\Attributes\Route;
use App\Models\Posts;
use App\Models\User;
use App\Utils\Auth;
use App\Controllers\Errors\ViewErrors;
use \JsonException;
use \stdClass;
#[Prefix(pattern: '/api/v1/(:root)', onError: [ViewErrors::class, 'onRestError'])]
class RestController extends BaseController
{
#[Route('/api/v1/posts/(:root)', middleware: 'before', methods: ['ANY'])]
public function auth(): int {}
#[Route('/api/v1/', methods: ['ANY'])]
public function index(): int {}
#[Route('/api/v1/posts', methods: ['GET'])]
public function list(): int {}
#[Route('/api/v1/posts/(:int)', methods: ['GET'])]
public function read(int $post_id): int {}
#[Route('/api/v1/posts/create', methods: ['POST'])]
public function create(): int {}
#[Route('/api/v1/posts/update/(:int)', methods: ['PUT'])]
public function update(int $post_id): int {}
#[Route('/api/v1/posts/delete/(:int)', methods: ['DELETE'])]
public function delete(int $post_id): int {}
}
- 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.
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
use \Luminova\Routing\Router;
// Api endpoints index
$router->any('/', 'RestController::index');
$router->bind('/v1/posts', function(Router $router) {
$router->middleware(Router::ANY_METHODS, '/(:root)', 'RestController::auth');
// CRUD routes for posts
$router->get('/', 'RestController::list'); // Retrieve all posts
$router->get('/(:int)', 'RestController::read'); // Retrieve a specific post by ID
$router->post('/create', 'RestController::create'); // Create a new post
$router->put('/update/(:int)', 'RestController::update'); // Update an existing post
$router->delete('/delete/(:int)', 'RestController::delete'); // Delete a post by ID
});
- Middleware: Protects the routes with authentication.
- Index handling: Ensures that invalid routes are redirected the the
index
method.
Method-Based Routing Index Controller
In your index.php context
method, add routes prefixes to handle API endpoint request as below:
// public/index.php
<?php
// Leave other unchanged
Boot::http()->router->context(
[
'prefix' => Prefix::API,
'error' => [ViewErrors::class, 'onRestError']
],
[
'prefix' => Prefix::WEB,
'error' => [ViewErrors::class, 'onWebError']
],
[
'prefix' => Prefix::CLI
]
)->run();
Step 2: Define the Models
Posts Model:
The model represents the Posts
entity and interacts with the database.
// app/Models/Posts.php
<?php
namespace App\Models;
use Luminova\Base\BaseModel;
class Posts extends BaseModel
{
protected string $table = 'posts';
protected string $primaryKey = 'pid';
protected bool $cacheable = true;
}
Users Model:
The model represents the Users
entity and interacts with the database.
// app/Models/Users.php
<?php
namespace App\Models;
use Luminova\Base\BaseModel;
class Users extends BaseModel
{
protected string $table = 'users';
protected string $primaryKey = 'user_id';
protected bool $cacheable = false;
}
Step 3: Database Management
1. Create the Database Migration Class:
Define your database migration table blueprint.
// app/Database/Migrations/PostsMigration.php
<?php
namespace App\Database\Migrations;
use Luminova\Database\Migration;
use Luminova\Database\Table;
use Luminova\Database\Schema;
class PostsMigration extends Migration
{
/**
* Defines the 'posts' table structure.
*/
public function up(): void
{
Schema::create('posts', function (Table $table) {
$table->prettify = true;
$table->database = Table::SQLITE;
$table->id('pid');
$table->uuid('post_uuid')->nullable(false)->unique();
$table->integer('user_id', 5)->nullable(false);
$table->string('post_title')->nullable(false);
$table->string('post_image')->nullable();
$table->text('post_body')->nullable(false);
$table->timestamps();
return $table;
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
}
2. Create the Table Seeder Class:
Add seed data to populate your posts
table.
// app/Database/Seeder/PostsSeeder.php
<?php
namespace App\Database\Seeders;
use Luminova\Database\Seeder;
use Luminova\Database\Builder;
class PostsSeeder extends Seeder
{
/**
* Inserts sample data into 'posts' table.
*/
public function run(Builder $builder): void
{
$builder->table('posts')->insert([
[
'post_uuid' => func()->uuid(),
'user_id' => 101,
'post_title' => 'Getting Started with PHP Luminova',
'post_body' => 'Learn the basics of setting up and running your first project with PHP Luminova framework.',
],
// Additional seed data
]);
}
}
3. Configure Database Connection:
Set up your database configuration in the .env
file. Here, SQLITE
is used as an example.
# /.env
database.connection = PDO
database.pdo.engine = sqlite
database.development.sqlite.path = writeable/database/development.sqlite
database.mysql.socket = true
database.mysql.socket.path = /var/mysql/mysql.sock
Note:The
socket
configuration is needed for database access on the command line.Adjust the socket path based on your environment.
4. Run Migrations and Seeders:
Once configured, run migrations first:
php novakit db:migrate --class=PostsMigration
After a successful migration, seed the database:
php novakit db:seed --class=PostsSeeder
Step 4: Implement Controller Methods
Each method in the RestController
class corresponds to a specific CRUD operation, processing incoming API requests and interacting with the Post
model.
auth
Middleware Authentication
Handles API authentication and usage quota validation for requests made to the /api/v1/posts
endpoint.
public function auth(): int {}
- HTTP Method:
ANY /api/v1/posts/
- Functionality:
- Validates the bearer token and user ID.
- Checks the user's quota limit and update used if within limits.
- Returns a success status if authentication and quota checks pass.
- Responds with a
401 Unauthorized
status and an error message if authentication fails.
index
Handle Invalid API Endpoints
Processes requests to the API root or invalid endpoints.
public function index(): int {}
- HTTP Method:
ANY /api/v1/{invalid}
- Functionality:
- Renders a
401 Unauthorized
response for invalid or missing endpoints.
- Renders a
list
Retrieve All Posts
Retrieves a paginated list of posts. Use query parameters for pagination, e.g., GET /api/v1/posts?limit=10&offset=2
.
public function list(): int {}
- HTTP Method:
GET /api/v1/posts
- Functionality:
- Fetches all posts from the database.
- Returns:
200 OK
with a list of posts if they exist.204 No Content
if no posts are available.- Includes a
GET
link in each response item for easier navigation.
read
Retrieve a Single Post
Fetches a specific post by ID from the URL, e.g., GET /api/v1/posts/2
.
public function read(int $post_id): int {}
- HTTP Method:
GET /api/v1/posts/{id}
- Functionality:
- Retrieves a post by its ID.
- Returns:
200 OK
with the post data if found.204 No Content
if the post does not exist.
create
Create a New Post
Creates a new post using the data provided in the request body.
public function create(): int {}
- HTTP Method:
POST /api/v1/posts/create
- Functionality:
- Accepts
image
andbody
containing JSON keystitle
,body
anduserId
. - Inserts a new post into the database.
- Returns:
200 OK
if the post is created successfully.- Relevant error codes for validation or server errors.
- Accepts
- Validation:
- Ensures
title
,body
, anduserId
are provided and meet the required criteria.
- Ensures
update
Update an Existing Post
Updates an existing post by ID, using data from the request body.
public function update(int $post_id): int {}
- HTTP Method:
PUT /api/v1/posts/{post_id}
- Functionality:
- Requires request body
body
a JSON with optionaltitle
andcontent
fields. - Updates the specified post in the database.
- Returns:
200 OK
if the update is successful.- An error message if the update fails.
- Requires request body
delete
Delete a Post
Deletes a specific post by its ID.
public function delete(int $post_id): int {}
- HTTP Method:
DELETE /api/v1/posts/delete/{post_id}
- Functionality:
- Removes the post with the given ID from the database.
- Returns:
204 No Content
if the deletion is successful.- An error message if the operation fails.