Relay is the service container and static proxy system built for the Laika Framework. It gives you a lightweight dependency injection container (RelayRegistry), a clean static proxy base (Relay), and a two-phase provider system (RelayProvider + ProviderRegistry) that lets third-party packages register their own services into the framework.
Part of
laikait/laika-core· Requires PHP 8.1+
- How It Works
- File Structure
- RelayRegistry
- Auto-Wiring
- RelayProvider
- ProviderRegistry
- Bootstrap
- Third-Party Integration
- Relay — The Static Proxy
- Testing
- Exceptions
Your Code
│
▼
Auth::check() ← Relay proxy (static call)
│
▼
RelayRegistry::make('auth') ← Container (resolves & caches)
│
▼
Laika\Core\Auth\Auth::check() ← Real class (real method)
There are three independent pieces:
| Class | Role |
|---|---|
RelayRegistry |
The container. Holds bindings, resolves and caches instances. |
Relay |
Abstract base. Forwards static calls to the resolved instance. |
RelayProvider |
Integration point. Packages extend this to register services. |
ProviderRegistry |
Manages provider lifecycle — calls register() then boot(). |
services/ # NAMESPACE: Laika\Service
└── Template/
├── Asset.php # HTML Template Asset Relay Class Container
├── Meta.php # HTML Template Meta Relay Class Container
└── Activity.php # Activity Relay Class Container
└── Api.php # Api Relay Class Container
└── ClientAuth.php # ClientAuth Relay Class Container
└── Config.php # Config Relay Class Container
└── Cookie.php # Cookie Relay Class Container
└── CSRF.php # CSRF Relay Class Container
└── Date.php # Date Relay Class Container
└── DB.php # DB Relay Class Container
└── Directory.php # Directory Relay Class Container
└── Email.php # Email Relay Class Container
└── File.php # File Relay Class Container
└── Hook.php # Hook Relay Class Container
└── Image.php # Image Relay Class Container
└── Infra.php # Infra Relay Class Container
└── IP.php # IP Relay Class Container
└── Local.php # Local Relay Class Container
└── Math.php # Math Relay Class Container
└── Meta.php # Meta Relay Class Container (Get Class Container Comments)
└── Nav.php # Nav Relay Class Container
└── Option.php # Option Relay Class Container
└── Page.php # Page Relay Class Container
└── Redirect.php # Redirect Relay Class Container
└── Regex.php # Regex Relay Class Container
└── Request.php # Request Relay Class Container
└── Response.php # Response Relay Class Container
└── StaffAuth.php # StaffAuth Relay Class Container
└── Token.php # Token Relay Class Container
└── Unique.php # Unique Relay Class Container
└── Upload.php # Upload Relay Class Container
└── Url.php # Url Relay Class Container
└── Vault.php # Vault Relay Class Container
└── Visitor.php # Visitor Relay Class Container
src/ # NAMESPACE: Laika\Relay
└── Exceptions/
├── RelayException.php # Exception Class
└── Relay.php # Abstract base — extend to create a proxy
└── RelayRegistry.php # The container
└── RelayProvider.php # Relay Provider
└── ProviderRegistry.php # Manages provider loading and booting
└── CoreProviders.php # Core Services Container
services/[Laika\Service\*] is a convention, not a requirement. Relay classes can live anywhere.
The container. All services are registered here before the application starts handling requests.
use Laika\Relay\RelayRegistry;
$registry = new RelayRegistry();Register an already-constructed object directly.
$registry->instance(string $key, object $instance): staticThe registry stores exactly the object you give it — it never calls new. The object is available immediately on make().
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$registry->instance('db', $pdo);Use when the object already exists before the registry is set up, or when construction has side effects that must be controlled manually.
Register a singleton binding — built once on the first make() call, then cached and reused for the lifetime of the request.
$registry->singleton(string $key, Closure|string $concrete, array $args = []): static// Class string — no args needed
$registry->singleton('session', Session::class);
// Class string — with primitive args (positional)
$registry->singleton('mailer', Mailer::class, ['smtp']);
// Class string — with primitive args (named)
$registry->singleton('queue', DatabaseDriver::class, [
'table' => 'async_jobs',
'retries' => 3,
]);
// Closure — manual control, receives the registry
$registry->singleton('db', function (RelayRegistry $r) {
$config = $r->make('config');
$pdo = new PDO(
$config->get('db.dsn'),
$config->get('db.user'),
$config->get('db.pass')
);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
});
// Closure — conditional factory
$registry->singleton('cache', function (RelayRegistry $r) {
return match ($r->make('config')->get('cache.driver')) {
'redis' => new RedisCache(),
'file' => new FileCache(),
default => new ArrayCache(),
};
});Use for stateful services shared across the entire request: session, auth, config, mailer, cache.
Register a transient binding — a brand-new instance is created on every make() call. Nothing is ever cached.
$registry->bind(string $key, Closure|string $concrete, array $args = []): static$registry->bind('validator', Validator::class);
$v1 = $registry->make('validator');
$v2 = $registry->make('validator');
// $v1 !== $v2 — completely independent instancesUse for stateless, disposable objects where shared state would be a bug: validators, form request objects, DTOs, value objects.
Resolve a binding by key and return the object.
$auth = $registry->make('auth');Resolution order:
1. Pre-bound instance (instance())
2. Cached singleton (already resolved on a prior make())
3. Singleton binding → build, cache, return
4. Transient binding → build fresh, return (no cache)
5. Bare class name → attempt direct auto-wire if class exists
6. RelayException → nothing matched
Check whether a key has any binding registered.
if ($registry->has('payment')) {
$gateway = $registry->make('payment');
}Clear a cached singleton instance, forcing re-resolution on the next make().
$registry->forgetInstance('date');
$registry->singleton('date', Date::class, ['America/New_York']);
// Next make('date') builds a fresh instance with the new args| Method | Who builds it | How many instances | When built | Cached |
|---|---|---|---|---|
instance() |
You | 1 (yours) | Before registration | Yes — immediately |
singleton() |
Registry | 1 | On first make() |
Yes — after first use |
bind() |
Registry | N (one per call) | On every make() |
Never |
Prefer
singleton()overinstance()for most services.singleton()is lazy — if nothing ever callsmake('x'), the object is never constructed.instance()is eager — the object exists the moment you register it, whether anything uses it or not.
When a class string is registered (not a Closure), the registry uses PHP reflection to resolve constructor parameters automatically.
// Auth::__construct(Session $session, Config $config)
// Both 'session' and 'config' are already in the registry → auto-wired
$registry->singleton('session', Session::class);
$registry->singleton('config', Config::class);
$registry->singleton('auth', Auth::class); // Session and Config injected automaticallyPer-parameter resolution order:
1. Type-hinted class found in registry → make() it
2. Type-hinted class not in registry, exists → build() recursively
3. Primitive — named key in $args → use it
4. Primitive — positional in $args → use it
5. Has a default value → use it
6. Nullable → pass null
7. Nothing matched → throw RelayException
Mixed — auto-wire objects, supply primitives:
// Mailer::__construct(Config $config, string $driver)
// Config is in the registry; 'smtp' cannot be auto-wired
$registry->singleton('mailer', Mailer::class, ['smtp']);The integration point for packages. Extend RelayProvider and implement register(). Optionally override boot().
<?php
namespace YourPackage;
use Laika\Relay\RelayProvider;
class YourRelayProvider extends RelayProvider
{
public function register(): void
{
// Bind your services here
$this->registry->singleton('your-service', YourService::class);
}
public function boot(): void
{
// Use other services here — everything is registered by now
$config = $this->registry->make('config');
$this->registry->make('your-service')->configure($config->get('your.key'));
}
}Only call bind(), singleton(), or instance() here. Do not call make().
When register() runs, other providers may not have registered their services yet. Calling make() at this stage risks a RelayException if the dependency is not yet bound.
public function register(): void
{
// ✅ Correct
$this->registry->singleton('payment', PaymentGateway::class);
$this->registry->bind('payment.invoice', Invoice::class);
// ❌ Wrong — 'config' may not be registered yet
$key = $this->registry->make('config')->get('payment.key');
}Called after all providers have run register(). Safe to call make() freely.
public function boot(): void
{
// ✅ Config-driven setup
$config = $this->registry->make('config');
$gateway = $this->registry->make('payment');
$gateway->setApiKey($config->get('payment.stripe_key'));
// ✅ Attach event listeners
$events = $this->registry->make('events');
$events->listen('order.placed', SendPaymentRequestListener::class);
// ✅ Register routes
Http::group('/payment', function () {
Http::post('/charge', 'PaymentController@charge');
Http::post('/webhook', 'PaymentController@webhook');
});
}register() |
boot() |
|
|---|---|---|
| Purpose | Promise a service exists | Use services that others promised |
| When called | Before other providers boot | After ALL providers have registered |
Call make()? |
✅ Safe — everything is registered | |
| Typical use | singleton, bind, instance |
Config setup, routes, middleware, events |
One-line rule:
register()= bind things.boot()= use things.
Manages the full provider lifecycle. Register providers in any order — the two-phase approach guarantees correctness.
use Laika\Relay\ProviderRegistry;
$providers = new ProviderRegistry($registry);
// Register accepts class string or instance
$providers->register(CoreRelayProvider::class);
$providers->register(new PaymentRelayProvider($registry));
// Boot all providers after all register() calls
$providers->boot();Duplicate registrations are silently ignored — registering the same provider class twice is safe.
has() — check if a provider is registered:
if ($providers->has(PaymentRelayProvider::class)) {
// ...
}Complete application bootstrap sequence:
<?php
use Laika\Relay\Relay;
use Laika\Relay\RelayRegistry;
use Laika\Relay\ProviderRegistry;
use Laika\Relay\Relays\CoreRelayProvider;
// 1. Build the container
$registry = new RelayRegistry();
// 2. Build the provider manager
$providers = new ProviderRegistry($registry);
// 3. Register the Laika core provider (always first)
$providers->register(CoreRelayProvider::class);
// 4. Register providers declared in config/app.php
foreach (config('app.providers') as $providerClass) {
$providers->register($providerClass);
}
// 5. Boot all providers (runs after all register() calls are complete)
$providers->boot();
// 6. Wire the registry into the Relay system
Relay::setRegistry($registry);config/app.php:
return [
'providers' => [
// Third-party packages
Acme\LaikaPayment\PaymentRelayProvider::class,
Acme\LaikaBarcode\BarcodeRelayProvider::class,
],
];This is how an external package integrates with Laika.
<?php
namespace Acme\LaikaPayment;
use Laika\Relay\RelayProvider;
class PaymentRelayProvider extends RelayProvider
{
public function register(): void
{
$this->registry->singleton('payment', PaymentGateway::class);
$this->registry->singleton('payment_webhook', WebhookHandler::class);
$this->registry->bind('payment_invoice', Invoice::class);
}
public function boot(): void
{
$key = $this->registry->make('config')->get('stripe_key');
$this->registry->make('payment')->setApiKey($key);
}
}<?php
namespace Acme\LaikaPayment\Relay;
use Laika\Relay\Relay;
/**
* @method static string charge(int $amount, string $currency)
* @method static bool verify(string $token)
* @method static array history(int $limit = 10)
*/
class Payment extends Relay
{
protected static function getRelayAccessor(): string
{
return 'payment';
}
}composer require acme/laika-payment
↓
Add PaymentRelayProvider to config/app.php
↓
Bootstrap: ProviderRegistry calls register() → binds 'payment'
↓
Bootstrap: ProviderRegistry calls boot() → configures with API key
↓
Relay::setRegistry($registry)
↓
Payment::charge() → __callStatic → make('payment') → PaymentGateway::charge()
Extend Relay, implement getRelayAccessor(), and document every proxied method with @method static tags.
<?php
declare(strict_types=1);
namespace Laika\Relay\Relays;
use Laika\Relay\Relay;
/**
* @method static bool attempt(array $credentials)
* @method static bool check()
* @method static bool guest()
* @method static mixed user()
* @method static void logout()
*/
class Auth extends Relay
{
protected static function getRelayAccessor(): string
{
return 'auth';
}
}@method tag rules:
- Always include
static— every call through a Relay is a static call. - Import types with
usestatements — same as real code. - Union return types are valid:
@method static Item|Builder end() - Array types are valid:
@method static Item[] all() - FQCNs or short names both work (short names require
use).
use Laika\Nav\Helper\Item;
use Laika\Nav\Builder;
/**
* @method static Item|Builder end()
* @method static Item[] all()
* @method static Item|null find(string $name)
*/Import the Relay class, not the underlying service class.
// ✅ Correct — imports the Relay proxy
use Laika\Service\StaffAuth;
use Laika\Service\ClientAuth;
use Laika\Service\Config;
...
if (Auth::check()) {
$name = Config::get('app', 'name');
Session::set('last_page', '/dashboard');
}// ❌ Wrong — imports the real class, static calls fail
use Laika\Core\Auth\Auth;
Auth::check(); // Fatal: Non-static method cannot be called staticallyYou do not need a Relay class to access a service. The registry is always available:
// Direct registry access — no Relay class needed
$session = Relay::getRegistry()->make('session');
$session->set('user_id', 42);Chaining works automatically. The first static call goes through __callStatic and returns whatever the underlying method returns. If that's $this, subsequent calls are normal instance calls.
// Entire chain — first call is static, rest are instance
Date::now()
->setTimezone('Asia/Dhaka')
->modify('+7 days')
->setFormat('d M Y')
->format();
Date::fromFormat('d/m/Y', '01/04/2025')
->toUtc()
->toIso8601();
Date::setTimestamp(time())
->modify('+1 hour')
->humanDiff();Chaining ends when a method returns a non-object (string, int, array). Do not chain after terminal methods like format(), getTimestamp(), or toArray().
Use swap() to replace the resolved instance for a specific Relay — for example, switching auth guard in middleware.
<?php
namespace App\Middleware;
use Laika\Core\Auth\Auth as AuthService;
use Laika\Service\Auth;
class AdminMiddleware
{
public function handle(): void
{
Auth::swap(new AuthService(guard: 'admin', table: 'admins'));
// All subsequent Auth:: calls now resolve against the admin guard
}
}Or re-register via the registry for a cleaner approach:
Relay::getRegistry()->forgetInstance('auth');
Relay::getRegistry()->singleton('auth', AuthService::class, ['admin', 'admins']);use Laika\Service\Auth;
protected function setUp(): void
{
$mock = $this->createMock(\Laika\Core\Auth\Auth::class);
$mock->method('check')->willReturn(true);
Auth::swap($mock);
}
protected function tearDown(): void
{
Auth::clearResolvedInstance();
}
public function test_dashboard_requires_auth(): void
{
$this->assertTrue(Auth::check()); // uses mock
}use Laika\Relay\Relay;
use Laika\Relay\RelayRegistry;
protected function setUp(): void
{
$testRegistry = new RelayRegistry();
$testRegistry->instance('auth', $this->createMock(Auth::class));
$testRegistry->instance('session', $this->createMock(Session::class));
$testRegistry->instance('config', $this->createMock(Config::class));
Relay::swapRegistry($testRegistry);
}
swapRegistry()is intentionally separate fromsetRegistry()so that accidental double-calls in production code throw aRelayException, while tests can swap freely.
All errors throw Laika\Relay\Exceptions\RelayException.
| Situation | Message |
|---|---|
setRegistry() called more than once |
RelayRegistry has already been set. |
getRegistry() before setRegistry() |
RelayRegistry has not been set. |
| No binding found for key | No binding registered for [key]. |
| Class not found during build | Class [ClassName] not found. |
| Unresolvable constructor parameter | Cannot resolve parameter [$name] for [ClassName]. |
| Method not found on resolved instance | Method [method] does not exist on [ClassName]. |
| RelayProvider class not found | RelayProvider [ClassName] class not found. |
| Provider does not extend RelayProvider | [ClassName] must extend Laika\Relay\RelayProvider. |
MIT — see LICENSE for details.
Author: Showket Ahmed
Email: [email protected]
Package: laikait/laika-relay
GitHub: laikait/laika-relay