File: /home/xedaptot/be.naniguide.com/app/Library/ModuleManager.php
<?php
namespace App\Library;
use App\Library\Facades\Hook;
/**
* ModuleManager — typed extension surface for plugins that wrap or replace
* parts of the customer-facing application (e.g. a "frontend skin" plugin).
*
* Methods are typed entry points; the Hook system underneath is an
* implementation detail. Callers (LoginController, middleware, plugin
* ServiceProvider) never reference Hook:: directly — swap to Laravel
* events later without touching call sites.
*
* Growth policy: keep this class small. Add a typed method only when an
* extension point is stable and used by more than one plugin. Experimental
* surfaces stay on raw Hook:: until promoted here.
*/
final class ModuleManager
{
private const H_ROUTE_DECISION = 'frontend-route-decision';
// -------------------------------------------------------------------------
// Registration API — plugins call from their ServiceProvider::boot()
// -------------------------------------------------------------------------
/**
* Per-request gate for routes in the 'frontend' middleware group.
*
* @param callable $resolver fn(\Illuminate\Http\Request $request): ?string
* Return null to continue, or a URL string to
* redirect the request there.
*
* Throws if a second plugin tries to claim this slot.
*/
public static function setRouteResolver(callable $resolver): void
{
Hook::set(self::H_ROUTE_DECISION, $resolver);
}
/**
* Declarative whitelist — list every route-name pattern (fnmatch globs)
* the plugin wants to keep accessible inside the host's `frontend`
* middleware group. Anything not matching aborts with HTTP 404.
*
* The plugin author edits ONE array as the source of truth — no inline
* resolver closure, no separate redirect target to keep in sync. When
* a new host route surfaces that customers must reach (e.g. a future
* `refactor.account.email_verify`), add the pattern here.
*
* For non-trivial decisions (per-plan entitlements, custom redirect),
* use {@see setRouteResolver()} directly instead.
*/
public static function setAllowedRoutes(array $patterns): void
{
self::setRouteResolver(function ($request) use ($patterns) {
$name = $request->route()?->getName() ?? '';
foreach ($patterns as $p) {
if ($p === $name || fnmatch($p, $name)) {
return null; // continue
}
}
abort(404);
});
}
// -------------------------------------------------------------------------
// Resolution API — core calls at runtime
//
// resolveRouteDecision() calls Hook::setIfEmpty(...) immediately before
// perform(), following the established pattern in SubscriberController
// and Plugin.php. Boot order is irrelevant: if a plugin's set() ran
// during boot, setIfEmpty() here no-ops; if no plugin registered, the
// default gets installed lazily on first request.
// -------------------------------------------------------------------------
public static function resolveRouteDecision($request): ?string
{
Hook::setIfEmpty(self::H_ROUTE_DECISION, fn($r) => null);
return Hook::perform(self::H_ROUTE_DECISION, [$request]);
}
}