Search by

hi-folks / data-block

roberto

Data class for managing nested arrays and JSON data.

Package info

github.com/Hi-Folks/data-block

pkg:composer/hi-folks/data-block

Statistics

Installs: 3 596

Dependents: 0

Suggesters: 0

Stars: 145

Open Issues: 3

v2.1.0 2026-09-20 21:27 UTC

README

PHP Data Block package

Data Block Package

Latest Version Total Downloads
Packagist License Supported PHP Versions GitHub last commit
Tests

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:

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 the Operator class

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 to 0.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 Operator class is defined in the use HiFolks\DataType\Enums\Operator namespace.

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 empty Block.
  • skip(5) returns everything after the first five items.
  • A negative value passed to skip() throws InvalidArgumentException because its meaning would be ambiguous.
  • slice() follows PHP's array_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. Use filter() 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:

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 with map(). In 2.0, forEach() ignores callback return values and returns the original block.
  • map(), filter(), partition(), forEach(), reduce(), and groupByFunction() receive the current item and then its key.
  • groupByFunction() now follows the configured iteration representation: nested arrays are Block objects by default and native arrays after iterateBlock(false).
  • Rename the where() named argument preseveKeys to preserveKeys.
  • 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.

PhpStorm logo

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!

Roberto