PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / trunk
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler vtrunk
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
fluent-cart / app / Hooks / Handlers / BlockEditors / ProductReviewsBlockEditor.php

ProductReviewsBlockEditor.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler trunk, at app/Hooks/Handlers/BlockEditors/ProductReviewsBlockEditor.php

196 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Hooks\Handlers\BlockEditors;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Modules\Templating\AssetLoader;
7 use FluentCart\App\Services\Renderer\ProductReviewRenderer;
8 use FluentCart\App\Services\Reviews\LayoutPresets;
9 use FluentCart\App\Services\Translations\TransStrings;
10 use FluentCart\Framework\Support\Arr;
11
12 class ProductReviewsBlockEditor extends BlockEditor
13 {
14 protected static string $editorName = 'product-reviews';
15
16 public function supports(): array
17 {
18 return [
19 'html' => false,
20 'align' => true,
21 'typography' => [
22 'fontSize' => true,
23 'lineHeight' => true,
24 '__experimentalFontFamily' => true,
25 '__experimentalFontWeight' => true,
26 '__experimentalDefaultControls' => [
27 'fontSize' => true,
28 ],
29 ],
30 'color' => [
31 'text' => true,
32 'background' => true,
33 ],
34 'spacing' => [
35 'margin' => true,
36 'padding' => true,
37 ],
38 '__experimentalBorder' => [
39 'color' => true,
40 'radius' => true,
41 'style' => true,
42 'width' => true,
43 ],
44 ];
45 }
46
47 /**
48 * Container pattern (same as ProductInfoBlockEditor): without this,
49 * WordPress renders the inner blocks once before the render callback
50 * runs — outside the custom-product context and with all their query
51 * and hook side effects — and renderContainer() then renders them
52 * again.
53 */
54 protected function skipInnerBlocks(): bool
55 {
56 return true;
57 }
58
59 /**
60 * Children detect nesting server-side by this key's presence (the
61 * related_product_ids pattern) and follow the container's product,
62 * so a stale custom pick saved on a child can never win — saved
63 * content renders without the editor's query pinning ever running.
64 */
65 public function provideContext()
66 {
67 return [
68 'fluent-cart/review_container_query' => 'query_type',
69 ];
70 }
71
72 public function blockAttributes(): array
73 {
74 return [
75 // Must mirror the JS registration — WordPress prepares dynamic
76 // block attributes against this server-side schema, so an
77 // attribute missing here never reaches the render callback.
78 'query_type' => ['type' => 'string', 'default' => 'default'],
79 'product_id' => ['type' => ['string', 'number'], 'default' => ''],
80 // Mirrors the JS registration. Only remembers which layout the
81 // picker last built the section from — nothing reads it when
82 // rendering, because the blocks are the layout.
83 'preset' => ['type' => 'string', 'default' => 'classic'],
84 ];
85 }
86
87 public function getScripts(): array
88 {
89 return [
90 [
91 'source' => 'admin/BlockEditor/ProductReviews/ProductReviewsBlockEditor.jsx',
92 'dependencies' => ['wp-blocks', 'wp-components', 'wp-data', 'wp-block-editor', 'wp-element']
93 ]
94 ];
95 }
96
97 public function getStyles(): array
98 {
99 return [
100 'admin/BlockEditor/ProductReviews/style/product-reviews-block-editor.scss'
101 ];
102 }
103
104 public function localizeData(): array
105 {
106 return [
107 $this->getLocalizationKey() => [
108 'slug' => $this->slugPrefix,
109 'name' => static::getEditorName(),
110 'title' => __('Product Reviews', 'fluent-cart'),
111 'description' => __('Display customer reviews and ratings for a product.', 'fluent-cart'),
112 // Which layout presets the picker may apply. An editor-side
113 // gate on a convenience, not a lock on a capability: a preset
114 // only assembles blocks that are free in their own right, so
115 // the same arrangement stays buildable by hand and a site that
116 // lapses keeps the layouts it already published.
117 'is_pro' => App::isProActive(),
118 // The layouts the picker builds from. Declared in PHP so the
119 // block editor and anything else that composes these blocks
120 // read one description of them rather than keeping a copy each.
121 'presets' => LayoutPresets::all(),
122 ],
123 'fluent_cart_block_translation' => TransStrings::blockStrings(),
124 ];
125 }
126
127 public function render(array $shortCodeAttribute, $block = null)
128 {
129 $product = $this->resolveProduct($shortCodeAttribute);
130
131 if (!$product) {
132 return '';
133 }
134
135 AssetLoader::loadSingleProductAssets();
136
137 // Container mode: the block holds child blocks (Rating Summary /
138 // Write a Review / Review List), so it only resolves the product, sets
139 // it as the current-product context, and renders the children. That is
140 // what the editor scaffolds, so it is what a container normally is.
141 if ($block instanceof \WP_Block && !empty($block->inner_blocks)) {
142 return $this->renderContainer($shortCodeAttribute, $block, $product);
143 }
144
145 // Emptied of every child, it draws the whole section itself, to the
146 // renderer's own defaults — the same thing the shortcode draws.
147 $wrapper_attributes = get_block_wrapper_attributes([
148 'class' => 'fct-product-reviews-block',
149 ]);
150
151 ob_start();
152 (new ProductReviewRenderer($product->ID, []))->render();
153 $inner = ob_get_clean();
154
155 return static::wrapUnlessEmpty($wrapper_attributes, $inner);
156 }
157
158 protected function renderContainer(array $shortCodeAttribute, \WP_Block $block, $product)
159 {
160 // A custom pick swaps the product context through setup_postdata()
161 // only — its the_post action drives ProductDataSetup, so no globals
162 // are written here. Restoring goes through the same API: re-running
163 // setup_postdata() for whatever product was current before keeps the
164 // swap stack-safe when this container is nested inside another
165 // custom-product context, where wp_reset_postdata() alone would hand
166 // sibling blocks the main-query post instead of the outer product.
167 $isCustom = Arr::get($shortCodeAttribute, 'query_type', 'default') === 'custom';
168 $previousProduct = $isCustom ? fluent_cart_get_current_product() : null;
169
170 if ($isCustom) {
171 setup_postdata($product->ID);
172 }
173
174 $innerContent = '';
175 foreach ($block->inner_blocks as $innerBlock) {
176 if (isset($innerBlock->parsed_block)) {
177 $innerContent .= $innerBlock->render();
178 }
179 }
180
181 if ($isCustom) {
182 if ($previousProduct) {
183 setup_postdata($previousProduct->ID);
184 } else {
185 wp_reset_postdata();
186 }
187 }
188
189 $wrapper_attributes = get_block_wrapper_attributes([
190 'class' => 'fct-product-reviews-block fct-product-reviews-container',
191 ]);
192
193 return static::wrapUnlessEmpty($wrapper_attributes, $innerContent);
194 }
195 }
196