ali-rahimpoor / armm-framework
Agile REST Micro Module — a lightweight PHP framework for building REST APIs.
README
A lightweight PHP framework designed for building REST APIs. ARMM provides routing, dependency injection, middleware, authentication, and database connectivity, allowing you to focus directly on your application's business logic.
Features
- Regex-based Routing with support for parameters (
/projects/{id}) and route groups - Dependency Injection Container with auto-wiring through Reflection — no need to manually construct dependency chains
- Middleware Pipeline for authentication, CORS, and other shared application logic
- Request/Response Objects that provide unified handling for JSON and traditional form requests
- JsonResponse with a consistent response format for success and error responses
- HttpException for throwing meaningful HTTP errors from any layer of the application
- Session-based Authentication, suitable for applications with a limited number of users, such as personal admin panels
- Image Upload Handling with real MIME-type validation, unique file naming, configurable storage path, and automatic thumbnail generation via GD
- Explicit Config Errors when accessing missing configuration keys instead of silently returning
null - Simple Logger for recording errors and application events
Installation
composer require ali-rahimpoor/armm-framework
Or, until the package is published on Packagist, install it directly from GitHub:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/your-username/armm-framework" }
],
"require": {
"armm/framework": "dev-main"
}
}
Quick Start
// public/index.php require __DIR__ . '/../vendor/autoload.php'; use ARMM\Application; $app = new Application(basePath: dirname(__DIR__)); $app->loadRoutes(__DIR__ . '/../routes/api.php'); $app->run();
// routes/api.php use App\Controllers\ProjectController; use ARMM\Middleware\AuthMiddleware; $router->get('/projects', [ProjectController::class, 'index']); $router->get('/projects/{id}', [ProjectController::class, 'show']); $router->group([AuthMiddleware::class], function ($admin) { $admin->post('/projects', [ProjectController::class, 'store']); $admin->put('/projects/{id}', [ProjectController::class, 'update']); $admin->delete('/projects/{id}', [ProjectController::class, 'destroy']); });
// app/Controllers/ProjectController.php use ARMM\Exceptions\HttpException; use ARMM\Http\JsonResponse; use ARMM\Http\Request; final class ProjectController { public function __construct(private ProjectService $service) {} // The Container resolves it automatically public function index(Request $request): JsonResponse { return JsonResponse::success($this->service->getAll()); } public function show(Request $request): JsonResponse { $project = $this->service->find((int) $request->routeParam('id')); if (!$project) { throw HttpException::notFound('Project not found'); } return JsonResponse::success($project); } }
A complete, runnable example is available in examples/mini-api.
Configuration
Files under config/*.php must return an associative array. The filename becomes the configuration group name:
// config/app.php return [ 'timezone' => 'Asia/Tehran', 'cors_allowed_origins' => ['http://localhost:3000'], ];
$app->config()->get('app', 'timezone'); // 'Asia/Tehran', or throws an Exception if the key is missing $app->config()->getOr('app', 'debug', false); // Returns the default value if the key is missing
CORS
If you define the cors_allowed_origins key in config/app.php, Application::boot() automatically wires the CorsMiddleware with the configured origins — no manual binding is required.
You only need to add this middleware to the routes that should be accessible from your frontend:
$router->get('/projects', [ProjectController::class, 'index']) ->middleware(CorsMiddleware::class);
If you want to control the default behavior yourself, such as allowedMethods or allowedHeaders, you can explicitly override the binding before $app->run():
$app->container()->bind(CorsMiddleware::class, function ($c) { return new CorsMiddleware( allowedOrigins: ['https://example.com'], allowedMethods: ['GET', 'POST'], ); });
File Uploads
ARMM ships with an ARMM\Storage namespace for handling image uploads: UploadedFile (a typed wrapper around a raw $_FILES entry), FileValidator (real MIME-type and size validation), ImageProcessor (resize/thumbnail via GD), and FileStorage (unique naming, configurable disk path, and public URL resolution). FileValidator and FileStorage are auto-wired by the Container, so you only need to type-hint them in your controller's constructor.
Images must always be sent as multipart/form-data, separate from any JSON body:
const form = new FormData(); form.append('image', fileInput.files[0]); fetch('/products/images', { method: 'POST', body: form });
// app/Controllers/ImageUploadController.php use ARMM\Http\JsonResponse; use ARMM\Http\Request; use ARMM\Storage\FileStorage; use ARMM\Storage\FileValidator; final class ImageUploadController { public function __construct( private FileValidator $validator, private FileStorage $storage ) {} public function upload(Request $request): JsonResponse { $image = $request->uploadedFile('image'); $this->validator->validate($image); // throws HttpException::validation(422) if invalid $result = $this->storage->store($image, subdirectory: 'products', thumbnailSize: 200); return JsonResponse::created([ 'path' => $result['path'], 'url' => $result['url'], 'thumbnail_url' => $result['thumbnail_url'], ]); } }
Storage rules are configurable per project via config/storage.php; any key you omit falls back to a sensible default:
// config/storage.php return [ 'upload_path' => __DIR__ . '/../public/uploads', // default: "public/uploads" 'allowed_mime_types' => ['image/jpeg', 'image/png', 'image/webp', 'image/gif'], 'max_size_bytes' => 5 * 1024 * 1024, // 5MB ];
By default, files are stored under public/uploads, so store() returns a url you can use directly (e.g. <img src="...">). Setting upload_path outside of public/ returns null for url instead, since those files should be served through a dedicated route with your own access control.
A complete working example is available in examples/mini-api/app/Controllers/ImageUploadController.php.
Core Architecture
| Path | Responsibility |
|---|---|
src/Routing/ |
Route definition, registration, and matching |
src/Http/ |
Request, Response, and JsonResponse |
src/Middleware/ |
Middleware contract and Auth/CORS implementations |
src/Container/ |
Dependency Injection Container with auto-wiring |
src/Database/ |
Singleton PDO connection |
src/Config/ |
Configuration loading and access |
src/Auth/ |
Session-based authentication |
src/Storage/ |
Image upload validation, storage, and thumbnail generation |
src/Logging/ |
File-based error and event logging |
src/Exceptions/ |
HttpException for meaningful HTTP errors |
src/Application.php |
Central application entry point that ties everything together |
For an explanation of the architectural decisions — such as why the Container, Middleware, and explicit configuration errors are used — refer to the comments at the top of each class. Each class documents the reasoning behind its design.
Testing
php tests/manual_e2e_test.php
This script tests the complete Router → Middleware → Container → Response lifecycle using 20 real-world scenarios, without requiring a separate testing framework.
Versioning and Publishing to Packagist
-
Finalize
composer.json(name, description, license, etc.) -
Create a version tag:
git tag v1.0.0 && git push --tags -
Sign up at packagist.org and submit your GitHub repository
-
For future releases, simply create a new version tag; Packagist will detect the new release automatically
License
MIT