Skip to content

Latest commit

Β 

History

131 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SopaipillaPHP

A lightweight, zero-dependency PHP 8 micro-framework for building JSON APIs.

PHP License


Features

  • Attribute-based routing β€” #[Get('/path')], #[Post('/path')], …
  • DTO validation β€” structured input validation with typed DTOs
  • PSR-4 autoloading β€” no Composer required at runtime
  • ORM base model β€” thin PDO wrapper with SQLite and MySQL support
  • Security by default β€” XSS sanitization, HTTP method whitelist, secure session cookies, AES-256-GCM encryption
  • Environment config β€” .env loader with no third-party dependencies
  • Zero dependencies β€” pure PHP 8.1+, ext-pdo, ext-mbstring

Requirements

Requirement Version
PHP 8.1 or higher
Extensions pdo, pdo_sqlite or pdo_mysql, mbstring, openssl
Web server Apache (.htaccess included) or PHP built-in server

Quick Start

# Clone the repository
git clone https://github.com/madkoding/sopaipilla-php.git
cd sopaipilla-php

# Copy and configure environment
cp .env.example .env

# Start the built-in server
php -S localhost:8000 index.php

Visit http://localhost:8000/api/health to confirm the app is running.


Project Structure

.
β”œβ”€β”€ index.php                   # Entry point β€” autoloader, env, security, router
β”œβ”€β”€ .env                        # Local environment variables (not committed)
β”œβ”€β”€ .env.example                # Environment template
β”œβ”€β”€ .htaccess                   # Apache rewrite rules
β”‚
β”œβ”€β”€ App/                        # Application code (your domain)
β”‚   β”œβ”€β”€ AppController.php       # Root and health endpoints
β”‚   β”œβ”€β”€ database.php            # Database config (reads from .env)
β”‚   └── Users/                  # Example resource module
β”‚       β”œβ”€β”€ UsersController.php
β”‚       β”œβ”€β”€ UsersModel.php
β”‚       └── DTO/
β”‚           β”œβ”€β”€ CreateUserDTO.php
β”‚           β”œβ”€β”€ UpdateUserDTO.php
β”‚           └── ChangePasswordDTO.php
β”‚
└── Sopaipilla/                 # Framework core (do not modify for app logic)
    β”œβ”€β”€ Env.php                 # .env loader
    β”œβ”€β”€ Http/
    β”‚   └── ApiController.php   # Base controller β€” security headers, helpers
    β”œβ”€β”€ Database/
    β”‚   └── Model.php           # PDO-based ORM base class
    β”œβ”€β”€ Routing/
    β”‚   β”œβ”€β”€ Router.php          # Attribute-based HTTP router
    β”‚   └── Attributes/         # Get, Post, Put, Patch, Delete
    β”œβ”€β”€ Security/
    β”‚   β”œβ”€β”€ Security.php        # Input sanitization, HTTP hardening
    β”‚   β”œβ”€β”€ Crypt.php           # AES-256-GCM encryption + Argon2ID hashing
    β”‚   └── Session.php         # Secure session management
    └── Validation/
        β”œβ”€β”€ Dto.php             # Abstract DTO base class
        β”œβ”€β”€ Validator.php       # Rule-based field validator
        └── ValidationException.php

Request Lifecycle

flowchart TD
    A([HTTP Request]) --> B["Apache / .htaccess<br/>rewrite to index.php"]
    B --> C["PSR-4 Autoloader<br/>registers namespaces"]
    C --> D["Env::load<br/>reads .env file"]
    D --> E["Security::cleanAll<br/>sanitize superglobals<br/>validate HTTP method"]

    E --> F{"Router::dispatch<br/>match route attribute"}

    F -- No match --> G([404 JSON response])

    F -- Match found --> H["Extract URL parameters<br/>inject into method args"]

    H --> I{"Write endpoint?<br/>POST / PUT / PATCH"}

    I -- "No / GET / DELETE" --> J["Model::all / find / delete<br/>PDO query"]

    I -- Yes --> K["withDto<br/>read JSON body"]
    K --> L{"Validator<br/>check rules"}
    L -- Invalid --> M([422 Validation error])
    L -- Valid --> N["Build DTO object<br/>typed properties"]
    N --> O["Model::create / update<br/>PDO query"]

    J --> P["Controller builds<br/>response array"]
    O --> P

    P --> Q(["JSON response<br/>HTTP status code"])
