ji/donner

2.0 2023-12-13 05:21 UTC

This package is auto-updated.

Last update: 2025-06-13 23:23:15 UTC


README

Donner is a lightweight PHP framework designed to simplify web application development by providing tools for dynamic routing, request validation, structured responses, and file upload handling. It streamlines the process of building web applications and APIs by handling common tasks such as URI pattern matching, parameter validation, response formatting, and error handling, with support for PHP 8.1+ features like enums.

What is Donner?

Donner is a minimal PHP framework that aids in developing web applications and APIs by providing:

  • Dynamic Routing with Regex: Maps HTTP requests to controllers using URI patterns with placeholders (e.g., /user/{id}).
  • Request Validation: Validates and sanitizes input parameters and file uploads with type-safe methods.
  • Structured Responses: Ensures consistent response formats, including JSON, redirects, and paginated data.
  • Error Handling: Simplifies error reporting with custom exceptions and HTTP status codes.
  • File Upload Management: Handles file uploads with validation for size, type, and automatic cleanup.
  • Enum Support: Utilizes PHP 8.1 enums for robust HTTP status code definitions.

Purpose and Use Cases

Donner is ideal for:

  • Developers building lightweight web applications or APIs.
  • Projects requiring dynamic routing with regex-based URI patterns.
  • Applications needing robust input validation for query parameters and file uploads.
  • APIs that demand consistent response formats (e.g., JSON, paginated lists, redirects).
  • Projects leveraging PHP 8.1+ features like enums for type safety.
  • Small to medium-sized applications where simplicity and flexibility are key.

Getting Started with Donner

Installation via Composer

Ensure your PHP version is 8.1 or higher and the ext-json extension is enabled.

Install Donner via Composer by adding it to your composer.json or running:

composer require ji/donner

Recommended Project Structure

Organize your project as follows:

project/
├── composer.json
├── vendor/
│   └── autoload.php
├── index.php
└── src/
    ├── Controllers/
    │   ├── HomeController.php
    │   ├── UserController.php
    │   └── NotFoundController.php
    ├── Responses/
    │   └── CustomResponse.php
    ├── Router.php
    └── (Other application files)

Creating the Router

The router handles incoming requests and routes them to the appropriate controllers. Place it in src/Router.php.

Example Router.php:

<?php
// src/Router.php

namespace App;

use Donner\BasicRouter;
use App\Controllers\HomeController;
use App\Controllers\UserController;
use App\Controllers\NotFoundController;

class Router {
    public function handleRequest(): void {
        // Set headers
        header('Content-Type: application/json; charset=UTF-8');

        try {
            // Create router and add controllers
            $router = BasicRouter::create()
                ->addController(new HomeController())
                ->addController(new UserController())
                ->setNotFoundController(new NotFoundController());

            // Run the router
            $router->run();
        } catch (\Exception $exception) {
            // Handle exceptions and output error response
            http_response_code(500);
            echo json_encode([
                'error' => [
                    'error_code' => $exception->getCode(),
                    'error_message' => $exception->getMessage(),
                ]
            ]);
        }
    }
}

Explanation:

  • Headers: Sets JSON content type for API responses.

  • Router Setup: Instantiates BasicRouter, registers controllers, and sets a custom 404 handler.

  • Error Handling: Catches exceptions and returns a structured error response.

Then, in your index.php, instantiate and use the router:

Example index.php:

<?php
require_once 'vendor/autoload.php';

use App\Router;

$router = new Router();
$router->handleRequest();

Creating a New Controller

Controllers extend AbstractController and define routes using the URI constant. Each controller’s resolve method processes the request and returns a response.

Defining the Controller Class

Create a new class in src/Controllers/.

Example:

<?php
// src/Controllers/UserController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Response\MixedResponse;
use Donner\Utils\HTTPCode;

class UserController extends AbstractController {
    public const URI = '/user/{id}';
    public const ALLOWED_METHOD = self::METHOD_GET;

    public function resolve(): \Donner\Response\ResponseInterface {
        $userId = $this->params[0]; // Extracted from {id}
        return new MixedResponse(['user_id' => $userId], HTTPCode::OK);
    }
}

Implementing Controller Methods

Define the resolve method to handle the request logic. The URI pattern (e.g., /user/{id}) is matched using regex, and parameters are available in $this->params.

Controller: HomeController

Definition:

<?php
// src/Controllers/HomeController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Response\MixedResponse;
use Donner\Utils\HTTPCode;

class HomeController extends AbstractController {
    public const URI = '/';
    public const ALLOWED_METHOD = self::METHOD_GET;

    public function resolve(): \Donner\Response\ResponseInterface {
        return new MixedResponse(['message' => 'Welcome to Donner!'], HTTPCode::OK);
    }
}

Request Example:

GET / HTTP/1.1
Host: example.com

Response Example:

{
    "message": "Welcome to Donner!"
}

Controller: UserController

Definition:

<?php
// src/Controllers/UserController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Response\MixedResponse;
use Donner\Utils\HTTPCode;
use Donner\Exception\DonnerException;

