gcego/runtime
gcego/runtime is a lightweight Laravel/PHP package that provides a runtime layer for managing execution context and service bootstrapping. It integrates cleanly with existing applications and helps keep runtime concerns organized and predictable.
Install the kernel and its sibling packages:
composer require gcego/runtime:^1.0
Requirements are strict: PHP 8.3+, Laravel 13, plus gcego/core ^1.0, gcego/registry ^1.1 and gcego/blueprints ^1.0. Add gcego/rendering separately if you need any UI — it is no longer a transitive dependency.
Where to look first, in order:
Gcego\Runtime\Execution\Operation — the immutable value object you will build at every call site.Gcego\Runtime\Contracts\RuntimeInterface / Facades\Runtime — the single entry point (execute(), attempt(), contexts()).Gcego\Runtime\Execution\HandlerRegistry and the handler contracts — how you plug behaviour in.Gcego\Runtime\Context\ExecutionContext — what the runtime already knows about the caller, so you don't re-derive it.First use case — an action with data, from a controller:
use Gcego\Runtime\Facades\Runtime;
use Gcego\Runtime\Execution\Operation;
$result = Runtime::execute(
Operation::make('orders.cancel')
->action()
->on($order->id)
->with(['reason' => $request->string('reason')])
);
return $result->successful()
? back()->with('status', 'Cancelled')
: back()->withErrors($result->message());
The operation is identifier-only ('orders.cancel') and data-only (with()), so nothing in the request can influence which handler runs — only what it receives. That is the whole design premise and it is worth internalising before writing your first handler.
Before anything runs, grant the ability. Authorization is fail-closed: no gcego-runtime.execute Gate ability means denied, not allowed. In a fresh app, define it in AuthServiceProvider (or a policy) early, or every call returns an ExecutionDeniedException and you will spend an hour blaming the handler.
One operation per intent, four categories. action() mutates, query() reads, submit() handles forms, and package-defined categories cover everything else. Keep them separate even when an action would be a convenient place to also return a payload — the category is part of the contract that authorizers, middleware and diagnostics reason about.
Handlers stay thin and typed. Implement the narrowest contract (ActionHandler, QueryHandler, FormSubmissionHandler) and register into the HandlerRegistry. Cross-cutting concerns belong in ExecutionMiddleware declared via gcego-runtime.execution.middleware, or added wholesale for a package through Bootable::bootPackage(). Because the pipeline is resolve → authorize → handler → handler authorization → validation → middleware → handler, middleware is the last thing before the handler body — perfect for transactions, timing and audit, useless for short-circuiting validation. Use ValidatesOperation / AuthorizesOperation for that.
Two-tier authorization. The global ExecutionAuthorizer (GateExecutionAuthorizer, ability gcego-runtime.execute, checked as the context's user) decides "may this caller execute at all". Per-handler AuthorizesOperation decides "may this caller execute this operation on this target". Put ownership/tenancy rules in the handler-level check; put "is the app in a state where execution is allowed" in the global one.
Context is trusted and derived, never passed in. ExecutionContextFactory builds user, tenant, panel, locale and request metadata from the guard plus your TenantResolver / PanelResolver bindings. Read from $context inside handlers instead of trusting $request. Anything resembling a tenant switch belongs in a resolver, not in with().
Nesting with ExecutionScope. An operation can dispatch another; the scope keeps the originating subject and enforces execution.max_depth. This is the clean way to compose — a checkout.submit form handler calling orders.create — but budget for the depth limit rather than discovering it in production.
Observe, don't instrument. OperationExecuting, OperationExecuted and OperationFailed give you timing, audit trails and alerting without touching handler code:
Event::listen(OperationFailed::class, function ($event) {
report($event->exception);
Metrics::increment("op.failed.{$event->operation->identifier()}");
});
Diagnose with the inspector. RuntimeInspector::inspect() (now argument-free) returns a RuntimeReport whose $handlers property lists registered handlers, categories, overrides, the authorizer and the middleware stack. Expose it behind an admin gate or run it from tinker when a handler "isn't registered". The gcego-runtime.execution diagnostic key plugs into gcego/core's health surface.
Two call shapes. execute() for the throwing path and attempt() when you want to branch on the outcome rather than catch. Whichever you pick, keep the boundary at the controller/command layer so ExecutionException subclasses never leak into your domain code.
Testing. The facade is aliased as \Runtime and mockable — Runtime::shouldReceive('execute') keeps unit tests of controllers free of the whole pipeline. Test handlers directly by constructing the Operation and invoking them, then cover the wiring once in a feature test.
The upgrade is not incremental. If you are coming from 0.x, this release is a hard split. Everything rendering-shaped left the package: Rendering\*, Blade\BladeRenderer, Styling\*, State\*, Contracts\RendererInterface, StylingResolver, RenderAuthorizer, StatefulComponent, GateRenderAuthorizer, the rendering exceptions and the views. The component moved from <x-gcego-runtime::render> to <x-gcego-rendering::render>. Runtime::render(), renderAuthorized(), identityOf(), canRender() and hostedBlueprint() are now on Gcego\Rendering\RenderEngine — and hostedBlueprint() is renamed authorizedBlueprint(). Config moved from gcego-runtime.rendering.* / gcego-runtime.authorization.* to gcego-rendering.*, and the ability is now gcego-rendering.render. Grep for every one of those before you run the test suite.
Deleted, not deprecated. Rendering\BlueprintRenderer, Livewire\BlueprintHost, the renderers.blueprint and hosts.blueprint views, the gcego-runtime.livewire.* config and the runtime.livewire capability are gone in gcego/blueprints 1.0 — blueprints are no longer Livewire components. There is no shim; plan the rewrite rather than the upgrade.
Fail-closed means silent-looking failures. A missing ability, a resolver returning null, or an execution attempt before gcego/core has booted all surface as exceptions from a package you just installed. If you see ExecutionDeniedException, check the Gate ability first; if you see ContextViolationException, check your TenantResolver / PanelResolver bindings; if execution refuses to start entirely, you are calling it too early in the boot cycle.
ErrorCategory is your HTTP boundary. Each ExecutionException subclass carries an error category with an HTTP status and a public message. Map them once in your exception handler instead of writing per-controller try/catch. Resist the urge to expose HandlerFailedException internals — the public message exists precisely so you don't leak stack detail.
RuntimeConfig is now execution-only and throws InvalidConfigurationException. Old rendering keys in a cached config file will not be silently ignored in the way you might expect; run config:clear after editing.
RuntimeInspector::inspect() changed shape — no target argument, and RuntimeReport lost renderers and missingRenderers. RenderDiagnostic was renamed RuntimeDiagnostic. Any admin panel or health check reading those will fatal, not degrade.
Tune execution.max_depth deliberately. It is a guard against runaway recursion, not a queue depth. If you find yourself raising it, you probably want composition at the handler layer instead.
Extension points worth knowing: ExecutionExtension for gcego/core extensions, custom HandlerRegistry categories for package-defined operation types, and ObservesLifecycle if you need per-operation hooks that outlive a single event listener.
How can I help you explore Laravel packages today?