PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / blocks / registry.php

registry.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/blocks/registry.php

298 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shortcode → Block bridge: the registry.
4 *
5 * Single owner of every shortcode-block descriptor. Plugins register their
6 * shortcodes here (imperatively via {@see storeengine_register_shortcode_block()} or
7 * declaratively via the `storeengine_shortcode_block_registry` filter); the generic
8 * `ablocks/shortcode` block and its editor render purely from this data.
9 *
10 * Descriptors are validated on registration — malformed ones are skipped and
11 * logged rather than fataling, so one bad plugin can't take down the editor.
12 *
13 * @see docs/shortcode-block-bridge.md for the full v1 contract.
14 * @package StoreEngine\Blocks
15 */
16
17 namespace StoreEngine\Blocks;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 class Registry {
24
25 /**
26 * Descriptor format version. Bump only on breaking changes.
27 */
28 const SCHEMA_VERSION = 1;
29
30 /**
31 * Fixed v1 attribute types catalog. Adding is additive; renaming is breaking.
32 */
33 const TYPES = [
34 'text', 'textarea', 'number', 'range', 'toggle', 'select', 'radio',
35 'color', 'post-select', 'taxonomy-select', 'csv',
36 ];
37
38 const CHOICE_TYPES = [ 'select', 'radio' ];
39
40 /**
41 * @var array<string, array> Descriptors keyed by "owner/tag".
42 */
43 private array $descriptors = [];
44
45 /**
46 * Whether the declarative filter has been collected yet.
47 */
48 private bool $collected = false;
49
50 private static ?Registry $instance = null;
51
52 public static function instance(): Registry {
53 if ( null === self::$instance ) {
54 self::$instance = new self();
55 }
56
57 return self::$instance;
58 }
59
60 /**
61 * Register one shortcode-block descriptor. Returns false (and logs) when the
62 * descriptor is malformed.
63 */
64 public function register( array $descriptor ): bool {
65 $error = $this->validate( $descriptor );
66 if ( is_wp_error( $error ) ) {
67 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
68 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
69 error_log( sprintf(
70 '[shortcode-block] skipped descriptor for "%s/%s": %s',
71 $descriptor['owner'] ?? '?',
72 $descriptor['tag'] ?? '?',
73 $error->get_error_message()
74 ) );
75 }
76
77 return false;
78 }
79
80 $normalized = $this->normalize( $descriptor );
81 $key = $normalized['owner'] . '/' . $normalized['tag'];
82
83 // Keyed by owner/tag: two plugins registering the same bare tag coexist.
84 $this->descriptors[ $key ] = $normalized;
85
86 return true;
87 }
88
89 /**
90 * The full registry (both imperative + declarative registrations).
91 *
92 * @return array<int, array>
93 */
94 public function all(): array {
95 $this->collect();
96
97 return array_values( $this->descriptors );
98 }
99
100 /**
101 * A single descriptor by its fully-qualified "owner/tag" id, or null.
102 */
103 public function get( string $owner_tag ): ?array {
104 $this->collect();
105
106 return $this->descriptors[ $owner_tag ] ?? null;
107 }
108
109 /**
110 * Pull in declarative registrations once (lazy — after all plugins loaded).
111 */
112 private function collect(): void {
113 if ( $this->collected ) {
114 return;
115 }
116 $this->collected = true;
117
118 /**
119 * Declarative registration: return an array of descriptors.
120 *
121 * @param array[] $descriptors
122 */
123 $declarative = apply_filters( 'storeengine_shortcode_block_registry', [] );
124 foreach ( (array) $declarative as $descriptor ) {
125 if ( is_array( $descriptor ) ) {
126 $this->register( $descriptor );
127 }
128 }
129 }
130
131 /* -------------------------------------------------------------- */
132 /* Validation */
133 /* -------------------------------------------------------------- */
134
135 /**
136 * @return true|\WP_Error
137 */
138 private function validate( array $d ) {
139 foreach ( [ 'tag', 'owner', 'title' ] as $required ) {
140 if ( empty( $d[ $required ] ) || ! is_string( $d[ $required ] ) ) {
141 return new \WP_Error( 'missing_field', "missing required field \"$required\"" );
142 }
143 }
144
145 if ( isset( $d['content'] ) ) {
146 $mode = $d['content']['mode'] ?? 'plain';
147 if ( ! in_array( $mode, [ 'plain', 'innerblocks' ], true ) ) {
148 return new \WP_Error( 'bad_content_mode', "content.mode must be plain|innerblocks" );
149 }
150 }
151
152 if ( isset( $d['preview']['mode'] )
153 && ! in_array( $d['preview']['mode'], [ 'server', 'static', 'none' ], true ) ) {
154 return new \WP_Error( 'bad_preview_mode', 'preview.mode must be server|static|none' );
155 }
156
157 foreach ( (array) ( $d['attributes'] ?? [] ) as $i => $attr ) {
158 $err = $this->validate_attribute( $attr, $i );
159 if ( is_wp_error( $err ) ) {
160 return $err;
161 }
162 }
163
164 return true;
165 }
166
167 /**
168 * @return true|\WP_Error
169 */
170 private function validate_attribute( $attr, $i ) {
171 if ( ! is_array( $attr ) || empty( $attr['name'] ) || empty( $attr['label'] ) ) {
172 return new \WP_Error( 'bad_attribute', "attribute #$i needs name + label" );
173 }
174 $type = $attr['type'] ?? 'text';
175 if ( ! in_array( $type, self::TYPES, true ) ) {
176 return new \WP_Error( 'bad_type', "attribute \"{$attr['name']}\" has unknown type \"$type\"" );
177 }
178 if ( in_array( $type, self::CHOICE_TYPES, true ) && empty( $attr['options'] ) ) {
179 return new \WP_Error( 'missing_options', "attribute \"{$attr['name']}\" ($type) requires options" );
180 }
181 if ( 'range' === $type && ( ! isset( $attr['min'] ) || ! isset( $attr['max'] ) ) ) {
182 return new \WP_Error( 'missing_range', "attribute \"{$attr['name']}\" (range) requires min + max" );
183 }
184 if ( 'post-select' === $type && empty( $attr['post_type'] ) ) {
185 return new \WP_Error( 'missing_post_type', "attribute \"{$attr['name']}\" (post-select) requires post_type" );
186 }
187 if ( 'taxonomy-select' === $type && empty( $attr['taxonomy'] ) ) {
188 return new \WP_Error( 'missing_taxonomy', "attribute \"{$attr['name']}\" (taxonomy-select) requires taxonomy" );
189 }
190
191 return true;
192 }
193
194 /* -------------------------------------------------------------- */
195 /* Normalization (fill defaults so consumers can trust the shape) */
196 /* -------------------------------------------------------------- */
197
198 private function normalize( array $d ): array {
199 $out = [
200 'tag' => $d['tag'],
201 'owner' => $d['owner'],
202 'id' => $d['owner'] . '/' . $d['tag'],
203 'title' => $d['title'],
204 'category' => $d['category'] ?? ucfirst( $d['owner'] ),
205 'description' => $d['description'] ?? '',
206 'icon' => $d['icon'] ?? 'shortcode',
207 'keywords' => array_values( (array) ( $d['keywords'] ?? [] ) ),
208 'preview' => [
209 'mode' => $d['preview']['mode'] ?? 'server',
210 'note' => $d['preview']['note'] ?? '',
211 ],
212 'attributes' => array_map( [ $this, 'normalize_attribute' ], (array) ( $d['attributes'] ?? [] ) ),
213 // Optional native-block mapping: the aBlocks (or other) block this
214 // shortcode converts to, and how its atts map onto that block's attrs.
215 'ablocks_block' => isset( $d['ablocks_block'] ) ? (string) $d['ablocks_block'] : '',
216 'ablocks_map' => ( isset( $d['ablocks_map'] ) && is_array( $d['ablocks_map'] ) ) ? $d['ablocks_map'] : [],
217 'schema_version' => (int) ( $d['schema_version'] ?? self::SCHEMA_VERSION ),
218 ];
219
220 if ( isset( $d['content'] ) ) {
221 $out['content'] = [
222 'supported' => (bool) ( $d['content']['supported'] ?? true ),
223 'mode' => $d['content']['mode'] ?? 'plain',
224 'label' => $d['content']['label'] ?? __( 'Content', 'storeengine' ),
225 ];
226 }
227
228 return $out;
229 }
230
231 private function normalize_attribute( array $a ): array {
232 $type = $a['type'] ?? 'text';
233 $out = [
234 'name' => $a['name'],
235 'label' => $a['label'],
236 'type' => $type,
237 'default' => $a['default'] ?? self::default_for_type( $type ),
238 'help' => $a['help'] ?? '',
239 'placeholder' => $a['placeholder'] ?? '',
240 'required' => (bool) ( $a['required'] ?? false ),
241 'group' => $a['group'] ?? __( 'Settings', 'storeengine' ),
242 'sanitize' => $a['sanitize'] ?? self::sanitize_for_type( $type ),
243 ];
244
245 if ( in_array( $type, self::CHOICE_TYPES, true ) ) {
246 $out['options'] = array_values( (array) ( $a['options'] ?? [] ) );
247 }
248 foreach ( [ 'min', 'max', 'step' ] as $k ) {
249 if ( isset( $a[ $k ] ) ) {
250 $out[ $k ] = $a[ $k ];
251 }
252 }
253 if ( isset( $a['post_type'] ) ) {
254 $out['post_type'] = $a['post_type'];
255 }
256 if ( isset( $a['taxonomy'] ) ) {
257 $out['taxonomy'] = $a['taxonomy'];
258 }
259 if ( isset( $a['depends_on']['name'] ) ) {
260 $out['depends_on'] = [
261 'name' => $a['depends_on']['name'],
262 'value' => $a['depends_on']['value'] ?? '',
263 ];
264 }
265
266 return $out;
267 }
268
269 private static function default_for_type( string $type ) {
270 switch ( $type ) {
271 case 'number':
272 case 'range':
273 return 0;
274 case 'toggle':
275 return 'false';
276 default:
277 return '';
278 }
279 }
280
281 private static function sanitize_for_type( string $type ): string {
282 switch ( $type ) {
283 case 'number':
284 case 'range':
285 return 'int';
286 case 'color':
287 return 'color';
288 case 'csv':
289 return 'csv';
290 case 'post-select':
291 case 'taxonomy-select':
292 return 'key';
293 default:
294 return 'text';
295 }
296 }
297 }
298