Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion src/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions src/Traits/QueryableBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
115 changes: 55 additions & 60 deletions tests/Feature/QueryBlockTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -115,30 +104,36 @@
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");
expect($assets)->toHaveCount(16);
expect($assets->get("3.filename"))->toStartWith("https://a.story");
});