Valhalla Framework

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-framework

Create a new service

valhalla new project orders-service

Install dependencies

cd orders-service
composer install

Start development server

php -S 127.0.0.1:8080 -t public

Getting 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.example

Core 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:

MethodReturnDescription
getIsValid()booltrue when all fields pass every rule
fails()boolInverse of getIsValid() — convenient for early returns
getErrors()arrayNested array of error messages keyed by field name

Available Rules

No parameters

ConstantString formValidates
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 formValidates
'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...
}
Note:A field missing from $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'),
    ],
  ],
];
DriverBehaviour
singleOne file per channel, never rotated
dailyOne file per day (channel-YYYY-MM-DD.log), files older than days are deleted automatically
stackGroups multiple channels (same write semantics as single for the default channel)

Log levels (lowest → highest)

DEBUGINFONOTICEWARNINGERRORCRITICALALERTEMERGENCY

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]);
Warning:Referencing a channel name that does not exist in config throws 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 batch

Doctrine — 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:rollback

To 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_table

Run Migrations

valhalla migrate

Rollback

valhalla migrate:rollback

Infrastructure

Docker & Podman

Generate a complete Docker or Podman setup with all services in one command.

Install Docker

valhalla install:docker

Generates 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 up

Infrastructure

Queue Workers

Process background jobs reliably with Valhalla's built-in queue worker.

valhalla queue:work

Starts 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 dependencies

Scaffolding

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 middleware

ORM & 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 claims

Containers

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 containers

Queue

valhalla queue:work       # process queued jobs

Agents

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)