From d1b591f4df9e5068d8613316dc4c96bcc75e7ee1 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 14:21:46 +0000 Subject: [PATCH 1/6] Tidies up constructor argument documentation, applying WP coding standards --- includes/class-child-block.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/class-child-block.php b/includes/class-child-block.php index dcc1c07..70a1a02 100644 --- a/includes/class-child-block.php +++ b/includes/class-child-block.php @@ -66,13 +66,13 @@ class Child_Block { /** * Data input function. * - * @param string $name The child block's name (must be hyphen separated). - * @param string $label The child block's label. - * @param array $fields An array of field definitions in ACF format. - * @param string $template A path to the render template. + * @param string $name The child block's name (must be hyphen separated). + * @param string $label The child block's label. + * @param array $fields An array of field definitions in ACF format. + * @param string $template A path to the render template. * @param Child_Block[] $child_blocks (Optional) Array of child blocks. - * @param string $icon (Optional) Icon for Child block. - * @param array $supports (Optional) Array of supports configuration. + * @param string $icon (Optional) Icon for Child block. + * @param array $supports (Optional) Array of supports configuration. */ public function __construct( string $name, From 22350036704976d094c605001b6ebfefd23709d7 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 14:22:23 +0000 Subject: [PATCH 2/6] Replace set_child_blocks function with a generic setter and replace items that consume this function. --- includes/class-child-block.php | 23 +++++++++++++++++++---- includes/class-helpers.php | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/includes/class-child-block.php b/includes/class-child-block.php index 70a1a02..6b03792 100644 --- a/includes/class-child-block.php +++ b/includes/class-child-block.php @@ -102,11 +102,26 @@ public function __get( string $property ) { } /** - * Sets the child blocks array. + * Function to run when setting any properties. * - * @param Child_Block[] $child_blocks Array of child blocks. + * @param string $name Name of property to set. + * @param mixed $value Value of the property. + * + * @throws \InvalidArgumentException Thrown if you are trying to set an invalid property. + * + * @return void */ - public function set_child_blocks( array $child_blocks ): void { - $this->child_blocks = $child_blocks; + public function __set( string $name, mixed $value ): void { + // Check the property is actually present on the class. + if ( ! property_exists( $this, $name ) ) { + throw new \InvalidArgumentException( sprintf( 'The property "%s" does not exist on the %s class.', esc_html( $name ), esc_html( __CLASS__ ) ) ); + } + + // We shouldn't allow name to be changed as it can have a number of negative side effects. + if ( 'name' === $name ) { + throw new \InvalidArgumentException( 'The "name" property is read-only and cannot be modified.' ); + } + + $this->{$name} = $value; } } diff --git a/includes/class-helpers.php b/includes/class-helpers.php index f1ef8b0..679d6af 100644 --- a/includes/class-helpers.php +++ b/includes/class-helpers.php @@ -178,7 +178,7 @@ public static function replace_child_block_by_path( array $existing_child_blocks $remaining_path, $new_child_block ); - $child_block->set_child_blocks( $updated_child_blocks ); + $child_block->child_blocks = $updated_child_blocks; } // We've processed the block we need to, therefore From 952fed07a5d282303c02566946e6fde74105b372 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 14:39:53 +0000 Subject: [PATCH 3/6] Adds a new helper function to find a child block by path from an array of existing child blocks. --- includes/class-helpers.php | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/includes/class-helpers.php b/includes/class-helpers.php index 679d6af..5b1a1f0 100644 --- a/includes/class-helpers.php +++ b/includes/class-helpers.php @@ -188,4 +188,53 @@ public static function replace_child_block_by_path( array $existing_child_blocks return $existing_child_blocks; } + + /** + * Finds a child block from an array of existing child blocks based on its path. + * + * @param Child_Block[] $existing_child_blocks An array of existing child blocks to search in. + * @param string $path A "/" separated path to block for example "table/row/cell". + * + * @throws \InvalidArgumentException If we cannot parse the data provided or find the block. + * + * @return Child_Block + */ + public static function get_child_block_by_path( array $existing_child_blocks, string $path ): Child_Block { + // Parse and validate path segments. + $path_segments = array_values( array_filter( explode( '/', $path ) ) ); + if ( empty( $path_segments ) ) { + throw new \InvalidArgumentException( sprintf( 'Invalid path provided to get_child_block_by_path: "%s". Path must not be empty and must contain at least one valid segment.', esc_html( $path ) ) ); + } + + $current_blocks = $existing_child_blocks; + + foreach ( $path_segments as $index => $path_segment ) { + // Find matching child block at current level using array_filter. + $matching_blocks = array_filter( + $current_blocks, + function ( $child_block ) use ( $path_segment ) { + return $child_block->name === $path_segment; + } + ); + + // Throw exception if path segment not found. + if ( empty( $matching_blocks ) ) { + throw new \InvalidArgumentException( sprintf( 'Child block not found at path segment "%s" in path "%s".', esc_html( $path_segment ), esc_html( $path ) ) ); + } + + // Get the first matching block (should only be one). + $found_block = reset( $matching_blocks ); + if ( false === $found_block ) { + throw new \InvalidArgumentException( sprintf( 'Unexpected error: failed to retrieve child block at path segment "%s" in path "%s".', esc_html( $path_segment ), esc_html( $path ) ) ); + } + + // If this is the last segment, return the found block. + if ( count( $path_segments ) - 1 === $index ) { + return $found_block; + } + + // Otherwise, dive down into the found block's child blocks. + $current_blocks = $found_block->child_blocks; + } + } } From 99b095f7f23a167f432a5e7d95d910ddde671df7 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 14:45:06 +0000 Subject: [PATCH 4/6] Adds missing comment start. --- includes/class-helpers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-helpers.php b/includes/class-helpers.php index 1d8e475..2820470 100644 --- a/includes/class-helpers.php +++ b/includes/class-helpers.php @@ -218,6 +218,7 @@ public static function get_block_attribute( string $attribute_name, WP_Block|nul return null; } + /* * Finds a child block from an array of existing child blocks based on its path. * * @param Child_Block[] $existing_child_blocks An array of existing child blocks to search in. From 98950ab469ca097f396eb63c3a8739ebcefdd4c0 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 15:07:36 +0000 Subject: [PATCH 5/6] Tidy up redundant checks, rename variable and fix comment coding standards issue. --- includes/class-helpers.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/includes/class-helpers.php b/includes/class-helpers.php index 2820470..2af8165 100644 --- a/includes/class-helpers.php +++ b/includes/class-helpers.php @@ -218,7 +218,7 @@ public static function get_block_attribute( string $attribute_name, WP_Block|nul return null; } - /* + /** * Finds a child block from an array of existing child blocks based on its path. * * @param Child_Block[] $existing_child_blocks An array of existing child blocks to search in. @@ -246,24 +246,19 @@ function ( $child_block ) use ( $path_segment ) { } ); - // Throw exception if path segment not found. - if ( empty( $matching_blocks ) ) { - throw new \InvalidArgumentException( sprintf( 'Child block not found at path segment "%s" in path "%s".', esc_html( $path_segment ), esc_html( $path ) ) ); - } - // Get the first matching block (should only be one). - $found_block = reset( $matching_blocks ); - if ( false === $found_block ) { - throw new \InvalidArgumentException( sprintf( 'Unexpected error: failed to retrieve child block at path segment "%s" in path "%s".', esc_html( $path_segment ), esc_html( $path ) ) ); + $matched_block = reset( $matching_blocks ); + if ( false === $matched_block ) { + throw new \InvalidArgumentException( sprintf( 'Child block not found at path segment "%s" in path "%s".', esc_html( $path_segment ), esc_html( $path ) ) ); } // If this is the last segment, return the found block. if ( count( $path_segments ) - 1 === $index ) { - return $found_block; + return $matched_block; } // Otherwise, dive down into the found block's child blocks. - $current_blocks = $found_block->child_blocks; + $current_blocks = $matched_block->child_blocks; } } } From deb768601825121b0b690227273e9357198482d6 Mon Sep 17 00:00:00 2001 From: Jamie Sykes Date: Tue, 11 Nov 2025 15:22:55 +0000 Subject: [PATCH 6/6] Works the new get_child_block_by_path helper function into the documentation and uses it in the replace_child_block_by_path functions. --- docs/.vitepress/config.mjs | 3 +- docs/helpers/get_child_block_by_path.md | 211 ++++++++++++++++++++ docs/helpers/replace_child_block_by_path.md | 135 ++++++------- 3 files changed, 278 insertions(+), 71 deletions(-) create mode 100644 docs/helpers/get_child_block_by_path.md diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 6dd1fbd..efba8ad 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -79,7 +79,8 @@ export default defineConfig({ { text: 'render_inner_blocks_in_post_context', link: '/helpers/render_inner_blocks_in_post_context' }, { text: 'render_blocks_with_dynamic_context', link: '/helpers/render_blocks_with_dynamic_context' }, { text: 'add_dynamic_context_to_blocks', link: '/helpers/add_dynamic_context_to_blocks' }, - { text: 'replace_child_block_by_path', link: 'helpers/replace_child_block_by_path' }, + { text: 'get_child_block_by_path', link: '/helpers/get_child_block_by_path' }, + { text: 'replace_child_block_by_path', link: '/helpers/replace_child_block_by_path' }, { text: 'set_acf_block_mode', link: '/helpers/set_acf_block_mode' }, ] }, diff --git a/docs/helpers/get_child_block_by_path.md b/docs/helpers/get_child_block_by_path.md new file mode 100644 index 0000000..3987817 --- /dev/null +++ b/docs/helpers/get_child_block_by_path.md @@ -0,0 +1,211 @@ +--- +title: get_child_block_by_path +editLink: false +--- + +# get_child_block_by_path + +## Description + +The `get_child_block_by_path()` method finds a child block from an array of existing child blocks based on its path. This method is particularly useful when you need to access and modify a specific child block within a nested child block structure. + +### Responsibility + +This method recursively searches through an array of child blocks using a path-based approach (e.g., "table/row/cell") to locate a specific child block. It validates the path and throws an exception if the block cannot be found, ensuring type safety and preventing silent failures. + +### Arguments + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `$existing_child_blocks` | `Child_Block[]` | Yes | An array of existing child blocks to search in | +| `$path` | `string` | Yes | A "/" separated path to block (e.g., "table/row/cell" or "table/row/cell/cell-content") | + +### Return Value + +- **Type**: `Child_Block` +- **Description**: Returns the matching Child_Block instance if found +- **Throws**: `\InvalidArgumentException` if the path is invalid or the block cannot be found + +## Path Format + +The path parameter uses a forward-slash (`/`) separated format to navigate through nested child blocks: + +- Each segment represents a child block name +- The path is traversed from top to bottom +- Example: `"table/row/cell"` means: + - Find a child block named "table" + - Within that, find a child block named "row" + - Within that, find a child block named "cell" + - Return that "cell" block + +## Pairing with replace_child_block_by_path + +This function pairs exceptionally well with `replace_child_block_by_path()`. The typical workflow is: + +1. Use `get_child_block_by_path()` to retrieve the existing child block you want to modify +2. Make your modifications to the retrieved block (e.g., add fields, change configuration) +3. Use `replace_child_block_by_path()` to replace the original block with your modified version + +This pattern allows you to extend child blocks defined in parent classes without having to completely recreate the entire child block hierarchy. + +## Examples + +### Basic Usage - Retrieving a Child Block + +This example shows how to retrieve a child block from a nested structure: + +```php +use Creode_Blocks\Helpers; + +protected function child_blocks(): array { + $existing_child_blocks = parent::child_blocks(); + + // Get a specific child block by path + $cell_block = Helpers::get_child_block_by_path( + $existing_child_blocks, + 'table/row/cell' + ); + + // Now you can access the block's properties + $fields = $cell_block->fields; + $template = $cell_block->template; +} +``` + +### Extending a Child Block with Additional Fields + +This is the most common use case - extending a child block defined in a parent class by adding new fields: + +```php +use Creode_Blocks\Table_Block as Base_Table_Block; +use Creode_Blocks\Helpers; + +class Table extends Base_Table_Block { + + /** + * {@inheritdoc} + */ + protected function name(): string { + return 'table'; + } + + /** + * {@inheritdoc} + */ + protected function label(): string { + return 'Comparison Table'; + } + + /** + * {@inheritdoc} + */ + protected function child_blocks(): array { + $existing_child_blocks = parent::child_blocks(); + + // Get the existing cell-content child block + $cell_content_child_block = Helpers::get_child_block_by_path( + $existing_child_blocks, + 'table/row/cell/cell-content' + ); + + // Get the existing fields array and add the new field to it + $fields = $cell_content_child_block->fields; + $fields[] = array( + 'key' => 'creode_field', + 'label' => 'Creode Field', + 'name' => 'creode_field', + 'type' => 'select', + 'choices' => array( + '' => 'hooray', + '1' => 'woohoo', + '2' => 'I did it', + ), + ); + $cell_content_child_block->fields = $fields; + + // Replace the original block with the modified version + $replaced_child_blocks = Helpers::replace_child_block_by_path( + $existing_child_blocks, + 'table/row/cell/cell-content', + $cell_content_child_block + ); + + return $replaced_child_blocks; + } +} +``` + +### Modifying Multiple Properties + +You can modify multiple properties of the retrieved child block: + +```php +use Creode_Blocks\Helpers; + +protected function child_blocks(): array { + $existing_child_blocks = parent::child_blocks(); + + $target_block = Helpers::get_child_block_by_path( + $existing_child_blocks, + 'section/header' + ); + + // Modify multiple properties + // Get the existing fields array, modify it, then reassign it + $fields = $target_block->fields; + $fields[] = array( + 'key' => 'new_field', + 'label' => 'New Field', + 'name' => 'new_field', + 'type' => 'text', + ); + $target_block->fields = $fields; + + $target_block->template = __DIR__ . '/templates/custom-header.php'; + + // Replace with modified version + return Helpers::replace_child_block_by_path( + $existing_child_blocks, + 'section/header', + $target_block + ); +} +``` + +## Error Handling + +The method throws `\InvalidArgumentException` in two scenarios: + +1. **Invalid path**: If the path is empty or contains no valid segments +2. **Block not found**: If any segment in the path cannot be matched to an existing child block + +```php +use Creode_Blocks\Helpers; + +try { + $block = Helpers::get_child_block_by_path( + $existing_child_blocks, + 'invalid/path/to/block' + ); +} catch ( \InvalidArgumentException $e ) { + // Handle the error - block not found at specified path + error_log( $e->getMessage() ); +} +``` + +## Use Cases + +This helper is particularly useful when: + +- **Extending parent block classes**: You want to add fields or modify properties of a child block defined in a parent class +- **Selective modifications**: You only need to modify specific child blocks in a complex hierarchy +- **Dynamic child block manipulation**: You need to programmatically access and modify nested child blocks +- **Pairing with replace_child_block_by_path**: You want to retrieve, modify, and replace a child block in one workflow + +## Notes + +- The path matching is case-sensitive and must exactly match the child block names +- Only the first matching child block at each level is returned (if multiple child blocks share the same name, only the first one is found) +- The method validates the entire path before returning, ensuring the block exists at the specified location +- Always use this method in conjunction with `replace_child_block_by_path()` when you need to persist your modifications + diff --git a/docs/helpers/replace_child_block_by_path.md b/docs/helpers/replace_child_block_by_path.md index add9a71..1ff1653 100644 --- a/docs/helpers/replace_child_block_by_path.md +++ b/docs/helpers/replace_child_block_by_path.md @@ -38,84 +38,78 @@ The path parameter uses a forward-slash (`/`) separated format to navigate throu - Within that, find a child block named "cell" - Replace that "cell" block with the new one +## Pairing with get_child_block_by_path + +This function pairs exceptionally well with [`get_child_block_by_path()`](/helpers/get_child_block_by_path). The recommended workflow is: + +1. Use `get_child_block_by_path()` to retrieve the existing child block you want to modify +2. Make your modifications to the retrieved block (e.g., add fields, change configuration) +3. Use `replace_child_block_by_path()` to replace the original block with your modified version + +This approach is much easier than manually recreating the entire `Child_Block` instance, especially when you only want to add a few fields or make small modifications to an existing child block. + ## Examples -### Basic Usage - Overriding a Child Block +### Basic Usage - Extending a Child Block -This example shows how to override a child block defined in a parent class by replacing it with a new version that includes additional fields: +This example shows the recommended approach for extending a child block defined in a parent class. First, retrieve the existing block using [`get_child_block_by_path()`](/helpers/get_child_block_by_path), modify it, and then replace it. This approach preserves all existing fields and configuration: ```php +use Creode_Blocks\Table_Block as Base_Table_Block; use Creode_Blocks\Helpers; -use Creode_Blocks\Child_Block; -/** - * {@inheritdoc} - */ -protected function child_blocks(): array { - $existing_child_blocks = parent::child_blocks(); - - // Replace the cell-content child block with an enhanced version - $replaced_child_blocks = Helpers::replace_child_block_by_path( - $existing_child_blocks, - 'table/row/cell/cell-content', - new Child_Block( - 'cell-content', - 'Table Cell Content', - array( - array( - 'key' => 'field_table_cell_style', - 'label' => 'Style', - 'name' => 'style', - 'type' => 'select', - 'choices' => array( - '' => 'None', - '1' => 'No Padding', - ), - ), - array( - 'key' => 'field_table_cell_curved_corners', - 'label' => 'Curved corners', - 'name' => 'curved_corners', - 'type' => 'checkbox', - 'choices' => array( - 'top-left' => 'Top Left', - 'top-right' => 'Top Right', - 'bottom-right' => 'Bottom Right', - 'bottom-left' => 'Bottom Left', - ), - ), - $this->get_icon_field_schema( 'field_table_cell_icon', true ), - array( - 'key' => 'field_table_cell_icon_color', - 'label' => 'Icon Color', - 'name' => 'icon_color', - 'type' => 'radio', - 'choices' => $this->get_color_choices(), - 'conditional_logic' => array( - array( - array( - 'field' => 'field_table_cell_icon', - 'operator' => '!=', - 'value' => '', - ), - ), - ), - ), - ), - CREODE_BLOCKS_PLUGIN_FOLDER . '/blocks/table/templates/cell-content.php', - array(), - 'text', - array( - 'mode' => false, - 'color' => array( - 'text' => true, - 'background' => true, - ), +class Table extends Base_Table_Block { + + /** + * {@inheritdoc} + */ + protected function name(): string { + return 'table'; + } + + /** + * {@inheritdoc} + */ + protected function label(): string { + return 'Comparison Table'; + } + + /** + * {@inheritdoc} + */ + protected function child_blocks(): array { + $existing_child_blocks = parent::child_blocks(); + + // Get the existing cell-content child block + $cell_content_child_block = Helpers::get_child_block_by_path( + $existing_child_blocks, + 'table/row/cell/cell-content' + ); + + // Get the existing fields array and add the new field to it + $fields = $cell_content_child_block->fields; + $fields[] = array( + 'key' => 'creode_field', + 'label' => 'Creode Field', + 'name' => 'creode_field', + 'type' => 'select', + 'choices' => array( + '' => 'hooray', + '1' => 'woohoo', + '2' => 'I did it', ), - ), - ); - - return $replaced_child_blocks; + ); + $cell_content_child_block->fields = $fields; + + // Replace the original block with the modified version + $replaced_child_blocks = Helpers::replace_child_block_by_path( + $existing_child_blocks, + 'table/row/cell/cell-content', + $cell_content_child_block + ); + + return $replaced_child_blocks; + } } ``` @@ -155,6 +149,7 @@ This helper is particularly useful when: - **Extending parent block classes**: You want to add fields to a child block defined in a parent class without modifying the parent - **Customizing child blocks**: You need to modify the template, fields, or configuration of a nested child block - **Selective overrides**: You only want to change specific child blocks in a complex hierarchy rather than redefining everything +- **Working with existing blocks**: When combined with [`get_child_block_by_path()`](/helpers/get_child_block_by_path), you can easily retrieve, modify, and replace existing child blocks without recreating them from scratch ## Notes