PluginProbe
WooCommerce / 11.1.0-beta.2
WooCommerce v11.1.0-beta.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Blocks / Templates / ArchiveProductTemplatesCompatibility.php
ArchiveProductTemplatesCompatibility.php
424 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Automattic\WooCommerce\Blocks\Templates;
3
4 /**
5 * ArchiveProductTemplatesCompatibility class.
6 *
7 * To bridge the gap on compatibility with PHP hooks and Product Archive blockified templates.
8 *
9 * @internal
10 */
11 class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility {
12
13 /**
14 * The custom ID of the loop item block as the replacement of the core/null block.
15 */
16 const LOOP_ITEM_ID = 'product-loop-item';
17
18 /**
19 * The data of supported hooks, containing the hook name, the block name,
20 * position, and the callbacks.
21 *
22 * @var array $hook_data The hook data.
23 */
24 protected $hook_data;
25
26 /**
27 * Update the render block data to inject our custom attribute needed to
28 * determine which blocks belong to an inherited Products block.
29 *
30 * @param array $parsed_block The block being rendered.
31 * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content.
32 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
33 *
34 * @return array
35 */
36 public function update_render_block_data( $parsed_block, $source_block, $parent_block ) {
37
38 if ( ! $this->is_archive_template() ) {
39 return $parsed_block;
40 }
41
42 /**
43 * Custom data can be injected to top level block only, as Gutenberg
44 * will use this data to render the blocks and its nested blocks.
45 */
46 if ( $parent_block ) {
47 return $parsed_block;
48 }
49
50 $this->inner_blocks_walker( $parsed_block );
51
52 return $parsed_block;
53 }
54
55 /**
56 * Inject hooks to rendered content of corresponding blocks.
57 *
58 * @param mixed $block_content The rendered block content.
59 * @param mixed $block The parsed block data.
60 * @return string
61 */
62 public function inject_hooks( $block_content, $block ) {
63 if ( ! $this->is_archive_template() ) {
64 return $block_content;
65 }
66 /**
67 * If the block is not inherited, we don't need to inject hooks.
68 */
69 if ( empty( $block['attrs']['isInherited'] ) ) {
70 return $block_content;
71 }
72
73 $block_name = $block['blockName'];
74
75 if ( $this->is_null_post_template( $block ) ) {
76 $block_name = self::LOOP_ITEM_ID;
77 }
78
79 $block_hooks = array_filter(
80 $this->hook_data,
81 function ( $hook ) use ( $block_name ) {
82 return in_array( $block_name, $hook['block_names'], true );
83 }
84 );
85
86 // We want to inject hooks to the core/post-template or product template block only when the products exist:
87 // https://github.com/woocommerce/woocommerce-blocks/issues/9463.
88 if ( $this->is_post_or_product_template( $block_name ) && ! empty( $block_content ) ) {
89 $this->restore_default_hooks();
90 $content = sprintf(
91 '%1$s%2$s%3$s',
92 $this->get_hooks_buffer( $block_hooks, 'before' ),
93 $block_content,
94 $this->get_hooks_buffer( $block_hooks, 'after' )
95 );
96 $this->remove_default_hooks();
97 return $content;
98 }
99
100 $supported_blocks = array_merge(
101 array(),
102 ...array_map(
103 function ( $hook ) {
104 return $hook['block_names'];
105 },
106 array_values( $this->hook_data )
107 )
108 );
109
110 if ( ! in_array( $block_name, $supported_blocks, true ) ) {
111 return $block_content;
112 }
113
114 if (
115 'core/query-no-results' === $block_name
116 ) {
117
118 /**
119 * `core/query-no-result` is a special case because it can return two
120 * different content depending on the context. We need to check if the
121 * block content is empty to determine if we need to inject hooks.
122 */
123 if ( empty( trim( $block_content ) ) ) {
124 return $block_content;
125 }
126
127 $this->restore_default_hooks();
128
129 $content = sprintf(
130 '%1$s%2$s%3$s',
131 $this->get_hooks_buffer( $block_hooks, 'before' ),
132 $block_content,
133 $this->get_hooks_buffer( $block_hooks, 'after' )
134 );
135
136 $this->remove_default_hooks();
137
138 return $content;
139 }
140
141 if ( empty( $block_content ) ) {
142 return $block_content;
143 }
144
145 return sprintf(
146 '%1$s%2$s%3$s',
147 $this->get_hooks_buffer( $block_hooks, 'before' ),
148 $block_content,
149 $this->get_hooks_buffer( $block_hooks, 'after' )
150 );
151 }
152
153 /**
154 * The hook data to inject to the rendered content of blocks. This also
155 * contains hooked functions that will be removed by remove_default_hooks.
156 *
157 * The array format:
158 * [
159 * <hook-name> => [
160 * block_name => <block-name>,
161 * position => before|after,
162 * hooked => [
163 * <function-name> => <priority>,
164 * ...
165 * ],
166 * permanently_removed_actions => [
167 * <function-name>
168 * ]
169 * ],
170 * ]
171 * Where:
172 * - hook-name is the name of the hook that will be replaced.
173 * - block-name is the name of the block that will replace the hook.
174 * - position is the position of the block relative to the hook.
175 * - hooked is an array of functions hooked to the hook that will be
176 * replaced. The key is the function name and the value is the
177 * priority.
178 * - permanently_removed_actions is an array of functions that we do not want to re-add after they have been removed to avoid duplicate content with the Products block and its inner blocks.
179 */
180 protected function set_hook_data() {
181 $this->hook_data = array(
182 'woocommerce_before_main_content' => array(
183 'block_names' => array( 'core/query', 'woocommerce/product-collection' ),
184 'position' => 'before',
185 'hooked' => array(
186 'woocommerce_output_content_wrapper' => 10,
187 'woocommerce_breadcrumb' => 20,
188 ),
189 ),
190 'woocommerce_after_main_content' => array(
191 'block_names' => array( 'core/query', 'woocommerce/product-collection' ),
192 'position' => 'after',
193 'hooked' => array(
194 'woocommerce_output_content_wrapper_end' => 10,
195 ),
196 ),
197 'woocommerce_before_shop_loop_item_title' => array(
198 'block_names' => array( 'core/post-title' ),
199 'position' => 'before',
200 'hooked' => array(
201 'woocommerce_show_product_loop_sale_flash' => 10,
202 'woocommerce_template_loop_product_thumbnail' => 10,
203 ),
204 ),
205 'woocommerce_shop_loop_item_title' => array(
206 'block_names' => array( 'core/post-title' ),
207 'position' => 'after',
208 'hooked' => array(
209 'woocommerce_template_loop_product_title' => 10,
210 ),
211 ),
212 'woocommerce_after_shop_loop_item_title' => array(
213 'block_names' => array( 'core/post-title' ),
214 'position' => 'after',
215 'hooked' => array(
216 'woocommerce_template_loop_rating' => 5,
217 'woocommerce_template_loop_price' => 10,
218 ),
219 ),
220 'woocommerce_before_shop_loop_item' => array(
221 'block_names' => array( self::LOOP_ITEM_ID ),
222 'position' => 'before',
223 'hooked' => array(
224 'woocommerce_template_loop_product_link_open' => 10,
225 ),
226 ),
227 'woocommerce_after_shop_loop_item' => array(
228 'block_names' => array( self::LOOP_ITEM_ID ),
229 'position' => 'after',
230 'hooked' => array(
231 'woocommerce_template_loop_product_link_close' => 5,
232 'woocommerce_template_loop_add_to_cart' => 10,
233 ),
234 ),
235 'woocommerce_before_shop_loop' => array(
236 'block_names' => array( 'core/post-template', 'woocommerce/product-template' ),
237 'position' => 'before',
238 'hooked' => array(
239 'woocommerce_output_all_notices' => 10,
240 'woocommerce_result_count' => 20,
241 'woocommerce_catalog_ordering' => 30,
242 ),
243 'permanently_removed_actions' => array(
244 'woocommerce_output_all_notices',
245 'woocommerce_result_count',
246 'woocommerce_catalog_ordering',
247 ),
248 ),
249 'woocommerce_after_shop_loop' => array(
250 'block_names' => array( 'core/post-template', 'woocommerce/product-template' ),
251 'position' => 'after',
252 'hooked' => array(
253 'woocommerce_pagination' => 10,
254 ),
255 'permanently_removed_actions' => array(
256 'woocommerce_pagination',
257 ),
258 ),
259 'woocommerce_no_products_found' => array(
260 'block_names' => array( 'core/query-no-results' ),
261 'position' => 'before',
262 'hooked' => array(
263 'wc_no_products_found' => 10,
264 ),
265 'permanently_removed_actions' => array(
266 'wc_no_products_found',
267 ),
268 ),
269 'woocommerce_archive_description' => array(
270 'block_names' => array( 'core/term-description' ),
271 'position' => 'before',
272 'hooked' => array(
273 'woocommerce_taxonomy_archive_description' => 10,
274 'woocommerce_product_archive_description' => 10,
275 ),
276 ),
277 );
278 }
279
280 /**
281 * Check if current page is a product archive template.
282 */
283 private function is_archive_template() {
284 return is_shop() || is_product_taxonomy();
285 }
286
287 /**
288 * Loop through inner blocks recursively to find the Products blocks that
289 * inherits query from template.
290 *
291 * @param array $block Parsed block data.
292 */
293 private function inner_blocks_walker( &$block ) {
294 if (
295 $this->is_products_block_with_inherit_query( $block ) || $this->is_product_collection_block_with_inherit_query( $block )
296 ) {
297 $this->inject_attribute( $block );
298 $this->remove_default_hooks();
299 }
300
301 if ( ! empty( $block['innerBlocks'] ) ) {
302 array_walk( $block['innerBlocks'], array( $this, 'inner_blocks_walker' ) );
303 }
304 }
305
306 /**
307 * Restore default hooks except the ones that are not supposed to be re-added.
308 */
309 private function restore_default_hooks() {
310 foreach ( $this->hook_data as $hook => $data ) {
311 if ( ! isset( $data['hooked'] ) ) {
312 continue;
313 }
314 foreach ( $data['hooked'] as $callback => $priority ) {
315 if ( ! in_array( $callback, $data['permanently_removed_actions'] ?? array(), true ) ) {
316 add_action( $hook, $callback, $priority );
317 }
318 }
319 }
320 }
321
322 /**
323 * Check whether block is within the product-query namespace.
324 *
325 * @param array $block Parsed block data.
326 */
327 private function is_block_within_namespace( $block ) {
328 $attributes = $block['attrs'];
329
330 return isset( $attributes['__woocommerceNamespace'] ) && 'woocommerce/product-query/product-template' === $attributes['__woocommerceNamespace'];
331 }
332
333 /**
334 * Check whether block has isInherited attribute assigned.
335 *
336 * @param array $block Parsed block data.
337 */
338 private function is_block_inherited( $block ) {
339 $attributes = $block['attrs'];
340
341 $outcome = isset( $attributes['isInherited'] ) && 1 === $attributes['isInherited'];
342
343 return $outcome;
344 }
345
346 /**
347 * The core/post-template has two different block names:
348 * - core/post-template when the wrapper is rendered.
349 * - core/null when the loop item is rendered.
350 *
351 * @param array $block Parsed block data.
352 */
353 private function is_null_post_template( $block ) {
354 $block_name = $block['blockName'];
355
356 return 'core/null' === $block_name && ( $this->is_block_inherited( $block ) || $this->is_block_within_namespace( $block ) );
357 }
358
359 /**
360 * Check whether block is a Post template.
361 *
362 * @param string $block_name Block name.
363 */
364 private function is_post_template( $block_name ) {
365 return 'core/post-template' === $block_name;
366 }
367
368 /**
369 * Check whether block is a Product Template.
370 *
371 * @param string $block_name Block name.
372 */
373 private function is_product_template( $block_name ) {
374 return 'woocommerce/product-template' === $block_name;
375 }
376
377 /**
378 * Check if block is either a Post template or a Product Template
379 *
380 * @param string $block_name Block name.
381 */
382 private function is_post_or_product_template( $block_name ) {
383 return $this->is_post_template( $block_name ) || $this->is_product_template( $block_name );
384 }
385
386 /**
387 * Check if the block is a Products block that inherits query from template.
388 *
389 * @param array $block Parsed block data.
390 */
391 private function is_products_block_with_inherit_query( $block ) {
392 return 'core/query' === $block['blockName'] &&
393 isset( $block['attrs']['namespace'] ) &&
394 'woocommerce/product-query' === $block['attrs']['namespace'] &&
395 isset( $block['attrs']['query']['inherit'] ) &&
396 $block['attrs']['query']['inherit'];
397 }
398
399 /**
400 * Check if the block is a Product Collection block that inherits query from template.
401 *
402 * @param array $block Parsed block data.
403 */
404 private function is_product_collection_block_with_inherit_query( $block ) {
405 return 'woocommerce/product-collection' === $block['blockName'] &&
406 isset( $block['attrs']['query']['inherit'] ) &&
407 $block['attrs']['query']['inherit'];
408 }
409
410
411 /**
412 * Recursively inject the custom attribute to all nested blocks.
413 *
414 * @param array $block Parsed block data.
415 */
416 private function inject_attribute( &$block ) {
417 $block['attrs']['isInherited'] = 1;
418
419 if ( ! empty( $block['innerBlocks'] ) ) {
420 array_walk( $block['innerBlocks'], array( $this, 'inject_attribute' ) );
421 }
422 }
423 }
424