*/ private const ENVIRONMENT_KEYS = array( 'php_version', 'wp_version', 'rest_available', 'object_cache_available', 'cron_healthy', ); /** * Required keys for the `database` branch. * * @var list */ private const DATABASE_KEYS = array( 'schema_version', 'tables_ok', ); /** * Required keys for each row of the `providers` branch. * * @var list */ private const PROVIDER_ROW_KEYS = array( 'key', 'configured', 'last_success_at', 'last_error', ); /** * Required keys for the `jobs` branch. * * @var list */ private const JOBS_KEYS = array( 'queued', 'failed', ); /** * Required keys for the `observability` branch. * * @var list */ private const OBSERVABILITY_KEYS = array( 'logs_available', 'audit_available', 'request_id_tracing', 'correlation_id_tracing', ); /** * Required keys for the `entry_points` branch. * * The `audit_summary` slot was added in the Phase 2 KI-006 * follow-up that landed `GET /affilun/v1/audit/events/summary` * (§12.5); it is a sibling of `audit` rather than a child of * the audit slot so the diagnostics surface keeps a flat * key namespace. * * @var list */ private const ENTRY_POINTS_KEYS = array( 'logs', 'audit', 'audit_summary', ); /** * Environment axes per §12.1. * * @var array */ private array $environment; /** * Database axes per §12.1. * * @var array */ private array $database; /** * Per-provider rows per §12.1. * * @var list> */ private array $providers; /** * Job-status histogram restricted to `queued` and `failed` * per §12.1. * * @var array{queued: int, failed: int} */ private array $jobs; /** * Observability axes per §12.1. * * @var array */ private array $observability; /** * Entry-point URL pointers per §12.1. * * @var array{logs: ?string, audit: ?string, audit_summary: ?string} */ private array $entryPoints; /** * Construct the result with explicit values for every branch. * * @param array $environment Environment axes. * @param array $database Database axes. * @param list> $providers Per-provider rows. * @param array{queued: int, failed: int} $jobs Job-status histogram. * @param array $observability Observability axes. * @param array{logs: ?string, audit: ?string, audit_summary: ?string} $entryPoints Entry-point URL pointers. * * @throws InvalidArgumentException When any branch is missing * a required key, when a * provider row is malformed, * or when `$jobs` / * `$observability` / * `$entryPoints` carry values * outside the declared types. */ public function __construct( array $environment, array $database, array $providers, array $jobs, array $observability, array $entryPoints ) { self::assertKeys( 'environment', $environment, self::ENVIRONMENT_KEYS ); self::assertKeys( 'database', $database, self::DATABASE_KEYS ); foreach ( $providers as $index => $row ) { if ( ! is_array( $row ) ) { $message = sprintf( 'DiagnosticsResult: providers[%d] must be an array; got %s.', (int) $index, gettype( $row ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } self::assertKeys( sprintf( 'providers[%d]', (int) $index ), $row, self::PROVIDER_ROW_KEYS ); } self::assertKeys( 'jobs', $jobs, self::JOBS_KEYS ); if ( ! is_int( $jobs['queued'] ) || ! is_int( $jobs['failed'] ) ) { $message = 'DiagnosticsResult: jobs.queued and jobs.failed must be ints.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } self::assertKeys( 'observability', $observability, self::OBSERVABILITY_KEYS ); foreach ( self::OBSERVABILITY_KEYS as $key ) { if ( ! is_bool( $observability[ $key ] ) ) { $message = sprintf( 'DiagnosticsResult: observability.%s must be bool.', $key ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } self::assertKeys( 'entry_points', $entryPoints, self::ENTRY_POINTS_KEYS ); foreach ( self::ENTRY_POINTS_KEYS as $key ) { $value = $entryPoints[ $key ]; if ( $value !== null && ! is_string( $value ) ) { $message = sprintf( 'DiagnosticsResult: entry_points.%s must be string or null.', $key ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } $this->environment = $environment; $this->database = $database; $this->providers = array_values( $providers ); $this->jobs = array( 'queued' => $jobs['queued'], 'failed' => $jobs['failed'], ); $this->observability = array(); foreach ( self::OBSERVABILITY_KEYS as $key ) { $this->observability[ $key ] = (bool) $observability[ $key ]; } $this->entryPoints = array( 'logs' => $entryPoints['logs'], 'audit' => $entryPoints['audit'], 'audit_summary' => $entryPoints['audit_summary'], ); } /** * Environment axes per §12.1. * * @return array */ public function environment(): array { return $this->environment; } /** * Database axes per §12.1. * * @return array */ public function database(): array { return $this->database; } /** * Per-provider rows per §12.1. * * @return list> */ public function providers(): array { return $this->providers; } /** * Job-status histogram per §12.1. * * @return array{queued: int, failed: int} */ public function jobs(): array { return $this->jobs; } /** * Observability axes per §12.1. * * @return array */ public function observability(): array { return $this->observability; } /** * Entry-point URL pointers per §12.1. * * @return array{logs: ?string, audit: ?string, audit_summary: ?string} */ public function entryPoints(): array { return $this->entryPoints; } /** * Render the §12.1 success envelope `data` shape: * * `array( * 'environment' => array, * 'database' => array, * 'providers' => list>, * 'jobs' => array{queued: int, failed: int}, * 'observability' => array, * 'entry_points' => array{logs: ?string, audit: ?string, audit_summary: ?string}, * )` * * Key order matches the §12.1 example so the wire shape is * stable across PHP versions and JSON encoders. * * @return array */ public function toArray(): array { return array( 'environment' => $this->environment, 'database' => $this->database, 'providers' => $this->providers, 'jobs' => $this->jobs, 'observability' => $this->observability, 'entry_points' => $this->entryPoints, ); } /** * Assert that `$source` carries every key in `$requiredKeys`. * * @param string $branch Branch name for error messages. * @param array $source Map to validate. * @param list $requiredKeys Closed set of required keys. * * @throws InvalidArgumentException When any required key is * missing. */ private static function assertKeys( string $branch, array $source, array $requiredKeys ): void { foreach ( $requiredKeys as $key ) { if ( ! array_key_exists( $key, $source ) ) { $message = sprintf( 'DiagnosticsResult: %s is missing required key "%s".', $branch, $key ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } } } }