Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.88% covered (success)
93.88%
46 / 49
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Single_Report_Page_Registry
95.83% covered (success)
95.83%
46 / 48
88.89% covered (warning)
88.89%
8 / 9
21
0.00% covered (danger)
0.00%
0 / 1
 register
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 get_registered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_registered_by_path
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 find_registered_by_path
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 get_all_registered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_available_pages
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 is_registered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ensure_hydrated
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 get_instance
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Registry of the single-report pages registered with the dashboard.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10require_once __DIR__ . '/dashboard-grammar.php';
11
12/**
13 * Holds the single-report pages per dashboard, hydrated once after `init`.
14 *
15 * @since $$next-version$$
16 */
17final class Single_Report_Page_Registry {
18
19    /**
20     * Action fired once, with the registry, when it hydrates on its first read after `init`.
21     */
22    const REGISTER_ACTION = 'jetpack_premium_analytics_register_single_report_pages';
23
24    /**
25     * Registered pages, keyed by dashboard name then page id.
26     *
27     * @var array<string, array<string, Single_Report_Page>>
28     */
29    private $registered_pages = array();
30
31    /**
32     * Whether the registration action has fired.
33     *
34     * @var bool
35     */
36    private $hydrated = false;
37
38    /**
39     * Main instance.
40     *
41     * @var self|null
42     */
43    private static $instance = null;
44
45    /**
46     * Registers a page.
47     *
48     * @param string $dashboard_name Dashboard identifier.
49     * @param string $id             Page identifier, namespaced.
50     * @param array  $args           Page arguments.
51     * @return Single_Report_Page|false The page on success, false on failure.
52     */
53    public function register( $dashboard_name, $id, $args = array() ) {
54        if ( ! is_string( $dashboard_name ) || 1 !== preg_match( '/^' . get_dashboard_name_pattern() . '$/', $dashboard_name ) ) {
55            _doing_it_wrong( __METHOD__, esc_html__( 'Dashboard names must be lowercase strings of letters, numbers, and hyphens, optionally separated by underscores.', 'jetpack-premium-analytics-pkg' ), 'jetpack-premium-analytics-$$next-version$$' );
56            return false;
57        }
58
59        if ( ! is_string( $id ) || 1 !== preg_match( '/^' . get_dashboard_section_id_pattern() . '$/', $id ) ) {
60            _doing_it_wrong( __METHOD__, esc_html__( 'Single-report page IDs must contain a namespace prefix. Example: my-plugin/my-entity', 'jetpack-premium-analytics-pkg' ), 'jetpack-premium-analytics-$$next-version$$' );
61            return false;
62        }
63
64        if ( $this->is_registered( $dashboard_name, $id ) ) {
65            /* translators: 1: Dashboard name. 2: Page ID. */
66            $message = sprintf( __( 'Single-report page "%2$s" is already registered for dashboard "%1$s".', 'jetpack-premium-analytics-pkg' ), $dashboard_name, $id );
67            _doing_it_wrong( __METHOD__, esc_html( $message ), 'jetpack-premium-analytics-$$next-version$$' );
68            return false;
69        }
70
71        $page = new Single_Report_Page( $dashboard_name, $id, $args );
72
73        // The client routes by path, so two ids may not share one.
74        $holder = $this->find_registered_by_path( $dashboard_name, $page->path );
75        if ( $holder ) {
76            /* translators: 1: Dashboard name. 2: Page path. 3: ID of the page already using the path. */
77            $message = sprintf( __( 'Single-report page path "%2$s" is already used on dashboard "%1$s" by "%3$s".', 'jetpack-premium-analytics-pkg' ), $dashboard_name, $page->path, $holder->id );
78            _doing_it_wrong( __METHOD__, esc_html( $message ), 'jetpack-premium-analytics-$$next-version$$' );
79            return false;
80        }
81
82        if ( ! isset( $this->registered_pages[ $dashboard_name ] ) ) {
83            $this->registered_pages[ $dashboard_name ] = array();
84        }
85
86        $this->registered_pages[ $dashboard_name ][ $id ] = $page;
87
88        return $page;
89    }
90
91    /**
92     * A registered page, or null.
93     *
94     * @param string $dashboard_name Dashboard identifier.
95     * @param string $id             Page identifier.
96     * @return Single_Report_Page|null
97     */
98    public function get_registered( $dashboard_name, $id ) {
99        $this->ensure_hydrated();
100
101        return $this->registered_pages[ $dashboard_name ][ $id ] ?? null;
102    }
103
104    /**
105     * The registered page using a path, or null.
106     *
107     * @param string $dashboard_name Dashboard identifier.
108     * @param string $path           Page path.
109     * @return Single_Report_Page|null
110     */
111    public function get_registered_by_path( $dashboard_name, $path ) {
112        $this->ensure_hydrated();
113
114        return $this->find_registered_by_path( $dashboard_name, $path );
115    }
116
117    /**
118     * Looks a path up without hydrating.
119     *
120     * @param string $dashboard_name Dashboard identifier.
121     * @param string $path           Page path.
122     * @return Single_Report_Page|null
123     */
124    private function find_registered_by_path( $dashboard_name, $path ) {
125        foreach ( $this->registered_pages[ $dashboard_name ] ?? array() as $page ) {
126            if ( $page->path === $path ) {
127                return $page;
128            }
129        }
130
131        return null;
132    }
133
134    /**
135     * Every registered page of a dashboard, keyed by id.
136     *
137     * @param string $dashboard_name Dashboard identifier.
138     * @return array<string, Single_Report_Page>
139     */
140    public function get_all_registered( $dashboard_name ) {
141        $this->ensure_hydrated();
142
143        return $this->registered_pages[ $dashboard_name ] ?? array();
144    }
145
146    /**
147     * The pages available to the current reader, as a list.
148     *
149     * @param string $dashboard_name Dashboard identifier.
150     * @return Single_Report_Page[]
151     */
152    public function get_available_pages( $dashboard_name ) {
153        $pages = array_filter(
154            $this->get_all_registered( $dashboard_name ),
155            static function ( Single_Report_Page $page ) {
156                return $page->is_available();
157            }
158        );
159
160        return array_values( $pages );
161    }
162
163    /**
164     * Whether a page id is registered. Does not hydrate: `register()` relies on it.
165     *
166     * @param string $dashboard_name Dashboard identifier.
167     * @param string $id             Page identifier.
168     * @return bool
169     */
170    public function is_registered( $dashboard_name, $id ) {
171        return isset( $this->registered_pages[ $dashboard_name ][ $id ] );
172    }
173
174    /**
175     * Fires the registration action once, after `init`.
176     */
177    private function ensure_hydrated() {
178        if ( $this->hydrated ) {
179            return;
180        }
181
182        // Latching this early would drop every registrant hooked later, so the read skips the action.
183        if ( ! did_action( 'init' ) ) {
184            _doing_it_wrong( __METHOD__, esc_html__( 'Single-report pages are read after init. A read before it does not hydrate the registry and answers only what was registered directly.', 'jetpack-premium-analytics-pkg' ), 'jetpack-premium-analytics-$$next-version$$' );
185            return;
186        }
187
188        // Latched before the action so a registrant that reads the registry cannot re-enter.
189        $this->hydrated = true;
190
191        /**
192         * Fires once when the single-report page registry hydrates.
193         *
194         * @since $$next-version$$
195         *
196         * @param Single_Report_Page_Registry $registry The registry being hydrated.
197         */
198        do_action( self::REGISTER_ACTION, $this );
199    }
200
201    /**
202     * The main instance.
203     *
204     * @return self
205     */
206    public static function get_instance() {
207        if ( null === self::$instance ) {
208            self::$instance = new self();
209        }
210
211        return self::$instance;
212    }
213}