PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.3
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.3
4.0.3 4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 341 releases
profile-builder / form-builder / form-builder-row-projection.php

form-builder-row-projection.php in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.3, at form-builder/form-builder-row-projection.php

263 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Read-side projection: legacy field rows → parsed-block trees. The inverse of
4 * the flatten loop in form-builder-projection.php. Pure helpers, no hooks.
5 */
6
7 if ( ! defined( 'ABSPATH' ) ) exit;
8
9 /**
10 * Generates block markup from legacy field meta.
11 *
12 * The per-form list (`wppb_*_fields`) is flat — Repeater sub-fields live in a
13 * separate option keyed by the parent's meta-name (legacy contract), and are
14 * emitted here as innerBlocks.
15 */
16 function wppb_fb_build_block_markup_for_form( $post_id, $post_type ) {
17 $meta_key = $post_type === 'wppb-rf-cpt' ? 'wppb_rf_fields' : 'wppb_epf_fields';
18 $entries = get_post_meta( $post_id, $meta_key, true );
19 $manage = wppb_fb_manage_fields();
20
21 if ( ! is_array( $entries ) ) $entries = array();
22
23 wppb_fb_prime_repeater_option_caches( $manage );
24
25 // Index manage_fields for quick attribute lookup
26 $manage_by_id = array();
27 foreach ( $manage as $item ) {
28 if ( isset( $item['id'] ) ) $manage_by_id[ (int) $item['id'] ] = $item;
29 }
30
31 // Resolve block names via the registry (the canonical reverse of by_block_name).
32 // Keyed by PB field type -> block name.
33 $registry = WPPB_FB_Field_Registry::all();
34
35 $top_context = array(
36 'context' => 'form-render',
37 'post_id' => $post_id,
38 'post_type' => $post_type,
39 'is_subfield' => false,
40 );
41 $sub_context = array(
42 'context' => 'form-render',
43 'post_id' => $post_id,
44 'post_type' => $post_type,
45 'is_subfield' => true,
46 );
47
48 $repeater_active = wppb_fb_is_repeater_module_active();
49
50 // Registration forms' mandatory fields get `lock.remove = true` injected on
51 // read so Gutenberg's `canRemoveBlocks` gate closes the keyboard / toolbar /
52 // dispatch removal paths. `lock` is in no field block's attribute schema, so
53 // the flatten loop can't persist it and the per-edit mirror baselines it.
54 $is_registration_form = $post_type === 'wppb-rf-cpt';
55 $mandatory_field_types = $is_registration_form ? wppb_fb_mandatory_registration_field_types() : array();
56
57 $blocks = array();
58 foreach ( $entries as $entry ) {
59 // An entry without a usable `id` can't be resolved to a registry row —
60 // there is nothing to look up. Skip it explicitly instead of emitting
61 // `Undefined array key "id"` and falling through to the null-block path.
62 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) continue;
63
64 $id = (int) $entry['id'];
65 $global = isset( $manage_by_id[ $id ] ) ? $manage_by_id[ $id ] : array();
66
67 // Skip Repeater rows entirely when the module is off. The
68 // underlying legacy data (manage_fields row + get_option(meta-name)
69 // sub-field defs + user_meta rows) is left untouched — re-enabling
70 // the module brings the field back into the canvas as-is.
71 if ( ! $repeater_active && ( $global['field'] ?? '' ) === 'Repeater' ) continue;
72
73 $block = wppb_fb_row_to_block( $global, $id, $registry, $top_context );
74 if ( $block === null ) continue;
75
76 if ( $is_registration_form && in_array( $global['field'] ?? '', $mandatory_field_types, true ) ) {
77 $existing_lock = isset( $block['attrs']['lock'] ) && is_array( $block['attrs']['lock'] ) ? $block['attrs']['lock'] : array();
78 $block['attrs']['lock'] = array_merge( $existing_lock, array( 'remove' => true ) );
79 }
80
81 // Repeaters: hydrate innerBlocks from get_option( $meta_name ).
82 if ( ( $global['field'] ?? '' ) === 'Repeater' && ! empty( $global['meta-name'] ) ) {
83 $group = get_option( $global['meta-name'], 'not_set' );
84 if ( is_array( $group ) ) {
85 foreach ( $group as $subrow ) {
86 $sub_id = (int) ( $subrow['id'] ?? 0 );
87 $subblock = wppb_fb_row_to_block( $subrow, $sub_id, $registry, $sub_context );
88 if ( $subblock === null ) continue;
89
90 // Sub-fields are scope-locked to their parent Repeater
91 // client-side only (a CSS overlay — see RepeaterScopeGuard.js
92 // and _repeater-scope.scss), so intra-Repeater reorder by drag
93 // keeps working. Nothing is injected server-side here.
94 $block['innerBlocks'][] = $subblock;
95 }
96 }
97 // serialize_block() walks innerContent, treating nulls as child-block
98 // placeholders. Without this, children are silently dropped during
99 // serialization even though innerBlocks is populated.
100 $block['innerContent'] = array_fill( 0, count( $block['innerBlocks'] ), null );
101 }
102
103 $blocks[] = $block;
104 }
105
106 // Lets add-ons inject structural blocks (e.g. Multi-Step Forms step-break
107 // separators) into the regenerated tree at positions derived from their
108 // own per-form storage. Receives ($blocks, $post_id, $post_type).
109 $blocks = apply_filters( 'wppb_fb_form_blocks', $blocks, $post_id, $post_type );
110
111 return serialize_blocks( $blocks );
112 }
113
114 /**
115 * Field types that are mandatory on every Registration form. Mirrors the legacy
116 * `wppb_disable_delete_on_default_mandatory_fields()` rule; enforced by the
117 * DELETE route (403) and by the `lock.remove` injection on read. Filterable.
118 */
119 function wppb_fb_mandatory_registration_field_types() {
120 return apply_filters( 'wppb_fb_mandatory_registration_field_types', array(
121 'Default - Username',
122 'Default - E-mail',
123 'Default - Password',
124 ) );
125 }
126
127 /**
128 * Copies a legacy field row's block-attribute keys, dropping the two non-attribute
129 * keys (`id`, `field`) every caller surfaces separately. Single source of truth for
130 * that reserved set, so the two read paths can't drift.
131 */
132 function wppb_fb_row_attrs_without_meta( $row ) {
133 $attrs = array();
134 foreach ( $row as $key => $val ) {
135 if ( $key === 'id' || $key === 'field' ) continue;
136 $attrs[ $key ] = $val;
137 }
138 return $attrs;
139 }
140
141 /**
142 * Converts a legacy field row into a parsed-block array. Returns null when the
143 * row's field type isn't registered (add-on deactivated). $context is forwarded
144 * to `wppb_fb_row_to_block_attrs` so add-ons can hydrate per-form attributes
145 * that don't live on the row itself.
146 */
147 function wppb_fb_row_to_block( $row, $id, $registry, $context = array() ) {
148 $field_type = isset( $row['field'] ) ? $row['field'] : '';
149 if ( $field_type === '' || ! isset( $registry[ $field_type ] ) ) {
150 return null;
151 }
152 $attrs = array( 'id' => $id ) + wppb_fb_row_attrs_without_meta( $row );
153 $attrs = apply_filters( 'wppb_fb_row_to_block_attrs', $attrs, $row, $field_type, $context );
154 return array(
155 'blockName' => $registry[ $field_type ]['block'],
156 'attrs' => $attrs,
157 'innerBlocks' => array(),
158 'innerHTML' => '',
159 'innerContent' => array(),
160 );
161 }
162
163 /**
164 * Converts a legacy field row into the Existing Fields bridge's entry shape.
165 * Returns null when the field type has no registered block, so the panel never
166 * produces a block Gutenberg can't parse.
167 */
168 function wppb_fb_existing_fields_row_to_entry( $row, $registry, $is_subfield = false ) {
169 if ( ! isset( $row['field'], $row['id'] ) ) return null;
170 $field_type = $row['field'];
171 if ( ! isset( $registry[ $field_type ] ) ) return null;
172
173 $attrs = wppb_fb_row_attrs_without_meta( $row );
174
175 // Same filter as wppb_fb_row_to_block(), distinct context. Per-form
176 // attributes (e.g. Multi-Step Forms break-point flags) MUST NOT be
177 // injected here — this is global state from wppb_manage_fields and the
178 // panel's "linked, not cloned" semantics would leak per-form state across
179 // forms. Add-ons listen for context === 'form-render' only.
180 $attrs = apply_filters( 'wppb_fb_row_to_block_attrs', $attrs, $row, $field_type, array(
181 'context' => 'existing-fields-bridge',
182 'is_subfield' => (bool) $is_subfield,
183 ) );
184
185 return array(
186 'id' => (int) $row['id'],
187 'fieldType' => $field_type,
188 'blockName' => $registry[ $field_type ]['block'],
189 'title' => isset( $row['field-title'] ) ? (string) $row['field-title'] : '',
190 'metaName' => isset( $row['meta-name'] ) ? (string) $row['meta-name'] : '',
191 'attributes' => $attrs,
192 'innerBlocks' => array(),
193 );
194 }
195
196 /**
197 * Pre-warms the options cache for every Repeater meta-name in a snapshot, so the
198 * hydration loops don't issue one un-cached `get_option()` per Repeater row.
199 * No-op on WP < 6.4 — the bridge still works, just without the batched prefetch.
200 */
201 function wppb_fb_prime_repeater_option_caches( $manage ) {
202 if ( ! function_exists( 'wp_prime_option_caches' ) ) return;
203 if ( ! is_array( $manage ) ) return;
204
205 $names = array();
206 foreach ( $manage as $row ) {
207 if ( ( $row['field'] ?? '' ) === 'Repeater' && ! empty( $row['meta-name'] ) ) {
208 $names[] = (string) $row['meta-name'];
209 }
210 }
211 if ( $names ) wp_prime_option_caches( $names );
212 }
213
214 /**
215 * True when `$meta_name` is a Repeater meta-name in `$manage` (sub-field REST gate).
216 */
217 function wppb_fb_is_repeater_meta_name( $meta_name, $manage ) {
218 if ( ! is_string( $meta_name ) || $meta_name === '' ) return false;
219 if ( ! is_array( $manage ) ) return false;
220 foreach ( $manage as $row ) {
221 if ( ( $row['field'] ?? '' ) === 'Repeater' && (string) ( $row['meta-name'] ?? '' ) === $meta_name ) {
222 return true;
223 }
224 }
225 return false;
226 }
227
228 /** Hydrate a Repeater entry's `innerBlocks` from `get_option( $meta_name )`. */
229 function wppb_fb_hydrate_repeater_subfields( array &$entry, array $registry ) {
230 if ( ( $entry['fieldType'] ?? '' ) !== 'Repeater' ) return;
231 if ( ( $entry['metaName'] ?? '' ) === '' ) return;
232
233 $group = get_option( $entry['metaName'], 'not_set' );
234 if ( ! is_array( $group ) ) return;
235
236 foreach ( $group as $sub_row ) {
237 $sub_entry = wppb_fb_existing_fields_row_to_entry( $sub_row, $registry, true );
238 if ( $sub_entry !== null ) $entry['innerBlocks'][] = $sub_entry;
239 }
240 }
241
242 /**
243 * Snapshot wppb_manage_fields for the Existing Fields panel. Repeaters must be hydrated.
244 */
245 function wppb_fb_existing_fields_bridge() {
246 $manage = wppb_fb_manage_fields();
247 if ( ! is_array( $manage ) ) return array();
248
249 wppb_fb_prime_repeater_option_caches( $manage );
250
251 $registry = WPPB_FB_Field_Registry::all();
252 $fields = array();
253 foreach ( $manage as $row ) {
254 $entry = wppb_fb_existing_fields_row_to_entry( $row, $registry );
255 if ( $entry === null ) continue;
256
257 wppb_fb_hydrate_repeater_subfields( $entry, $registry );
258
259 $fields[] = $entry;
260 }
261 return $fields;
262 }
263