1<?php
2/**
3 * Blocks API: WP_Block_Patterns_Registry class
4 *
5 * @package WordPress
6 * @subpackage Blocks
7 * @since 5.5.0
8 */
9
10/**
11 * Class used for interacting with block patterns.
12 *
13 * @since 5.5.0
14 */
15#[AllowDynamicProperties]
16final class WP_Block_Patterns_Registry {
17 /**
18 * Registered block patterns array.
19 *
20 * @since 5.5.0
21 * @var array[]
22 */
23 private $registered_patterns = array();
24
25 /**
26 * Patterns registered outside the `init` action.
27 *
28 * @since 6.0.0
29 * @var array[]
30 */
31 private $registered_patterns_outside_init = array();
32
33 /**
34 * Container for the main instance of the class.
35 *
36 * @since 5.5.0
37 * @var WP_Block_Patterns_Registry|null
38 */
39 private static $instance = null;
40
41 /**
42 * Registers a block pattern.
43 *
44 * @since 5.5.0
45 * @since 5.8.0 Added support for the `blockTypes` property.
46 * @since 6.1.0 Added support for the `postTypes` property.
47 * @since 6.2.0 Added support for the `templateTypes` property.
48 * @since 6.5.0 Added support for the `filePath` property.
49 *
50 * @param string $pattern_name Block pattern name including namespace.
51 * @param array $pattern_properties {
52 * List of properties for the block pattern.
53 *
54 * @type string $title Required. A human-readable title for the pattern.
55 * @type string $content Optional. Block HTML markup for the pattern.
56 * If not provided, the content will be retrieved from the `filePath` if set.
57 * If both `content` and `filePath` are not set, the pattern will not be registered.
58 * @type string $description Optional. Visually hidden text used to describe the pattern
59 * in the inserter. A description is optional, but is strongly
60 * encouraged when the title does not fully describe what the
61 * pattern does. The description will help users discover the
62 * pattern while searching.
63 * @type int $viewportWidth Optional. The intended width of the pattern to allow for a scaled
64 * preview within the pattern inserter.
65 * @type bool $inserter Optional. Determines whether the pattern is visible in inserter.
66 * To hide a pattern so that it can only be inserted programmatically,
67 * set this to false. Default true.
68 * @type string[] $categories Optional. A list of registered pattern categories used to group
69 * block patterns. Block patterns can be shown on multiple categories.
70 * A category must be registered separately in order to be used here.
71 * @type string[] $keywords Optional. A list of aliases or keywords that help users discover
72 * the pattern while searching.
73 * @type string[] $blockTypes Optional. A list of block names including namespace that could use
74 * the block pattern in certain contexts (placeholder, transforms).
75 * The block pattern is available in the block editor inserter
76 * regardless of this list of block names.
77 * Certain blocks support further specificity besides the block name
78 * (e.g. for `core/template-part` you can specify areas
79 * like `core/template-part/header` or `core/template-part/footer`).
80 * @type string[] $postTypes Optional. An array of post types that the pattern is restricted
81 * to be used with. The pattern will only be available when editing one
82 * of the post types passed on the array. For all the other post types
83 * not part of the array the pattern is not available at all.
84 * @type string[] $templateTypes Optional. An array of template types where the pattern fits.
85 * @type string $filePath Optional. The full path to the file containing the block pattern content.
86 * }
87 * @return bool True if the pattern was registered with success and false otherwise.
88 */
89 public function register( $pattern_name, $pattern_properties ) {
90 if ( ! isset( $pattern_name ) || ! is_string( $pattern_name ) ) {
91 _doing_it_wrong(
92 __METHOD__,
93 __( 'Pattern name must be a string.' ),
94 '5.5.0'
95 );
96 return false;
97 }
98
99 if ( ! isset( $pattern_properties['title'] ) || ! is_string( $pattern_properties['title'] ) ) {
100 _doing_it_wrong(
101 __METHOD__,
102 __( 'Pattern title must be a string.' ),
103 '5.5.0'
104 );
105 return false;
106 }
107
108 if ( ! isset( $pattern_properties['filePath'] ) ) {
109 if ( ! isset( $pattern_properties['content'] ) || ! is_string( $pattern_properties['content'] ) ) {
110 _doing_it_wrong(
111 __METHOD__,
112 __( 'Pattern content must be a string.' ),
113 '5.5.0'
114 );
115 return false;
116 }
117 }
118
119 $pattern = array_merge(
120 $pattern_properties,
121 array( 'name' => $pattern_name )
122 );
123
124 $this->registered_patterns[ $pattern_name ] = $pattern;
125
126 // If the pattern is registered inside an action other than `init`, store it
127 // also to a dedicated array. Used to detect deprecated registrations inside
128 // `admin_init` or `current_screen`.
129 if ( current_action() && 'init' !== current_action() ) {
130 $this->registered_patterns_outside_init[ $pattern_name ] = $pattern;
131 }
132
133 return true;
134 }
135
136 /**
137 * Unregisters a block pattern.
138 *
139 * @since 5.5.0
140 *
141 * @param string $pattern_name Block pattern name including namespace.
142 * @return bool True if the pattern was unregistered with success and false otherwise.
143 */
144 public function unregister( $pattern_name ) {
145 if ( ! $this->is_registered( $pattern_name ) ) {
146 _doing_it_wrong(
147 __METHOD__,
148 /* translators: %s: Pattern name. */
149 sprintf( __( 'Pattern "%s" not found.' ), $pattern_name ),
150 '5.5.0'
151 );
152 return false;
153 }
154
155 unset( $this->registered_patterns[ $pattern_name ] );
156 unset( $this->registered_patterns_outside_init[ $pattern_name ] );
157
158 return true;
159 }
160
161 /**
162 * Retrieves the content of a registered block pattern.
163 *
164 * @since 6.5.0
165 *
166 * @param string $pattern_name Block pattern name including namespace.
167 * @param bool $outside_init_only Optional. Return only patterns registered outside the `init` action. Default false.
168 * @return string The content of the block pattern.
169 */
170 private function get_content( $pattern_name, $outside_init_only = false ) {
171 if ( $outside_init_only ) {
172 $patterns = &$this->registered_patterns_outside_init;
173 } else {
174 $patterns = &$this->registered_patterns;
175 }
176
177 $file_path = $patterns[ $pattern_name ]['filePath'] ?? '';
178 $is_stringy = is_string( $file_path ) || ( is_object( $file_path ) && method_exists( $file_path, '__toString' ) );
179 $pattern_path = $is_stringy ? realpath( (string) $file_path ) : null;
180 if (
181 ! isset( $patterns[ $pattern_name ]['content'] ) &&
182 is_string( $pattern_path ) &&
183 ( str_ends_with( $pattern_path, '.php' ) || str_ends_with( $pattern_path, '.html' ) ) &&
184 is_file( $pattern_path ) &&
185 is_readable( $pattern_path )
186 ) {
187 ob_start();
188 include $patterns[ $pattern_name ]['filePath'];
189 $patterns[ $pattern_name ]['content'] = ob_get_clean();
190 unset( $patterns[ $pattern_name ]['filePath'] );
191 }
192
193 return $patterns[ $pattern_name ]['content'];
194 }
195
196 /**
197 * Retrieves an array containing the properties of a registered block pattern.
198 *
199 * @since 5.5.0
200 *
201 * @param string $pattern_name Block pattern name including namespace.
202 * @return array|null Registered pattern properties or `null` if the pattern is not registered.
203 */
204 public function get_registered( $pattern_name ) {
205 if ( ! $this->is_registered( $pattern_name ) ) {
206 return null;
207 }
208
209 $pattern = $this->registered_patterns[ $pattern_name ];
210 $content = $this->get_content( $pattern_name );
211 $pattern['content'] = apply_block_hooks_to_content(
212 $content,
213 $pattern,
214 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
215 );
216
217 return $pattern;
218 }
219
220 /**
221 * Retrieves all registered block patterns.
222 *
223 * @since 5.5.0
224 *
225 * @param bool $outside_init_only Return only patterns registered outside the `init` action.
226 * @return array[] Array of arrays containing the registered block patterns properties,
227 * and per style.
228 */
229 public function get_all_registered( $outside_init_only = false ) {
230 $patterns = $outside_init_only
231 ? $this->registered_patterns_outside_init
232 : $this->registered_patterns;
233 $hooked_blocks = get_hooked_blocks();
234
235 foreach ( $patterns as $index => $pattern ) {
236 $content = $this->get_content( $pattern['name'], $outside_init_only );
237 $patterns[ $index ]['content'] = apply_block_hooks_to_content(
238 $content,
239 $pattern,
240 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
241 );
242 }
243
244 return array_values( $patterns );
245 }
246
247 /**
248 * Checks if a block pattern is registered.
249 *
250 * @since 5.5.0
251 *
252 * @param string|null $pattern_name Block pattern name including namespace.
253 * @return bool True if the pattern is registered, false otherwise.
254 */
255 public function is_registered( $pattern_name ) {
256 return isset( $pattern_name, $this->registered_patterns[ $pattern_name ] );
257 }
258
259 public function __wakeup() {
260 if ( ! $this->registered_patterns ) {
261 return;
262 }
263 if ( ! is_array( $this->registered_patterns ) ) {
264 throw new UnexpectedValueException();
265 }
266 foreach ( $this->registered_patterns as $value ) {
267 if ( ! is_array( $value ) ) {
268 throw new UnexpectedValueException();
269 }
270 }
271 $this->registered_patterns_outside_init = array();
272 }
273
274 /**
275 * Utility method to retrieve the main instance of the class.
276 *
277 * The instance will be created if it does not exist yet.
278 *
279 * @since 5.5.0
280 *
281 * @return WP_Block_Patterns_Registry The main instance.
282 */
283 public static function get_instance() {
284 if ( null === self::$instance ) {
285 self::$instance = new self();
286 }
287
288 return self::$instance;
289 }
290}
291
292/**
293 * Registers a new block pattern.
294 *
295 * @since 5.5.0
296 *
297 * @param string $pattern_name Block pattern name including namespace.
298 * @param array $pattern_properties List of properties for the block pattern.
299 * See WP_Block_Patterns_Registry::register() for accepted arguments.
300 * @return bool True if the pattern was registered with success and false otherwise.
301 */
302function register_block_pattern( $pattern_name, $pattern_properties ) {
303 return WP_Block_Patterns_Registry::get_instance()->register( $pattern_name, $pattern_properties );
304}
305
306/**
307 * Unregisters a block pattern.
308 *
309 * @since 5.5.0
310 *
311 * @param string $pattern_name Block pattern name including namespace.
312 * @return bool True if the pattern was unregistered with success and false otherwise.
313 */
314function unregister_block_pattern( $pattern_name ) {
315 return WP_Block_Patterns_Registry::get_instance()->unregister( $pattern_name );
316}
317