hi-folks / data-block
Data class for managing nested arrays and JSON data.
Requires
- php: ^8.2|^8.3|^8.4|^8.5
- swaggest/json-schema: ^0.12.42
- symfony/http-client-contracts: ^3.4.4
- symfony/yaml: ^6.4|^7.1|^8.0
Requires (Dev)
- laravel/pint: ^1.2
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11
- rector/rector: ^2
- symfony/http-client: ^7.4|^8.0
Suggests
- symfony/http-client: HTTP client implementation
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.1.0
- v2.0.0
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.10
- v0.3.9
- v0.3.8
- v0.3.7
- v0.3.6
- v0.3.5
- v0.3.4
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.1
- v0.1.0
- v0.0.6
- v0.0.5
- v0.0.4
- v0.0.3
- v0.0.2
- v0.0.1
- dev-feat/php85
- dev-feat/extract-where
- dev-feat/getstringstrict
- dev-upgrade-test
- dev-fix/misunderstanding-in-operator-18
- dev-feat/has-method
This package is auto-updated.
Last update: 2026-09-20 21:29:03 UTC
README
Data Block Package
PHP Package for Managing Nested Data
This PHP package provides classes and methods for easily managing, querying, filtering, and setting nested data structures. The PHP Data Block package offers a streamlined approach to handling nested data, whether you're working with complex JSON data, hierarchical configurations, or deeply nested arrays.Articles about PHP DataBlock
If you want to learn more about DataBlock through real-world examples and tutorials, here are some articles that mention or use it:
- Laravel News β Handle Nested Data Structures in PHP with the Data Block Package
- Roberto Butti β Handling Nested PHP Arrays Using DataBlock
- Roberto Butti β Exploring Real-World APIs with DataBlock
What you can do with PHP Data Block
For example, with PHP Data Block, you can retrieve complex JSON from an API and then filter, sort, and handle the data. For example, here you can:
- Retrieve the list of the repository from GitHub
- Order the repository by the number of stars received (ascending, starting from the repos with more stars)
- Loop the results
- Get values from elements
<?php use HiFolks\DataType\Block; require './vendor/autoload.php'; $url = 'https://api.github.com/orgs/hi-folks/repos'; Block::fromJsonUrl($url) ->orderBy('stargazers_count', 'desc') ->forEach( function ($item) { echo $item->get('full_name').' : '; echo $item->get('stargazers_count').PHP_EOL; } );
Then, you can do more, you can:
- Extract only the fields you need using the
select()method - Filter the elements with the
where()method and using theOperatorclass
Here is an example:
<?php use HiFolks\DataType\Block; use HiFolks\DataType\Enums\Operator; require './vendor/autoload.php'; $url = 'https://api.github.com/orgs/hi-folks/repos'; Block::fromJsonUrl($url) ->select('full_name', 'stargazers_count') ->where('stargazers_count', Operator::GREATER_THAN, 0) ->orderBy('stargazers_count', 'desc') ->forEach( function ($item) { echo $item->get('full_name').' : '; echo $item->get('stargazers_count').PHP_EOL; } );
This is just an overview as an appetizer :)
Now, let's explore the classes and methods PHP Data Block provides.
One "core" element of the PHP Data Block package is the Block PHP class.
The Block class
The Block class offers comprehensive methods to create, manage, and access nested data structures.
The Block class provides various methods, including:
- Creating structures from Arrays, JSON, and YAML files.
- Querying nested data with ease.
- Filtering data based on specific criteria.
- Setting and updating values within deeply nested structures.
Installing and using the Block class
For adding to your projects, the Block class with its methods and helpers, you can run the composer require command:
composer require hi-folks/data-block
To support the development, you can "star" β the repository: https://github.com/Hi-Folks/data-block
Then, in your PHP files, you can import the HiFolks\DataType\Block Namespace:
use HiFolks\DataType\Block;
Method for creating Block objects
To show the capabilities of the following methods, I will use this nested associative array:
$fruitsArray = [ "avocado" => [ 'name' => 'Avocado', 'fruit' => 'π₯', 'wikipedia' => 'https://en.wikipedia.org/wiki/Avocado', 'color'=>'green', 'rating' => 8 ], "apple" => [ 'name' => 'Apple', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Apple', 'color' => 'red', 'rating' => 7 ], "banana" => [ 'name' => 'Banana', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Banana', 'color' => 'yellow', 'rating' => 8.5 ], "cherry" => [ 'name' => 'Cherry', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Cherry', 'color' => 'red', 'rating' => 9 ], ];
The static make() method
With the static make() method, you can generate a Block object from an associative array:
$data = Block::make($fruitsArray);
The $data object is an instance of the Block class.
In the case you want to initialize an empty Block object, you can call the make() method with no parameters:
$data = Block::make();
Once you initialize the Block object, you can use its methods.
The get() method
The get() method supports keys/indexes with the dot (or custom) notation for retrieving values from nested arrays.
It returns the original type of data. If you need to obtain a Block object, you should use the getBlock() method instead of get().
For example:
$data->get('avocado'); // returns an array $data->get('avocado.color'); // returns the string "green"
For example, with the $fruitsArray sample data, the $data->get("avocado") is:
- an array;
- has five elements;
For example, the $data->get("avocado.color") is:
- a string;
- has the value "green";
The $data->get("avocado.rating") is:
- a numeric;
- specifically an integer;
The $data->get("banana.rating") is:
- a numeric;
- specifically a float;
You can customize the notation with a different character:
$data->get('apple#fruit', charNestedKey: '#'); // π
If you are going to access a not valid key, a null value is returned:
$value = $data->get('apple.notexists'); // null
You can define a default value in the case the key doesn't exist:
$value = $data->get( 'apple.notexists', 'π« ' ); // π«
And you can combine the default value and the nested character:
$value = $data->get( 'apple#notexists', 'π« ', '#' ); // π«
Parsing and formatting dates
Use getDate() when you need a nullable DateTimeImmutable. Supply the known
input format for tabular exports so parsing is unambiguous:
$closeDate = $row->getDate( 'Close Date', inputFormat: '!d/m/Y', ); echo $closeDate?->format('Y-m-d');
The ! is part of PHP's date format syntax and resets fields not present in
the input, such as the time, to a predictable value. When inputFormat is
omitted, PHP's normal date parser is used, which is convenient for ISO values:
$createdAt = $row->getDate('created_at');
Use requireDate() when missing, empty, or invalid input must stop processing:
$closeDate = $row->requireDate( 'Close Date', inputFormat: '!d/m/Y', );
For a nullable formatted string in one step, use getFormattedDate():
$normalized = $row->getFormattedDate( 'Close Date', inputFormat: '!d/m/Y', outputFormat: 'Y-m-d', ); // '2026-09-20' or null
| Need | Method | Result |
|---|---|---|
| Parse an optional value | getDate() |
?DateTimeImmutable |
| Require a valid value | requireDate() |
DateTimeImmutable or exception |
| Parse and format an optional value | getFormattedDate() |
?string |
All three methods support nested paths, custom key separators, and an optional
DateTimeZone. Explicit-format parsing checks both PHP parser errors and
warnings, so impossible calendar dates such as 31/02/2026 are rejected. It
never falls back to automatic parsing when the explicit format does not match.
getFormattedDateTime() is deprecated as of 2.1.0. Existing calls continue to
work, but new code should use getFormattedDate() for nullable formatting or
requireDate()->format() when invalid input must throw.
The getFormattedByte() for getting and formatting a 'byte' field value
The getFormattedByte() method retrieves and formats a byte value from a specified field in a data block. It converts the raw byte count into a more human-readable format (e.g., GB, MB, etc.), with an optional precision parameter to control the number of decimal places displayed.
Parameters:
$path(string): The path to the field containing the byte value (e.g., "assets.0.total_bytes").$precision(int): (Optional) Number of decimal places to include in the formatted result. The default is 2.
Example usage:
$data1->getFormattedByte("assets.0.total_bytes"); // Returns "5.98 GB" $data1->getFormattedByte("assets.1.total_bytes"); // Returns "2.18 GB" $data1->getFormattedByte("assets.1.total_bytes", 5); // Returns "2.18288 GB" $data1->getFormattedByte("assets.1.total_bytes", 0); // Returns "2 GB"
Key Features:
- Automatic unit conversion: converts bytes into appropriate units (e.g., KB, MB, GB) based on the size.
- Customizable precision: you can specify the number of decimal places for the output, making it flexible for various use cases.
The getString() method
The getString() method retrieves the value of a specified field as a string from a data block. If the field does not exist or is null, it returns a default value, which can be customized.
Parameters:
$path(string): The path to the field (e.g., "0.commit.author.date").$default(string|null): (Optional) The default value to return if the field doesn't exist. Defaults to null.
Example Usage:
$data1->getString("0.commit.author.date"); // Returns the field value as a string $data1->getString("0.commit.author.notexists"); // Returns null $data1->getString("0.commit.author.notexists", "AA"); // Returns "AA" $data1->getString("0.commit.comment_count"); // Returns "0" as a string even if the field value is an integer
The getStringStrict() method
The getStringStrict() method retrieves the value of a specified field as a string from a data block. If the field does not exist or is null, it returns a default string value, which can be customized ("" by default).
Parameters:
$path(string): The path to the field (e.g., "0.commit.author.date").$default(string): (Optional) The default value to return if the field doesn't exist. Defaults to "".
Example Usage:
$data1->getStringStrict("0.commit.author.date"); // Returns the field value as a string $data1->getStringStrict("0.commit.author.notexists"); // Returns an empty string "" $data1->getStringStrict("0.commit.author.notexists", "AA"); // Returns "AA" $data1->getStringStrict("0.commit.comment_count"); // Returns "0" as a string even if the field value is an integer
The getInt() method
The getInt() method retrieves the value of a specified field as a integer from a data block. If the field does not exist or is null, it returns a default value, which can be customized (null by default).
Parameters:
$path(string): The path to the field (e.g., "0.author.id").$default(null|int): (Optional) The default value to return if the field doesn't exist. Defaults to null.$charNestedKey(string): the character separator for nested field names. The default is ".".
Example usage:
$data1->getInt("0.author.id"); // Returns the field value as an integer, for example 678434 $data1->getInt("0.author.idx"); // Returns null because the field doesn't exists $data1->getInt("0.author.idx", 44); // Returns 44 because the field doesn't exists, and you set a default, in this case 44
The getIntStrict() method
The getIntStrict() method retrieves the value of a specified field as a integer from a data block. If the field does not exist or is null, it returns a default value, which can be customized (0 by default).
Parameters:
$path(string): The path to the field (e.g., "0.author.id").$default(int): (Optional) The default value to return if the field doesn't exist. Defaults to 0.$charNestedKey(string): the character separator for nested field names. The default is ".".
Example usage:
$data1->getIntStrict("0.author.id"); // Returns the field value as an integer, for example 678434 $data1->getIntStrict("0.author.idx"); // Returns 0 because the field doesn't exists, and the method is strict $data1->getIntStrict("0.author.idx", 44); // Returns 44 because the field doesn't exists, and you set a default, in this case 44
The getFloat() method
The getFloat() method retrieves the value of a specified field as a float from a data block. If the field does not exist or is null, it returns a default value, which can be customized (null by default).
Parameters:
$path(string): The path to the field (e.g.,"0.author.score").$default(null|float): (Optional) The default value to return if the field doesn't exist. Defaults to null.$charNestedKey(string): The character separator for nested field names. The default is".".
Example usage:
$data1->getFloat("0.author.score"); // Returns the field value as a float, for example 4.75 $data1->getFloat("0.author.notexists"); // Returns null because the field doesn't exist $data1->getFloat("0.author.notexists", 1.5); // Returns 1.5 because the field doesn't exist, and you set a default
The getFloatStrict() method
The getFloatStrict() method retrieves the value of a specified field as a float from a data block. If the field does not exist or is null, it returns a default value, which can be customized (0.0 by default).
Parameters:
$path(string): The path to the field (e.g.,"0.author.score").$default(float): (Optional) The default value to return if the field doesn't exist. Defaults to0.0.$charNestedKey(string): The character separator for nested field names. The default is".".
Example usage:
$data1->getFloatStrict("0.author.score"); // Returns the field value as a float, for example 4.75 $data1->getFloatStrict("0.author.notexists"); // Returns 0.0 because the field doesn't exist, and the method is strict $data1->getFloatStrict("0.author.notexists", 1.5); // Returns 1.5 because the field doesn't exist, and you set a default
The getBoolean() method
The getBoolean() method retrieves the value of a specified field as a boolean from a data block. If the field does not exist or is null, it returns a default value, which can be customized (null by default).
Parameters:
$path(string): The path to the field (e.g., "0.author.id").$default(null|bool): (Optional) The default value to return if the field doesn't exist. Defaults to null.$charNestedKey(string): the character separator for nested field names. The default is ".".
Example usage:
$data1->getBoolean("0.author.site_admin"); // Returns the field value as an boolean, for example true $data1->getBoolean("0.author.site_admin_notexists"); // Returns null because the field doesn't exists $data1->getBoolean("0.author.site_admin_notexists", true); // Returns true because the field doesn't exists, and you set a default, in this case true
The getBooleanStrict() method
The getBooleanStrict() method retrieves the value of a specified field as a boolean from a data block. If the field does not exist or is null, it returns a strict boolean default value, which can be customized (false by default).
Parameters:
$path(string): The path to the field (e.g., "0.author.id").$default(bool): (Optional) The default value to return if the field doesn't exist. Defaults to false.$charNestedKey(string): the character separator for nested field names. The default is ".".
Example usage:
$data1->getBooleanStrict("0.author.site_admin"); // Returns the field value as an boolean, for example true $data1->getBooleanStrict("0.author.site_admin_notexists"); // Returns false because the field doesn't exists $data1->getBooleanStrict("0.author.site_admin_notexists", true); // Returns true because the field doesn't exists, and you set a default, in this case true
The getBlock() method
If you need to manage a complex array (nested array) or an array obtained from a complex JSON structure, you can access a portion of the array and obtain the Block object via the getBlock() method.
Let's see an example:
$appleData = $data->getBlock("apple") // $data is the Block instance so that you can access // to the Block methods like count() $data->getBlock("apple")->count();
If the element accessed via getBlock() is a scalar type (integer, float, string, etc.), a Block object (with just one element) will be returned using getBlock().
For example, $data->getBlock("avocado") returns a Block object with five elements.
For example, $data->getBlock("avocado.color") returns a Block object with just one element.
If you are going to access a non-valid key, an empty Block object is returned, so the $data->getBlock("avocado.notexists") returns a Block object with a length equal to 0.
Missing key behavior
By default, accessing a non-existing key returns the provided default value silently.
You can configure three behaviors:
- Silent (default)
- Warning (non-fatal)
- Exception
Silent (default)
$fruits = Block::make($fruitsArray); // OR if you want to be more explicit: $fruits = Block::make($fruitsArray)->silentOnMissingKey(); $nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // no warning, no exception
Warning
$fruits = Block::make($fruitsArray)->warnOnMissingKey(); $nothing = $fruits->get("a-missing-key", "DEFAULT VALUE"); // PHP warning
Exception
$fruits = Block::make($fruitsArray) ->throwOnMissingKey(); $nothing = $fruits->get("a-missing-key"); // throws exception
You can also pass your own exception class (must extend \Throwable).
$fruits->throwOnMissingKey( MyMissingKeyException::class, "The key in the JSON configuration file does not exist?" ); $nothing = $fruits->get("a-missing-key"); // throws your custom exception
Summary for the "missing key behavior"
| Mode | Result |
|---|---|
| Silent (default behavior) | Returns default value |
| Warning | Emits warning, returns default value |
| Exception | Throws exception |
The set() method
The set() method supports keys with the dot (or custom) notation for setting values for nested data.
If a key doesn't exist, the set() method creates one and sets the value.
If a key already exists, the set() method will replace the value related to the key.
Parameters
key(int|string): The key to which the value should be assigned. If a string is provided, you can use dot notation to set nested values.value(mixed): The value to be assigned to the specified key.charNestedKey(string, optional): The character used for dot notation in nested keys. Defaults to..
Returns
self: Returns the instance of the class for method chaining.
Example Usage
$articleText = "Some words as a sample sentence"; $textField = Block::make(); $textField->set("type", "doc"); $textField->set("content.0.content.0.text", $articleText); $textField->set("content.0.content.0.type", "text"); $textField->set("content.0.type", "paragraph");
So when you try to set a nested key as "content.0.content.0.text", it will be created elements as a nested array.
Once you set the values, you can access them via get() (or getBlock()) methods:
$textField->get("content.0.content.0.text");
Extracting Keys
Via the keys() method, you can retrieve the list of the keys:
$data = Block::make($fruitsArray); $keys = $data->keys(); /* Array ( [0] => avocado [1] => apple [2] => banana [3] => cherry ) */
You can retrieve the keys of a nested element, combining the usage of getBlock() and the keys():
$data = Block::make($fruitsArray); $keys = $data->getBlock("avocado")->keys(); /* Array ( [0] => name [1] => fruit [2] => wikipedia [3] => color [4] => rating ) */
Exporting data
Exporting to array with toArray()
The toArray() method can access the native array (associative and nested).
This is helpful when manipulating data with the Block class, and at a certain point, you need to send the data to your function or a function from a third-party package that expects to receive a native array as a parameter.
$file = "./composer.json"; $composerContent = Block::fromJsonFile($file); // here you can manage $composerContent with Block methods // end then exports the Block data into a native array $array = $composerContent->toArray();
Exporting to JSON string with toJson()
If you need to generate a valid JSON string using the content of the Block object, you can use the toJson() method.
This is helpful when you are manipulating data with the Block class and at a certain point need to send the data in JSON string format to your own function or a function from a third-party package that expects to receive a JSON string as a parameter.
$data = Block::make($fruitsArray); $jsonString = $data->toJson(); // JSON string with "pretty print"
Exporting to YAML string with toYaml()
If you need to generate a valid YAML string using the content of the Block object, you can use the toYaml() method.
This is helpful when manipulating data with the Block class and, at a certain point, need to send the data in YAML string format to your function or a function from a third-party package that expects to receive a YAML string as a parameter.
$data = Block::make($fruitsArray); $yamlString = $data->toYaml(); // YAML string
Saving JSON to a file with saveToJson()
If you need to save the JSON string in a file using the content of the Block object, you can use the saveToJson() method.
This is helpful when you are manipulating data with the Block class and at a certain point need to save the data in JSON string format to a file.
The saveToJson() method has two parameters:
filename: the first parameter (mandatory) with the filename;overwrite: the second parameter (optional), If the file exists, the file is not saved by default, unless you set the overwrite parameter as true.
$data = Block::make($fruitsArray); $jsonString = $data->saveToJson('./fruits.json', true);
Loading Data
Loading Data from JSON file
$file = "./composer.json"; $composerContent = Block::fromJsonFile($file); echo $composerContent->get("name"); // for example: "hi-folks/data-block" echo $composerContent->get("authors.0.name"); // for example: "Roberto B."
Loading Data from JSON URL
You can build your Block data from a remote JSON (like an API).
For example, you can use the fromJsonUrl() method to build a Block object from the latest commits via GitHub API.
Retrieving JSON API into a Block object is useful for applying the methods provided by the Block class, for example, filtering the data. In the example, I'm going to filter the commit based on the name of the author of the commit:
$url = "https://api.github.com/repos/hi-folks/data-block/commits"; $commits = Block::fromJsonUrl($url); $myCommits = $commits->where("commit.author.name", Operator::LIKE, "Roberto"); foreach ($myCommits as $value) { echo $value->get("commit.message") . PHP_EOL; }
Loading Data from YAML file
$file = "./.github/workflows/run-tests.yml"; $workflow = Block::fromYamlFile($file); echo $workflow->get("name"); // Name of the GitHub Action Workflow echo $workflow->get("jobs.test.runs-on"); echo $workflow->get("on.0"); // push , the first event
Loading CSV files
CSV loading supports three memory strategies. Choose the one that matches the file size and the operation you need to perform.
| Goal | Method | Memory behavior |
|---|---|---|
Use the complete file as one Block |
fromCsvFile() |
Loads every row into memory |
| Process one row at a time | streamCsvFile() |
Keeps only the current row in memory |
Use Block operations on bounded batches |
chunkCsvFile() |
Keeps one configurable chunk in memory |
Eager loading
Use fromCsvFile() for files that comfortably fit in memory:
$opportunities = Block::fromCsvFile( '/exports/opportunities.csv', encoding: 'Windows-1252', ); $total = $opportunities ->whereNotNull('Amount') ->sum('Amount');
The first row is used as field names by default. Quoted delimiters and quoted multiline fields are parsed correctly.
Row-by-row streaming
Use streamCsvFile() for very large Salesforce or reporting exports. It returns a Generator of Block rows and does not materialize the complete file:
foreach (Block::streamCsvFile( '/exports/opportunities.csv', encoding: 'Windows-1252', ) as $opportunity) { if ($opportunity->getFloatStrict('Amount') > 0) { // Process or persist this row before reading the next one. } }
Iteration is lazy: the file is opened and rows are parsed only as they are requested. The file handle is closed when iteration finishes or when the generator is released after stopping early.
Processing chunks
Use chunkCsvFile() when you need existing collection methods without holding the entire export in memory:
$total = 0; foreach (Block::chunkCsvFile( '/exports/opportunities.csv', chunkSize: 1_000, encoding: 'Windows-1252', ) as $opportunities) { $total += $opportunities ->whereNotNull('Amount') ->sum('Amount'); }
Filtering, mapping, validation, counting, and summing are safe to perform per chunk. Operations requiring the complete datasetβsuch as a global orderBy(), top-N query, or complete groupingβmust not be calculated independently per chunk. For averages, combine the total sum and item count across chunks.
CSV options and validation
All three methods support the same parsing options:
| Option | Default | Purpose |
|---|---|---|
delimiter |
, |
Field separator |
enclosure |
" |
Quoted-field character |
escape |
Empty | Explicit RFC 4180-oriented escape behavior |
encoding |
UTF-8 |
Source encoding converted to UTF-8 |
header |
true |
Use the first non-empty row as field names |
skipEmptyRows |
true |
Ignore physically empty CSV records |
rowWidth |
CsvRowWidth::STRICT |
Handle rows whose field count differs from the header |
normalize |
null |
Optionally transform each row while it is read |
requiredHeaders |
[] |
Require specific case-sensitive header names while allowing additional columns |
Header names must be present and unique. UTF-8 BOM bytes are removed from the first header automatically.
Use requiredHeaders to detect an unexpected export schema before processing
data. Every missing header is reported in one exception, additional columns are
allowed, and header-only files are supported:
$opportunities = Block::fromCsvFile( '/exports/opportunities.csv', requiredHeaders: [ 'Opportunity Name', 'Stage', 'Amount', ], );
Required names are matched strictly and case-sensitively after source-encoding
conversion and UTF-8 BOM removal. requiredHeaders cannot be combined with
header: false because positional CSV records have no names to validate.
The same option works with eager, streaming, and chunked loading. Generators
are lazy, so streamCsvFile() and chunkCsvFile() open the file and validate
its headers when iteration begins. Validation always completes before the
first row or chunk is yielded.
Choose an explicit row-width policy:
use HiFolks\DataType\Enums\CsvRowWidth; CsvRowWidth::STRICT; // Throw with the CSV record number on any mismatch. CsvRowWidth::PAD; // Pad short rows with null; reject rows with extra fields. CsvRowWidth::SKIP; // Skip rows whose width does not match the header.
CSV values remain strings by default. This avoids corrupting identifiers, leading zeros, dates, and large numbers through automatic type guessing. Use normalize when explicit conversion is appropriate:
$opportunities = Block::fromCsvFile( '/exports/opportunities.csv', normalize: fn(Block $row): array => [ ...$row->toArray(), 'Amount' => $row->getFloatStrict('Amount'), 'IsClosed' => $row->getStringStrict('IsClosed') === 'true', ], );
Loading Data from JSON URL via Symfony HttpClient
If you want more control over the HTTP request (headers, authentication, timeouts, retries, etc.) or your environment restricts PHP stream functions (for example allow_url_fopen=0), you can use fromHttpJsonUrl().
This method relies on your Symfony HttpClient implementation and allows you to pass any request options supported by the client.
use HiFolks\DataType\Block; use HiFolks\DataType\Enums\Operator; use Symfony\Component\HttpClient\HttpClient; $url = "https://api.github.com/repos/hi-folks/data-block/commits"; $client = HttpClient::create(); $commits = Block::fromHttpJsonUrl($url, $client, [ 'headers' => [ 'User-Agent' => 'my-app', 'Accept' => 'application/json', ], ]); $myCommits = $commits->where("commit.author.name", Operator::LIKE, "Roberto"); foreach ($myCommits as $value) { echo $value->get("commit.message") . PHP_EOL; }
Don't forget to install the client's implementation composer require symfony/http-client
Adding and appending elements
Appending the elements of a Block object to another Block object
If you have a Block object, you can add elements from another Block object. One use case is if you have multiple JSON files and want to retrieve paginated content from an API. In this case, you want to create one Block object with all the elements from every JSON file.
$data1 = Block::fromJsonFile("./data/commits-10-p1.json"); $data2 = Block::fromJsonFile("./data/commits-10-p2.json"); $data1->count(); // 10 $data2->count(); // 10 $data1->append($data2); $data1->count(); // 20 $data2->count(); // 10
Appending the elements of an array to a Block object
If you have an array, you can add elements to a Block object. Under the hood, a Block object is an array (that potentially can be a nested array). Appending an array will add elements at the root level:
$data1 = Block::make(["a","b"]); $arrayData2 = ["c","d"]; $data1->count(); // 2 $data1->append($arrayData2); $data1->count(); // 4
Appending an element
If you need to append an element as a single element (even if it is an array or a Block object), you can use the appendItem() function:
$data1 = Block::make(["a", "b"]); $arrayData2 = ["c", "d"]; $data1->appendItem($arrayData2); $data1->count(); // 3 because a, b, and the whole array c,d as single element $data1->toArray(); /* [ 'a', 'b', [ 'c', 'd', ], ] */
Querying, sorting data
The where() method
You can filter data elements for a specific key with a specific value. You can also set the operator
$composerContent = Block::fromJsonString($jsonString); $banners = $composerContent->getBlock("story.content.body")->where( "component", Operator::EQUAL, "banner", );
With the where() method, the filtered data keeps the original keys.
If you want to avoid preserving the keys and set new integer keys starting from 0, you can set the fourth parameter (preserveKeys) as false.
$composerContent = Block::fromJsonString($jsonString);
$banners = $composerContent->getBlock("story.content.body")->where(
"component",
Operator::NOT_EQUAL,
"banner",
+ false
);
With where() method you can use different operators, like "==", ">", "<" etc.
You can use also the has operator in the case your nested data contains arrays or in operator in the case you want to check if your data field value is included in an array of elements.
The operators
The Operator class provides a set of predefined constants that represent comparison and logical operators. This ensures type safety and prevents errors from using invalid or misspelled operators in your data comparisons.
Supported Operators:
Operator::EQUAL(==)Operator::STRICT_EQUAL(===)Operator::GREATER_THAN(>)Operator::LESS_THAN(<)Operator::GREATER_THAN_OR_EQUAL(>=)Operator::LESS_THAN_OR_EQUAL(<=)Operator::NOT_EQUAL(!=)Operator::STRICT_NOT_EQUAL(!==)Operator::IN(array inclusion)Operator::HAS(array containment)Operator::LIKE(string contains)
The
Operatorclass is defined in theuse HiFolks\DataType\Enums\Operatornamespace.
Unknown operators throw InvalidArgumentException; they never fall back to another comparison. Operator::EQUAL and Operator::NOT_EQUAL intentionally use PHP's loose comparison rules. Use Operator::STRICT_EQUAL and Operator::STRICT_NOT_EQUAL when values must also have the same type.
Missing, null, and incompatible values
Query behavior is designed to avoid accidental matches from PHP type coercion:
| Condition | Missing field | Explicit null |
Incompatible value |
|---|---|---|---|
| Equality operators | Never matches | Compared using the selected loose or strict operator | Compared using the selected operator |
<, <=, >, >= |
Never matches | Never matches | Never matches |
IN |
Never matches | Matches only when null is in the list |
Uses strict membership |
HAS |
Never matches | Never matches | Never matches unless the field is an array or Block |
LIKE |
Never matches | Never matches | Never matches unless both values are scalar |
Relational comparisons accept non-empty strings, integers, and floats. This means consistently formatted date strings such as ISO YYYY-MM-DD dates can be compared, while missing, null, empty-string, boolean, array, and object values are excluded.
Null, range, and inclusion helpers
Use whereNull() when a field is missing or explicitly null. It does not match 0, false, or an empty string:
$withoutAmount = $rows->whereNull('amount'); $withAmount = $rows->whereNotNull('amount');
whereBetween() performs an inclusive range comparison and supports nested fields. Missing, null, empty, and incompatible values are excluded:
$closingThisQuarter = $rows->whereBetween( 'close_date', '2026-01-01', '2026-03-31', );
whereIn() provides a readable inclusion query and uses strict comparison by default, preventing values such as 0, "0", and false from matching each other:
$activeStages = $rows->whereIn( 'stage', ['Proposal', 'Negotiation'], );
Pass strict: false only when PHP's loose membership comparison is explicitly required. All query helpers preserve keys; pass preserveKeys: false to return a zero-based result.
The in operator
The in operator is used within the where method to filter elements from a data collection based on whether a specific field's value exists within a given array of values.
The behavior is as follows:
$data->where("field", Operator::IN, ["value1", "value2", ...])
If the field's value exists in the provided array with the same value and type, the element is included in the result. Example: Filtering fruits by color that match either "green" or "black"
$greenOrBlack = $data->where("color", Operator::IN, ["green", "black"]);
You should use the in operator if your field is a scalar type (for example string or number) and you need to check if it is included in a list of values (array).
The has operator
The has operator is used within the where method to filter elements from a data collection based on whether a specific field contains a given value, typically in cases where the field holds an array or a collection of tags or attributes. The behavior is as follows:
$data->where("field", Operator::HAS, "value")
For example if you have posts and each post can have multiple tags, you can filter posts with a specific tag:
$url = "https://dummyjson.com/posts"; $posts = Block ::fromJsonUrl($url) ->getBlock("posts"); $lovePosts = $posts->where("tags", Operator::HAS, "love");
Summary in VS has
The in operator filters elements by matching a field's value against an array of possible values. If the value exists in the array, the element is included in the result. An empty array returns no results.
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
$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
Use orderBy() with a SortCriterion to sort data by one field. The named
asc() and desc() constructors make the direction explicit, provide good IDE
autocomplete, and prevent invalid directions.
use HiFolks\DataType\SortCriterion; $sorted = $orders->orderBy(SortCriterion::desc('totals.amount'));
For example, if you want to retrieve the data at story.content.body key and sort them by component key:
$composerContent = Block::fromJsonString($jsonString); $bodyComponents = $composerContent ->getBlock('story.content.body') ->orderBy(SortCriterion::asc('component'));
You can also order data for a nested attribute.
Consider retrieving a remote JSON like the dummy JSON posts and then ordering the posts via the reactions.likes nested field in descending order:
use HiFolks\DataType\Block; use HiFolks\DataType\SortCriterion; $posts = Block ::fromJsonUrl("https://dummyjson.com/posts") ->getBlock("posts"); echo $posts->count(); // 30 $mostLikedPosts = $posts->orderBy( SortCriterion::desc('reactions.likes'), ); $mostLikedPosts->dump();
Sorting is stable: rows with the same value keep their original relative
order. Existing keys are also preserved, which is especially useful when keys
are IDs. Chain values() when you need a zero-based list instead:
$sorted = $orders->orderBy(SortCriterion::desc('totals.amount')); $sortedAndReindexed = $sorted->values();
The earlier syntax remains supported for backward compatibility and for
directions coming from configuration. String directions are case-insensitive
and invalid values throw an InvalidArgumentException:
use HiFolks\DataType\Enums\SortDirection; $orders->orderBy('totals.amount', SortDirection::DESC); $orders->orderBy('totals.amount', 'DESC');
Missing and null field values are placed last for both ascending and
descending order. Arrays and objects are not directly comparable, so they are
also placed last. This makes incomplete or unexpectedly structured rows behave
predictably rather than affecting the useful sorted values.
The orderByMany() method
Use orderByMany() when the next field should break ties in the previous one.
Criteria are applied from left to right and nested fields are supported:
use HiFolks\DataType\SortCriterion; $report = $opportunities->orderByMany([ SortCriterion::asc('Assigned Solution Engineer'), SortCriterion::desc('_created_date'), ]);
orderByMany() accepts a list of SortCriterion objects, giving single-field
and multi-field sorting the same API. An empty list returns an unchanged copy.
The sort() method
Use sort() when ordering depends on a custom business rule rather than field
directions:
$sorted = $opportunities->sort( fn (Block $a, Block $b): int => ($a->get('probability') * $a->get('amount')) <=> ($b->get('probability') * $b->get('amount')), );
The comparator must return an integer below, equal to, or above zero. It
receives Block rows by default; when iteration is configured with
Block::make($rows, false) or iterateBlock(false), it receives the raw array
rows instead. Like orderBy(), callback sorting is stable, preserves keys, and
does not modify the original Block.
| Need | Method |
|---|---|
| Sort by one field | orderBy(SortCriterion::desc('amount')) |
| Sort by several fields | orderByMany([SortCriterion::asc('team'), SortCriterion::desc('amount')]) |
| Apply a custom comparison rule | sort(fn (Block $a, Block $b): int => ...) |
| Reindex sorted results | ->values() |
These methods sort an in-memory Block. For very large CSV exports, stream or chunk the input first and avoid a global sort unless the selected data fits in memory.
The select() method
The select() method allows you to select only the needed fields.
You can list the field names you need as parameters for the select() method.
For example:
use HiFolks\DataType\Block; $dataTable = [ ['product' => 'Desk', 'price' => 200, 'active' => true], ['product' => 'Chair', 'price' => 100, 'active' => true], ['product' => 'Door', 'price' => 300, 'active' => false], ['product' => 'Bookcase', 'price' => 150, 'active' => true], ['product' => 'Door', 'price' => 100, 'active' => true], ]; $table = Block::make($dataTable); $data = $table ->select('product' , 'price'); print_r($data->toArray());
You can combine the select(), the where(), and the orderBy() method.
If you want to retrieve elements with product and price keys, with a price greater than 100 and ordered by price:
$table = Block::make($dataTable); $data = $table ->select('product' , 'price') ->where('price', Operator::GREATER_THAN, 100) ->orderBy("price"); print_r($data->toArray()); /* Array ( [0] => Array ( [product] => Bookcase [price] => 150 ) [1] => Array ( [product] => Desk [price] => 200 ) [2] => Array ( [product] => Door [price] => 300 ) ) */
The groupBy() method
Groups the elements of the Block object by a specified field.
This method takes a field name as an argument and groups the elements of the Block object based on the values of that field. Each element is grouped into an associative array where the keys are the values of the specified field and the values are arrays of elements that share that key.
use HiFolks\DataType\Block; $data = Block::make([ ['type' => 'fruit', 'name' => 'apple'], ['type' => 'fruit', 'name' => 'banana'], ['type' => 'vegetable', 'name' => 'carrot'], ]); $grouped = $data->groupBy('type'); $grouped->dumpJson(); /* { "fruit": [ { "type": "fruit", "name": "apple" }, { "type": "fruit", "name": "banana" } ], "vegetable": [ { "type": "vegetable", "name": "carrot" } ] } */
Grouping preserves valid values, including 0, "0", false, and an empty string. PHP uses the same array key for 0, "0", and false, so those values belong to group 0. This follows PHP's normal array-key behavior and avoids silently dropping valid data.
Rows whose grouping field is missing, null, or cannot be used as an array key are skipped by default. Pass defaultGroup when those rows should be collected explicitly instead:
$grouped = $data->groupBy('type', defaultGroup: 'unknown');
Using an explicit default keeps missing data visible without confusing it with valid falsey values.
The groupByFunction() method
The groupByFunction() method groups items using custom callback logic. Like the other callback-based methods, it receives the current item followed by its key. Nested arrays are provided as Block objects by default.
$fruits = [ ['name' => 'Apple', 'type' => 'Citrus', 'quantity' => 15], ['name' => 'Banana', 'type' => 'Tropical', 'quantity' => 10], ['name' => 'Orange', 'type' => 'Citrus', 'quantity' => 8], ['name' => 'Mango', 'type' => 'Tropical', 'quantity' => 5], ['name' => 'Lemon', 'type' => 'Citrus', 'quantity' => 12] ]; $fruitsBlock = Block::make($fruits); $groupedByQuantityRange = $fruitsBlock->groupByFunction( fn(Block $fruit): string => match (true) { $fruit->getIntStrict('quantity') < 10 => 'Low', $fruit->getIntStrict('quantity') < 15 => 'Medium', default => 'High', }, ); // It returns: /* { "High": [ { "name": "Apple", "type": "Citrus", "quantity": 15 } ], "Medium": [ { "name": "Banana", "type": "Tropical", "quantity": 10 }, { "name": "Lemon", "type": "Citrus", "quantity": 12 } ], "Low": [ { "name": "Orange", "type": "Citrus", "quantity": 8 }, { "name": "Mango", "type": "Tropical", "quantity": 5 } ] } */
Aggregating numeric values
Use sum(), average(), min(), and max() to aggregate a field across all rows. Field names support the same nested paths as get().
$orders = Block::make([ ['currency' => 'EUR', 'totals' => ['amount' => 10]], ['currency' => 'USD', 'totals' => ['amount' => '20.50']], ['currency' => 'EUR', 'totals' => ['amount' => 30]], ]); $orders->sum('totals.amount'); // 60.5 $orders->average('totals.amount'); // 20.166666666666668 $orders->min('totals.amount'); // 10 $orders->max('totals.amount'); // 30
When no field is provided, the methods aggregate the values in the Block directly:
Block::make([10, 20, 30])->sum(); // 60
Integer values, floating-point values, and numeric strings are included. Missing fields, null, booleans, and non-numeric values are ignored. An empty selection returns 0 from sum() and null from average(), min(), and max().
Use the grouped aggregation helpers when you need one result for each value of another field:
$ordersByCurrency = $orders->countBy('currency'); $totalsByCurrency = $orders->sumBy('currency', 'totals.amount'); $ordersByCurrency->toArray(); // ['EUR' => 2, 'USD' => 1] $totalsByCurrency->toArray(); // ['EUR' => 40, 'USD' => 20.5]
The available grouped helpers are countBy(), sumBy(), averageBy(), minBy(), and maxBy(). countBy() counts every row in each group, regardless of missing or null values in other fields. The numeric helpers take a second argument specifying the field to aggregate. All field arguments support nested paths.
To count only rows matching a condition, filter the collection before calling countBy(). For example, this counts opportunities with a non-null amount for each stage:
$opportunitiesByStage = $opportunities ->whereNotNull('Amount') ->countBy('Stage');
The same pattern works with where(), whereNull(), whereBetween(), whereIn(), or callback-based filter().
Filtering and sorting associative keys
Grouped aggregations return associative Blocks whose keys are the group names.
Use withoutKeys() to exclude unwanted groups and sortKeys() to produce a
predictable report order without leaving the fluent API:
use HiFolks\DataType\Enums\SortDirection; $totals = $opportunities ->sumBy('Amount Currency', '_amount') ->withoutKeys('') ->sortKeys(SortDirection::ASC);
Both methods return a new Block, preserve key/value associations and iteration
mode, and leave the original unchanged. withoutKeys() accepts multiple keys
and silently ignores missing ones. sortKeys() defaults to ascending order.
Integer keys are compared numerically and string keys lexicographically with
case sensitivity. For mixed keys, integers come before strings in ascending
order; descending order reverses that complete ordering. PHP automatically
converts numeric-string array keys such as '10' to integers, so they cannot be
distinguished from integer keys after the array is created.
They also accept defaultGroup for rows with a missing or null grouping field:
$ordersByCurrency = $orders->countBy( groupField: 'currency', defaultGroup: 'unknown', ); $totalsByCurrency = $orders->sumBy( groupField: 'currency', valueField: 'totals.amount', defaultGroup: 'unknown', );
Custom aggregations with reduce()
Use reduce() when the built-in numeric operations do not cover the required aggregation. The callback receives the accumulated value, the current item, and its key:
$totalsByCurrency = $orders->reduce( function (array $totals, Block $order): array { $currency = $order->getStringStrict('currency'); $totals[$currency] = ($totals[$currency] ?? 0) + $order->getFloatStrict('totals.amount'); return $totals; }, [], ); // ['EUR' => 40.0, 'USD' => 20.5]
The exists() method
You can use the exists() method to check if an element that meets a certain condition exists. This method is a convenient way to determine if any records match your query without needing to count them explicitly.
Hereβs how you can use it:
$has = $composerContent ->getBlock("story.content.body")->where( "component", "banner", )->exists();
This will return true if a banner component exists, and false if it does not.
Navigating a collection
Navigation helpers keep limiting and endpoint access inside the fluent Block API. take(), skip(), and slice() return new blocks without modifying the original data. The first*() and last*() methods return an individual item.
| Goal | Method | Example |
|---|---|---|
| Keep the first items | take() |
$rows->take(15) |
| Move past earlier items | skip() |
$rows->skip(15) |
| Select an exact window | slice() |
$rows->slice(15, 10) |
| Require an endpoint item | first() / last() |
$rows->first() |
| Read an optional endpoint | firstOrNull() / lastOrNull() |
$rows->lastOrNull() |
These methods make reporting pipelines concise and readable:
$priorities = $rows ->orderBy('close_date') ->take(15) ->select('name', 'stage', 'amount');
Keys and reindexing
take(), skip(), and slice() preserve keys so record identities are not changed unexpectedly. Call values() when the result must be a zero-based list:
$page = $rows->skip(15)->take(15)->values();
Limits and offsets
take(5)returns the first five items.take(-5)returns the last five items.take(0)returns an emptyBlock.skip(5)returns everything after the first five items.- A negative value passed to
skip()throwsInvalidArgumentExceptionbecause its meaning would be ambiguous. slice()follows PHP'sarray_slice()offset and length semantics, including negative values.
First and last items
first() and last() throw UnderflowException when the block is empty. Use firstOrNull() and lastOrNull() when an empty result is expected:
$firstRequired = $rows->first(); $firstOptional = $rows->firstOrNull();
Because null is also a valid item value, the nullable methods cannot distinguish an empty block from a first or last item whose value is null. Use first() or last() when that distinction matters.
Endpoint methods use the same representation as iteration: an array item is returned as a Block by default and as a native array after iterateBlock(false). Scalar items are returned unchanged.
Looping Data
The Block class implements the Iterator interface.
While looping an array via Block, by default, if the current element should be an array, a Block is returned so that you can access the Block method for handling the current array item in the loop.
For example, with the previous code, if you loop through $data (which is a Block object), each element in each iteration of the loop will be an array with two elements, with the keys product and price.
If in the loop you need to manage the current element via Block class, you should manually call the Block::make, for example:
$table = Block::make($dataTable); foreach ($table as $key => $item) { echo $item->get("price"); }
You can apply filters and then loop into the result:
$table = Block::make($dataTable); $data = $table ->select('product', 'price') ->where('price', Operator::GREATER_THAN, 100, false); foreach ($data as $key => $item) { echo $item->get("price"); // returns an integer }
If you want to loop through $data and obtain the current $item variable as an array you should set false as a second parameter in the static make() method:
$table = Block::make($dataTable, false); $data = $table->select('product', 'price')->where('price', Operator::GREATER_THAN, 100, false); foreach ($data as $key => $item) { print_r($item); // $item is an array }
The iterateBlock() method
With the iterateBlock() method, you can switch from array or Block for nested lists inside the main Block object if you already instanced it as a Block object.
In the example above, you have the $table Block object.
You can loop across the items of the $table object.
If each item in the loop is itself an array (so an array of arrays), you can retrieve it as an array or a Block, depending on your needs:
$table = Block::make($dataTable); foreach ($table as $key => $item) { expect($item)->toBeInstanceOf(Block::class); expect($key)->toBeInt(); expect($item->get("price"))->toBeGreaterThan(10); } // iterateBlock(false if you need array instad of a nested Block) foreach ($table->iterateBlock(false) as $key => $item) { expect($item)->toBeArray(); expect($key)->toBeInt(); expect($item["price"])->toBeGreaterThan(10); }
Choosing the right callback helper
Each helper has one clear responsibility. Keeping these operations separate makes a data pipeline easier to read and prevents callback return values from changing data accidentally.
| Goal | Method | Example | Result |
|---|---|---|---|
| Compare one field with a known operator | where() |
where('amount', Operator::GREATER_THAN, 0) |
A filtered Block |
| Select items using custom logic | filter() |
filter(fn(Block $row): bool => ...) |
A filtered Block |
| Split by custom logic | partition() |
partition(fn(Block $row): bool => ...) |
Matching and non-matching Blocks |
| Transform every item | map() |
map(fn(Block $row): array => ...) |
A transformed Block |
| Perform a side effect | forEach() |
forEach(fn(Block $row) => logger($row)) |
The original Block |
| Combine all items | reduce() |
reduce(fn($total, $row) => ..., 0) |
The accumulated value |
This distinction makes fluent code communicate its intention directly:
filter() selects, partition() separates, map() transforms, forEach()
observes, and reduce() combines.
where() vs filter()
Both methods select items, but they express different kinds of conditions:
Use
where()for one field comparison. Usefilter()when the condition needs custom callback logic.
| Use case | Method |
|---|---|
amount > 0 |
where() |
amount > 0 AND status !== cancelled |
filter() |
| Custom business rule | filter() |
| Both sides of a custom business rule | partition() |
Internally, where() can be understood as a convenient specialized filter, while filter() is the flexible escape hatch.
For a direct field, operator, and value comparison, where() is shorter and communicates the rule clearly:
$paidOrders = $orders->where( 'amount', Operator::GREATER_THAN, 0, );
Use filter() when the decision involves multiple fields, nullable values, calculations, or an application-specific rule:
$actionableOrders = $orders->filter( fn(Block $order): bool => $order->getFloatStrict('amount') > 0 && $order->getStringStrict('status') !== 'cancelled', );
where() is the convenient declarative option; filter() is the flexible callback-based option.
Transforming items with map()
map() transforms every item and returns the callback results in a new Block. Use it when the shape or value of each item needs to change. Keys are preserved, and the original block is unchanged, so transformations can be chained without losing the source data.
$url = "https://dummyjson.com/posts"; $posts = Block::fromJsonUrl($url) // Load the Block from the remote URL ->getBlock("posts") // get the `posts` as Block object ->where( field:"tags", operator: Operator::HAS, value: "love", preserveKeys: false, ) // filter the posts, selecting only the posts with tags "love" ->map(fn(Block $element): array => [ "title" => strtoupper((string) $element->get("title")), "tags" => count($element->get("tags")), ]); // The `$posts` object is an instance of the `Block` class. // The `$posts` object contains the items that matches the `where` method. // You can access to the elements via the nested keys // $posts->get("0.title"); // "HOPES AND DREAMS WERE DASHED THAT DAY." // $posts->get("0.tags"); // 3
Selecting items with filter()
Use filter() when where() is not expressive enoughβfor example, when a decision depends on multiple fields, nullable data, or application-specific logic. The callback must return a boolean, which prevents ambiguous truthy or falsey results. Keys are preserved so records keep their identity.
$priorities = $rows->filter( fn(Block $row): bool => $row->getStringStrict('assigned_se') === '' && $row->getFloatStrict('amount') > 0, );
Call values() when a zero-based list is needed explicitly:
$priorities = $priorities->values();
Splitting items with partition()
Use partition() when both sides of the same condition are needed. It evaluates
each item exactly once and returns a two-element array: the first Block contains
items for which the callback returned true, and the second contains those for
which it returned false.
[$assigned, $unassigned] = $opportunities->partition( fn (Block $row): bool => $row->getStringStrict('Assigned Solution Engineer') !== '', );
Every source item belongs to exactly one result, so no opportunity is lost
between the two groups. This is particularly useful here because missing and
null values become an empty string through getStringStrict() and therefore
belong to $unassigned.
Both returned Blocks preserve the original keys and iteration representation;
the source Block is unchanged. Use values() on either result when a zero-based
list is required:
$assigned = $assigned->values(); $unassigned = $unassigned->values();
Like filter(), the callback receives the item followed by its key and must
return an actual boolean. Returning values such as 1, 0, or a non-empty
string throws an UnexpectedValueException, preventing ambiguous truthy and
falsey classification.
Performing side effects with forEach()
forEach() visits every item without replacing the block's values. It returns the original Block, so it is intended for logging, output, notifications, or other side effects. Its callback result is intentionally ignored; this prevents a logging callback from accidentally turning the data into null values. Use map() when callback results should become the new values.
$rows->forEach( fn(Block $row, int|string $key) => logger()->info( 'Processing row', ['key' => $key, 'row' => $row->toArray()], ), );
Callback arguments and item representation
map(), filter(), partition(), forEach(), reduce(), and
groupByFunction() consistently pass the item first and its key second.
Callbacks may omit the key when it is not needed. This shared contract makes
callbacks reusable and removes method-specific argument surprises.
Nested arrays are Block objects by default. After calling iterateBlock(false), the same callback methods receive native arrays instead. Scalar items remain scalar in either mode.
Validating Data
You can validate the data in the Block object with JSON schema. JSON Schema is a vocabulary used to annotate and validate JSON documents.
More info about JSON Schema: https://json-schema.org/learn/getting-started-step-by-step
If you need some common/popular schemas, you can find some schemas here: https://www.schemastore.org/json/ For example:
- Schema for validating the
composer.jsonfile: https://getcomposer.org/schema.json - Schema for validating the GitHub Actions workflows: https://json.schemastore.org/github-workflow.json
Or you can build your own schema according to the JSON schema specifications: https://json-schema.org/learn/getting-started-step-by-step#create-a-schema-definition
$file = "./.github/workflows/run-tests.yml"; $workflow = Block::fromYamlFile($file); $workflow->validateJsonViaUrl( 'https://json.schemastore.org/github-workflow' ); // TRUE if the Block is a valid GitHub Actions Workflow
Or you can define your own schema:
$schemaJson = <<<'JSON' { "type": "array", "items" : { "type": "object", "properties": { "name": { "type": "string" }, "fruit": { "type": "string" }, "wikipedia": { "type": "string" }, "color": { "type": "string" }, "rating": { "type": "number" } } } } JSON;
And then validate it with your Block object:
$fruitsArray = [ [ 'name' => 'Avocado', 'fruit' => 'π₯', 'wikipedia' => 'https://en.wikipedia.org/wiki/Avocado', 'color' => 'green', 'rating' => 8, ], [ 'name' => 'Apple', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Apple', 'color' => 'red', 'rating' => 7, ], [ 'name' => 'Banana', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Banana', 'color' => 'yellow', 'rating' => 8.5, ], [ 'name' => 'Cherry', 'fruit' => 'π', 'wikipedia' => 'https://en.wikipedia.org/wiki/Cherry', 'color' => 'red', 'rating' => 9, ], ]; $data = Block::make($fruitsArray); $data->validateJsonWithSchema($schemaJson); // true if the Block is valid.
If you are starting to use the Data Block and testing it just to gain confidence, implementing different scenarios, or testing a non-valid JSON, try changing the "rating" type from number to integer (the validation should fail because in the JSON, we have ratings with decimals). And, yes, to change on the fly the schema you can use the Block object :)
// load the schema as Block object... $schemaBlock = Block::fromJsonString($schemaJson); // so that you can change the type $schemaBlock->set( "items.properties.rating.type", "integer" ); // the validation should be false because integer vs number $data->validateJsonWithSchema( $schemaBlock->toJson() );
Applying functions
The applyField() method applies a callable function to the value of a specified field and sets the result to another field. This method supports method chaining.
Parameters
key(string|int): The key of the field whose value will be processed.targetKey(string|int): The key where the result of the callable function should be stored.callable(callable): The function to apply to the field value. This function should accept a single argument (the value of the field) and return the processed value.
Returns
self: Returns the instance of the class for method chaining.
Example Usage
<?php // Assuming $object is an instance of the class that contains the applyField method $object ->set('name', 'John Doe') ->applyField('name', 'uppercase_name', function($value) { return strtoupper($value); }); echo $object->get('uppercase_name'); // Outputs: JOHN DOE
Testing
composer test
Upgrading from 1.x to 2.0
Version 2.0 makes callback behavior consistent and separates transformation from side effects:
- Replace transformation-style
forEach()calls withmap(). In 2.0,forEach()ignores callback return values and returns the original block. map(),filter(),partition(),forEach(),reduce(), andgroupByFunction()receive the current item and then its key.groupByFunction()now follows the configured iteration representation: nested arrays areBlockobjects by default and native arrays afteriterateBlock(false).- Rename the
where()named argumentpreseveKeystopreserveKeys. values()now explicitly returns a zero-indexed block.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on reporting security vulnerabilities.
License
The MIT License (MIT). Please see License File for more information.
Thanks to
Thank you to everyone who has provided feedback, opened issues, or created pull requests. A special thanks to all the contributors! You can view the full list of contributors in this section.
The PHP ecosystem offers many tools that help developers enhance productivity, reliability, and efficiency. One such tool is JetBrains PhpStorm. JetBrains supports the open-source community by offering licenses for various open-source projects. More information can be found in the Open Source section of the JetBrains website.
Iβm thrilled to share that JetBrains has provided an Open Source license for the PHP Data Block project. This recognition of PHP Data Block as a valuable open-source software fills me with joy.
Thank you!
