*/ private array $pages; /** * @since 1.0.0 * * @param array $pages All admin pages, in display order. */ public function __construct( array $pages ) { $this->pages = array_values( array_filter( $pages, static fn( $p ) => $p instanceof AdminPage ) ); } /** * Registers Plugin service hooks. * * @since 1.0.0 * * @return array> */ public function get_hooks(): array { return array( array( 'admin_menu', array( $this, 'register_menu' ) ), ); } /** * Registers the top-level menu and all submenu pages. * * @since 1.0.0 * @hook admin_menu * * @return void */ public function register_menu(): void { $top_cap = CapabilityMap::for( 'view_report' ); // Top-level entry — points to the Report page. add_menu_page( __( 'Postlinkly', 'postlinkly' ), __( 'Postlinkly', 'postlinkly' ), $top_cap, self::TOP_LEVEL_SLUG, array( $this, 'render_top_level' ), $this->get_menu_icon(), 80 ); // Register each page as a submenu. The first submenu replaces // the auto-generated duplicate of the top-level entry. foreach ( $this->pages as $page ) { add_submenu_page( self::TOP_LEVEL_SLUG, $page->get_page_title(), $page->get_menu_title(), $page->get_capability(), $page->get_slug(), array( $page, 'render' ) ); } } /** * Returns the data URI for the menu SVG icon. * * WordPress accepts a base64-encoded SVG data URI as the `$icon_url` * argument to `add_menu_page()`. This keeps the icon self-contained * (no extra HTTP request) and lets WP tint it with the active admin * colour scheme. * * @since 1.0.0 * * @return string Data URI string. */ private function get_menu_icon(): string { $path = defined( 'POSTLINKLY_PLUGIN_DIR' ) ? (string) POSTLINKLY_PLUGIN_DIR . 'assets/images/menu-icon.svg' : ''; if ( '' !== $path && is_readable( $path ) ) { $svg = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read; wp_remote_get is for HTTP. if ( is_string( $svg ) && '' !== $svg ) { return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Standard WP pattern for inline SVG menu icons. } } // Fallback to a dashicon if the SVG file is missing. return 'dashicons-admin-links'; } /** * Fallback render callback for the top-level menu entry. * * In practice WordPress always routes to the first submenu, so this * is only called when no pages are registered. * * @since 1.0.0 * * @return void */ public function render_top_level(): void { echo '

' . esc_html__( 'Postlinkly', 'postlinkly' ) . '

'; } }