Important
Learning Project Disclaimer
This repository is a personal self-learning project built from scratch to explore low-level architectural patterns in PHP. To maximize learning outcomes, little to no AI assistance was utilized during its implementation. This is also not intended for production environments.
Rather than relying on modern heavyweight frameworks, this project implements a lightweight custom framework architecture in Vanilla PHP to handle routing, dependency injection, and database abstraction.
The routing layer uses custom attributes (#[Route]) to map HTTP pathways directly to single-action controllers (invokables).
- Registration: Routes are registered dynamically in
config/actions.php. - Attribute Parsing: Uses PHP Reflection to extract attributes (
#[Route(path: '...', method: '...')]), map them to controller invoke methods, and evaluate required method parameters. - Middleware Pipeline: Routes can declare middleware callbacks (e.g., check login status or anonymous routing).
- Authentication Guard: Routes starting with
/api/are automatically intercepted and authorized via the API authentication system.
An autowiring DI container that recursively resolves dependencies using reflection:
- Constructor dependencies are automatically instantiated and injected.
- Shared instances (e.g.,
ContainerandRequest) are registered globally during bootstrap.
To support customizable entities, the project implements a dynamic table-per-field strategy:
- When a tenant creates a new field (e.g., slug
subtitle), the system automatically runs a DDL query to generate a dedicated table namedfield_data_[field_slug](e.g.,field_data_subtitle). - This dynamic table stores user-specific, context-specific field values mapped back to the primary content entities, providing database-level isolation.
Provides query-building utilities to automate base database operations:
findAll(): Automatically resolves dynamically generated WHERE conditions, query bindings, offsets, and paginates responses by fetchinglimit + 1rows to determine if a next page exists.buildWhereClauses(): Converts PHP arrays into parameterized SQL statements supporting various operators (=,!=,LIKE,IN,BETWEEN,IS NULL, etc.).
- Rate Limiting (
src/Core/RateLimiter.php): A session-based rate limiter using IP-based and email-based key hashing to throttle high-frequency requests (e.g., registration attempts). - CSRF Protection (
src/Core/CsrfToken.php): Generates and validates CSRF tokens to secure web-based POST requests. - API Authentication (
src/Core/ApiAuth.php): Protects/api/*endpoints. It parses standard HTTP Basic Authentication where the credentials are:The system matches the token and verified host header origins with the tenant's registered API keys.Authorization: Basic base64(email:api_token)
The initial system migration (migration/000.init.php) creates the core relational tables:
erDiagram
USER ||--o{ VERIFICATION : "has"
USER ||--o{ API_KEY : "has"
USER ||--o{ CONTENT_TYPE : "owns"
USER ||--o{ CONTENT : "owns"
CONTENT_TYPE ||--o{ FIELD : "defines"
CONTENT_TYPE ||--o{ CONTENT : "groups"
user: Stores account details (email, hashed password, role, verification status).verification: Holds short-lived verification tokens for email confirmation.api_key: Stores user API keys, mapped to a uniquesite_hostvalue for domain restriction.content_type: Represents custom collections defined by tenants (e.g., "blog posts", "products").field: Lists custom attributes assigned to a content type (text, longtext, number, date, time, datetime, email, entity_reference).content: Serves as the base record index for a custom content entry.field_data_[field_slug](Dynamic): Dynamically generated database tables that hold values for custom fields.
| Path | Methods | Middlewares | Description |
|---|---|---|---|
/ |
GET |
None | Application landing page |
/login |
GET, POST |
isAnonymous |
User login page / session creation |
/register |
GET, POST |
isAnonymous |
Tenant registration (rate-limited) |
/dashboard |
GET, POST |
isLoggedIn |
Admin developer console & API key creator |
/logout |
GET |
isLoggedIn |
Logs out the current user session |
All endpoints below are authenticated using API Basic auth (Authorization: Basic base64(email:api_token)) and require an Origin header.
| Path | Method | Description | Payload Example / Query Params |
|---|---|---|---|
/apikeys/create |
POST |
Create a new domain-restricted API Key | {"host": "example.com"} |
/api/content-types/create |
POST |
Create a custom content type | {"label": "Articles"} |
/api/content-types |
GET |
List all available content types | Query: limit, offset |
/api/fields/create |
POST |
Create a new custom field & dynamic table | {"contentTypeId": 1, "label": "Subtitle", "type": "text"} |
/api/fields |
GET |
List all custom fields | Query: limit, offset |
/api/fields/save-data |
POST |
Save custom field value to dynamic database table | {"fieldId": 1, "contentTypeId": 1, "contentId": 5, "value": "A Cool Post"} |
/api/content/create |
POST |
Instantiate a new content record | {"label": "Post Title", "contentTypeId": 1} |
/api/contents |
GET |
List all content instances | Query: limit, offset |
/api/contents/update |
PATCH/PUT |
Update content properties | {"args": {"label": "New Title"}, "conditions": {"id": 5}} |
/api/users/create |
POST |
Programmatic user registration | {"email": "...", "password": "..."} |
/api/test |
GET |
API testing / health check | None |
The project uses DDEV for local environment virtualization.
- Docker / Docker Desktop
- DDEV CLI
Clone the repository and start the virtual container system:
ddev startInstall Composer dependencies inside the DDEV environment:
ddev composer installA helper script is provided to completely refresh the local MySQL database and run the migrations:
chmod +x fresh-migrate.sh
./fresh-migrate.shOnce running, the application is reachable at:
- Web dashboard:
https://php-rest-api.ddev.site(or the URL outputted byddev start) - Database connection details are automatically managed by DDEV environment variables.