adaiasmagdiel / erlenmeyer
Erlenmeyer is a lightweight PHP framework designed to create web applications simply and efficiently.
Requires
- adaiasmagdiel/hermes: ^1.0
README
Erlenmeyer is a lightweight PHP framework designed for simplicity and efficiency in building web applications. Inspired by the minimalism of Python's Flask, Erlenmeyer is not a direct clone but a unique solution tailored for PHP developers. Currently in its early stages, it's perfect for small projects, APIs, or microservices where a lean setup is preferred. I created Erlenmeyer to streamline my own projects, but it's open for anyone seeking a straightforward, no-frills framework.
Why Erlenmeyer?
- Lightweight: Minimal dependencies and a small footprint.
- Flexible: Easy to customize for small or medium-sized applications.
- Practical: Simplifies routing, middleware, and static asset handling.
- Developer-Friendly: Clear API with intuitive
Request
andResponse
classes.
Table of Contents
- Requirements
- Installation
- Features
- Get Started
- Configuring Web Server
- Minimal Application Example
- Using Middlewares
- Configuring CORS
- Serving Static Assets
- Dynamic Routes
- Advanced Usage
- Handling File Uploads
- Using Templates
- Testing
- Reference
- App Class
- Assets Class
- Request Class
- Response Class
- Error Handling
- Contributing
- License
Requirements
To use Erlenmeyer, ensure your environment meets the following:
- PHP 8.1 or higher
- Composer
- Web server: Apache with
mod_rewrite
or Nginx - PHP extensions:
json
,mbstring
- Optional:
getallheaders
for enhanced header support
Installation
Install Erlenmeyer via Composer:
composer require adaiasmagdiel/erlenmeyer
Ensure Composer is installed. After installation, include Erlenmeyer in your PHP project using Composer's autoload:
require_once 'vendor/autoload.php';
Features
Erlenmeyer provides essential tools for rapid web development:
-
Simple Routing: Define routes for HTTP methods (GET, POST, etc.), including dynamic routes with parameters.
-
Middlewares: Apply custom logic globally or per route.
-
CORS Support: Configure Cross-Origin Resource Sharing for APIs.
-
Request and Response Classes: Handle HTTP requests and responses securely.
-
Static Asset Serving: Serve CSS, JS, and images with caching and security.
Tip: Disable auto-serving for full control over static files.
Get Started
Configuring Web Server
Erlenmeyer requires URL rewriting to route requests through index.php
. Below are configurations for common web servers.
Apache (.htaccess)
Place the following .htaccess
file in your project root:
RewriteEngine On RewriteBase / # Serve existing files/directories directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [QSA,L]
Nginx
Add the following to your Nginx server block:
location / { try_files $uri $uri/ /index.php?$query_string; }
Note: Ensure your web server has write permissions for the assets directory if auto-serving is enabled.
Minimal Application Example
Create a simple application:
<?php require_once 'vendor/autoload.php'; use AdaiasMagdiel\Erlenmeyer\App; use AdaiasMagdiel\Erlenmeyer\Request; use AdaiasMagdiel\Erlenmeyer\Response; // With auto-serving enabled (default) $app = new App(); // With auto-serving disabled // $app = new App(autoServeAssets: false); $app->get('/', function (Request $req, Response $res, $params) { $res->withHtml("<h1>Welcome to Erlenmeyer!</h1>")->send(); }); $app->run();
This example:
- Defines a GET route for the root (
/
). - Sends a simple HTML response using the
Response
class.
Using Middlewares
Middlewares add logic before or after request processing. Example of a global middleware to measure execution time:
$app->addMiddleware(function (Request $req, Response $res, callable $next, $params) { $start = microtime(true); $next($req, $res, $params); $time = microtime(true) - $start; echo "<p>Execution time: $time seconds</p>"; });
For a route-specific middleware (e.g., authentication):
$app->get('/protected/[id]', function (Request $req, Response $res, $params) { $res->withHtml("<h1>Protected Route for ID: {$params->id}</h1>")->send(); }, [function (Request $req, Response $res, callable $next, $params) { if (!isset($_SESSION['user'])) { $res->redirect('/login'); } else { $next($req, $res, $params); } }]);
Note: Middlewares for dynamic routes receive a
$params
object containing route parameters (e.g.,$params->id
).
Configuring CORS
Set CORS for a specific route:
$app->get('/api/data', function (Request $req, Response $res, $params) { $res->setCORS([ 'origin' => 'https://my-site.com', 'methods' => ['GET', 'POST'], 'headers' => ['Content-Type'] ]); $res->withJson(['data' => 'example'])->send(); });
Apply CORS globally with a middleware:
$app->addMiddleware(function (Request $req, Response $res, callable $next, $params) { $res->setCORS([ 'origin' => ['https://my-site.com', 'https://api.my-site.com'], 'methods' => 'GET,POST', 'headers' => 'Content-Type', 'credentials' => true ]); $next($req, $res, $params); });
Tip: Avoid
origin: '*'
in production. Specify trusted domains for security.
Serving Static Assets
Erlenmeyer serves static files (e.g., CSS, JS, images) from the /public
directory by default, accessible at /assets/*
. The Assets
class ensures security with path traversal prevention and performance with caching headers (ETag
, Last-Modified
).
Customize the assets directory or route:
$app = new App(__DIR__ . '/my-assets', '/static');
To disable auto-serving:
$app = new App(autoServeAssets: false);
Dynamic Routes
Erlenmeyer supports dynamic routes with parameters, such as /user/[id]
or /blog/[category]/[slug]
. Parameters are extracted from the URL and passed to the route handler as a single object ($params
), where each parameter is accessible as a property (e.g., $params->id
).
Example:
$app->get('/user/[id]', function (Request $req, Response $res, $params) { if (!isset($params->id)) { $res->withError(400, 'Missing user ID')->send(); return; } $res->withJson(['user_id' => $params->id])->send(); }); $app->get('/blog/[category]/[slug]', function (Request $req, Response $res, $params) { if (!isset($params->category) || !isset($params->slug)) { $res->withError(400, 'Missing parameters')->send(); return; } $res->withJson([ 'category' => $params->category, 'slug' => $params->slug ])->send(); });
- How it works:
- For the URL
/user/123
, the handler receives$params->id = '123'
. - For
/blog/tech/my-post
, the handler receives$params->category = 'tech'
and$params->slug = 'my-post'
. - Parameters are automatically extracted from the route pattern and passed as an object.
- For the URL
Tip: Always validate
$params
properties to handle missing or invalid values.
Advanced Usage
Using Templates
Render HTML from a template file:
$app->get('/template', function (Request $req, Response $res, $params) { $res->withTemplate(__DIR__ . '/views/template.php', ['name' => 'User'])->send(); });
Example template (views/template.php
):
<!DOCTYPE html> <html> <head> <title>Erlenmeyer Template</title> </head> <body> <h1>Hello, <?php echo htmlspecialchars($name); ?>!</h1> </body> </html>
Reference
App Class
The App
class is the core of Erlenmeyer, managing routes, middlewares, and execution.
- Key Methods:
__construct(string $assetsDir = "/public", string $assetsRoute = "/assets", bool $autoServeAssets = true)
: Initializes the application.addMiddleware(callable $middleware)
: Adds a global middleware. The middleware receivesRequest
,Response
,callable $next
, and an optional$params
object for dynamic routes.set404Handler(callable $action)
: Sets a custom 404 handler.route(string $method, string $route, callable $action, array $middlewares = [])
: Registers a generic route. The$action
receivesRequest
,Response
, and a$params
object (stdClass) for dynamic routes (e.g.,/user/[id]
).get(string $route, callable $action, array $middlewares = [])
: Registers a GET route. The$action
receivesRequest
,Response
, and a$params
object for dynamic routes.post(string $route, callable $action, array $middlewares = [])
: Registers a POST route. The$action
receivesRequest
,Response
, and a$params
object for dynamic routes.run()
: Starts the application.
Assets Class
The Assets
class manages static file serving with security and caching.
- Key Methods:
isAssetRequest(): bool
: Checks if the request is for a static asset.serveAsset(): bool
: Serves the asset with headers (e.g.,Content-Type
,ETag
).detectMimeType(string $filePath): string
: Detects the MIME type based on file extension.
Request Class
The Request
class encapsulates HTTP request data and supports dependency injection for testing.
- Key Methods:
getHeader(string $name): ?string
: Returns a header value.getMethod(): string
: Returns the HTTP method.getUri(): string
: Returns the request URI.getQueryParams(): array
: Returns query parameters.getFormData(): array
: Returns POST form data.getJson(bool $assoc = true): mixed
: Returns decoded JSON body. ThrowsRuntimeException
if decoding fails.getRawBody(): ?string
: Returns the raw request body.getFiles(): array
: Returns uploaded files.isAjax(): bool
: Checks if the request is AJAX.isSecure(): bool
: Checks if the connection is HTTPS.
Response Class
The Response
class manages HTTP responses.
- Key Methods:
setStatusCode(int $code): self
: Sets the HTTP status code.setHeader(string $name, string $value): self
: Sets a header.setContentType(string $contentType): self
: Sets the content type.withHtml(string $html): self
: Sets HTML content.withJson($data, int $options = JSON_PRETTY_PRINT): self
: Sets JSON content. ThrowsRuntimeException
if serialization fails.withText(string $text): self
: Sets plain text content.redirect(string $url, int $statusCode = 302): self
: Redirects to a URL.withFile(string $filePath): self
: Sends a file as a download. UsesAssets::detectMimeType
for MIME type.setCORS(array $options): self
: Configures CORS headers.send(): void
: Sends the response.
Error Handling
Erlenmeyer throws exceptions in specific cases:
InvalidArgumentException
: Invalid parameters (e.g., invalid HTTP method, assets directory, or status code).RuntimeException
: Runtime issues (e.g., response already sent, JSON decoding/serialization errors, or unreadable files).
Use try-catch blocks to handle errors gracefully:
$app->get('/data', function (Request $req, Response $res, $params) { try { $data = $req->getJson(); $res->withJson(['result' => $data])->send(); } catch (RuntimeException $e) { $res->withError(400, $e->getMessage())->send(); } });
Contributing
Erlenmeyer is in its early stages, and contributions are welcome! To report bugs, suggest features, or submit pull requests, visit the GitHub repository.
License
Erlenmeyer is licensed under the GPLv3. See the LICENSE file for details.