From 0bcac0ad57fc1e27ae52cab28c07e928d8af67ad Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Thu, 1 Jan 2026 22:55:49 +0100 Subject: [PATCH 1/2] Add extractWhere() method --- CHANGELOG.md | 4 ++ README.md | 25 +++++++ src/Traits/QueryableBlock.php | 26 +++++++ tests/Feature/QueryBlockTest.php | 116 +++++++++++++++---------------- 4 files changed, 111 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d799089..56092bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.0.3 - WIP +- Adding the `extractWhere()` method that allows you to recursively query data elements and extract all elements that match a given property/value pair. +- Fix `offsetAccess.invalidOffset` phpstan warning + ## 1.0.2 - 2025-10-05 - Adding `getIntStrict()` method for returning strict int. - Adding `getStringStrict()` method for returning strict string. diff --git a/README.md b/README.md index 5d150e7..ca415ae 100644 --- a/README.md +++ b/README.md @@ -679,6 +679,31 @@ The `in` operator filters elements by matching a field's value against an array The `has` operator filters elements by checking if a specific value exists within a field (usually an array or a collection). If the value exists, the element is included in the result. Non-existent values return no matches. +### The `extractWhere()` method +The `extractWhere()` method allows you to recursively query data elements and extract all elements that match a given property/value pair. + +It is especially useful when working with deeply nested data structures (for example JSON content trees), where matching items may appear at any depth. + +The implementation: +- Recursively scans the entire Block +- Finds all elements that: + - Contain the given $property + - Have a value strictly equal (===) to $value +- Returns a new Block instance containing only the matched items +- The original datablock is not modified + +```php +$jsonString = file_get_contents('./tests/data/story.json'); + +$story = Block::fromJsonString($jsonString); + +// Extract all items where "fieldtype" === "asset" +$assets = $story->extractWhere('fieldtype', 'asset'); + +// Debug output +$assets->dump(); +``` + ### The `orderBy()` method You can order or sort data for a specific key. For example, if you want to retrieve the data at `story.content.body` key and sort them by `component` key: diff --git a/src/Traits/QueryableBlock.php b/src/Traits/QueryableBlock.php index a999f92..520b8c4 100644 --- a/src/Traits/QueryableBlock.php +++ b/src/Traits/QueryableBlock.php @@ -175,6 +175,32 @@ public function groupByFunction(callable $groupFunction): self return self::make($result); } + public function extractWhere(string $property, mixed $value): self + { + $results = []; + + $scan = function ($item) use (&$results, &$scan, $property, $value): void { + if (is_array($item)) { + // Match the property/value pair + if ( + array_key_exists($property, $item) + && $item[$property] === $value + ) { + $results[] = $item; + } + + // Scan deeper + foreach ($item as $subItem) { + $scan($subItem); + } + } + }; + + $scan($this->data); + + return self::make($results); + } + private static function castVariableForStrval( mixed $property, ): bool|float|int|string|null { diff --git a/tests/Feature/QueryBlockTest.php b/tests/Feature/QueryBlockTest.php index 7f16bf0..c63dc6e 100644 --- a/tests/Feature/QueryBlockTest.php +++ b/tests/Feature/QueryBlockTest.php @@ -3,84 +3,73 @@ use HiFolks\DataType\Block; use HiFolks\DataType\Enums\Operator; -test('Query Block', function (): void { +test("Query Block", function (): void { $jsonString = file_get_contents("./tests/data/story.json"); $composerContent = Block::fromJsonString($jsonString); - $banners = $composerContent->getBlock("story.content.body")->where( - "component", - "==", - "banner", - ); + $banners = $composerContent + ->getBlock("story.content.body") + ->where("component", "==", "banner"); expect($banners)->toHaveCount(2); expect($banners->get("0.headline"))->toBe("New banner"); - expect($banners->get("4.headline"))->toBe("Top Five Discoveries, Curiosity Rover at Mars"); + expect($banners->get("4.headline"))->toBe( + "Top Five Discoveries, Curiosity Rover at Mars", + ); $composerContent = Block::fromJsonString($jsonString); - $banners = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::NOT_EQUAL, - "banner", - false, - ); + $banners = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::NOT_EQUAL, "banner", false); expect($banners)->toHaveCount(8); expect($banners->get("0.component"))->toBe("hero-section"); expect($banners->get("4.component"))->toBe("grid-section"); - }); -test('Query and select Block', function (): void { +test("Query and select Block", function (): void { $jsonString = file_get_contents("./tests/data/story.json"); $composerContent = Block::fromJsonString($jsonString); - $banners = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::EQUAL, - "banner", - )->select("headline"); + $banners = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::EQUAL, "banner") + ->select("headline"); expect($banners)->toHaveCount(2); expect($banners->get("0"))->toHaveCount(1); expect($banners->get("0.headline"))->toBe("New banner"); expect($banners->get("1"))->toHaveCount(1); - expect($banners->get("1.headline"))->toBe("Top Five Discoveries, Curiosity Rover at Mars"); + expect($banners->get("1.headline"))->toBe( + "Top Five Discoveries, Curiosity Rover at Mars", + ); $composerContent = Block::fromJsonString($jsonString); - $banners = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::NOT_EQUAL, - "banner", - false, - ); + $banners = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::NOT_EQUAL, "banner", false); expect($banners)->toHaveCount(8); expect($banners->get("0.component"))->toBe("hero-section"); expect($banners->get("4.component"))->toBe("grid-section"); }); -test('Order Block', function (): void { +test("Order Block", function (): void { $jsonString = file_get_contents("./tests/data/story.json"); $composerContent = Block::fromJsonString($jsonString); - $bodyComponents = $composerContent->getBlock("story.content.body")->orderBy( - "component", - "asc", - ); + $bodyComponents = $composerContent + ->getBlock("story.content.body") + ->orderBy("component", "asc"); expect($bodyComponents)->toHaveCount(10); expect($bodyComponents->get("0.component"))->toBe("banner"); expect($bodyComponents->get("9.component"))->toBe("text-section"); - $bodyComponents = $composerContent->getBlock("story.content.body")->orderBy( - "component", - "desc", - ); + $bodyComponents = $composerContent + ->getBlock("story.content.body") + ->orderBy("component", "desc"); expect($bodyComponents)->toHaveCount(10); expect($bodyComponents->get("9.component"))->toBe("banner"); expect($bodyComponents->get("0.component"))->toBe("text-section"); - }); - -it('local dummyjson post', function (): void { - +it("local dummyjson post", function (): void { $response = Block::fromJsonFile("./tests/data/dummy-posts-30.json"); expect($response)->toBeInstanceOf(Block::class); expect($response)->toHaveCount(4); @@ -115,30 +104,37 @@ expect($posts->get("0.reactions.likes"))->toBe(192); }); -test('Query Block with has', function (): void { +test("Query Block with has", function (): void { $jsonString = file_get_contents("./tests/data/story.json"); $composerContent = Block::fromJsonString($jsonString); - $has = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::EQUAL, - "banner", - )->exists(); + $has = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::EQUAL, "banner") + ->exists(); expect($has)->toBeTrue(); - $has = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::NOT_EQUAL, - "banner", - )->exists(); + $has = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::NOT_EQUAL, "banner") + ->exists(); expect($has)->toBeTrue(); - $has = $composerContent->getBlock("story.content.body")->where( - "component", - Operator::EQUAL, - "bannerXXX", - )->exists(); + $has = $composerContent + ->getBlock("story.content.body") + ->where("component", Operator::EQUAL, "bannerXXX") + ->exists(); expect($has)->toBeFalse(); - $has = $composerContent->getBlock("story.content.body")->where( - "component", - "banner", - )->exists(); + $has = $composerContent + ->getBlock("story.content.body") + ->where("component", "banner") + ->exists(); expect($has)->toBeTrue(); }); + +test("Query Block extractWhere", function (): void { + $jsonString = file_get_contents("./tests/data/story.json"); + + $story = Block::fromJsonString($jsonString); + $assets = $story->extractWhere("fieldtype", "asset"); + $assets->dump(); + expect($assets)->toHaveCount(16); + expect($assets->get("3.filename"))->toStartWith("https://a.story"); +}); From e9dad23f8a0f3e9ad702e121b346d12c5316cea1 Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Fri, 2 Jan 2026 08:42:08 +0100 Subject: [PATCH 2/2] fix Possibly invalid array key type string|null. --- src/Block.php | 6 +++++- tests/Feature/QueryBlockTest.php | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Block.php b/src/Block.php index 11e1219..7100f7a 100644 --- a/src/Block.php +++ b/src/Block.php @@ -165,8 +165,12 @@ public function set(int|string $key, mixed $value, string $charNestedKey = "."): $array = &$array[$key]; } + $key = array_shift($keys); + + if (!is_null($key)) { + $array[$key] = $value; + } - $array[array_shift($keys)] = $value; return $this; } $this->data[$key] = $value; diff --git a/tests/Feature/QueryBlockTest.php b/tests/Feature/QueryBlockTest.php index c63dc6e..4e3f01a 100644 --- a/tests/Feature/QueryBlockTest.php +++ b/tests/Feature/QueryBlockTest.php @@ -134,7 +134,6 @@ $story = Block::fromJsonString($jsonString); $assets = $story->extractWhere("fieldtype", "asset"); - $assets->dump(); expect($assets)->toHaveCount(16); expect($assets->get("3.filename"))->toStartWith("https://a.story"); });