Loading

Environment Variables

Copy .env.example to .env and adjust to your environment:

# Application
APP_ENV=development
APP_NAME="SopaipillaPHP App"
RANDOM_SEED=change-this-to-a-long-random-secret

# Database driver: sqlite | mysql
DB_CONNECTION=sqlite

# SQLite (used when DB_CONNECTION=sqlite)
DB_DATABASE=:memory:

# MySQL (used when DB_CONNECTION=mysql)
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=sopaipilla
DB_USERNAME=root
DB_PASSWORD=
DB_CHARSET=utf8mb4

Note: RANDOM_SEED is required if you use Crypt::encrypt() / Crypt::decrypt(). A RuntimeException is thrown if it is undefined.


Creating a Resource Module

1. Model

// App/Posts/PostsModel.php
namespace App\Posts;

use Sopaipilla\Database\Model;

class PostsModel extends Model
{
    protected static string $table      = 'posts';
    protected static string $connection = 'sqlite';
    protected static array  $fillable   = ['title', 'body'];
    protected static array  $schema     = [
        'id    INTEGER PRIMARY KEY AUTOINCREMENT',
        'title TEXT    NOT NULL',
        'body  TEXT',
    ];
}

2. DTO

// App/Posts/DTO/CreatePostDTO.php
namespace App\Posts\DTO;

use Sopaipilla\Validation\Dto;

final class CreatePostDTO extends Dto
{
    public string $title;
    public string $body;

    protected static function rules(): array
    {
        return [
            'title' => ['required' => true, 'min' => 3, 'max' => 200],
            'body'  => ['required' => true],
        ];
    }

    protected static function build(array $data): static
    {
        $dto = new static();
        $dto->title = trim($data['title']);
        $dto->body  = trim($data['body']);
        return $dto;
    }
}

3. Controller

// App/Posts/PostsController.php
namespace App\Posts;

use Sopaipilla\Routing\Attributes\{Get, Post, Delete};
use Sopaipilla\Http\ApiController;
use App\Posts\DTO\CreatePostDTO;

class PostsController extends ApiController
{
    public function __construct()
    {
        parent::__construct();
        PostsModel::migrate();
    }

    #[Get('/api/posts')]
    public function index()
    {
        $data = PostsModel::all();
        return $this->json(['data' => $data, 'meta' => ['total' => count($data)]]);
    }

    #[Get('/api/posts/{id}')]
    public function show($id)
    {
        return $this->okOr404(PostsModel::find((int) $id), 'Post not found');
    }

    #[Post('/api/posts')]
    public function store()
    {
        return $this->withDto(CreatePostDTO::class,
            fn($dto) => $this->okOr201(PostsModel::create($dto->toArray()))
        );
    }

    #[Delete('/api/posts/{id}')]
    public function destroy($id)
    {
        return $this->okOr404(PostsModel::delete((int) $id), 'Post not found');
    }
}

4. Register the controller

// index.php
use App\Posts\PostsController;

$router->registerController(new PostsController());

ApiController Helpers

All controllers extending ApiController have access to:

Method Description
$this->json($data, $status) JSON response with success: true
$this->error($message, $status) JSON error response
$this->okOr201($data) 201 Created or 500 on falsy
$this->okOr404($data, $msg) 200 OK or 404 Not Found on falsy
$this->withDto($class, $fn) Validate input via DTO, then execute callback
$this->input() Read and sanitize JSON request body

Routing

Routes are defined via PHP 8 Attributes on controller methods:

#[Get('/api/resource')]
#[Post('/api/resource')]
#[Put('/api/resource/{id}')]
#[Patch('/api/resource/{id}')]
#[Delete('/api/resource/{id}')]

