laikait/laika-relay
Composer 安装命令:
composer require laikait/laika-relay
包简介
Laika PHP Framework Relay Service
README 文档
README
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+
Table of Contents
- How It Works
- File Structure
- RelayRegistry
- Auto-Wiring
- RelayProvider
- ProviderRegistry
- Bootstrap
- Third-Party Integration
- Relay — The Static Proxy
- Testing
- Exceptions
How It Works
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(). |
File Structure & Default Services
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
└── Relays/
├── Relay.php # Abstract base — extend to create a proxy
├── RelayRegistry.php # The container
├── RelayProvider.php # Abstract base for service providers
└── ProviderRegistry.php # Manages provider loading and booting
services/[Laika\Service\*] is a convention, not a requirement. Relay classes can live anywhere.
RelayRegistry
The container. All services are registered here before the application starts handling requests.
use Laika\Relay\RelayRegistry; $registry = new RelayRegistry();
instance()
Register an already-constructed object directly.
$registry->instance(string $key, object $instance): static
The 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.
singleton()
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.
bind()
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 instances
Use for stateless, disposable objects where shared state would be a bug: validators, form request objects, DTOs, value objects.
make()
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
has()
Check whether a key has any binding registered.
if ($registry->has('payment')) { $gateway = $registry->make('payment'); }
forgetInstance()
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
Lifetime Comparison
| 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.
Auto-Wiring
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 automatically
Per-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']);
RelayProvider
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')); } }
register()
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'); }
boot()
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() vs boot()
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()? |
⚠️ Risky — others may not be ready | ✅ Safe — everything is registered |
| Typical use | singleton, bind, instance |
Config setup, routes, middleware, events |
One-line rule:
register()= bind things.boot()= use things.
ProviderRegistry
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)) { // ... }
Bootstrap
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, ], ];
Third-Party Integration
This is how an external package integrates with Laika.
1. Create a RelayProvider
<?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); } }
2. Ship a Relay class (optional but recommended)
<?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'; } }
Full flow
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()
Relay — The Static Proxy
Creating a Relay Class
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) */
Using a Relay
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 statically
You 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);
Method Chaining
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().
Switching Instances at Runtime
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']);
Testing
Inject a mock for one test
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 }
Replace the entire registry for full isolation
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.
Exceptions
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. |
License
MIT — see LICENSE for details.
Author: Showket Ahmed
Email: riyadhtayf@gmail.com
Package: laikait/laika-relay
GitHub: laikait/laika-relay
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 1
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-21