Documentation
Complete implementation guide for Narciso: routing, middleware, database, API, and views.
Installation
Install via Composer:
composer require marcuwynu23/narciso
Requires PHP 7.4+ (or as required by your environment). No other dependencies.
Quick start
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Marcuwynu23\Narciso\Application;
$app = new Application();
$app->setViewPath(__DIR__ . '/views');
$app->route('GET', '/', function ($app) {
$app->json(['message' => 'Welcome to Narciso']);
});
$app->run();
Run with PHP built-in server:
php -S localhost:8080 -t public
(or your document root).
Application
The Application class is the single entry point. Key methods:
setViewPath(string $path)— Directory for view files (e.g.views/).run()— Execute middleware stack and dispatch to the matching route. Call once at the end of your entry script.
When no route matches, Narciso returns 404 with Content-Type: application/json and body {"error":"Not Found","path":"/requested-path"}.
Routing
Registering routes
route(string $method, string $path, callable $callback): self
Method: GET, POST, PUT, PATCH, DELETE (case-insensitive).
Path: Exact path or named parameters with :name (e.g. /users/:id, /posts/:slug/comments/:cid). Param names must match [a-zA-Z_][a-zA-Z0-9_]*.
Callback: function (Application $app, array $params). $params contains captured path parameters (e.g. ['id' => '42']).
Examples
// Exact path
$app->route('GET', '/', function ($app) {
$app->json(['message' => 'Hello']);
});
// Path parameters
$app->route('GET', '/users/:id', function ($app, $params) {
$app->json(['user_id' => $params['id']]);
});
$app->route('GET', '/posts/:slug/comments/:cid', function ($app, $params) {
$app->json([
'post_slug' => $params['slug'],
'comment_id' => $params['cid'],
]);
});
// POST with JSON body
$app->route('POST', '/echo', function ($app) {
$body = $app->requestPost();
$app->json(['received' => $body]);
});
Matching uses the request URI path (without query string). With the built-in server, the script name may be part of the path (e.g. /index.php/); design your routes accordingly or use a front controller.
Middleware
Middleware runs in order before the route handler. First added = first executed (before request), last runs closest to the response.
use($middleware)
Accepts:
- Callable:
function ($app, $next) { ...; return $next(); }. Call$next()to continue the pipeline. - MiddlewareInterface: Class with
handle(callable $next)that calls$next()to continue.
$app->use(function ($app, $next) {
// before
$result = $next();
// after
return $result;
});
$app->use(new class implements \Marcuwynu23\Narciso\Middleware\MiddlewareInterface {
public function handle(callable $next) {
// before
return $next();
}
});
useSecurityHeaders(?array $headers = null)
Adds security-related HTTP headers to every response. Default headers:
X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: geolocation=(), microphone=(), camera=()
Pass a custom ['Header-Name' => 'value'] array to override, or null for defaults.
useCors(...)
Configurable CORS middleware. Parameters:
$origins— e.g.['*']or['https://app.example.com']$methods— e.g.['GET','POST','PUT','PATCH','DELETE','OPTIONS']$headers— e.g.['Content-Type','Authorization','X-Requested-With','Accept']$credentials— allow credentials (defaultfalse)$maxAge— preflight cache in seconds (default86400)
$app->useCors(
['*'],
['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'],
false,
86400
);
On OPTIONS requests the middleware responds with 204 and exits.
useRateLimit(int $maxRequests, int $windowSeconds)
Rate limit per client (by IP; respects X-Forwarded-For). In-memory store — use Redis or similar in production for multi-process.
$app->useRateLimit(60, 60); // 60 requests per 60 seconds
When exceeded, responds with 429 Too Many Requests, Retry-After header, and JSON body {"error":"Too Many Requests","retry_after":60}.
Database
handleDatabase(array $config): self
Connects and exposes $app->db (mysqli for MySQL, SQLite3 for SQLite).
MySQL
$app->handleDatabase([
'type' => 'mysql',
'host' => 'localhost',
'database' => 'mydb',
'user' => 'user', // or 'username'
'password' => 'secret',
]);
$app->db is a mysqli instance. Throws RuntimeException on connection failure.
SQLite
$app->handleDatabase([
'type' => 'sqlite',
'database' => '/path/to/file.sqlite', // or ':memory:'
]);
$app->db is an SQLite3 instance. Requires the sqlite3 extension.
Request
requestPost(): ?array— Readsphp://input, JSON-decodes, and returns an array ornullif invalid/empty. Use for POST/PUT/PATCH JSON bodies.requestRaw(): string— Returns the raw request body as a string.
Responses
json($data, int $statusCode = 200)
Sets Content-Type: application/json; charset=utf-8, status code, and outputs json_encode($data).
sendAPI($data, array $options = [])
Responds as JSON or XML. Format can be forced or auto-detected from ?format=json|xml or Accept header. Options:
format—'json','xml', ornull(auto)statusCode— HTTP status (default200)root— XML root tag (default'response')xmlItemName— Tag name for list items (default'item')
$data can be array or object; for XML it is converted to a tree. Numeric arrays become repeated xmlItemName elements.
getPreferredApiFormat(): string
Returns 'json' or 'xml' from query ?format= or Accept header. Default 'json'.
arrayToXml(array $data, string $rootTag, string $itemTag): string
Public helper to convert an array to an XML string (UTF-8, SimpleXMLElement).
Views
setViewPath(string $path) — Base directory for view files.
render(string $view, array $data = []): void — Includes a PHP file from the view path. $view is the path without .php (e.g. /home.view or /about/about.view). Variables are extracted into the view (e.g. 'message' => 'Hi' becomes $message).
$app->setViewPath(__DIR__ . '/views');
$app->route('GET', '/', function ($app) {
$app->render('/home.view', ['message' => 'Hello']);
});
// includes views/home.view.php with $message
Redirect
redirect(string $url, int $statusCode = 302): void — Sends Location header and exits. Use for redirects.
Technology signature (X-Powered-By)
setPoweredBy($value): self
false— RemoveX-Powered-By(recommended for production).''— Send blank value.'Express'(or any string) — Custom value.null— Leave server default unchanged.
Applied when run() is called.
Session
handleSession(string $name = "Narciso"): self — Sets session name and calls session_start(). Use when you need $_SESSION.
Deprecated CORS: handleCORS() reflects the request Origin and allows credentials. Prefer useCors() for configurable CORS.
Full example
Combining routing, middleware, database, API, and views:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Marcuwynu23\Narciso\Application;
$app = new Application();
$app->setViewPath(__DIR__ . '/views');
$app->setPoweredBy(false);
$app->handleSession('MyApp');
$app->useSecurityHeaders();
$app->useCors(['*'], ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], ['Content-Type', 'Authorization']);
$app->useRateLimit(60, 60);
$app->handleDatabase(['type' => 'sqlite', 'database' => ':memory:']);
$app->db->exec('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)');
$app->db->exec("INSERT OR IGNORE INTO items (id, name) VALUES (1, 'Alpha'), (2, 'Beta')");
$app->route('GET', '/', function ($app) {
$app->json(['app' => 'Narciso', 'routes' => ['GET /api', 'GET /api/items', 'GET /page']]);
});
$app->route('GET', '/api', function ($app) {
$app->sendAPI(['message' => 'API', 'time' => date('c')]);
});
$app->route('GET', '/api/items', function ($app) {
$res = $app->db->query('SELECT id, name FROM items');
$rows = [];
while ($row = $res->fetchArray(SQLITE3_ASSOC)) $rows[] = $row;
$app->sendAPI(['items' => $rows]);
});
$app->route('GET', '/api/items/:id', function ($app, $params) {
$id = (int) $params['id'];
$stmt = $app->db->prepare('SELECT id, name FROM items WHERE id = :id');
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$r = $stmt->execute();
$row = $r->fetchArray(SQLITE3_ASSOC);
if (!$row) { $app->json(['error' => 'Not found'], 404); return; }
$app->sendAPI($row);
});
$app->route('GET', '/page', function ($app) {
$app->render('/home.view', ['message' => 'Hello']);
});
$app->run();