From cf418c000fc50876088d7ea1fa46398471a7cacd Mon Sep 17 00:00:00 2001
From: David Herrera
Date: Wed, 26 Feb 2025 11:19:57 -0500
Subject: [PATCH 1/4] Add `Matched_Blocks` class
---
src/blocks/class-matched-blocks.php | 39 +++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 src/blocks/class-matched-blocks.php
diff --git a/src/blocks/class-matched-blocks.php b/src/blocks/class-matched-blocks.php
new file mode 100644
index 0000000..51235c8
--- /dev/null
+++ b/src/blocks/class-matched-blocks.php
@@ -0,0 +1,39 @@
+ $args Args for {@see match_blocks()}.
+ * @param Serialized_Blocks $origin Blocks to search.
+ */
+ public function __construct(
+ private readonly array $args,
+ private readonly Serialized_Blocks $origin,
+ ) {}
+
+ /**
+ * Serialized block content.
+ *
+ * @return string
+ */
+ public function serialized_blocks(): string {
+ $matched = match_blocks( $this->origin->serialized_blocks(), $this->args );
+
+ return \is_array( $matched ) ? serialize_blocks( $matched ) : ''; // @phpstan-ignore-line argument.type
+ }
+}
From 1c106ed91f2e894cdb708aac7846db1e3cf99045 Mon Sep 17 00:00:00 2001
From: David Herrera
Date: Wed, 26 Feb 2025 11:33:45 -0500
Subject: [PATCH 2/4] Add experimental support for matching blocks with XPath
---
CHANGELOG.md | 11 +-
README.md | 201 ++++++++++++++++++
composer.json | 9 +-
src/internals/class-block-normalizer.php | 135 ++++++++++++
src/internals/class-blocks-normalizer.php | 113 ++++++++++
src/match-blocks.php | 62 ++++--
tests/Unit/Blocks/MatchedBlocksTest.php | 42 ++++
.../Unit/MatchBlocksExperimentalXPathTest.php | 97 +++++++++
8 files changed, 652 insertions(+), 18 deletions(-)
create mode 100644 src/internals/class-block-normalizer.php
create mode 100644 src/internals/class-blocks-normalizer.php
create mode 100644 tests/Unit/Blocks/MatchedBlocksTest.php
create mode 100644 tests/Unit/MatchBlocksExperimentalXPathTest.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1baae85..81db6fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ This library adheres to [Semantic Versioning](https://semver.org/) and [Keep a C
Nothing yet.
+## 4.1.0
+
+### Added
+
+- `__experimental_xpath` parameter for matching blocks by XPath queries.
+- `Matched_Blocks` implementation of the `Alley\WP\Types\Blocks` interface from the [Type Extensions](https://github.com/alleyinteractive/wp-type-extensions/) library.
+
## 4.0.0
### Changed
@@ -16,13 +23,13 @@ Nothing yet.
### Changed
-* Reduce uses of validators within validators.
+- Reduce uses of validators within validators.
## 3.0.0
### Changed
-* "Classic" blocks, which have inner HTML but no block name, are no longer considered empty.
+- "Classic" blocks, which have inner HTML but no block name, are no longer considered empty.
## 2.0.1
diff --git a/README.md b/README.md
index 20efe15..b4d5ca6 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Blocks can be matched by:
* Whether the block represents only space (`skip_empty_blocks`)
* Whether the block has inner blocks (`has_innerblocks`)
* Custom validation classes (`is_valid`)
+* Xpath queries (`__experimental_xpath`) ([huh?](#matching-blocks-with-xpath))
Passing matching parameters is optional; all non-empty blocks match by default.
@@ -658,6 +659,206 @@ $valid = new \Alley\WP\Validator\Nonempty_Block();
$valid->isValid( $blocks[0] ); // false
```
+## Matching blocks with XPath
+
+`match_blocks()` has **experimental** support for matching blocks with XPath queries. These are made possible by converting the source blocks to a custom XML structure.
+
+**This feature may be changed without backwards compatibility in future releases.**
+
+### Basic usage
+
+Find all paragraph blocks that are inner blocks of a cover block:
+
+```php
+ '//block[blockName="core/cover"]/innerBlocks/block[blockName="core/paragraph"]',
+ ],
+);
+```
+
+Find list blocks with zero or one list items:
+
+```php
+ '//block[blockName="core/list" and count(innerBlocks/block[blockName="core/list-item"]) <= 1]',
+ ],
+);
+```
+
+Find the second paragraph block:
+
+```php
+ '//block[blockName="core/paragraph"][2]',
+ ],
+);
+```
+
+Find full-width images:
+
+```php
+ '//block[blockName="core/image"][attrs/sizeSlug="full"]',
+ ],
+);
+```
+
+The XML document currently has the following structure:
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+For example, this block HTML:
+
+```html
+
+
The Common category includes the following blocks: Paragraph, image, headings, list, gallery, quote, audio, cover, video.
+
+
+
+
This italic paragraph is right aligned.
+
+
+
+
+
+
+
+
+
+
+
Cover block with background image
+
+
+
+
+```
+
+will be converted to this XML:
+
+```xml
+
+
+ core/paragraph
+
+
+ The Common category includes the following blocks: Paragraph, image, headings, list, gallery, quote, audio, cover, video.
+]]>
+
+
+
+ core/paragraph
+
+ right
+
+
+ This italic paragraph is right aligned.
+]]>
+
+
+
+ core/image
+
+ 968
+ full
+ is-style-circle-mask
+
+
+
+]]>
+
+
+
+ core/cover
+
+ https://example.com/wp-content/uploads/2008/06/dsc04563-12.jpg
+ 759
+ 274
+
+
+
+ core/paragraph
+
+ center
+ Write title…
+ large
+
+
+ Cover block with background image
+ ]]>
+
+
+
+
+
+
+
+]]>
+
+
+```
+
+### Limitations
+
+Although it's possible to use XPath queries in conjunction with other `match_blocks()` arguments, the results with some arguments might be unexpected.
+
+Typically, `match_blocks()` returns the blocks that match all the arguments. But when the `__experimental_xpath` argument is used, the set of source blocks will be first reduced to the blocks that match the XPath query, and then the remaining arguments will be applied.
+
+For example, compare these sets of arguments:
+
+```php
+ 'core/paragraph',
+ 'position' => 3,
+ ],
+);
+
+$blocks = \Alley\WP\match_blocks(
+ $post,
+ [
+ '__experimental_xpath' => '//block[blockName="core/paragraph"]',
+ 'position' => 3,
+ ],
+);
+```
+
+In the top example, the third block in the set of blocks will be returned, but only if it's a paragraph.
+
+In the bottom example, the XPath query will match all paragraphs in the document, regardless of their depth, and then the third paragraph out of that set will be returned.
+
## About
### License
diff --git a/composer.json b/composer.json
index e3ef16f..ba383e1 100644
--- a/composer.json
+++ b/composer.json
@@ -11,8 +11,11 @@
],
"require": {
"php": "^8.2",
- "alleyinteractive/composer-wordpress-autoloader": "^1.0.0",
- "alleyinteractive/laminas-validator-extensions": "^2.0.0"
+ "ext-simplexml": "*",
+ "alleyinteractive/composer-wordpress-autoloader": "^1.0",
+ "alleyinteractive/laminas-validator-extensions": "^2.0",
+ "alleyinteractive/wp-type-extensions": "dev-fix/match-blocks",
+ "symfony/serializer": "^7.2"
},
"require-dev": {
"alleyinteractive/alley-coding-standards": "^2.0",
@@ -69,4 +72,4 @@
],
"tidy": "[ $COMPOSER_DEV_MODE -eq 0 ] || composer normalize"
}
-}
\ No newline at end of file
+}
diff --git a/src/internals/class-block-normalizer.php b/src/internals/class-block-normalizer.php
new file mode 100644
index 0000000..bf742c5
--- /dev/null
+++ b/src/internals/class-block-normalizer.php
@@ -0,0 +1,135 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @package wp-match-blocks
+ */
+
+namespace Alley\WP\Internals;
+
+use Alley\WP\Blocks\Blocks;
+use Alley\WP\Blocks\Parsed_Block;
+use Alley\WP\Types\Single_Block;
+use Symfony\Component\Serializer\Exception\BadMethodCallException;
+use Symfony\Component\Serializer\Exception\CircularReferenceException;
+use Symfony\Component\Serializer\Exception\ExceptionInterface;
+use Symfony\Component\Serializer\Exception\ExtraAttributesException;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\LogicException;
+use Symfony\Component\Serializer\Exception\RuntimeException;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Normalizes an instance of the Single_Block interface.
+ */
+final class Block_Normalizer implements NormalizerInterface, DenormalizerInterface {
+ /**
+ * Normalizes an object into a set of arrays/scalars.
+ *
+ * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
+ * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
+ * reference handler can fix it.
+ * @throws LogicException Occurs when the normalizer is not called in an expected context.
+ * @throws ExceptionInterface Occurs for all the other cases of errors.
+ *
+ * @param mixed $object Object to normalize.
+ * @param string|null $format Format the normalization result will be encoded as.
+ * @param array $context Context options for the normalizer.
+ * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is
+ * encoded as an object not an array.
+ */
+ public function normalize(
+ mixed $object,
+ ?string $format = null,
+ array $context = [],
+ ): array|string|int|float|bool|\ArrayObject|null {
+ $pb = $object->parsed_block();
+
+ return [
+ 'blockName' => $object->block_name(),
+ 'attrs' => $pb['attrs'],
+ 'innerBlocks' => Blocks::from_parsed_blocks( $pb['innerBlocks'] ),
+ 'innerHTML' => $pb['innerHTML'],
+ 'origin' => $object->serialized_blocks(),
+ ];
+ }
+
+ /**
+ * Checks whether the given class is supported for normalization by this normalizer.
+ *
+ * @param mixed $data Data to normalize.
+ * @param string|null $format The format being (de-)serialized from or into.
+ * @param array $context Context options for the normalizer.
+ */
+ public function supportsNormalization( mixed $data, ?string $format = null, array $context = [] ): bool {
+ return $data instanceof Single_Block && 'xml' === $format;
+ }
+
+ /**
+ * Returns the types potentially supported by this normalizer.
+ *
+ * For each supported formats (if applicable), the supported types should be
+ * returned as keys, and each type should be mapped to a boolean indicating
+ * if the result of supportsNormalization() can be cached or not
+ * (a result cannot be cached when it depends on the context or on the data.)
+ * A null value means that the normalizer does not support the corresponding
+ * type.
+ *
+ * Use type "object" to match any classes or interfaces,
+ * and type "*" to match any types.
+ *
+ * @param string|null $format The format being (de-)serialized from or into.
+ * @return array
+ */
+ public function getSupportedTypes( ?string $format ): array {
+ return [ '*' => true ];
+ }
+
+ /**
+ * Denormalizes data back into an object of the given class.
+ *
+ * @throws BadMethodCallException Occurs when the normalizer is not called in an expected context.
+ * @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported.
+ * @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data.
+ * @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data.
+ * @throws LogicException Occurs when the normalizer is not supposed to denormalize.
+ * @throws RuntimeException Occurs if the class cannot be instantiated.
+ * @throws ExceptionInterface Occurs for all the other cases of errors.
+ *
+ * @param mixed $data Data to restore.
+ * @param string $type The expected class to instantiate.
+ * @param string|null $format Format the given data was extracted from.
+ * @param array $context Options available to the denormalizer.
+ * @return Single_Block
+ */
+ public function denormalize( mixed $data, string $type, ?string $format = null, array $context = [] ): mixed {
+ $origin = parse_blocks( $data['origin'] );
+
+ return new Parsed_Block( $origin[0] );
+ }
+
+ /**
+ * Checks whether the given class is supported for denormalization by this normalizer.
+ *
+ * @param mixed $data Data to denormalize from.
+ * @param string $type The class to which the data should be denormalized.
+ * @param string|null $format The format being deserialized from.
+ * @param array $context Context options for the normalizer.
+ * @return bool
+ */
+ public function supportsDenormalization(
+ mixed $data,
+ string $type,
+ ?string $format = null,
+ array $context = []
+ ): bool {
+ return Single_Block::class === $type && 'xml' === $format;
+ }
+}
diff --git a/src/internals/class-blocks-normalizer.php b/src/internals/class-blocks-normalizer.php
new file mode 100644
index 0000000..974ee34
--- /dev/null
+++ b/src/internals/class-blocks-normalizer.php
@@ -0,0 +1,113 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @package wp-match-blocks
+ */
+
+namespace Alley\WP\Internals;
+
+use Alley\WP\Blocks\Parsed_Block;
+use Alley\WP\Types\Serialized_Blocks;
+use Alley\WP\Validator\Nonempty_Block;
+use Symfony\Component\Serializer\Exception\CircularReferenceException;
+use Symfony\Component\Serializer\Exception\ExceptionInterface;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\LogicException;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Normalizes an instance of the Serialized_Blocks interface.
+ */
+final class Blocks_Normalizer implements NormalizerInterface {
+ /**
+ * Validates that only non-empty blocks are serialized.
+ *
+ * @var Nonempty_Block
+ */
+ private Nonempty_Block $nonempty_block;
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ $this->nonempty_block = new Nonempty_Block();
+ }
+
+ /**
+ * Normalizes an object into a set of arrays/scalars.
+ *
+ * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
+ * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
+ * reference handler can fix it.
+ * @throws LogicException Occurs when the normalizer is not called in an expected context.
+ * @throws ExceptionInterface Occurs for all the other cases of errors.
+ *
+ * @param mixed $object Object to normalize.
+ * @param string|null $format Format the normalization result will be encoded as.
+ * @param array $context Context options for the normalizer.
+ * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is
+ * encoded as an object not an array.
+ */
+ public function normalize(
+ mixed $object,
+ ?string $format = null,
+ array $context = [],
+ ): array|string|int|float|bool|\ArrayObject|null {
+ $parsed = parse_blocks( $object->serialized_blocks() );
+
+ if ( ! is_array( $parsed ) ) {
+ return [];
+ }
+
+ $parsed = array_filter( $parsed, [ $this->nonempty_block, 'isValid' ] );
+
+ if ( count( $parsed ) === 0 ) {
+ return [];
+ }
+
+ return [
+ 'block' => array_map(
+ fn ( $block ) => new Parsed_Block( $block ),
+ $parsed
+ ),
+ ];
+ }
+
+ /**
+ * Checks whether the given class is supported for normalization by this normalizer.
+ *
+ * @param mixed $data Data to normalize.
+ * @param string|null $format The format being (de-)serialized from or into.
+ * @param array $context Context options for the normalizer.
+ * @return bool
+ */
+ public function supportsNormalization( mixed $data, ?string $format = null, array $context = [] ): bool {
+ return $data instanceof Serialized_Blocks && 'xml' === $format;
+ }
+
+ /**
+ * Returns the types potentially supported by this normalizer.
+ *
+ * For each supported formats (if applicable), the supported types should be
+ * returned as keys, and each type should be mapped to a boolean indicating
+ * if the result of supportsNormalization() can be cached or not
+ * (a result cannot be cached when it depends on the context or on the data.)
+ * A null value means that the normalizer does not support the corresponding
+ * type.
+ *
+ * Use type "object" to match any classes or interfaces,
+ * and type "*" to match any types.
+ *
+ * @param string|null $format The format being (de-)serialized from or into.
+ * @return array
+ */
+ public function getSupportedTypes( ?string $format ): array {
+ return [ '*' => true ];
+ }
+}
diff --git a/src/match-blocks.php b/src/match-blocks.php
index ed57d92..56f5416 100644
--- a/src/match-blocks.php
+++ b/src/match-blocks.php
@@ -13,12 +13,19 @@
namespace Alley\WP;
use Alley\Validator\FastFailValidatorChain;
+use Alley\WP\Blocks\Blocks;
+use Alley\WP\Internals\Block_Normalizer;
+use Alley\WP\Internals\Blocks_Normalizer;
+use Alley\WP\Types\Single_Block;
use Alley\WP\Validator\Block_InnerHTML;
use Alley\WP\Validator\Block_Name;
use Alley\WP\Validator\Block_Offset;
use Alley\WP\Validator\Block_InnerBlocks_Count;
use Alley\WP\Validator\Nonempty_Block;
use Laminas\Validator\ValidatorInterface;
+use SimpleXMLElement;
+use Symfony\Component\Serializer\Encoder\XmlEncoder;
+use Symfony\Component\Serializer\Serializer;
use WP_Block_Parser_Block;
use WP_Post;
@@ -98,18 +105,19 @@ function match_blocks( $source, $args = [] ) {
$args = wp_parse_args(
$args,
[
- 'attrs' => [],
- 'count' => false,
- 'flatten' => false,
- 'has_innerblocks' => null,
- 'is_valid' => null,
- 'limit' => -1,
- 'name' => '',
- 'nth_of_type' => null,
- 'position' => null,
- 'skip_empty_blocks' => true,
- 'with_attrs' => [],
- 'with_innerhtml' => null,
+ 'attrs' => [],
+ 'count' => false,
+ 'flatten' => false,
+ 'has_innerblocks' => null,
+ 'is_valid' => null,
+ 'limit' => - 1,
+ 'name' => '',
+ 'nth_of_type' => null,
+ 'position' => null,
+ 'skip_empty_blocks' => true,
+ 'with_attrs' => [],
+ 'with_innerhtml' => null,
+ '__experimental_xpath' => null,
],
);
@@ -150,6 +158,34 @@ function match_blocks( $source, $args = [] ) {
$blocks = Internals\flatten_blocks( $blocks );
}
+ if ( \is_string( $args['__experimental_xpath'] ) && strlen( $args['__experimental_xpath'] ) > 0 ) {
+ $serializer = new Serializer(
+ normalizers: [
+ new Block_Normalizer(),
+ new Blocks_Normalizer(),
+ ],
+ encoders: [
+ new XmlEncoder(
+ [
+ 'cdata_wrapping' => true,
+ 'xml_root_node_name' => 'blocks',
+ ],
+ ),
+ ],
+ );
+
+ $xml_content = $serializer->serialize( Blocks::from_parsed_blocks( $blocks ), 'xml', );
+ $xml_element = new SimpleXMLElement( $xml_content );
+ $xpath_matches = $xml_element->xpath( $args['__experimental_xpath'] );
+
+ if ( is_array( $xpath_matches ) ) {
+ $blocks = array_map(
+ fn ( $match ) => $serializer->deserialize( $match->asXML(), Single_Block::class, 'xml' )->parsed_block(),
+ $xpath_matches,
+ );
+ }
+ }
+
try {
$validator = new FastFailValidatorChain( [] );
@@ -236,7 +272,7 @@ function match_blocks( $source, $args = [] ) {
// These are 1-based indices. Map them to 0-based.
$nth_of_type = Internals\parse_nth_of_type( $args['nth_of_type'], \count( $matches ) );
$nth_indices = array_map(
- fn( $nth ) => (int) $nth - 1,
+ fn ( $nth ) => (int) $nth - 1,
$nth_of_type
);
diff --git a/tests/Unit/Blocks/MatchedBlocksTest.php b/tests/Unit/Blocks/MatchedBlocksTest.php
new file mode 100644
index 0000000..f54af5f
--- /dev/null
+++ b/tests/Unit/Blocks/MatchedBlocksTest.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @package wp-match-blocks
+ */
+
+namespace Alley\WP\Tests\Unit\Blocks;
+
+use Alley\WP\Blocks\Block_Content;
+use Alley\WP\Blocks\Matched_Blocks;
+use Mantle\Testkit\Test_Case;
+
+/**
+ * Tests the Matched_Blocks class.
+ */
+class MatchedBlocksTest extends Test_Case {
+ /**
+ * Match blocks in the origin instance.
+ */
+ public function test_serialized_blocks() {
+ $matched = new Matched_Blocks(
+ [
+ 'name' => 'alley/bar',
+ ],
+ new Block_Content(
+ <<
+
+
+HTML,
+ ),
+ );
+
+ $this->assertSame( '', $matched->serialized_blocks() );
+ }
+}
diff --git a/tests/Unit/MatchBlocksExperimentalXPathTest.php b/tests/Unit/MatchBlocksExperimentalXPathTest.php
new file mode 100644
index 0000000..ebd7e74
--- /dev/null
+++ b/tests/Unit/MatchBlocksExperimentalXPathTest.php
@@ -0,0 +1,97 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ *
+ * @package wp-match-blocks
+ */
+
+namespace Alley\WP\Tests\Unit;
+
+use Mantle\Testkit\Test_Case;
+
+use function Alley\WP\match_blocks;
+
+/**
+ * Tests the `__experimental_xpath` parameter to `match_blocks()`.
+ */
+final class MatchBlocksExperimentalXPathTest extends Test_Case {
+ /**
+ * Example block HTML.
+ *
+ * @var string
+ */
+ private const BLOCKS = <<
+
The Common category includes the following blocks: Paragraph, image, headings, list, gallery, quote, audio, cover, video.
+
+
+
+
This italic paragraph is right aligned.
+
+
+
+
+
+
+
+
+
+
+
Cover block with background image
+
+
+
+
+HTML;
+
+ /**
+ * Blocks matching the given XPath query should be matched.
+ */
+ public function test_xpath_query() {
+ $matched = match_blocks(
+ self::BLOCKS,
+ [
+ '__experimental_xpath' => '//block[blockName="core/paragraph"]',
+ ],
+ );
+
+ $this->assertCount( 3, $matched );
+ }
+
+ /**
+ * Blocks matching the given XPath query should be matched.
+ */
+ public function test_xpath_root_query() {
+ $matched = match_blocks(
+ self::BLOCKS,
+ [
+ '__experimental_xpath' => '/blocks/block[blockName="core/paragraph"]',
+ ],
+ );
+
+ $this->assertCount( 2, $matched );
+ }
+
+ /**
+ * Blocks matching the given XPath query should be matched.
+ */
+ public function test_xpath_innerblocks_query() {
+ $matched = match_blocks(
+ self::BLOCKS,
+ [
+ '__experimental_xpath' => '//block[blockName="core/cover"]/innerBlocks/block[blockName="core/paragraph"]',
+ ],
+ );
+
+ $this->assertCount( 1, $matched );
+ $this->assertSame(
+ '
Cover block with background image
',
+ trim( $matched[0]['innerHTML'] ),
+ );
+ }
+}
From 0698df058172c13ff26dd5969b677803da735b15 Mon Sep 17 00:00:00 2001
From: David Herrera
Date: Sat, 1 Mar 2025 01:43:05 -0500
Subject: [PATCH 3/4] Make types stricter
---
src/internals/class-block-normalizer.php | 29 ++++++++++++++++-------
src/internals/class-blocks-normalizer.php | 20 +++++++++-------
2 files changed, 33 insertions(+), 16 deletions(-)
diff --git a/src/internals/class-block-normalizer.php b/src/internals/class-block-normalizer.php
index bf742c5..3277308 100644
--- a/src/internals/class-block-normalizer.php
+++ b/src/internals/class-block-normalizer.php
@@ -14,6 +14,7 @@
use Alley\WP\Blocks\Blocks;
use Alley\WP\Blocks\Parsed_Block;
+use Alley\WP\Types\Serialized_Blocks;
use Alley\WP\Types\Single_Block;
use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
@@ -39,17 +40,19 @@ final class Block_Normalizer implements NormalizerInterface, DenormalizerInterfa
* @throws LogicException Occurs when the normalizer is not called in an expected context.
* @throws ExceptionInterface Occurs for all the other cases of errors.
*
+ * @phpstan-param array $context
+ * @phpstan-return array{blockName: ?string, attrs: array, innerBlocks: Serialized_Blocks, innerHTML: string, origin: string}
+ *
* @param mixed $object Object to normalize.
* @param string|null $format Format the normalization result will be encoded as.
* @param array $context Context options for the normalizer.
- * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is
- * encoded as an object not an array.
+ * @return array
*/
- public function normalize(
- mixed $object,
- ?string $format = null,
- array $context = [],
- ): array|string|int|float|bool|\ArrayObject|null {
+ public function normalize( mixed $object, ?string $format = null, array $context = [] ): array {
+ if ( ! $object instanceof Single_Block ) {
+ throw new InvalidArgumentException( 'The object must implement the Single_Block interface.' );
+ }
+
$pb = $object->parsed_block();
return [
@@ -64,6 +67,8 @@ public function normalize(
/**
* Checks whether the given class is supported for normalization by this normalizer.
*
+ * @phpstan-param array $context
+ *
* @param mixed $data Data to normalize.
* @param string|null $format The format being (de-)serialized from or into.
* @param array $context Context options for the normalizer.
@@ -86,7 +91,7 @@ public function supportsNormalization( mixed $data, ?string $format = null, arra
* and type "*" to match any types.
*
* @param string|null $format The format being (de-)serialized from or into.
- * @return array
+ * @return bool[]
*/
public function getSupportedTypes( ?string $format ): array {
return [ '*' => true ];
@@ -103,6 +108,8 @@ public function getSupportedTypes( ?string $format ): array {
* @throws RuntimeException Occurs if the class cannot be instantiated.
* @throws ExceptionInterface Occurs for all the other cases of errors.
*
+ * @phpstan-param array $context
+ *
* @param mixed $data Data to restore.
* @param string $type The expected class to instantiate.
* @param string|null $format Format the given data was extracted from.
@@ -110,6 +117,10 @@ public function getSupportedTypes( ?string $format ): array {
* @return Single_Block
*/
public function denormalize( mixed $data, string $type, ?string $format = null, array $context = [] ): mixed {
+ if ( ! is_array( $data ) || ! isset( $data['origin'] ) || ! is_string( $data['origin'] ) ) {
+ throw new InvalidArgumentException( 'The origin key must be set and be a string.' );
+ }
+
$origin = parse_blocks( $data['origin'] );
return new Parsed_Block( $origin[0] );
@@ -118,6 +129,8 @@ public function denormalize( mixed $data, string $type, ?string $format = null,
/**
* Checks whether the given class is supported for denormalization by this normalizer.
*
+ * @phpstan-param array $context
+ *
* @param mixed $data Data to denormalize from.
* @param string $type The class to which the data should be denormalized.
* @param string|null $format The format being deserialized from.
diff --git a/src/internals/class-blocks-normalizer.php b/src/internals/class-blocks-normalizer.php
index 974ee34..1c463f0 100644
--- a/src/internals/class-blocks-normalizer.php
+++ b/src/internals/class-blocks-normalizer.php
@@ -48,17 +48,19 @@ public function __construct() {
* @throws LogicException Occurs when the normalizer is not called in an expected context.
* @throws ExceptionInterface Occurs for all the other cases of errors.
*
+ * @phpstan-param array $context
+ * @phpstan-return array{block?: Parsed_Block[]}
+ *
* @param mixed $object Object to normalize.
* @param string|null $format Format the normalization result will be encoded as.
* @param array $context Context options for the normalizer.
- * @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is
- * encoded as an object not an array.
+ * @return array
*/
- public function normalize(
- mixed $object,
- ?string $format = null,
- array $context = [],
- ): array|string|int|float|bool|\ArrayObject|null {
+ public function normalize( mixed $object, ?string $format = null, array $context = [] ): array {
+ if ( ! $object instanceof Serialized_Blocks ) {
+ throw new InvalidArgumentException( 'The object must be an instance of Serialized_Blocks' );
+ }
+
$parsed = parse_blocks( $object->serialized_blocks() );
if ( ! is_array( $parsed ) ) {
@@ -82,6 +84,8 @@ public function normalize(
/**
* Checks whether the given class is supported for normalization by this normalizer.
*
+ * @phpstan-param array $context
+ *
* @param mixed $data Data to normalize.
* @param string|null $format The format being (de-)serialized from or into.
* @param array $context Context options for the normalizer.
@@ -105,7 +109,7 @@ public function supportsNormalization( mixed $data, ?string $format = null, arra
* and type "*" to match any types.
*
* @param string|null $format The format being (de-)serialized from or into.
- * @return array
+ * @return bool[]
*/
public function getSupportedTypes( ?string $format ): array {
return [ '*' => true ];
From 947b5b7cad6d7075219da4239e440992d1ebb72c Mon Sep 17 00:00:00 2001
From: David Herrera
Date: Sat, 1 Mar 2025 01:44:51 -0500
Subject: [PATCH 4/4] Fix docs
---
src/internals/class-block-normalizer.php | 19 +------------------
src/internals/class-blocks-normalizer.php | 9 +--------
2 files changed, 2 insertions(+), 26 deletions(-)
diff --git a/src/internals/class-block-normalizer.php b/src/internals/class-block-normalizer.php
index 3277308..c9ef8c9 100644
--- a/src/internals/class-block-normalizer.php
+++ b/src/internals/class-block-normalizer.php
@@ -16,14 +16,7 @@
use Alley\WP\Blocks\Parsed_Block;
use Alley\WP\Types\Serialized_Blocks;
use Alley\WP\Types\Single_Block;
-use Symfony\Component\Serializer\Exception\BadMethodCallException;
-use Symfony\Component\Serializer\Exception\CircularReferenceException;
-use Symfony\Component\Serializer\Exception\ExceptionInterface;
-use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
-use Symfony\Component\Serializer\Exception\LogicException;
-use Symfony\Component\Serializer\Exception\RuntimeException;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -34,11 +27,7 @@ final class Block_Normalizer implements NormalizerInterface, DenormalizerInterfa
/**
* Normalizes an object into a set of arrays/scalars.
*
- * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
- * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
- * reference handler can fix it.
- * @throws LogicException Occurs when the normalizer is not called in an expected context.
- * @throws ExceptionInterface Occurs for all the other cases of errors.
+ * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
*
* @phpstan-param array $context
* @phpstan-return array{blockName: ?string, attrs: array, innerBlocks: Serialized_Blocks, innerHTML: string, origin: string}
@@ -100,13 +89,7 @@ public function getSupportedTypes( ?string $format ): array {
/**
* Denormalizes data back into an object of the given class.
*
- * @throws BadMethodCallException Occurs when the normalizer is not called in an expected context.
* @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported.
- * @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data.
- * @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data.
- * @throws LogicException Occurs when the normalizer is not supposed to denormalize.
- * @throws RuntimeException Occurs if the class cannot be instantiated.
- * @throws ExceptionInterface Occurs for all the other cases of errors.
*
* @phpstan-param array $context
*
diff --git a/src/internals/class-blocks-normalizer.php b/src/internals/class-blocks-normalizer.php
index 1c463f0..a1ac37d 100644
--- a/src/internals/class-blocks-normalizer.php
+++ b/src/internals/class-blocks-normalizer.php
@@ -15,10 +15,7 @@
use Alley\WP\Blocks\Parsed_Block;
use Alley\WP\Types\Serialized_Blocks;
use Alley\WP\Validator\Nonempty_Block;
-use Symfony\Component\Serializer\Exception\CircularReferenceException;
-use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
-use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
@@ -42,11 +39,7 @@ public function __construct() {
/**
* Normalizes an object into a set of arrays/scalars.
*
- * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
- * @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
- * reference handler can fix it.
- * @throws LogicException Occurs when the normalizer is not called in an expected context.
- * @throws ExceptionInterface Occurs for all the other cases of errors.
+ * @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer.
*
* @phpstan-param array $context
* @phpstan-return array{block?: Parsed_Block[]}