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.

+ + + +
Image Alignment 150x150
+ + + +
+
+ +

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 + + + Image Alignment 150x150 +]]> + + + + 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.

+ + + +
Image Alignment 150x150
+ + + +
+
+ +

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[]}