Structured execution boundaries and failure reports for controlled plugin flows.
The first OP-83 slice wraps code that a plugin owns:
- command handlers;
- scheduled task callbacks;
- form response handlers;
- packet handlers;
- any custom callback through
ErrorBoundary.
It catches Throwable, records sanitized context, preserves the original
throwable in the returned result and produces a bounded GuardReport.
It is not an anti-crash layer. Fatal process failures, engine faults and PocketMine-MP lifecycle failures outside the guarded callback remain the responsibility of PHP, PMMP logs and official crash dumps.
Use the standalone PHAR from the v1.0.0-dev pre-release, or install the
official EasyLibrary package:
/easylibrary packages install libruntimeguard development confirm
restart
/elprobe run libruntimeguard
LibRuntimeInspector is optional. When both packages are loaded, EasyLibrary
support bundles include Inspector findings translated from guard reports.
use imperazim\runtimeguard\RuntimeGuard;
$guard = (new RuntimeGuard(failureLimit: 100))->register('example-plugin');
$result = $guard->commands()->execute(
'profile',
fn(): string => $profileService->render($player),
owner: 'ExamplePlugin',
metadata: [
'sender' => $player->getName(),
'playerUuid' => $player->getUniqueId()->toString(),
]
);
if ($result->isFailure()) {
$logger->error('Profile command failed.');
}
$payload = $guard->report()->toArray();Use $result->unwrap() when the caller must rethrow the original failure.
Use $result->valueOr($fallback) only when a fallback is a deliberate part of
the feature contract.
RuntimeGuard::pmmp() creates small wrappers for PMMP-shaped callbacks. The
wrappers do not register commands, tasks, forms or packet handlers for you; they
only guard callbacks that your plugin already owns.
use imperazim\runtimeguard\GuardExecutionResult;
use imperazim\runtimeguard\RuntimeGuard;
$guard = (new RuntimeGuard())->register('example-plugin');
$pmmp = $guard->pmmp($this);
$commandHandler = $pmmp->command(
'profile',
fn($sender, $command, string $label, array $args): bool => $this->openProfile($sender),
static function(GuardExecutionResult $result, array $arguments): bool {
// Optional fallback. Without this handler, the original Throwable is
// rethrown after being recorded.
return false;
}
);
$taskHandler = $pmmp->task('profile-cache-refresh', fn(int $currentTick): void => $this->refreshCache());
$formHandler = $pmmp->form('settings', fn($player, mixed $data): void => $this->saveSettings($player, $data));
$packetHandler = $pmmp->packet('InventoryTransactionPacket', fn($event): void => $this->handlePacket($event));The adapter extracts scalar metadata such as owner name, sender/player name, UUID, command label, argument count, packet class and response type. It never stores the PMMP objects themselves in the report.
- Keep metadata scalar. Do not store
Player,Pluginor packet objects. - Secret-like metadata keys are redacted.
- Discord webhook URLs and bearer credentials are redacted from messages.
- Reports retain only the configured number of recent failures.
- Source paths are reduced to filenames and trace arguments are never stored.
register() publishes the guard through RuntimeGuardRegistry. This is
optional for local use, but enables EasyLibrary doctor/support reports to
discover the snapshot without retaining plugin, player or packet objects.
LibRuntimeInspector can consume libruntimeguard.report.v1 through its guard
report adapter. The integration is schema-based so both libraries remain
usable independently.