Restify-PHP is a native PHP 8+ micro-framework focused on simplicity, performance, and zero dependencies. Drop it into any environment, point the server at public/, and build APIs without Composer, scaffolding, or heavy bootstrapping.
Restify-PHP embraces three core principles:
Features at a glance:
class/ for instant availability.api/ plus attribute-driven routes with metadata.restify/packages) and Docker/docker-compose templates.php -S localhost:8000 -t public
Visit http://localhost:8000 to see the default JSON welcome payload.
GET /health returns a deployment-friendly heartbeat ({"status":"ok"}).
php restify-cli --help
php restify-cli run --host 0.0.0.0 --port 8080 --async
php restify-cli make:class App\Domain\UserService
php restify/tests/run.php
php restify-cli docs:openapi --format yaml --serve --port 8081
php restify-cli test --phpunit
composer require restify-php/restify-php
php vendor/bin/restify install
php restify-cli run --async
restify/ # Framework internals (bootstrap, config, routes, src, support, packages, tests)
bootstrap/ # Application bootstrapper
config/ # Framework configuration (middleware etc.)
routes/ # Route declarations (web.php, api.php, attributes)
src/ # Restify core
support/ # Framework utilities (Async facade, DB helper, ClassLoader, Env)
packages/ # Optional drop-in packages (autoloaded automatically)
tests/ # Lightweight test runner & specs
api/ # File-based API endpoints (auto-registered)
class/ # Your domain code (autoloaded & globally available)
docs/ # Documentation, generated OpenAPI specs, Swagger UI
public/ # Web document root (contains index.php & .htaccess)
storage/ # Logs, cache, temp directories
restify-cli # Optional CLI entry script
class/. Restify loads every .php file in this directory at startup.public/ (index.php, .htaccess, static files).restify/ as framework internals—modify with care.restify/packages are automatically autoloaded (see Packages & Contributions).public/index.php boots the application via restify/bootstrap/app.php.restify/bootstrap/app.php defines RESTIFY_BASE_PATH (framework) and RESTIFY_ROOT_PATH (project), registers the custom autoloader, loads .env, and returns Restify\Core\Application.Application:
restify/config/middleware.php.restify/routes/web.php, restify/routes/api.php, route attributes inside class/, and endpoints declared under api/.bootstrap.php files located under restify/packages/*.Restify\Http\Request objects and flow through the MiddlewarePipeline.Router matches paths and normalises handler output into Restify\Http\Response.Restify automatically loads two route files if present:
restify/routes/web.php – default web endpoints.restify/routes/api.php – API-specific routes (commonly prefixed with /api).Each file should return a closure receiving the shared Router instance.
<?php
use Restify\Http\Request;
use Restify\Routing\Router;
return static function (Router $router): void {
$router->get('/', static fn () => ['message' => 'Hello from Restify']);
$router->get('/api/health', static fn () => ['status' => 'ok']);
$router->post('/api/users', static function (Request $request) {
return ['data' => $request->body];
});
};
Drop PHP files into api/ for switch-like endpoints without touching the router. Restify derives the path from the file name, injects an $api helper, and registers method handlers automatically.
<?php
use Restify\Http\Request;
$api->get(static fn () => ['message' => 'List posts']);
$api->post(static fn (Request $request) => [
'created' => $request->body,
]);
$api->path('/api/posts/{id}')
->get(static fn (Request $request, int $id) => ['post' => $id])
->delete(static fn (int $id) => ['deleted' => $id]);
Alternatively, return a configuration array:
<?php
return [
'PATH' => '/api/ping',
'GET' => static fn () => ['pong' => true],
'POST' => static fn () => ['pong' => 'created'],
'FALLBACK' => static fn () => Restify\Http\Response::json([], 405, message: 'Try GET or POST.'),
];
api/users.php → /api/users, nested api/admin/index.php → /api/admin).GET, POST, PUT, PATCH, DELETE, OPTIONS, and ANY map HTTP verbs to handlers.PATH to override the generated URI and FALLBACK to customise unsupported method responses.Decorate public methods in class/ with the #[Route] attribute for attribute-driven routing.
<?php
declare(strict_types=1);
namespace App\Blog;
use Restify\Http\Request;
use Restify\Routing\Attributes\Route;
final class PostController
{
#[Route('GET', '/api/posts')]
public function index(): array
{
return ['posts' => $this->all()];
}
#[Route(['GET', 'HEAD'], '/api/posts/{id}')]
public function show(Request $request, int $id): array
{
return ['post' => $this->find($id)];
}
#[Route('POST', '/api/posts')]
public static function store(Request $request): array
{
return ['created' => $request->body];
}
}
public instance methods (constructor without required arguments) or public static.Handlers may return:
Restify\Http\Response for granular control.text/plain).Default JSON envelope:
{
"ok": true,
"status": 200,
"message": null,
"data": { },
"meta": { }
}
Both route files and attributes accept an optional metadata array that feeds the middleware pipeline, OpenAPI generator, and JSON Schema validator.
// restify/routes/api.php
$router->post('/api/users', static fn (Request $request) => create_user($request), [
'summary' => 'Create a user',
'tags' => ['Users'],
'request' => [
'schema' => 'CreateUser', // references config/schemas.php
'source' => 'body',
],
'responses' => [
'201' => [
'description' => 'User created',
'schema' => ['type' => 'object', 'properties' => ['id' => ['type' => 'integer']]],
],
'422' => ['description' => 'Validation failed'],
],
]);
For attributes, pass metadata using named parameters:
#[Route(
methods: 'POST',
path: '/api/posts',
summary: 'Publish a post',
request: ['schema' => 'PostPayload'],
responses: [
'201' => ['description' => 'Post stored'],
'422' => ['description' => 'Validation errors'],
]
)]
public function store(Request $request): array
{
// ...
}
restify/config/schemas.php using standard JSON Schema fragments.source controls validation target: body (default), query, headers, or cookies.422 JSON payload listing validation errors.return Restify\Http\Response::json(
['user' => $user],
status: 201,
meta: ['request_id' => $request->headers['X-Request-Id'] ?? null],
message: 'User created.'
);
return Restify\Http\Response::text('Plain content', status: 204);
Global middleware is configured in restify/config/middleware.php. Restify ships with a production-ready stack that covers observability, safety, and developer experience:
| Middleware | Purpose | Config source |
|---|---|---|
Restify\Middleware\ExceptionMiddleware |
Converts uncaught exceptions into JSON envelopes, optional stack traces, logs incidents | restify/config/exceptions.php |
Restify\Middleware\CorsMiddleware |
Applies CORS headers, preflight caching, credential rules | restify/config/cors.php |
Restify\Middleware\RateLimitMiddleware |
APCu-backed rate limiting with per-IP buckets | .env (RATE_LIMIT_*) |
Restify\Middleware\AuthenticationMiddleware |
Header-driven token/JWT enforcement, per-endpoint secrets | restify/config/auth.php |
Restify\Middleware\LoggingMiddleware |
Structured request/response logging to file and database with redaction + duration | restify/config/logging.php |
restify/config/middleware.php wires each middleware with constructor parameters:
<?php
use Restify\Middleware\AuthenticationMiddleware;
use Restify\Middleware\CorsMiddleware;
use Restify\Middleware\ExceptionMiddleware;
use Restify\Middleware\LoggingMiddleware;
use Restify\Middleware\RateLimitMiddleware;
use Restify\Support\Config;
$exceptions = Config::get('exceptions', []);
$cors = Config::get('cors', []);
$auth = Config::get('auth', []);
$logging = Config::get('logging', []);
return [
'global' => [
[ExceptionMiddleware::class, [$exceptions]],
[CorsMiddleware::class, [$cors]],
[RateLimitMiddleware::class, [
'limit' => (int) ($_ENV['RATE_LIMIT_MAX'] ?? 60),
'seconds' => (int) ($_ENV['RATE_LIMIT_WINDOW'] ?? 60),
]],
[AuthenticationMiddleware::class, [$auth]],
[LoggingMiddleware::class, [$logging]],
],
];
Restify\Middleware\MiddlewareInterface.[ClassName::class, [arg1, arg2]].Add custom middleware by pushing to the global array or by building route-specific pipelines before dispatching.
Restify\Support\Async wraps the Fiber-based event loop (Restify\Core\Async) and exposes helpers:
Async::run(callable $callback)Async::parallel(array $callbacks)Async::http(string|array $request, array $options = [])Async::socket(string $host, int $port, string $payload = '', float $timeout = 5.0)Async::background(string $script, array $arguments = [])Async::json(array $data, int $status = 200, array $meta = [], ?string $message = null)Enable async mode via the CLI:
php restify-cli run --async
When Fibers or coroutine extensions are unavailable, Restify logs a warning and continues synchronously.
Restify\Support\DB centralises access to popular databases (MySQL/MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, MongoDB). Connections are cached and obtained directly from environment variables—no Composer packages required.
mysql / mariadbpgsql / postgres / postgresqlsqlsrv / mssqloci / oraclesqlitemongodb / mongoThe default connection is controlled via DB_CONNECTION (default mysql). Driver-specific overrides are available; fallback keys (DB_HOST, DB_PORT, etc.) are shared.
| Driver | Required keys |
|---|---|
| mysql | DB_CONNECTION=mysql, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD |
| pgsql | DB_CONNECTION=pgsql, DB_PGSQL_HOST, DB_PGSQL_PORT, DB_PGSQL_DATABASE, etc. |
| sqlsrv | DB_CONNECTION=sqlsrv, DB_SQLSRV_HOST, DB_SQLSRV_PORT, DB_SQLSRV_DATABASE, etc. |
| oracle | DB_CONNECTION=oracle, DB_ORACLE_HOST, DB_ORACLE_PORT, DB_ORACLE_SERVICE, etc. |
| sqlite | DB_CONNECTION=sqlite, DB_SQLITE_PATH (or DB_DATABASE) |
| mongodb | DB_CONNECTION=mongodb, or provide DB_MONGO_URI/DB_MONGO_HOST/DB_MONGO_PORT, etc. |
Example .env snippet:
DB_CONNECTION=pgsql
DB_PGSQL_HOST=localhost
DB_PGSQL_PORT=5432
DB_PGSQL_DATABASE=restify
DB_PGSQL_USERNAME=restify
DB_PGSQL_PASSWORD=secret
use Restify\Support\DB;
$pdo = DB::connection(); // Uses DB_CONNECTION or defaults to mysql
$pg = DB::connection('pgsql'); // Explicit connection
$rows = $pdo->query('SELECT NOW() AS current_time')->fetchAll();
For MongoDB:
$mongo = DB::connection('mongodb'); // Returns MongoDB\Client (requires mongodb extension)
Connections are cached; call DB::disconnect('pgsql') or DB::disconnect() to reset.
Restify\\Support\\Cache wraps APCu and OPcache for high-speed caching and opcode priming. If either extension is missing, these helpers silently fall back to no-ops (your code keeps working).
Cache::put($key, $value, $seconds) / Cache::get($key) / Cache::delete($key) / Cache::clear()Cache::remember($key, fn () => expensive(), $ttl) for memoised results.Cache::primeOpcode($directory) precompiles PHP files; Cache::flushOpcode() resets OPcache.Cache::rateLimit($key, $limit, $window) and is wired into the default middleware.Enable APCu (set apc.enabled=1 and apc.enable_cli=1 for CLI) plus Zend OPcache (opcache.enable=1, opcache.enable_cli=1) in php.ini for best performance.
Configured via environment variables:
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW=60
Set RATE_LIMIT_MAX=0 to disable throttling entirely.
Restify combines structured logging, database auditing, rate limiting, and OpenAPI metadata to deliver full-stack observability.
Restify\Middleware\LoggingMiddleware captures every request:
storage/logs/restify.log (path configurable via LOG_PATH).restify_logs when a PDO connection exists.password, token, secret, authorization).Configure behaviour in restify/config/logging.php or via .env:
LOGGING_ENABLED=true
LOG_LEVEL=info
LOG_REQUEST_BODY=true
LOG_RESPONSE_BODY=false
LOG_BODY_LIMIT=2048
LOG_DATABASE_ENABLED=true
Create the supporting tables once:
php restify-cli log
Restify\Middleware\RateLimitMiddleware throttles requests per IP using APCu:
RATE_LIMIT_MAX=120
RATE_LIMIT_WINDOW=60
When APCu is missing, the middleware gracefully allows all traffic (log a warning in development).
restify/config/cors.php drives CorsMiddleware (origins, headers, credentials). Preflights are short-circuited with a 204 response and proper caching headers.
GET /health offers a JSON uptime snapshot for load balancers and monitors.
The bundled authentication workflow issues tokens tied to specific endpoints and enforces them through middleware.
php restify-cli log once (this ensures the shared tables exist).php restify-cli authentication.md5, sha1, or jwt), provide the target endpoint, and select the authentication scheme (basic or bearer).restify_tokens and prints it to the console (JWTs include the generated secret).Authorization header (Basic <token> or Bearer <token>).Restify\Middleware\AuthenticationMiddleware validates incoming requests against stored tokens. Endpoints without tokens remain public.AUTH_PUBLIC_PATHS=/health,/docs and customise the inspected header with AUTH_HEADER=X-Api-Key.AUTH_ENABLED=false (useful for local development).Use php restify-cli to interact with your project:
| Command | Description | Usage Example |
|---|---|---|
install |
Publish the Restify skeleton into your project. | php restify-cli install |
run |
Serve the application with optional async runtime. | php restify-cli run --host 0.0.0.0 --port 9000 --async |
make:class |
Generate a class stub inside class/. |
php restify-cli make:class App\\Services\\Billing |
log |
Initialise logging and authentication tables. | php restify-cli log |
authentication |
Issue authentication tokens tied to specific endpoints. | php restify-cli authentication |
docs:openapi |
Generate OpenAPI docs (JSON/YAML) and optional Swagger UI. | php restify-cli docs:openapi --format yaml --serve |
test |
Run the PHPUnit suite or built-in runner with passthrough flags. | php restify-cli test --phpunit --filter=ExampleTest |
Helpful flags:
php restify-cli --helpphp restify-cli help runphp restify-cli help logphp restify-cli help authenticationphp restify-cli docs:openapi --helpphp restify-cli help testThe install command copies the skeleton (restify/, api/, class/, etc.) into your project. Composer users get this automatically via the supplied post-install script, but you can re-run it any time: php restify-cli install (or php vendor/bin/restify install).
Restify provides two ways to execute tests:
php restify-cli test auto-detects vendor/bin/phpunit and falls back to the native runner.php restify/tests/run.php always uses the built-in harness.Composer convenience:
composer test # delegates to restify-cli test
php restify-cli test --phpunit --filter=ExampleTest
php restify/tests/run.php # explicitly invoke the native runner
Restify\Testing\TestCase boots the full framework, giving you helpers such as call(), json(), and composed assertions (Assert::equals, Assert::status, Assert::json, etc.). Place additional tests in tests/ (project root); the runner pulls from both restify/tests and your application’s tests directory.
See the example snippets in the previous sections (domain class, restify/routes/api.php, class/App/Blog/PostController.php, api/posts.php, and middleware). Combine them to produce a fully functional demo API.
Restify-PHP welcomes community extensions. Packages are simple drop-in directories located under restify/packages. The autoloader automatically resolves any namespace by translating it into that path.
restify/packages/Acme/Feature.Acme\Feature\Service → restify/packages/Acme/Feature/Service.php).bootstrap.php file for initialisation (register routes, middleware, observers, etc.). It is included automatically during bootstrap.restify/packages/<Vendor>/<Package>.bootstrap.php for setup steps and environment keys.restify/packages/ and update documentation if needed.restify/tests/ (run php restify/tests/run.php).Packages that follow these guidelines remain portable and easy for other Restify-PHP users to adopt.
php restify-cli run --host 127.0.0.1 --port 8000 --docroot public
Spin up the bundled container images:
docker compose up --build
# App -> http://localhost:8000, Redis cache -> 6379
Dockerfile ships with PHP 8.2 CLI + PDO extensions + opcache.docker-compose.yml mounts the repository for live reload and provisions a Redis instance (optional caching backend).RESTIFY_PORT, APP_ENV, APP_DEBUG, APP_URL.DocumentRoot to the public/ directory.mod_rewrite..htaccess already supplied for clean URLs, extensionless PHP, and security headers.public/.htaccess:
/status → status.php).index.php..env, logs, and tool manifests.X-Frame-Options, X-Content-Type-Options, Referrer-Policy, etc.).server {
listen 80;
server_name restify.local;
root /var/www/restify/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~ /\.(env|git|ht) {
deny all;
}
}
public/. Never expose restify/ or class/..env.example to .env and protect it..htaccess security headers in your production server.Async::background() scripts to avoid user-controlled execution.storage/)..htaccess blocks .log downloads by default.APP_NAME, APP_ENV, APP_DEBUG, APP_TIMEZONE, APP_URL, APP_VERSION.RESTIFY_ASYNC (set automatically by the async CLI runner).RATE_LIMIT_MAX, RATE_LIMIT_WINDOW.AUTH_ENABLED, AUTH_PUBLIC_PATHS, AUTH_HEADER, AUTH_SECRET.LOGGING_ENABLED, LOG_LEVEL, LOG_PATH, LOG_BODY_LIMIT, LOG_REQUEST_BODY, LOG_RESPONSE_BODY, LOG_DATABASE_ENABLED, LOG_SENSITIVE_FIELDS.CORS_ENABLED, CORS_ALLOWED_ORIGINS, CORS_ALLOWED_METHODS, CORS_ALLOWED_HEADERS, CORS_EXPOSED_HEADERS, CORS_ALLOW_CREDENTIALS, CORS_MAX_AGE.EXCEPTIONS_ENABLED, EXCEPTIONS_REPORT, EXCEPTIONS_TRACE, EXCEPTIONS_LOG_LEVEL.RESTIFY_PORT for compose binding.restify/config/schemas.php.'CreateUser') within route metadata or attributes.php restify/tests/run.php).Restify-PHP is released under the MIT License. Use it freely in commercial and open-source projects alike.