__`; * `tab____`); migrate_pins() moves a user's pins into the one row, once (Prefs::migrate()). * - Since Rooster One's navigation was fixed at the source (25 Oct 2026, its docs/test-failures-2026-10-24.md * sections 3e and 3f): every row of its registry carries `section`, `icon` and `page`, and every group definition * `colour` and `icon`; the adapter reads them (0.13.0) and the interim rules of D (sibling pages, the page a card's * URL named, an alias's page, a keyword icon map, a colour table) are gone. in_nav() stays: three modules are tabs * of another screen and say so with `appears_in_nav()`. * - D (24 Oct 2026): the e-shop Manager's sections within a group (`get_section_definitions()`, each module's * `get_section()`) are the library's groups, so the menu bar's dropdowns keep Rooster One's columns; its six * personas (`AIEO_Personas::defs()`) are the audiences, and a row's audiences are the personas for which * `AIEO_Personas::rank()` does not say "unlikely" (Rooster One's own relevance, not a second map; a row every * persona sees carries none, which the library reads as everyone); migrate_prefs() moves a user's pins and persona * choice into the one row and deletes the six old keys. * * register_product() is D's switch: Rooster One calls it on `rooster/admin/register`. * * @package RoosterAdmin */ namespace Rooster\Admin\Adapters; use Rooster\Admin\Prefs; use Rooster\Admin\Registry; defined( 'ABSPATH' ) || exit; /** * Static. */ final class RoosterOne { /** * The product slug. */ public const PRODUCT = 'rooster-one'; /** * Rooster One's classes (strings: the adapter loads without Rooster One). */ private const ONE_REGISTRY = '\AIEO_Capability_Registry'; private const ONE_DMM = '\AIEO_DMM'; private const ONE_PERSONAS = '\AIEO_Personas'; /** * The product's colour: the ecosystem's blue, Rooster One's since its first chrome. */ private const COLOUR = '#1e63ce'; /** * The old pin store (user meta) and its token prefixes (AIEO_DMM::PINS_META, CARD_PIN_PREFIX, TAB_PIN_PREFIX). */ private const PINS_META = 'aieo_welcome_pins'; private const CARD_PREFIX = 'card__'; private const TAB_PREFIX = 'tab__'; /** * The other old user-meta keys (AIEO_Personas::META, META_AT; the Home layout's order and columns): the persona * choice moves into the one row, the layout starts afresh (another shape). */ private const PERSONAS_META = 'aieo_personas'; private const PERSONAS_AT_META = 'aieo_personas_at'; private const LAYOUT_METAS = array( 'aieo_welcome_group_order', 'aieo_welcome_card_cols' ); /** * The one label of the adapter's own: the e-shop Manager's "Home" group is the Start pill (the product's Home is * the library's). Every other label, colour and icon comes from `AIEO_DMM::get_group_definitions()`. */ private const HOME_GROUP = 'home'; private const HOME_LABEL = 'Start'; /** * The section a row without a group lands in (a guard: every row of the registry names one since 25 Oct 2026). */ private const FALLBACK_SECTION = 'tools'; /** * Rooster One's index page ("All Features", `dmm_tab=welcome`): the library's Home replaces it (D), so its row * and its cards are not registered; a pin to it is dropped by the migration (an unknown id), and Rooster One * redirects its URL to the Home. */ public const INDEX_PAGE = 'welcome'; /** * Whether Rooster One's registry is loaded. */ public static function registry_present(): bool { return class_exists( self::ONE_REGISTRY ) && class_exists( self::ONE_DMM ) && defined( 'AIEO_CAPABILITY' ); } /** * The id table for a row's id: a page keeps its name under the prefix; a tool `tool:` and a tab * `tab::` take `/` for `:` ('' for an id that cannot be one of ours). * * @param string $id A row id of Rooster One's registry. */ public static function id_of_row( string $id ): string { $id = str_replace( ':', '/', trim( $id ) ); return '' === $id || 1 !== preg_match( '/^[a-z0-9][a-z0-9_\/.-]{0,99}$/', $id ) ? '' : self::PRODUCT . '/' . $id; } /** * The id table for a stored pin (`aieo_welcome_pins`): a page's id; `card____`, the tool whose * URL ends with that anchor on that page; `tab____`, the tab `tab::`. * * @param string $old The pin token. * @param array>|null $rows Rooster One's rows (null = read them). */ public static function id_of( string $old, ?array $rows = null ): string { $old = trim( $old ); if ( str_starts_with( $old, self::CARD_PREFIX ) ) { $rest = substr( $old, strlen( self::CARD_PREFIX ) ); $at = strrpos( $rest, '__' ); if ( false === $at || 0 === $at ) { return ''; } $page = substr( $rest, 0, $at ); $anchor = substr( $rest, $at + 2 ); $rows = $rows ?? self::rows(); foreach ( $rows as $id => $row ) { if ( 'tool' === (string) ( $row['kind'] ?? '' ) && $page === (string) ( $row['page'] ?? '' ) ) { $url = (string) ( $row['url'] ?? '' ); if ( str_ends_with( $url, '#' . $anchor ) || str_ends_with( $url, '#aieo-card-' . $anchor ) ) { return self::id_of_row( (string) $id ); } } } return ''; } if ( str_starts_with( $old, self::TAB_PREFIX ) ) { $rest = substr( $old, strlen( self::TAB_PREFIX ) ); $at = strrpos( $rest, '__' ); if ( false === $at || 0 === $at ) { return ''; } return self::id_of_row( 'tab:' . substr( $rest, 0, $at ) . ':' . substr( $rest, $at + 2 ) ); } return 1 === preg_match( '/^[a-z0-9][a-z0-9_-]{0,60}$/', $old ) ? self::id_of_row( $old ) : ''; } /** * Rooster One's rows (id => row). * * @return array> */ private static function rows(): array { return self::registry_present() ? (array) call_user_func( array( self::ONE_REGISTRY, 'all' ) ) : array(); } /** * Rooster One's welcome in the library's anatomy (0.21.0), from the welcome form's definition (AIEO_Forms, * includes/dmm/data/forms.php), one source: the hero, the title, the intro and its tail, the punch, the * caution, the folds. On Rooster One's own screens the form itself is the welcome (AIEO_Chrome::services()); * the library's dialog carries the same words wherever the form is not. * * @return array */ private static function welcome(): array { if ( ! class_exists( 'AIEO_Forms' ) || ! method_exists( 'AIEO_Forms', 'definitions' ) ) { return array(); } $defs = \AIEO_Forms::definitions(); $form = is_array( $defs['forms']['welcome'] ?? null ) ? $defs['forms']['welcome'] : array(); if ( array() === $form ) { return array(); } $blocks = array(); foreach ( is_array( $form['blocks'] ?? null ) ? $form['blocks'] : array() as $block ) { if ( ! is_array( $block ) || '' === (string) ( $block['x'] ?? '' ) ) { continue; } $blocks[] = array( 'title' => (string) $block['x'], 'lede' => (string) ( $block['lede'] ?? '' ), 'body' => is_array( $block['body'] ?? null ) ? $block['body'] : array(), ); } return array( 'hero' => (string) ( $form['hero'] ?? '' ), 'title' => (string) ( $form['title'] ?? '' ), 'intro' => array_values( array_filter( array( (string) ( $form['intro'] ?? '' ), (string) ( $form['intro_tail'] ?? '' ) ) ) ), 'punch' => (string) ( $form['punch'] ?? '' ), 'caution' => is_array( $form['caution'] ?? null ) ? $form['caution'] : array(), 'blocks' => $blocks, ); } /** * Whether the site's tier opens the Pro rows. */ private static function pro_active(): bool { return self::registry_present() && 'free' !== (string) call_user_func( array( self::ONE_REGISTRY, 'site_tier' ) ); } /** * The product (CONTRACT §2) from Rooster One's own data: the e-shop Manager's groups as sections in their * priority order, each with the colour and icon its definition names (one palette, the plugin's, for the side * menu's stripes, the toolbar and the chrome alike), its sections as the dropdown's columns. * * @return array */ public static function product(): array { $dmm = self::registry_present() ? call_user_func( array( self::ONE_DMM, 'instance' ) ) : null; $groups = null !== $dmm ? (array) $dmm->get_group_definitions() : array(); $within = null !== $dmm && method_exists( $dmm, 'get_section_definitions' ) ? (array) $dmm->get_section_definitions() : array(); usort( $groups, static fn ( $a, $b ): int => (int) ( $a['priority'] ?? 0 ) <=> (int) ( $b['priority'] ?? 0 ) ); $sections = array(); foreach ( $groups as $group ) { $id = (string) ( $group['id'] ?? '' ); if ( '' === $id ) { continue; } $named = array(); $stacks = array(); foreach ( (array) ( $within[ $id ] ?? array() ) as $column ) { $key = is_array( $column ) ? (string) ( $column['id'] ?? '' ) : ''; if ( '' === $key ) { continue; } $named[ $key ] = wp_specialchars_decode( (string) ( $column['label'] ?? $key ), ENT_QUOTES ); if ( ! empty( $column['stacks'] ) && array() !== $stacks ) { $stacks[ count( $stacks ) - 1 ][] = $key; // flagged `stacks`: under the section before it, in its column } else { $stacks[] = array( $key ); } } $colour = (string) ( $group['colour'] ?? $group['color'] ?? '' ); $sections[ $id ] = array( 'label' => self::HOME_GROUP === $id ? self::HOME_LABEL : wp_specialchars_decode( (string) ( $group['label'] ?? $id ), ENT_QUOTES ), 'colour' => 1 === preg_match( '/^#[0-9a-fA-F]{6}$/', $colour ) ? strtolower( $colour ) : '#a1a1aa', 'icon' => '' !== (string) ( $group['icon'] ?? '' ) ? (string) $group['icon'] : 'layout-grid', 'groups' => $named, // the e-shop Manager's sections within the group: the dropdown's columns 'stacks' => $stacks, ); } if ( ! isset( $sections[ self::FALLBACK_SECTION ] ) ) { $sections[ self::FALLBACK_SECTION ] = array( 'label' => 'Tools', 'colour' => '#a1a1aa', 'icon' => 'wrench', ); } $pro = self::pro_active(); return array( 'slug' => self::PRODUCT, 'name' => 'Rooster One', 'words' => array( 'Rooster', 'One' ), 'colour' => self::COLOUR, 'version' => defined( 'AIEO_VERSION' ) ? (string) AIEO_VERSION : '', 'strapline' => __( 'The e-shop Manager: catalogue, pricing, storefront, SEO, insights and promotion on one page.', 'ai-eshop-optimizer' ), 'welcome' => self::welcome(), 'links' => array( 'manual' => 'https://rooster.systems/category/en/documentation/rooster-one/', 'help' => 'https://rooster.systems/help-and-support/', 'feedback' => 'https://rooster.systems/help-and-support/feedback/', 'chat' => 'https://invite.viber.com/?g2=AQBLYG4uDl%2B7a1bwZ7%2FJ%2Bw6MeguHY3TT2Id2N%2FwfC%2F%2FnzlWGnVxA0eJPIk8L%2F4B%2B', // the Rooster community on Viber (0.20.0; the old bar's Viber item) 'review' => 'https://wordpress.org/support/plugin/ai-eshop-optimizer/reviews/#new-post', 'product' => 'https://rooster.systems/', 'directory' => 'https://wordpress.org/plugins/ai-eshop-optimizer/', 'pro' => 'https://rooster.systems/', ), 'tier' => array( 'label' => $pro ? _x( 'Pro', 'the tier pill', 'ai-eshop-optimizer' ) : _x( 'Free', 'the tier pill', 'ai-eshop-optimizer' ), 'on' => $pro, 'detail' => '', ), 'home' => array( 'parent' => 'ai-eshop-optimizer', 'slug' => 'rooster-one-home', 'capability' => defined( 'AIEO_CAPABILITY' ) ? (string) AIEO_CAPABILITY : 'manage_options', ), 'sections' => $sections, 'audiences' => self::audiences(), 'screens' => array( 'pages' => array( 'ai-eshop-dmm', 'ai-eshop-optimizer', 'ai-eshop-chat' ), 'ids' => array(), ), ); } /** * The personas as the audiences (CONTRACT §2): the label and the sentence are Rooster One's own; the persona * whose `likely` is `*` (the developer) sees everything; the colours are the library's palette. * * @return array> */ public static function audiences(): array { if ( ! class_exists( self::ONE_PERSONAS ) ) { return array(); } $out = array(); foreach ( (array) call_user_func( array( self::ONE_PERSONAS, 'defs' ) ) as $code => $persona ) { $out[ (string) $code ] = array( 'label' => wp_specialchars_decode( (string) ( $persona['label'] ?? $code ), ENT_QUOTES ), 'sentence' => wp_specialchars_decode( (string) ( $persona['sentence'] ?? '' ), ENT_QUOTES ), 'all' => in_array( '*', (array) ( $persona['likely'] ?? array() ), true ), ); } return $out; } /** * Whether the e-shop Manager's own navigation lists a page (its module's `appears_in_nav()`; true for a page * without a module). The old menu bar, side menu and index left out the modules that say no (the superseded * reports' replacements, My settings, Fulfillment, Product orders analytics); the library does the same through * `listed`. * * @param string $page_id A page id of Rooster One's registry. */ public static function in_nav( string $page_id ): bool { if ( '' === $page_id || ! self::registry_present() ) { return true; } $module = call_user_func( array( call_user_func( array( self::ONE_DMM, 'instance' ) ), 'get_module' ), $page_id ); return ! ( is_object( $module ) && method_exists( $module, 'appears_in_nav' ) ) || (bool) $module->appears_in_nav(); } /** * The personas a row is for, by Rooster One's own relevance rule (AIEO_Personas::rank(): a persona names the row's * id, its group or its group/section as core or likely, or sees everything with `*`); every persona for a row all * of them see (a group no persona may demote: home, tools, setup; a destination no persona mentions: silence means * show), because the library shows an entry to a chosen persona only when the lists meet (Personas::matches()), * an empty list being "no persona". Empty only on a site without personas. The definitions are read once * (persona_sets()); 877 rows cost about a millisecond, where a call of rank() per row and persona cost 740 ms. * * @param string $id The row's id (a page's, a tool's). * @param string $group The row's group (the library's section). * @param string $section The row's section within the group (the library's group). * @return list */ public static function audiences_of( string $id, string $group, string $section ): array { $sets = self::persona_sets(); if ( array() === $sets['codes'] ) { return array(); } if ( in_array( $group, $sets['never_demoted'], true ) ) { return $sets['codes']; } $gs = '' !== $section ? $group . '/' . $section : ''; $mapped = ( '' !== $id && isset( $sets['mapped_ids'][ $id ] ) ) || ( '' !== $group && isset( $sets['mapped'][ $group ] ) ) || ( '' !== $gs && isset( $sets['mapped'][ $gs ] ) ); if ( ! $mapped ) { return $sets['codes']; // silence means show: likely for everyone } $for = array(); foreach ( $sets['codes'] as $code ) { $d = $sets['defs'][ $code ]; if ( $d['all'] || ( '' !== $id && isset( $d['ids'][ $id ] ) ) || ( '' !== $group && isset( $d['targets'][ $group ] ) ) || ( '' !== $gs && isset( $d['targets'][ $gs ] ) ) ) { $for[] = $code; } } return $for; } /** * The persona definitions as lookup tables, read once per request: per persona its ids, its core and likely * targets (a group or `group/section`) and whether it sees all; the union of every mapped target and id; the * groups no persona may demote. * * @return array{codes: list, defs: array, targets: array}>, mapped: array, mapped_ids: array, never_demoted: list} */ private static function persona_sets(): array { static $sets = null; if ( null !== $sets ) { return $sets; } $sets = array( 'codes' => array(), 'defs' => array(), 'mapped' => array(), 'mapped_ids' => array(), 'never_demoted' => class_exists( self::ONE_PERSONAS ) && defined( self::ONE_PERSONAS . '::NEVER_DEMOTED' ) ? array_map( 'strval', (array) constant( self::ONE_PERSONAS . '::NEVER_DEMOTED' ) ) : array( 'home', 'tools', 'setup' ), ); if ( ! class_exists( self::ONE_PERSONAS ) ) { return $sets; } foreach ( (array) call_user_func( array( self::ONE_PERSONAS, 'defs' ) ) as $code => $d ) { $code = (string) $code; $targets = array(); $all = false; foreach ( array_merge( (array) ( $d['core'] ?? array() ), (array) ( $d['likely'] ?? array() ) ) as $t ) { $t = (string) $t; if ( '*' === $t ) { $all = true; } elseif ( '' !== $t ) { $targets[ $t ] = true; $sets['mapped'][ $t ] = true; } } $ids = array(); foreach ( (array) ( $d['ids'] ?? array() ) as $i ) { $i = (string) $i; if ( '' !== $i ) { $ids[ $i ] = true; $sets['mapped_ids'][ $i ] = true; } } $sets['codes'][] = $code; $sets['defs'][ $code ] = array( 'all' => $all, 'ids' => $ids, 'targets' => $targets, ); } return $sets; } /** * One of Rooster One's rows as a catalogue row (null for an action: not a destination). * * @param array $row The row (id, label, kind, page, group, tier, url, description, keywords). * @param bool $pro_active Whether the site's tier opens the Pro rows. * @param string $capability The e-shop Manager's capability. * @param string $section The row's section within its group (the e-shop Manager's, as the registry writes it; the library's group; '' = the row's own). * @param list $audiences The personas the row is for (audiences_of()). * @param bool $listed Whether the menu bar and the Home list it (in_nav(): the e-shop Manager's own * navigation leaves the superseded reports' replacements out). * @return array|null */ public static function map_row( array $row, bool $pro_active, string $capability, string $section = '', array $audiences = array(), bool $listed = true ): ?array { $kind = array( 'page' => 'page', 'tab' => 'tab', 'tool' => 'card' )[ (string) ( $row['kind'] ?? '' ) ] ?? ''; $id = self::id_of_row( (string) ( $row['id'] ?? '' ) ); if ( '' === $kind || '' === $id ) { return null; } $available = 'pro' !== (string) ( $row['tier'] ?? 'free' ) || $pro_active; $group = (string) ( $row['group'] ?? '' ); return array( 'id' => $id, 'kind' => $kind, 'product' => self::PRODUCT, 'section' => '' !== $group ? $group : self::FALLBACK_SECTION, 'group' => '' !== $section ? $section : (string) ( $row['section'] ?? '' ), 'title' => (string) ( $row['label'] ?? $row['id'] ), 'description' => (string) ( $row['description'] ?? '' ), 'url' => (string) ( $row['url'] ?? '' ), 'icon' => (string) ( $row['icon'] ?? '' ), // the module's, the tab's or the card's own (the registry guesses for the rest) 'capabilities' => array( $capability ), 'keywords' => array_values( array_filter( explode( ' ', (string) ( $row['keywords'] ?? '' ) ) ) ), 'audiences' => $audiences, 'listed' => $listed, 'available' => $available, 'badge' => $available ? '' : 'Pro', 'parent' => 'page' !== $kind ? self::id_of_row( (string) ( $row['page'] ?? '' ) ) : '', ); } /** * The catalogue rows (CONTRACT §3) from Rooster One's. * * @return list> */ public static function entries(): array { $pro = self::pro_active(); $cap = defined( 'AIEO_CAPABILITY' ) ? (string) AIEO_CAPABILITY : 'manage_options'; $rows = self::rows(); $in_nav = array(); // page id => whether the e-shop Manager's own navigation lists it $out = array(); foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $kind = (string) ( $row['kind'] ?? '' ); $page = 'page' === $kind ? (string) ( $row['id'] ?? '' ) : (string) ( $row['page'] ?? '' ); if ( self::INDEX_PAGE === $page ) { continue; // the old index: the library's Home (gone from the plugin on 25 Oct 2026; a stale registry may still list it) } if ( ! isset( $in_nav[ $page ] ) ) { $in_nav[ $page ] = self::in_nav( $page ); } $above = is_array( $rows[ $page ] ?? null ) ? $rows[ $page ] : array(); $group = self::group_alias( '' !== (string) ( $row['group'] ?? '' ) ? (string) $row['group'] : (string) ( $above['group'] ?? '' ) ); $section = '' !== (string) ( $row['section'] ?? '' ) ? (string) $row['section'] : (string) ( $above['section'] ?? '' ); $row['group'] = $group; $entry = self::map_row( $row, $pro, $cap, $section, self::audiences_of( (string) ( $row['id'] ?? '' ), $group, $section ), $in_nav[ $page ] ); if ( null !== $entry ) { $out[] = $entry; } } foreach ( self::link_entries( $pro, $cap ) as $entry ) { $out[] = $entry; } return self::with_pages( $out ); } /** * A group slug through Rooster One's aliases (`AIEO_DMM::resolve_group_alias()`): a row that still names a merged * group (a tool a module adds through `aieo_capabilities` with `group => 'seo'` after SEO moved into Promote) lands * in the section its group became, instead of being rejected as an unknown section (MENU-AUDIT round 2). * * @param string $group The row's group. */ public static function group_alias( string $group ): string { if ( '' === $group || ! self::registry_present() ) { return $group; } $dmm = call_user_func( array( self::ONE_DMM, 'instance' ) ); return method_exists( $dmm, 'resolve_group_alias' ) ? (string) $dmm->resolve_group_alias( $group ) : $group; } /** * The rows without the tabs and cards whose page is not among them (the cards of a module that is not loaded): they * have no screen to open, where RoosterLms::rooted() would stand each on its own at the top of its section's last * group (MENU-AUDIT L1). Pure. * * @param list> $rows Rows. * @return list> */ public static function with_pages( array $rows ): array { $ids = array_fill_keys( array_map( static fn( array $row ): string => (string) $row['id'], $rows ), true ); return array_values( array_filter( $rows, static fn( array $row ): bool => Registry::PAGE === $row['kind'] || isset( $ids[ (string) $row['parent'] ] ) ) ); } /** * The e-shop Manager's link items (`get_nav_link_items()`: the Set Up pill's deep links into the Analytics and * AI Chat pages' cards, which are not registry rows) as page entries of their group and section, with their icon. * * @param bool $pro_active Whether the site's tier opens the Pro rows. * @param string $capability The e-shop Manager's capability. * @return list> */ private static function link_entries( bool $pro_active, string $capability ): array { if ( ! self::registry_present() ) { return array(); } $dmm = call_user_func( array( self::ONE_DMM, 'instance' ) ); if ( ! method_exists( $dmm, 'get_nav_link_items' ) ) { return array(); } $out = array(); foreach ( (array) $dmm->get_nav_link_items() as $item ) { if ( ! is_array( $item ) || '' === (string) ( $item['id'] ?? '' ) || '' === (string) ( $item['url'] ?? '' ) ) { continue; } $id = (string) $item['id']; $group = self::group_alias( (string) ( $item['group'] ?? '' ) ); $entry = self::map_row( array( 'id' => $id, 'label' => wp_specialchars_decode( (string) ( $item['label'] ?? $id ), ENT_QUOTES ), 'kind' => 'page', 'page' => '', 'group' => $group, 'tier' => 'free', 'url' => (string) $item['url'], 'description' => '', 'icon' => (string) ( $item['icon'] ?? '' ), ), $pro_active, $capability, (string) ( $item['section'] ?? '' ), self::audiences_of( $id, $group, (string) ( $item['section'] ?? '' ) ) ); if ( null !== $entry ) { $out[] = $entry; } } return $out; } /** * Moves a user's pins from `aieo_welcome_pins` into the one row, once: the result is the row's pins; the meta * is deleted after. * * @param int $user_id User. * @return list */ public static function migrate_pins( int $user_id ): array { if ( $user_id <= 0 || ! self::registry_present() ) { return array(); } $old = get_user_meta( $user_id, self::PINS_META, true ); if ( ! is_array( $old ) || array() === $old ) { return array(); } $rows = self::rows(); $pins = Prefs::migrate( $user_id, self::PRODUCT, array_values( array_filter( $old, 'is_string' ) ), static fn ( string $token ): string => self::id_of( $token, $rows ) ); delete_user_meta( $user_id, self::PINS_META ); return $pins; } /** * D: a user's preferences move into the one row, once: the pins (migrate_pins()), the persona choice * (`aieo_personas`, when the user answered: `aieo_personas_at`) as the audiences; the Home layout's order and * columns are deleted (another shape). The survey's role (`aieo_survey_role`) stays: it is the wizard's answer, not * the chrome's. * * @param int $user_id User. */ public static function migrate_prefs( int $user_id ): void { if ( $user_id <= 0 || ! self::registry_present() ) { return; } self::migrate_pins( $user_id ); if ( (int) get_user_meta( $user_id, self::PERSONAS_AT_META, true ) > 0 ) { $codes = get_user_meta( $user_id, self::PERSONAS_META, true ); if ( is_array( $codes ) && array() !== $codes ) { $registry = \Rooster\Admin\Library::registry(); $product = null !== $registry ? $registry->product( self::PRODUCT ) : null; $clean = null !== $product ? \Rooster\Admin\Personas::clean( $product, array_values( array_filter( $codes, 'is_string' ) ) ) : array(); if ( array() !== $clean && array() === (array) Prefs::get( $user_id, Prefs::AUDIENCES, self::PRODUCT, array() ) ) { Prefs::set( $user_id, Prefs::AUDIENCES, $clean, self::PRODUCT ); } } delete_user_meta( $user_id, self::PERSONAS_META ); delete_user_meta( $user_id, self::PERSONAS_AT_META ); } foreach ( self::LAYOUT_METAS as $meta ) { if ( '' !== (string) get_user_meta( $user_id, $meta, true ) ) { delete_user_meta( $user_id, $meta ); } } } /** * D's switch: registers the product and its rows (Rooster One calls it on `rooster/admin/register`). * * @param Registry $registry The registry. */ public static function register_product( Registry $registry ): void { if ( ! self::registry_present() ) { return; } $registry->register_product( self::product() ); $registry->register_entries( self::entries() ); } }