class UserController extends AbstractController {
    public const URI = '/user/{id}';
    public const ALLOWED_METHOD = self::METHOD_GET;

    public function resolve(): \Donner\Response\ResponseInterface {
        $userId = $this->params[0];
        if (!is_numeric($userId)) {
            throw new DonnerException(
                DonnerException::INVALID_REQUEST,
                'User ID must be numeric',
                HTTPCode::BAD_REQUEST
            );
        }
        return new MixedResponse(['user_id' => (int)$userId], HTTPCode::OK);
    }
}

Request Example:

GET /user/123 HTTP/1.1
Host: example.com

Response Example:

{
    "user_id": 123
}

Error Response Example:

GET /user/abc HTTP/1.1
Host: example.com
{
    "error": {
        "error_code": 0,
        "error_message": "User ID must be numeric"
    }
}

Controller: UploadController

Definition:

<?php
// src/Controllers/UploadController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Response\SuccessResponse;
use Donner\Exception\DonnerException;
use Donner\Utils\HTTPCode;

class UploadController extends AbstractController {
    public const URI = '/upload';
    public const ALLOWED_METHOD = self::METHOD_POST;

    public function resolve(): \Donner\Response\ResponseInterface {
        $file = $this->request->getFile('photo')
            ->required('Photo is required')
            ->maxSize(2 * 1024 * 1024, 'Photo must be under 2MB')
            ->file();

        if ($file && $file->isImage()) {
            return new SuccessResponse();
        }
        throw new DonnerException(
            DonnerException::INVALID_REQUEST,
            'Invalid photo',
            HTTPCode::BAD_REQUEST
        );
    }
}

Request Example:

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="photo"; filename="image.jpg"
Content-Type: image/jpeg

(binary image data)
------WebKitFormBoundary--

Response Example:

{
    "success": true
}

Creating a New Response

Responses extend AbstractResponse and define the structure of data returned to the client. Public properties are automatically serialized to JSON.

Example:

<?php
// src/Responses/UserResponse.php

namespace App\Responses;

use Donner\Response\AbstractResponse;
use Donner\Utils\HTTPCode;

class UserResponse extends AbstractResponse {
    public int $id;
    public string $name;
    public string $status;

    public function __construct(int $id, string $name, string $status) {
        parent::__construct(HTTPCode::OK);
        $this->id = $id;
        $this->name = $name;
        $this->status = $status;
    }

    public static function create(int $id, string $name, string $status): self {
        return new self($id, $name, $status);
    }
}

Usage in Controller:

<?php
// src/Controllers/UserResponseController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Utils\HTTPCode;
use App\Responses\UserResponse;

class UserResponseController extends AbstractController {
    public const URI = '/user-response/{id}';
    public const ALLOWED_METHOD = self::METHOD_GET;

    public function resolve(): \Donner\Response\ResponseInterface {
        $userId = $this->params[0];
        return UserResponse::create((int)$userId, 'Jane Doe', 'active');
    }
}

Request Example:

GET /user-response/123 HTTP/1.1
Host: example.com

Response Example:

{
    "id": 123,
    "name": "Jane Doe",
    "status": "active"
}

Using Enums

Enums define a set of named constants, used in Donner for HTTP status codes via the HTTPCode enum.

Defining an Enum

Example:

<?php
// src/Enums/UserStatus.php

namespace App\Enums;

enum UserStatus: string {
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case BANNED = 'banned';
}

Using Enums in Controllers

Example:

<?php
// src/Controllers/StatusController.php

namespace App\Controllers;

use Donner\Controller\AbstractController;
use Donner\Response\MixedResponse;
use Donner\Utils\HTTPCode;
use App\Enums\UserStatus;
use Donner\Exception\DonnerException;

class StatusController extends AbstractController {
    public const URI = '/status/{id}';
    public const ALLOWED_METHOD = self::METHOD_POST;

    public function resolve(): \Donner\Response\ResponseInterface {
        $userId = $this->params[0];
        $status = $this->request->get('status')
            ->enum(
                array_map(fn($case) => $case->value, UserStatus::cases()),
                'Invalid status'
            );
        return new MixedResponse([
            'user_id' => $userId,
            'status' => $status
        ], HTTPCode::OK);
    }
}

Request Example:

POST /status/123 HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

status=active

Response Example:

{
    "user_id": "123",
    "status": "active"
}

Handling Requests

Requests are handled by BasicRouter, which matches the request URI and HTTP method to a controller’s resolve method.

Example in Router:
$router = BasicRouter::create()
    ->addController(new UserController())
    ->run();

Error Handling

Throwing Errors

Use DonnerException to throw errors with custom codes, messages, and HTTP status codes.

Example:

if (!is_numeric($userId)) {
    throw new DonnerException(
        DonnerException::INVALID_REQUEST,
        'User ID must be numeric',
        HTTPCode::BAD_REQUEST
    );
}

Common Error Codes:

  • DonnerException::INVALID_REQUEST (0): Generic invalid request error.

Handling Errors in the Router

Errors are caught in Router.php and returned as structured JSON responses.

Example Error Response:

{
    "error": {
        "error_code": 0,
        "error_message": "User ID must be numeric"
    }
}