| 1 |
<?php |
| 2 |
/** |
| 3 |
* Registry of decentralized Gutenberg blocks for the Form Builder. |
| 4 |
* |
| 5 |
* Reads block.json files from the blocks directory to build the registry. |
| 6 |
*/ |
| 7 |
|
| 8 |
if ( ! defined( 'ABSPATH' ) ) exit; |
| 9 |
|
| 10 |
class WPPB_FB_Field_Registry { |
| 11 |
private static $blocks = null; |
| 12 |
|
| 13 |
// Reverse index (block name → field type), built lazily so `by_block_name()` |
| 14 |
// is O(1) — the flatten loop calls it once per PB block on canvas. |
| 15 |
private static $by_block_name = null; |
| 16 |
|
| 17 |
// Per-request parse cache, keyed by absolute path. Both `all()` and |
| 18 |
// `wppb_fb_register_blocks()` scan the same directory, so routing both through |
| 19 |
// here decodes each block.json at most once per request. |
| 20 |
private static $json_cache = array(); |
| 21 |
|
| 22 |
/** |
| 23 |
* Read/decode block.json once per request. Callers get a copy. |
| 24 |
* |
| 25 |
* @param string $path Absolute path to a block.json. |
| 26 |
* @return array|null |
| 27 |
*/ |
| 28 |
public static function read_block_json( $path ) { |
| 29 |
if ( array_key_exists( $path, self::$json_cache ) ) { |
| 30 |
return self::$json_cache[ $path ]; |
| 31 |
} |
| 32 |
$data = null; |
| 33 |
if ( is_readable( $path ) ) { |
| 34 |
$decoded = json_decode( file_get_contents( $path ), true ); |
| 35 |
if ( is_array( $decoded ) ) { |
| 36 |
$data = $decoded; |
| 37 |
} |
| 38 |
} |
| 39 |
self::$json_cache[ $path ] = $data; |
| 40 |
return $data; |
| 41 |
} |
| 42 |
|
| 43 |
public static function all() { |
| 44 |
if ( self::$blocks !== null ) return self::$blocks; |
| 45 |
|
| 46 |
self::$blocks = array(); |
| 47 |
$blocks_dir = __DIR__ . '/blocks/src/blocks'; |
| 48 |
if ( ! is_dir( $blocks_dir ) ) return array(); |
| 49 |
|
| 50 |
$cf_disabled = wppb_fb_cf_disabled_field_types(); |
| 51 |
$no_meta_name_types = wppb_fb_no_meta_name_field_types(); |
| 52 |
$default_meta_map = wppb_fb_default_field_meta_names(); |
| 53 |
|
| 54 |
$it = new DirectoryIterator( $blocks_dir ); |
| 55 |
foreach ( $it as $fileinfo ) { |
| 56 |
if ( $fileinfo->isDir() && ! $fileinfo->isDot() ) { |
| 57 |
$json_file = $fileinfo->getPathname() . '/block.json'; |
| 58 |
$json = self::read_block_json( $json_file ); |
| 59 |
if ( $json && isset( $json['name'] ) ) { |
| 60 |
// The field registry is strictly for `profile-builder/field-*` |
| 61 |
// blocks. Structural blocks (e.g. profile-builder/msf-step-break) |
| 62 |
// share the directory tree for bundling convenience but must |
| 63 |
// not appear here — they don't round-trip through manage_fields. |
| 64 |
if ( strpos( $json['name'], 'profile-builder/field-' ) !== 0 ) continue; |
| 65 |
|
| 66 |
// block.json's `wppbFieldType` is the canonical field-type |
| 67 |
// label; see get_field_type_from_block_name(). |
| 68 |
$field_type = self::get_field_type_from_block_name( $json['name'], $json ); |
| 69 |
$attributes = isset( $json['attributes'] ) ? $json['attributes'] : array(); |
| 70 |
|
| 71 |
// Any cross-cutting attribute MUST be injected through this |
| 72 |
// shared helper, or this shadow (which the flatten projection |
| 73 |
// reads) and the runtime registration silently drift. |
| 74 |
$attributes = wppb_fb_compute_field_attributes( |
| 75 |
$attributes, |
| 76 |
$field_type, |
| 77 |
$json['name'], |
| 78 |
array( |
| 79 |
'cf_disabled' => $cf_disabled, |
| 80 |
'default_meta_map' => $default_meta_map, |
| 81 |
'no_meta_name_types' => $no_meta_name_types, |
| 82 |
'context_label' => 'registry', |
| 83 |
) |
| 84 |
); |
| 85 |
|
| 86 |
self::$blocks[ $field_type ] = array( |
| 87 |
'block' => $json['name'], |
| 88 |
'title' => isset( $json['title'] ) ? $json['title'] : $field_type, |
| 89 |
'icon' => isset( $json['icon'] ) ? $json['icon'] : 'admin-generic', |
| 90 |
'attributes' => $attributes, |
| 91 |
// Add-on-provided type: stays in the registry so existing |
| 92 |
// instances render, but the inserter gates it. See |
| 93 |
// wppb_fb_inactive_addon_field_blocks(). |
| 94 |
'addon_gated' => ! empty( $json['wppbAddonFieldType'] ), |
| 95 |
); |
| 96 |
} |
| 97 |
} |
| 98 |
} |
| 99 |
|
| 100 |
return self::$blocks; |
| 101 |
} |
| 102 |
|
| 103 |
/** |
| 104 |
* PB field-type label from block name. Prefer `$metadata['wppbFieldType']` when present. |
| 105 |
* |
| 106 |
* @param string $block_name e.g. "profile-builder/field-input" |
| 107 |
* @param array|null $metadata Optional parsed block.json. |
| 108 |
* @return string |
| 109 |
*/ |
| 110 |
public static function get_field_type_from_block_name( $block_name, $metadata = null ) { |
| 111 |
if ( is_array( $metadata ) && ! empty( $metadata['wppbFieldType'] ) ) { |
| 112 |
return (string) $metadata['wppbFieldType']; |
| 113 |
} |
| 114 |
// Slug fallback, title-casing EVERY word so `field-foo-bar` becomes |
| 115 |
// `Foo Bar`. Surfaced under WP_DEBUG so silent drift gets caught. |
| 116 |
$slug = str_replace( 'profile-builder/field-', '', $block_name ); |
| 117 |
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( '_doing_it_wrong' ) ) { |
| 118 |
_doing_it_wrong( |
| 119 |
__METHOD__, |
| 120 |
sprintf( |
| 121 |
'Falling back to slug derivation for block name %s — pass the parsed block.json as $metadata so wppbFieldType is honored.', |
| 122 |
esc_html( $block_name ) |
| 123 |
), |
| 124 |
'3.16.0' |
| 125 |
); |
| 126 |
} |
| 127 |
return ucwords( str_replace( '-', ' ', $slug ) ); |
| 128 |
} |
| 129 |
|
| 130 |
public static function by_block_name( $block_name ) { |
| 131 |
if ( self::$by_block_name === null ) { |
| 132 |
$blocks = self::all(); |
| 133 |
self::$by_block_name = array(); |
| 134 |
foreach ( $blocks as $type => $data ) { |
| 135 |
if ( isset( $data['block'] ) ) { |
| 136 |
self::$by_block_name[ $data['block'] ] = $type; |
| 137 |
} |
| 138 |
} |
| 139 |
} |
| 140 |
if ( ! isset( self::$by_block_name[ $block_name ] ) ) { |
| 141 |
return null; |
| 142 |
} |
| 143 |
$type = self::$by_block_name[ $block_name ]; |
| 144 |
return array_merge( self::$blocks[ $type ], array( 'field_type' => $type ) ); |
| 145 |
} |
| 146 |
} |
| 147 |
|