Documentation
Everything you need to build high-performance PHP microservices with Valhalla.
Getting Started
Installation
Get up and running with Valhalla in seconds using Composer.
Install Valhalla CLI
composer global require asyassin10/valhalla-frameworkCreate a new service
valhalla new project orders-serviceInstall dependencies
cd orders-service
composer installStart development server
php -S 127.0.0.1:8080 -t publicGetting Started
Project Structure
Valhalla projects follow a clean, organized structure for rapid development.
orders-service/
src/
Controllers/
OrdersController.php
HealthController.php
Middleware/
InternalAuth.php
Services/
BillingService.php
routes/
api.php
config/
auth.php
services.php
logging.php
agents.php
database.php
database/
migrations/
public/
index.php
composer.json
.env.exampleCore Concepts
Routing
Define API routes using the Route:: facade or PHP 8 attributes.
Route Facade
use Valhalla\Framework\Facades\Route;
use Valhalla\Framework\Core\Response;
Route::get('/users', fn () => Response::json(User::all()));
Route::get('/users/{id}', fn (Request $req) => Response::json(User::find($req->route('id'))));
Route::post('/users', fn (Request $req) => Response::json(User::create($req->body())));
Route::put('/users/{id}', fn (Request $req) => Response::json(User::find($req->route('id'))->update($req->body())));
Route::delete('/users/{id}', fn (Request $req) => User::find($req->route('id'))->delete() ? Response::json(['ok' => true]) : Response::error());Attribute Routes
#[Get('/users')]
public function index(): Response {
return Response::json(User::all());
}
#[Get('/users/{id}')]
public function show(Request $request): Response {
return Response::json(User::find($request->route('id')));
}
#[Post('/users')]
#[Middleware(ValidateJsonMiddleware::class)]
public function store(Request $request): Response {
return Response::json(User::create($request->body()), 201);
}Core Concepts
Request & Response
Access request data and send responses with type safety.
Accessing Request Data
Route::post('/orders', fn (Request $request) => Response::json([
'body' => $request->body(),
'query' => $request->query('filter'),
'route_id' => $request->route('id'),
'header' => $request->header('X-Request-ID'),
]));Sending Responses
// JSON response
return Response::json(['message' => 'Success']);
// With status code
return Response::json(['created' => true], 201);
// With custom headers
return Response::json(['data' => $data], 200, ['X-Custom' => 'header']);
// Error response
return Response::error('Not found', 404);Core Concepts
Authentication
Issue and validate JWTs from the CLI or in your application.
Generate a JWT
valhalla auth:generate 1 "User Name"Generates a signed JWT for the given user ID that can be passed as a Bearer token on protected routes.
Protecting Routes
Route::group('/api', [AuthMiddleware::class], function () {
Route::get('/profile', fn (Request $req) => Response::json(
User::find($req->user()->id)
));
});Validation & Logging
Validation
Valhalla provides a fluent validator via Valhalla\Framework\Validator\Validator. Pass your data array and a rules map; get back a ValidationResponse you can inspect or return directly.
Quick Start
use Valhalla\Framework\Validator\RuleRegistrar;
use Valhalla\Framework\Validator\Validator;
$data = [
'email' => 'user@example.com',
'age' => 25,
'username' => 'alice',
];
$result = Validator::make($data, [
'email' => [RuleRegistrar::REQUIRED, RuleRegistrar::EMAIL],
'age' => [RuleRegistrar::REQUIRED, RuleRegistrar::INTEGER, 'min:18'],
'username' => [RuleRegistrar::REQUIRED, RuleRegistrar::STRING],
]);
if ($result->fails()) {
$errors = $result->getErrors();
}ValidationResponse API
Validator::make() always returns a ValidationResponse with three methods:
| Method | Return | Description |
|---|---|---|
| getIsValid() | bool | true when all fields pass every rule |
| fails() | bool | Inverse of getIsValid() — convenient for early returns |
| getErrors() | array | Nested array of error messages keyed by field name |
Available Rules
No parameters
| Constant | String form | Validates |
|---|---|---|
| RuleRegistrar::REQUIRED | 'required' | Value is present and not null |
| RuleRegistrar::STRING | 'string' | Value is a string |
| RuleRegistrar::EMAIL | 'email' | Valid e-mail address format |
| RuleRegistrar::INTEGER | 'integer' | Value is an integer |
| RuleRegistrar::NUMERIC | 'numeric' | Value is numeric (int or float) |
| RuleRegistrar::BOOLEAN | 'boolean' | Value is a boolean |
| RuleRegistrar::ARRAY | 'array' | Value is an array |
| RuleRegistrar::DATE | 'date' | Value is a parseable date string |
| RuleRegistrar::POSITIVE | 'positive' | Number is > 0 |
| RuleRegistrar::NEGATIVE | 'negative' | Number is < 0 |
With parameters
| String form | Validates |
|---|---|
| 'min:N' | Number ≥ N |
| 'max:N' | Number ≤ N |
| 'length:N' | String length === N |
| 'regex:pattern' | String matches PCRE pattern |
| 'between:min,max' | Number is between min and max (inclusive) |
| 'in:a,b,c' | Value is one of the listed values |
| 'not_in:a,b,c' | Value is not one of the listed values |
| 'after:YYYY-MM-DD' | Date is strictly after the given date |
| 'after_or_equal:YYYY-MM-DD' | Date is on or after the given date |
| 'before:YYYY-MM-DD' | Date is strictly before the given date |
| 'before_or_equal:YYYY-MM-DD' | Date is on or before the given date |
Usage Examples
Numeric range
$result = Validator::make($data, [
'age' => ['min:0', 'max:120'],
'percentage' => ['between:0,100'],
]);String rules
$result = Validator::make($data, [
'pin' => ['length:4'],
'username' => ['regex:/^[a-z0-9_]+$/'],
]);Enum-style allow / deny lists
$result = Validator::make($data, [
'status' => ['in:active,inactive,pending'],
'role' => ['not_in:superadmin,root'],
]);Date comparisons
$result = Validator::make($data, [
'start_date' => [RuleRegistrar::DATE, 'after:2024-01-01'],
'end_date' => [RuleRegistrar::DATE, 'before_or_equal:2025-12-31'],
]);In a controller
use Valhalla\Framework\Core\Request;
use Valhalla\Framework\Core\Response;
use Valhalla\Framework\Validator\RuleRegistrar;
use Valhalla\Framework\Validator\Validator;
public function store(Request $request): Response
{
$result = Validator::make($request->body(), [
'email' => [RuleRegistrar::REQUIRED, RuleRegistrar::EMAIL],
'password' => [RuleRegistrar::REQUIRED, RuleRegistrar::STRING, 'min:8'],
]);
if ($result->fails()) {
return Response::json(['errors' => $result->getErrors()], 422);
}
// proceed with valid data...
}$data is treated as null; pair REQUIRED with other rules to catch absent fields. Unknown rule names throw InvalidArgumentException at validation time.Validation & Logging
Logging
Valhalla ships a built-in logger accessible via the Log static facade. No third-party logging library is required at runtime.
Configuration
Create or edit config/logging.php in your application:
<?php
return [
'driver' => 'single', // 'single' | 'daily' | 'stack'
'level' => 'DEBUG', // minimum level written to the default channel
'path' => storage_path('logs'),
// optional named channels
'channels' => [
'payments' => [
'driver' => 'daily',
'level' => 'WARNING',
'path' => storage_path('logs/payments'),
'days' => 30, // keep rotated files for 30 days (default: 60)
],
'audit' => [
'driver' => 'single',
'level' => 'INFO',
'path' => storage_path('logs/audit'),
],
],
];| Driver | Behaviour |
|---|---|
| single | One file per channel, never rotated |
| daily | One file per day (channel-YYYY-MM-DD.log), files older than days are deleted automatically |
| stack | Groups multiple channels (same write semantics as single for the default channel) |
Log levels (lowest → highest)
A message is written only when its level is ≥ the channel's configured level. The default app channel skips this filter and always writes.
Basic Usage
use Valhalla\Framework\Log\Log;
Log::debug('Cache miss for key user:42');
Log::info('User logged in', ['user_id' => 42]);
Log::notice('Deprecated endpoint called');
Log::warning('Disk usage above 80 %');
Log::error('Payment gateway returned 500');
Log::critical('Database connection lost');
Log::alert('Replication lag exceeded threshold');
Log::emergency('Service is down');Every method accepts any value as $message — string, array, or any object — and an optional $context array.
Logging Different Types
Logging strings
Log::info('Order placed successfully');Logging arrays
Log::info(['order_id' => 123, 'total' => 49.99]);Logging objects
The logger uses reflection to extract all properties (including private/protected ones) and serialises them automatically.
class Order
{
public function __construct(
private int $id,
private float $total,
) {}
}
Log::info(new Order(123, 49.99));
// writes: {"id":123,"total":49.99}Log with context
Log::error('Payment failed', [
'user_id' => 7,
'gateway' => 'stripe',
'error_code' => 'card_declined',
]);Logging exceptions
logError captures exception class, file, line, code, and full stack trace automatically.
try {
// ...
} catch (\Throwable $e) {
Log::logError($e, ['user_id' => 7]);
}Named Channels
Switch to any channel defined in config/logging.php with Log::channel(name). The channel call returns the logger so you can chain the level method directly.
Log::channel('payments')->warning('Refund delayed', ['order_id' => 99]);
Log::channel('audit')->info('Admin deleted user', ['admin_id' => 1, 'target_id' => 42]);InvalidArgumentException.Log Format
[2025-06-01 14:32:05] app.INFO: User logged in
[2025-06-01 14:32:06] payments.WARNING: {"order_id":99}Each line follows the pattern: [timestamp] channel.LEVEL: message
Log Rotation
When the daily driver is used, old log files are pruned on every write call. The retention window is controlled by the days key in the channel config (default 60 days).
Database & ORM
ORM Setup
Valhalla ships with a pluggable ORM system. Install exactly one driver — Eloquent or Doctrine — and only that driver's directories appear in your project.
Eloquent — Active Record
Installs illuminate/database and creates src/Models. Use hand-written up/down migrations.
valhalla orm:install eloquent # install driver + create src/Models
valhalla make:model Order # generate a model
valhalla make:migration create_orders_table
valhalla migrate
valhalla migrate:rollback # undo the last batchDoctrine — Data Mapper
Installs doctrine/orm and creates src/Entities. Migrations are generated from entity metadata.
valhalla orm:install doctrine # install driver + create src/Entities
valhalla make:model Invoice # generate an entity
valhalla migrate:diff # generate migration from entity metadata
valhalla migrate
valhalla migrate:rollbackTo switch drivers, run valhalla orm:remove first to cleanly uninstall the current one.
Database & ORM
Migrations
Create and run database migrations to manage your schema.
Create a Migration
valhalla make:migration create_users_tableRun Migrations
valhalla migrateRollback
valhalla migrate:rollbackInfrastructure
Docker & Podman
Generate a complete Docker or Podman setup with all services in one command.
Install Docker
valhalla install:dockerGenerates Dockerfile, docker-compose.yml, and related configuration files.
Build & Run
valhalla build # build images
valhalla up # start the stack
valhalla logs # tail service logs
valhalla shell # open a shell in the app container
valhalla down # stop and clean upInfrastructure
Queue Workers
Process background jobs reliably with Valhalla's built-in queue worker.
valhalla queue:workStarts the queue worker process for processing background jobs.
Advanced
Service Client
Make service-to-service calls with built-in retries, timeouts, and circuit breakers.
use Valhalla\Framework\ServiceClient\Client;
$client = new Client('https://orders-service.local');
$response = $client
->withRetries(3)
->withTimeout(5)
->post('/orders', ['items' => [...]])
->json();Advanced
Local Agents
Run long-lived PHP TCP workers on the same machine, MCP-style. Great for scoring, summarization, and image analysis.
Lifecycle
valhalla agent:install summarizer 9501 # register agent on port 9501
valhalla agent:start summarizer # boot the agent process
valhalla agent:call summarizer summarize # invoke an action
valhalla agent:stop summarizer # shut down gracefully
valhalla agent:list # show agents, ports & status
valhalla agent:serve # run all agents (great for dev)Call from Route
Route::post('/score', fn (Request $req) => Response::json([
'score' => Agent::call('scoring', 'score', ['data' => $req->body()]),
]));Advanced
CLI Reference
All 28 first-party commands, grouped by purpose.
Project Setup
valhalla new project <name> # scaffold a new microservice
valhalla install # install Composer dependenciesScaffolding
valhalla make:controller <Name> # generate a controller
valhalla make:middleware <Name> # generate a middleware class
valhalla make:service <Name> # generate a service class
valhalla routes:list # print all routes with middlewareORM & Database
valhalla orm:install eloquent # install Eloquent ORM
valhalla orm:install doctrine # install Doctrine ORM
valhalla orm:remove # uninstall current ORM driver
valhalla make:model <Name> # create a model or entity
valhalla make:migration <name> # create a migration file
valhalla migrate # run pending migrations
valhalla migrate:rollback # rollback last batch
valhalla migrate:diff # generate diff migration (Doctrine)Authentication
valhalla auth:generate <user_id> <name> # sign a JWT token
valhalla auth:inspect <token> # inspect token claimsContainers
valhalla install:docker # scaffold Docker stack
valhalla install:podman # scaffold Podman stack
valhalla build # build container images
valhalla up # start the stack
valhalla logs # tail service logs
valhalla shell # open a shell in the app container
valhalla down # stop and remove containersQueue
valhalla queue:work # process queued jobsAgents
valhalla agent:install <name> <port> # register an agent on a port
valhalla agent:start <name> # boot the agent process
valhalla agent:call <name> <action> # invoke an agent action
valhalla agent:stop <name> # shut down gracefully
valhalla agent:list # list agents, ports & status
valhalla agent:serve # run all agents (dev mode)