URL parameters are injected as method arguments in order:

#[Get('/api/users/{userId}/posts/{postId}')]
public function show($userId, $postId) { ... }

Validation Rules

Rule Type Description
required bool Field must be present and non-empty
email bool Must be a valid email address
min int Minimum string length
max int Maximum string length
numeric bool Must be numeric
regex string Must match the given pattern
in array Must be one of the allowed values

Security

Layer Implementation
Input sanitization XSS patterns stripped from all superglobals on boot
HTTP method whitelist TRACE, CONNECT and custom methods return 405
Null byte detection Requests with null bytes in query string or body are rejected
Session cookies httponly, samesite=Lax, secure (when HTTPS)
HTTP security headers X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy
Encryption AES-256-GCM (authenticated) β€” prevents padding oracle and bit-flipping
Password hashing Argon2ID via password_hash()
Token generation random_bytes() (CSPRNG)

Available Endpoints (example app)

Method Path Description
GET / HTML index page
GET /api/health Application status
GET /api/users List all users
GET /api/users/{id} Get a user
GET /api/users/{id}/profile Get enriched user profile
POST /api/users Create a user
PUT /api/users/{id} Update a user
PATCH /api/users/{id}/password Change user password
DELETE /api/users/{id} Delete a user

License

MIT Β© madKoding


πŸš€ Quick Start (3 minutes)

1. Install

git clone https://github.com/madkoding/sopaipilla-php my-project
cd my-project

2. Run

php -S localhost:8000 index.php

3. Open your browser


πŸ“‚ Project Structure

my-project/
β”œβ”€β”€ App/              # Your application code
β”‚   β”œβ”€β”€ database.php  # Database config (reads from .env)
β”‚   └── Users/        # Example resource module
β”œβ”€β”€ Sopaipilla/       # Framework core (do not modify for app logic)
β”œβ”€β”€ index.php         # Single entry point
β”œβ”€β”€ .htaccess         # Apache rewrite rules
└── .env              # Environment variables

πŸ”Œ Database Connections

MySQL

// App/database.php
'mysql' => [
    'driver'   => 'mysql',
    'host'     => 'localhost',
    'database' => 'my_db',
    'username' => 'root',
    'password' => '',
],

PostgreSQL

'pgsql' => ['driver' => 'pgsql', 'host' => 'localhost', 'database' => 'my_db']

SQLite

'sqlite' => ['driver' => 'sqlite', 'database' => ':memory:']

SQL Server

'sqlsrv' => ['driver' => 'sqlsrv', 'host' => 'localhost', 'database' => 'my_db']

πŸ”Œ Endpoint Matrix

URL GET POST PUT DELETE
/api/users List all Create - -
/api/users/{id} Get one - Update Delete

πŸ“ Database Usage Example

use Sopaipilla\Database\Model;

// All records
$users = UsersModel::all();

// Find by ID
$user = UsersModel::find(1);

// Create
$created = UsersModel::create(['name' => 'John', 'email' => 'john@test.com']);

// Update
$updated = UsersModel::update(1, ['name' => 'John']);

// Delete
UsersModel::delete(1);

βœ… Why Sopaipilla?

  • Simple: no configuration overhead, just add a controller
  • Zero dependencies: pure PHP 8.1+ with PDO, no Composer required at runtime
  • Multi-DB: MySQL, PostgreSQL, SQLite, SQL Server
  • Secure by default: AES-256-GCM, Argon2ID, CSPRNG, XSS sanitization
  • Modern routing: PHP 8 Attributes β€” no route files needed

Get started now!

git clone https://github.com/madkoding/sopaipilla-php my-project
cd my-project && php -S localhost:8000 index.php

Última actualización: 2026-02-26 15:51:19 -03

About

SopaipillaPHP is a lightweight, zero-dependency PHP 8 micro-framework for building JSON APIs. Features PHP 8 attribute-based routing, structured input validation through typed DTOs, lightweight ORM with multi-database support, and security-by-default principles.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages