listUseCaseOverride = $listUseCaseOverride; $this->controllerOverride = $controllerOverride; $this->summaryUseCaseOverride = $summaryUseCaseOverride; } /** * Bind the §12.4 use case plus the composed * {@see AuditController}, then append one * {@see RouteDefinition} value to the bound * {@see RouteRegistrar}. * * Resolution order: * * 1. Resolve {@see AuditEventRepository} from * {@see PersistenceServiceProvider::BINDING_AUDIT_EVENT_REPOSITORY}. * Missing binding raises {@see LogicException} so a * misordered bootstrap fails loudly here rather than at * the first REST request. * * 2. Build (or accept the override for) the §12.4 * {@see ListAuditEventsUseCase} against the resolved * repository. * * 3. Build (or accept the override for) the * {@see AuditController} composed from the use case plus * a fresh {@see ProviderExceptionMapper} / * {@see CapabilityPolicy} / {@see RequestValidator} / * {@see RestRequestAdapter}. * * 4. Bind the use case and the controller under the * `BINDING_*` keys and cache the controller on this * provider so the route-callback closure can capture it. * * 5. Resolve {@see RouteRegistrar} from * {@see RestServiceProvider::BINDING_ROUTE_REGISTRAR} and * append the §12.4 {@see RouteDefinition}. * * @param ServiceProviderRegistry $registry Registry driving * this bootstrap pass. * * @throws LogicException When the persistence or REST provider * has not registered before this one. */ public function register( ServiceProviderRegistry $registry ): void { $events = $this->resolveAuditEventRepository( $registry ); $listUseCase = $this->listUseCaseOverride ?? new ListAuditEventsUseCase( $events ); $summaryUseCase = $this->summaryUseCaseOverride ?? new GetAuditEventsSummaryUseCase( $events ); $controller = $this->controllerOverride ?? new AuditController( $listUseCase, new ProviderExceptionMapper(), new CapabilityPolicy(), new RequestValidator(), new RestRequestAdapter(), null, $summaryUseCase ); $this->resolvedController = $controller; $registry->bind( self::BINDING_LIST_USE_CASE, $listUseCase ); $registry->bind( self::BINDING_SUMMARY_USE_CASE, $summaryUseCase ); $registry->bind( self::BINDING_CONTROLLER, $controller ); $registrar = $this->resolveRouteRegistrar( $registry ); $this->appendRoutes( $registrar ); } /** * No-op — routes are registered via * {@see RestServiceProvider}'s `rest_api_init` driver. * * @param HookRegistrar $hooks Shared hook registrar. */ public function boot( HookRegistrar $hooks ): void { unset( $hooks ); } /** * Resolve {@see AuditEventRepository} from the registry. * * @param ServiceProviderRegistry $registry Bootstrap registry. * * @throws LogicException When the binding is missing or the * bound value is not an * {@see AuditEventRepository}. */ private function resolveAuditEventRepository( ServiceProviderRegistry $registry ): AuditEventRepository { $key = PersistenceServiceProvider::BINDING_AUDIT_EVENT_REPOSITORY; if ( ! $registry->has( $key ) ) { $message = 'AuditServiceProvider requires PersistenceServiceProvider to register before it' . ' (missing AuditEventRepository binding).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $events = $registry->get( $key ); if ( ! $events instanceof AuditEventRepository ) { $message = sprintf( 'AuditServiceProvider expected AuditEventRepository under "%s"; got %s.', $key, is_object( $events ) ? get_class( $events ) : gettype( $events ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $events; } /** * Resolve {@see RouteRegistrar} from the registry. * * @param ServiceProviderRegistry $registry Bootstrap registry. * * @throws LogicException When the binding is missing or the * bound value is not a * {@see RouteRegistrar}. */ private function resolveRouteRegistrar( ServiceProviderRegistry $registry ): RouteRegistrar { $key = RestServiceProvider::BINDING_ROUTE_REGISTRAR; if ( ! $registry->has( $key ) ) { $message = 'AuditServiceProvider requires RestServiceProvider to register before it' . ' (missing RouteRegistrar binding).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $registrar = $registry->get( $key ); if ( ! $registrar instanceof RouteRegistrar ) { $message = sprintf( 'AuditServiceProvider expected RouteRegistrar under "%s"; got %s.', $key, is_object( $registrar ) ? get_class( $registrar ) : gettype( $registrar ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $registrar; } /** * Append the §12.4 list and §12.5 summary * {@see RouteDefinition} values to the supplied registrar. * * Both routes share the same base permission callback; the * {@see RouteDefinition} wrapper enforces the route-token * capability policy before the controller runs. * * @param RouteRegistrar $registrar Bound registrar. */ private function appendRoutes( RouteRegistrar $registrar ): void { $permissionCallback = static function ( $request = null ): bool { return $request !== false; }; $registrar->add( new RouteDefinition( RestNamespace::FULL, '/audit/events', RouteDefinition::METHOD_GET, AuditController::ROUTE_TOKEN_LIST, array(), $this->buildRouteCallback(), $permissionCallback ) ); $registrar->add( new RouteDefinition( RestNamespace::FULL, '/audit/events/summary', RouteDefinition::METHOD_GET, AuditController::ROUTE_TOKEN_SUMMARY, array(), $this->buildSummaryRouteCallback(), $permissionCallback ) ); } /** * Build a route-callback {@see Closure} that dispatches to * {@see AuditController::list()} and adapts the returned * envelope onto a {@see \WP_REST_Response} (or the pure-unit-mode * array shim). */ private function buildRouteCallback(): Closure { return function ( mixed $request ): mixed { $controller = $this->resolvedController; if ( $controller === null ) { $message = 'AuditServiceProvider route callback invoked before register() ran.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $envelope = $controller->list( $request ); $status = isset( $envelope[ AuditController::META_HTTP_STATUS ] ) && is_int( $envelope[ AuditController::META_HTTP_STATUS ] ) ? $envelope[ AuditController::META_HTTP_STATUS ] : 200; unset( $envelope[ AuditController::META_HTTP_STATUS ] ); if ( class_exists( '\\WP_REST_Response' ) ) { return new \WP_REST_Response( $envelope, $status ); } return array( 'data' => $envelope, 'status' => $status, ); }; } /** * Build a route-callback {@see Closure} that dispatches to * {@see AuditController::summary()} and adapts the returned * envelope onto a {@see \WP_REST_Response} (or the * pure-unit-mode array shim). * * Mirrors {@see self::buildRouteCallback()} verbatim except * for the dispatched method, so the §12.5 summary surface * inherits the same HTTP-status synthesis and pure-unit-mode * fallback the §12.4 list surface uses. */ private function buildSummaryRouteCallback(): Closure { return function ( mixed $request ): mixed { $controller = $this->resolvedController; if ( $controller === null ) { $message = 'AuditServiceProvider summary route callback invoked before register() ran.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $envelope = $controller->summary( $request ); $status = isset( $envelope[ AuditController::META_HTTP_STATUS ] ) && is_int( $envelope[ AuditController::META_HTTP_STATUS ] ) ? $envelope[ AuditController::META_HTTP_STATUS ] : 200; unset( $envelope[ AuditController::META_HTTP_STATUS ] ); if ( class_exists( '\\WP_REST_Response' ) ) { return new \WP_REST_Response( $envelope, $status ); } return array( 'data' => $envelope, 'status' => $status, ); }; } }