PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-premium-analytics / src / class-dashboard-section-registry.php

class-dashboard-section-registry.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-premium-analytics/src/class-dashboard-section-registry.php

291 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Dashboard Sections API: Dashboard_Section_Registry class.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8 namespace Automattic\Jetpack\PremiumAnalytics;
9
10 require_once __DIR__ . '/dashboard-grammar.php';
11
12 /**
13 * Stores Dashboard_Section instances keyed by dashboard and section ID.
14 *
15 * Hydrates on its first read: the registration action fires once, and every registrant, this
16 * package included, registers its sections from there. Reads happen after `init`, from wp-admin
17 * and from REST, so hooking the action is the one moment that covers both paths.
18 */
19 final class Dashboard_Section_Registry {
20
21 /**
22 * Action through which sections are registered, fired once on the first read.
23 *
24 * @since 0.9.0
25 * @var string
26 */
27 const REGISTER_ACTION = 'jetpack_premium_analytics_register_dashboard_sections';
28
29 /**
30 * Registered sections, as `$dashboard_name => $id => $section` pairs.
31 *
32 * @var array<string, Dashboard_Section[]>
33 */
34 private $registered_sections = array();
35
36 /**
37 * Whether the registration action has fired.
38 *
39 * @var bool
40 */
41 private $hydrated = false;
42
43 /**
44 * Container for the main instance of the class.
45 *
46 * @var Dashboard_Section_Registry|null
47 */
48 private static $instance = null;
49
50 /**
51 * Registers a dashboard section.
52 *
53 * @param string $dashboard_name Dashboard identifier.
54 * @param string $id Section identifier.
55 * @param array $args Optional. Section arguments.
56 * @return Dashboard_Section|false The registered section on success, or false on failure.
57 */
58 public function register( $dashboard_name, $id, $args = array() ) {
59 if ( ! $this->is_valid_dashboard_name( $dashboard_name ) ) {
60 _doing_it_wrong(
61 __METHOD__,
62 esc_html__( 'Dashboard names must be lowercase strings of letters, numbers, and hyphens, optionally separated by underscores.', 'jetpack-premium-analytics-pkg' ),
63 '0.1.0'
64 );
65 return false;
66 }
67
68 if ( ! $this->is_valid_section_id( $id ) ) {
69 _doing_it_wrong(
70 __METHOD__,
71 esc_html__( 'Dashboard section IDs must contain a namespace prefix. Example: my-plugin/my-custom-section', 'jetpack-premium-analytics-pkg' ),
72 '0.1.0'
73 );
74 return false;
75 }
76
77 if ( $this->is_registered( $dashboard_name, $id ) ) {
78 _doing_it_wrong(
79 __METHOD__,
80 sprintf(
81 /* translators: 1: Dashboard name. 2: Dashboard section ID. */
82 esc_html__( 'Dashboard section "%2$s" is already registered for dashboard "%1$s".', 'jetpack-premium-analytics-pkg' ),
83 esc_html( $dashboard_name ),
84 esc_html( $id )
85 ),
86 '0.1.0'
87 );
88 return false;
89 }
90
91 $section = new Dashboard_Section( $dashboard_name, $id, $args );
92
93 // The client keys tabs, URLs and stored layouts by slug, so two ids may not share one.
94 $holder = $this->find_registered_by_slug( $dashboard_name, $section->slug );
95 if ( $holder ) {
96 $message = sprintf(
97 /* translators: 1: Dashboard name. 2: Section slug. 3: ID of the section already using the slug. */
98 __( 'Dashboard section slug "%2$s" is already used on dashboard "%1$s" by "%3$s".', 'jetpack-premium-analytics-pkg' ),
99 $dashboard_name,
100 $section->slug,
101 $holder->id
102 );
103 // One line: tools/replace-next-version-tag.sh only rewrites the token in a single-line call.
104 _doing_it_wrong( __METHOD__, esc_html( $message ), 'jetpack-premium-analytics-0.9.0' );
105 return false;
106 }
107
108 if ( ! isset( $this->registered_sections[ $dashboard_name ] ) ) {
109 $this->registered_sections[ $dashboard_name ] = array();
110 }
111
112 $this->registered_sections[ $dashboard_name ][ $id ] = $section;
113
114 return $section;
115 }
116
117 /**
118 * Retrieves a registered section.
119 *
120 * @param string $dashboard_name Dashboard identifier.
121 * @param string $id Section identifier.
122 * @return Dashboard_Section|null The registered section, or null when absent.
123 */
124 public function get_registered( $dashboard_name, $id ) {
125 $this->ensure_hydrated();
126
127 if ( ! $this->is_registered( $dashboard_name, $id ) ) {
128 return null;
129 }
130
131 return $this->registered_sections[ $dashboard_name ][ $id ];
132 }
133
134 /**
135 * Retrieves a registered section by its URL-facing slug.
136 *
137 * @since 0.9.0
138 *
139 * @param string $dashboard_name Dashboard identifier.
140 * @param string $slug Section slug, e.g. `ads`.
141 * @return Dashboard_Section|null The registered section, or null when no section uses the slug.
142 */
143 public function get_registered_by_slug( $dashboard_name, $slug ) {
144 $this->ensure_hydrated();
145
146 return $this->find_registered_by_slug( $dashboard_name, $slug );
147 }
148
149 /**
150 * Finds a registered section by slug without hydrating: register() relies on it.
151 *
152 * @param string $dashboard_name Dashboard identifier.
153 * @param string $slug Section slug.
154 * @return Dashboard_Section|null
155 */
156 private function find_registered_by_slug( $dashboard_name, $slug ) {
157 foreach ( $this->registered_sections[ $dashboard_name ] ?? array() as $section ) {
158 if ( $section->slug === $slug ) {
159 return $section;
160 }
161 }
162
163 return null;
164 }
165
166 /**
167 * Retrieves all registered sections for a dashboard.
168 *
169 * @param string $dashboard_name Dashboard identifier.
170 * @return Dashboard_Section[] Map of `$id => $section` pairs.
171 */
172 public function get_all_registered( $dashboard_name ) {
173 $this->ensure_hydrated();
174
175 if ( ! isset( $this->registered_sections[ $dashboard_name ] ) ) {
176 // Unknown dashboards may be valid REST targets but have no sections registered.
177 return array();
178 }
179
180 return $this->registered_sections[ $dashboard_name ];
181 }
182
183 /**
184 * Retrieves available sections sorted by order.
185 *
186 * @param string $dashboard_name Dashboard identifier.
187 * @return Dashboard_Section[] Ordered list of available sections.
188 */
189 public function get_available_sections( $dashboard_name ) {
190 $sections = array_filter(
191 $this->get_all_registered( $dashboard_name ),
192 static function ( Dashboard_Section $section ) {
193 return $section->is_available();
194 }
195 );
196
197 uasort(
198 $sections,
199 static function ( Dashboard_Section $a, Dashboard_Section $b ) {
200 if ( $a->order === $b->order ) {
201 return strcmp( $a->id, $b->id );
202 }
203
204 return $a->order <=> $b->order;
205 }
206 );
207
208 return array_values( $sections );
209 }
210
211 /**
212 * Checks if a section is registered. Does not hydrate: register() relies on it, and a
213 * registrant may run before the action fires.
214 *
215 * @param string $dashboard_name Dashboard identifier.
216 * @param string $id Section identifier.
217 * @return bool True if registered.
218 */
219 public function is_registered( $dashboard_name, $id ) {
220 return isset( $this->registered_sections[ $dashboard_name ][ $id ] );
221 }
222
223 /**
224 * Fires the registration action once, on the first read after `init`.
225 *
226 * @return void
227 */
228 private function ensure_hydrated() {
229 if ( $this->hydrated ) {
230 return;
231 }
232
233 // Latching this early would drop every registrant hooked later, so the read skips the action.
234 if ( ! did_action( 'init' ) ) {
235 $message = __( 'Dashboard sections are read after init. A read before it does not hydrate the registry and answers only what was registered directly.', 'jetpack-premium-analytics-pkg' );
236 // One line: tools/replace-next-version-tag.sh only rewrites the token in a single-line call.
237 _doing_it_wrong( __METHOD__, esc_html( $message ), 'jetpack-premium-analytics-0.9.0' );
238 return;
239 }
240
241 // Latched before the action so a registrant that reads the registry cannot re-enter.
242 $this->hydrated = true;
243
244 /**
245 * Fires when the dashboard section registry hydrates, on its first read after `init`.
246 *
247 * Register sections here rather than on `init`: the registry is read from wp-admin and
248 * from REST, and each path loads it at a different moment. A registrant that may run
249 * twice guards with `is_registered()`.
250 *
251 * @since 0.9.0
252 *
253 * @param Dashboard_Section_Registry $registry The registry being hydrated.
254 */
255 do_action( self::REGISTER_ACTION, $this );
256 }
257
258 /**
259 * Utility method to retrieve the main instance of the class.
260 *
261 * @return Dashboard_Section_Registry The main instance.
262 */
263 public static function get_instance() {
264 if ( null === self::$instance ) {
265 self::$instance = new self();
266 }
267
268 return self::$instance;
269 }
270
271 /**
272 * Checks whether a dashboard name can be registered.
273 *
274 * @param mixed $dashboard_name Candidate dashboard name.
275 * @return bool
276 */
277 private function is_valid_dashboard_name( $dashboard_name ) {
278 return is_string( $dashboard_name ) && 1 === preg_match( '/^' . get_dashboard_name_pattern() . '$/', $dashboard_name );
279 }
280
281 /**
282 * Checks whether a section ID can be registered.
283 *
284 * @param mixed $id Candidate section ID.
285 * @return bool
286 */
287 private function is_valid_section_id( $id ) {
288 return is_string( $id ) && 1 === preg_match( '/^' . get_dashboard_section_id_pattern() . '$/', $id );
289 }
290 }
291