` carrying a generic developer-safe message — no * exception text reaches the rendered HTML so secrets / file paths * do not leak to admins. * * Escaping invariants: * * - Every visible text emission flows through `esc_html()`. * - Every attribute value flows through `esc_attr()`. * - Every URL flows through `esc_url()`. * * §12.1 注意 ("secrets MUST NOT appear in any field") is upheld by * the use-case → result-VO contract; the renderer does not scan * field values for credential shapes itself. * * @package Affilun\Presentation\Admin\Page */ declare(strict_types=1); namespace Affilun\Presentation\Admin\Page; use Affilun\Application\Diagnostics\UseCase\DiagnosticsResult; use Affilun\Application\Diagnostics\UseCase\GetDiagnosticsUseCase; use Throwable; /** * Renders the §13 Diagnostics admin page to stdout. * * See the file-level docblock for the page layout, the failure * posture, and the escaping invariants. */ final class DiagnosticsPageRenderer { /** * Settings page URL carrying the Operations tab hash. */ private const OPERATIONS_SETTINGS_PATH = 'options-general.php?page=affilun-settings#affilun-settings-tab-operations'; /** * Japanese label map for diagnostic environment / database / * observability keys. Keys not in this map fall back to the * raw key string so a future field still surfaces (best-effort * UX rather than a hard whitelist). * * @var array */ private const FIELD_LABELS = array( // Environment. 'php_version' => 'PHP バージョン', 'wp_version' => 'WordPress バージョン', 'rest_available' => 'REST API 利用可', 'object_cache_available' => '外部オブジェクトキャッシュ', 'cron_healthy' => 'WP-Cron 健全性', // Database. 'schema_version' => 'スキーマバージョン', 'tables_ok' => 'テーブル初期化完了', // Observability. 'logs_available' => 'ログ取得可', 'audit_available' => '監査イベント取得可', 'request_id_tracing' => 'request_id トレーシング', 'correlation_id_tracing' => 'correlation_id トレーシング', ); /** * §12.1 use case driving the page content. */ private GetDiagnosticsUseCase $useCase; /** * Construct the renderer with explicit collaborator injection. * * @param GetDiagnosticsUseCase $useCase §12.1 use case. */ public function __construct( GetDiagnosticsUseCase $useCase ) { $this->useCase = $useCase; } /** * Echo the §13 Diagnostics admin page HTML to stdout. */ public function render(): void { echo '
'; echo '

' . esc_html( '診断情報' ) . '

'; $this->renderEmbedded(); echo '
'; } /** * Echo diagnostics content without the outer admin-page wrapper. */ public function renderEmbedded(): void { try { $result = $this->useCase->execute(); } catch ( Throwable $e ) { unset( $e ); $this->renderErrorNotice(); return; } $this->renderEnvironment( $result ); $this->renderDatabase( $result ); $this->renderProviders( $result ); $this->renderJobs( $result ); $this->renderObservability( $result ); $this->renderEntryPoints( $result ); } /** * Render the §13 generic-error notice when the use case throws. * * The message is deliberately generic so a misconfigured * persistence stack does not leak exception text (which can * contain file paths / SQL fragments / credential shapes) into * the admin output. */ private function renderErrorNotice(): void { echo '

'; echo esc_html( '診断情報を取得できません。ログをご確認ください。' ); echo '

'; } /** * Render the §13 Environment definition list. * * @param DiagnosticsResult $result Backing result VO. */ private function renderEnvironment( DiagnosticsResult $result ): void { echo '

' . esc_html( '動作環境' ) . '

'; $this->renderInlineList( $result->environment() ); } /** * Render the §13 Database definition list. * * @param DiagnosticsResult $result Backing result VO. */ private function renderDatabase( DiagnosticsResult $result ): void { echo '

' . esc_html( 'データベース' ) . '

'; $this->renderInlineList( $result->database() ); } /** * Render a key/value map as an inline `