useCaseOverride = $useCaseOverride; $this->controllerOverride = $controllerOverride; } /** * Bind the §12.1 use case plus the composed * {@see DiagnosticsController}, then append one * {@see RouteDefinition} to the bound {@see RouteRegistrar}. * * @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 { $schemaVersions = $this->resolveSchemaVersionRepository( $registry ); $jobs = $this->resolveSyncJobRepository( $registry ); $clock = $this->resolveClock(); // Phase 12 slice 12c: the providers map is resolved from the // centralized ProviderRegistry binding published by // ProviderServiceProvider. When the binding is missing — // typically in tests that do not pre-seed it — the resolver // falls back to an empty registry so the §12.1 envelope // renders an empty `providers` branch rather than failing. $useCase = $this->useCaseOverride ?? new GetDiagnosticsUseCase( $schemaVersions, $jobs, $this->resolveProviderRegistry( $registry )->all(), null, $clock ); $controller = $this->controllerOverride ?? new DiagnosticsController( $useCase, new ProviderExceptionMapper(), new CapabilityPolicy(), new RestRequestAdapter() ); $this->resolvedController = $controller; $registry->bind( self::BINDING_USE_CASE, $useCase ); $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 SchemaVersionRepository} from the registry. * * @param ServiceProviderRegistry $registry Bootstrap registry. * * @throws LogicException When the binding is missing or the * bound value is not a * {@see SchemaVersionRepository}. */ private function resolveSchemaVersionRepository( ServiceProviderRegistry $registry ): SchemaVersionRepository { $key = PersistenceServiceProvider::BINDING_SCHEMA_VERSION_REPOSITORY; if ( ! $registry->has( $key ) ) { $message = 'DiagnosticsServiceProvider requires PersistenceServiceProvider to register before it' . ' (missing SchemaVersionRepository binding).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $repo = $registry->get( $key ); if ( ! $repo instanceof SchemaVersionRepository ) { $message = sprintf( 'DiagnosticsServiceProvider expected SchemaVersionRepository under "%s"; got %s.', $key, is_object( $repo ) ? get_class( $repo ) : gettype( $repo ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $repo; } /** * Resolve {@see SyncJobRepository} from the registry. * * @param ServiceProviderRegistry $registry Bootstrap registry. * * @throws LogicException When the binding is missing or the * bound value is not a * {@see SyncJobRepository}. */ private function resolveSyncJobRepository( ServiceProviderRegistry $registry ): SyncJobRepository { $key = PersistenceServiceProvider::BINDING_SYNC_JOB_REPOSITORY; if ( ! $registry->has( $key ) ) { $message = 'DiagnosticsServiceProvider requires PersistenceServiceProvider to register before it' . ' (missing SyncJobRepository binding).'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } $jobs = $registry->get( $key ); if ( ! $jobs instanceof SyncJobRepository ) { $message = sprintf( 'DiagnosticsServiceProvider expected SyncJobRepository under "%s"; got %s.', $key, is_object( $jobs ) ? get_class( $jobs ) : gettype( $jobs ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $jobs; } /** * Resolve the {@see ProviderRegistry} from the registry. * * Phase 12 slice 12c centralizes the §12.1 providers map under * {@see ProviderServiceProvider::BINDING_PROVIDER_REGISTRY}. * When the binding is missing — typically in tests that * pre-seed the diagnostics collaborators without standing up * the provider service provider — this method returns a fresh * empty {@see ProviderRegistry} so the §12.1 enumeration * renders an empty `providers` branch rather than failing * loudly. The fallback mirrors the Phase-11 minimum the * {@see GetDiagnosticsUseCase} constructor already documents * on the empty-registry case. * * @param ServiceProviderRegistry $registry Bootstrap registry. * * @throws LogicException When the binding exists but is not a * {@see ProviderRegistry}. */ private function resolveProviderRegistry( ServiceProviderRegistry $registry ): ProviderRegistry { $key = ProviderServiceProvider::BINDING_PROVIDER_REGISTRY; if ( ! $registry->has( $key ) ) { return new ProviderRegistry( array() ); } $value = $registry->get( $key ); if ( ! $value instanceof ProviderRegistry ) { $message = sprintf( 'DiagnosticsServiceProvider expected ProviderRegistry under "%s"; got %s.', $key, is_object( $value ) ? get_class( $value ) : gettype( $value ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $value; } /** * Resolve a {@see Clock} for the §12.1 use case. * * Mirrors {@see SyncServiceProvider::resolveClock()}: no Clock * binding is currently published, so the Phase-11 minimum * default-constructs a {@see SystemClock}. */ private function resolveClock(): Clock { return new SystemClock(); } /** * 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 = 'DiagnosticsServiceProvider 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( 'DiagnosticsServiceProvider 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.1 {@see RouteDefinition} to the supplied * registrar. * * @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, '/diagnostics', RouteDefinition::METHOD_GET, DiagnosticsController::ROUTE_TOKEN_READ, array(), $this->buildRouteCallback(), $permissionCallback ) ); } /** * Build a route-callback {@see Closure} that dispatches to * {@see DiagnosticsController::read()} 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 = 'DiagnosticsServiceProvider 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->read( $request ); $status = isset( $envelope[ DiagnosticsController::META_HTTP_STATUS ] ) && is_int( $envelope[ DiagnosticsController::META_HTTP_STATUS ] ) ? $envelope[ DiagnosticsController::META_HTTP_STATUS ] : 200; unset( $envelope[ DiagnosticsController::META_HTTP_STATUS ] ); if ( class_exists( '\\WP_REST_Response' ) ) { return new \WP_REST_Response( $envelope, $status ); } return array( 'data' => $envelope, 'status' => $status, ); }; } }