Skip to content

DataAccessor

DataAccessor provides a uniform way to read values from nested data structures including arrays, objects, Laravel Collections, and Eloquent Models. It supports dot-notation paths, numeric indices, and powerful wildcard operations.

use Event4u\DataHelpers\DataAccessor;
$data = [
'users' => [
['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 30],
['name' => 'Bob', 'email' => 'bob@example.com', 'age' => 25],
['name' => 'Charlie', 'email' => 'charlie@example.com', 'age' => 35],
],
];
$accessor = new DataAccessor($data);
// Simple path
$name = $accessor->get('users.0.name');
// Returns: 'Alice'
// Wildcard - extract all emails
$emails = $accessor->get('users.*.email');
// Returns: [
// 'users.0.email' => 'alice@example.com',
// 'users.1.email' => 'bob@example.com',
// 'users.2.email' => 'charlie@example.com',
// ]
// Default value
$country = $accessor->get('users.0.country', 'Unknown');
// Returns: 'Unknown' (path doesn't exist)

DataAccessor works with multiple data types:

  • Arrays - Nested arrays with any depth
  • Objects - Plain PHP objects with public properties
  • DTOs - Data Transfer Objects
  • Laravel Collections - Illuminate\Support\Collection
  • Eloquent Models - Including relationships
  • Arrayable - Any object implementing Arrayable
  • JsonSerializable - Any object implementing JsonSerializable
  • JSON strings - Automatically parsed
  • XML strings - Automatically parsed
use Event4u\DataHelpers\DataAccessor;
// From array
$accessor = new DataAccessor($array);
// From object
$accessor = new DataAccessor($object);
// From Collection
$accessor = new DataAccessor($collection);
// From Eloquent Model
$accessor = new DataAccessor($model);
// From JSON string
$accessor = new DataAccessor('{"user":{"name":"Alice"}}');
// From XML string
$accessor = new DataAccessor('<user><name>Alice</name></user>');
$data = [
'user' => [
'profile' => [
'name' => 'John Doe',
'email' => 'john@example.com',
],
],
];
$accessor = new DataAccessor($data);
// Dot-notation path
$name = $accessor->get('user.profile.name');
// Returns: 'John Doe'
// Non-existent path returns null
$phone = $accessor->get('user.profile.phone');
// Returns: null
// Provide default value as second parameter
$phone = $accessor->get('user.profile.phone', 'N/A');
// Returns: 'N/A' (path doesn't exist)
$name = $accessor->get('user.profile.name', 'Anonymous');
// Returns: 'John Doe' (path exists, default ignored)

Wildcards allow you to extract values from multiple items at once.

$data = [
'users' => [
['email' => 'alice@example.com'],
['email' => 'bob@example.com'],
['email' => 'charlie@example.com'],
],
];
$accessor = new DataAccessor($data);
$emails = $accessor->get('users.*.email');
// Returns associative array with full paths as keys:
// [
// 'users.0.email' => 'alice@example.com',
// 'users.1.email' => 'bob@example.com',
// 'users.2.email' => 'charlie@example.com',
// ]

The full path keys are intentional and provide:

  1. Stability - Keys remain consistent across operations
  2. Traceability - You know exactly where each value came from
  3. Integration - DataMutator and DataMapper consume this format
  4. Uniqueness - No key collisions in complex structures
$data = [
'users' => [
['email' => 'alice@example.com'],
['email' => null],
['email' => 'bob@example.com'],
],
];
$accessor = new DataAccessor($data);
$emails = $accessor->get('users.*.email');
// Returns:
// [
// 'users.0.email' => 'alice@example.com',
// 'users.1.email' => null,
// 'users.2.email' => 'bob@example.com',
// ]
// Filter out nulls if needed
$validEmails = array_filter($emails, fn($v) => $v !== null);
// [
// 'users.0.email' => 'alice@example.com',
// 'users.2.email' => 'bob@example.com',
// ]

Multiple wildcards in one path create a flat associative array with full dot-path keys.

$data = [
'users' => [
[
'name' => 'Alice',
'addresses' => [
'home' => ['city' => 'Berlin'],
'work' => ['city' => 'Hamburg'],
],
],
[
'name' => 'Bob',
'addresses' => [
'home' => ['city' => 'Munich'],
],
],
],
];
$accessor = new DataAccessor($data);
$cities = $accessor->get('users.*.addresses.*.city');
// Returns:
// [
// 'users.0.addresses.home.city' => 'Berlin',
// 'users.0.addresses.work.city' => 'Hamburg',
// 'users.1.addresses.home.city' => 'Munich',
// ]
$data = [
'departments' => [
[
'users' => [
['posts' => [['title' => 'Post 1'], ['title' => 'Post 2']]],
['posts' => [['title' => 'Post 3']]],
],
],
[
'users' => [
['posts' => [['title' => 'Post 4']]],
],
],
],
];
$accessor = new DataAccessor($data);
$titles = $accessor->get('departments.*.users.*.posts.*.title');
// Returns:
// [
// 'departments.0.users.0.posts.0.title' => 'Post 1',
// 'departments.0.users.0.posts.1.title' => 'Post 2',
// 'departments.0.users.1.posts.0.title' => 'Post 3',
// 'departments.1.users.0.posts.0.title' => 'Post 4',
// ]

DataAccessor seamlessly handles Laravel Collections.

use Illuminate\Support\Collection;
$data = [
'users' => collect([
['name' => 'Alice', 'email' => 'alice@example.com'],
['name' => 'Bob', 'email' => 'bob@example.com'],
]),
];
$accessor = new DataAccessor($data);
$emails = $accessor->get('users.*.email');
// Returns:
// [
// 'users.0.email' => 'alice@example.com',
// 'users.1.email' => 'bob@example.com',
// ]
$data = [
'orders' => collect([
[
'items' => collect([
['sku' => 'A', 'price' => 10],
['sku' => 'B', 'price' => 20],
]),
],
[
'items' => collect([
['sku' => 'C', 'price' => 30],
]),
],
]),
];
$accessor = new DataAccessor($data);
$skus = $accessor->get('orders.*.items.*.sku');
// Returns:
// [
// 'orders.0.items.0.sku' => 'A',
// 'orders.0.items.1.sku' => 'B',
// 'orders.1.items.0.sku' => 'C',
// ]
$data = [
'users' => collect([
['name' => 'Alice'],
['name' => 'Bob'],
]),
];
$accessor = new DataAccessor($data);
// Access specific index
$firstUser = $accessor->get('users.0.name');
// Returns: 'Alice'
// Wildcard still works
$allNames = $accessor->get('users.*.name');
// Returns: ['users.0.name' => 'Alice', 'users.1.name' => 'Bob']

DataAccessor works with Eloquent Models and their relationships.

$user = User::find(1);
$accessor = new DataAccessor($user);
$name = $accessor->get('name');
$email = $accessor->get('email');
$user = User::with('posts.comments')->first();
$accessor = new DataAccessor($user);
// Access relationship
$postTitles = $accessor->get('posts.*.title');
// Deep relationship access
$commentTexts = $accessor->get('posts.*.comments.*.text');
$users = User::with('posts')->get();
$accessor = new DataAccessor(['users' => $users]);
// Extract all post titles from all users
$allPostTitles = $accessor->get('users.*.posts.*.title');

DataAccessor automatically parses JSON and XML strings.

$json = '{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}';
$accessor = new DataAccessor($json);
$names = $accessor->get('users.*.name');
// Returns: ['users.0.name' => 'Alice', 'users.1.name' => 'Bob']
$firstAge = $accessor->get('users.0.age');
// Returns: 30
$xml = '<users><user><name>Alice</name></user><user><name>Bob</name></user></users>';
$accessor = new DataAccessor($xml);
$names = $accessor->get('users.user.*.name');
// Returns parsed XML as array structure
$data = [
'departments' => [
['users' => [['email' => 'a@x.com'], ['email' => 'b@x.com']]],
['users' => [['email' => 'c@x.com']]],
],
];
$accessor = new DataAccessor($data);
$emails = $accessor->get('departments.*.users.*.email');
// Returns:
// [
// 'departments.0.users.0.email' => 'a@x.com',
// 'departments.0.users.1.email' => 'b@x.com',
// 'departments.1.users.0.email' => 'c@x.com',
// ]
$accessor = new DataAccessor($config);
// Always provide sensible defaults
$theme = $accessor->get('app.settings.theme', 'default');
$timeout = $accessor->get('app.settings.timeout', 30);
$debug = $accessor->get('app.settings.debug', false);
$accessor = new DataAccessor($data);
$prices = $accessor->get('products.*.price');
// Calculate total
$totalPrice = array_sum($prices);
// Calculate average
$avgPrice = count($prices) > 0 ? array_sum($prices) / count($prices) : 0;
// Find max/min
$maxPrice = max($prices);
$minPrice = min($prices);
$data = [
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 25],
];
$accessor = new DataAccessor($data);
// Access specific index
$firstUser = $accessor->get('0.name');
// Returns: 'Alice'
// Use wildcard at root level
$allNames = $accessor->get('*.name');
// Returns: ['0.name' => 'Alice', '1.name' => 'Bob']
$data = ['users' => [
['email' => 'alice@x.com'],
['email' => null],
['email' => 'bob@x.com'],
]];
$accessor = new DataAccessor($data);
$emails = $accessor->get('users.*.email');
// Filter out nulls
$validEmails = array_filter($emails, fn($v) => $v !== null);
// Returns: ['users.0.email' => 'alice@x.com', 'users.2.email' => 'bob@x.com']
// Get only values (remove keys)
$emailList = array_values($validEmails);
// Returns: ['alice@x.com', 'bob@x.com']

When you need to extract the same field from multiple items, wildcards are more efficient than looping:

// ❌ Inefficient
$emails = [];
foreach ($data['users'] as $user) {
$emails[] = $user['email'];
}
// ✅ Efficient
$accessor = new DataAccessor($data);
$emails = $accessor->get('users.*.email');

Use DataAccessor to read values and DataMutator to write them into a new structure:

$accessor = new DataAccessor($sourceData);
$emails = $accessor->get('users.*.email');
$mutator = new DataMutator([]);
$mutator->set('contacts.*.email', $emails);

Avoid null checks by providing sensible defaults:

// ❌ Requires null check
$theme = $accessor->get('settings.theme');
if ($theme === null) {
$theme = 'default';
}
// ✅ Clean and safe
$theme = $accessor->get('settings.theme', 'default');

DataAccessor works seamlessly with Laravel Collections:

$accessor = new DataAccessor($data);
$prices = $accessor->get('products.*.price');
// Convert to Collection for chaining
$collection = collect($prices);
$filtered = $collection->filter(fn($p) => $p > 100)->values();

DataAccessor provides methods to analyze the structure of your data with type information.

The getStructure() method returns a flat array with dot-notation paths and type information:

use Event4u\DataHelpers\DataAccessor;
$data = [
'name' => 'John Doe',
'age' => 30,
'emails' => [
['email' => 'john@work.com', 'type' => 'work', 'verified' => true],
['email' => 'john@home.com', 'type' => 'home', 'verified' => false],
],
];
$accessor = new DataAccessor($data);
$structure = $accessor->getStructure();
// Returns:
// [
// 'name' => 'string',
// 'age' => 'int',
// 'emails' => 'array',
// 'emails.*' => 'array',
// 'emails.*.email' => 'string',
// 'emails.*.type' => 'string',
// 'emails.*.verified' => 'bool',
// ]

The getStructureMultidimensional() method returns a nested array structure:

$accessor = new DataAccessor($data);
$structure = $accessor->getStructureMultidimensional();
// Returns:
// [
// 'name' => 'string',
// 'age' => 'int',
// 'emails' => [
// '*' => [
// 'email' => 'string',
// 'type' => 'string',
// 'verified' => 'bool',
// ],
// ],
// ]

Arrays use wildcards (*) to represent the structure of all elements:

$data = [
'departments' => [
[
'name' => 'Engineering',
'employees' => [
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 25],
],
],
[
'name' => 'Sales',
'employees' => [
['name' => 'Charlie', 'age' => 35],
],
],
],
];
$accessor = new DataAccessor($data);
$structure = $accessor->getStructure();
// Returns:
// [
// 'departments' => 'array',
// 'departments.*' => 'array',
// 'departments.*.name' => 'string',
// 'departments.*.employees' => 'array',
// 'departments.*.employees.*' => 'array',
// 'departments.*.employees.*.name' => 'string',
// 'departments.*.employees.*.age' => 'int',
// ]

When array elements have different types, union types are returned:

$data = [
'values' => [
'string value',
42,
null,
true,
],
];
$accessor = new DataAccessor($data);
$structure = $accessor->getStructure();
// Returns:
// [
// 'values' => 'array',
// 'values.*' => 'bool|int|null|string',
// ]

Objects are returned with their full namespace:

use Event4u\DataHelpers\SimpleDTO;
class EmailDTO extends SimpleDTO
{
public function __construct(
public readonly string $email,
public readonly bool $verified,
) {}
}
$data = [
'contact' => new EmailDTO('john@example.com', true),
];
$accessor = new DataAccessor($data);
$structure = $accessor->getStructure();
// Returns:
// [
// 'contact' => '\EmailDTO',
// 'contact.email' => 'string',
// 'contact.verified' => 'bool',
// ]

Structure introspection is useful for:

  • API Documentation - Generate API schemas automatically
  • Validation - Verify data structure matches expectations
  • Type Checking - Ensure data types are correct
  • Debugging - Understand complex data structures
  • Code Generation - Generate TypeScript interfaces or PHP classes
  • Testing - Verify data structure in tests
// Example: Validate API response structure
$accessor = new DataAccessor($apiResponse);
$structure = $accessor->getStructure();
$expectedStructure = [
'status' => 'string',
'data' => 'array',
'data.users' => 'array',
'data.users.*' => 'array',
'data.users.*.id' => 'int',
'data.users.*.name' => 'string',
'data.users.*.email' => 'string',
];
foreach ($expectedStructure as $path => $expectedType) {
if (!isset($structure[$path]) || $structure[$path] !== $expectedType) {
throw new Exception("Invalid structure at path: $path");
}
}
  • Wildcards traverse all matching elements
  • Performance scales with the number of matches
  • For large datasets, consider filtering data first
// ❌ Slow on large datasets
$accessor = new DataAccessor($hugeDataset);
$allEmails = $accessor->get('users.*.email');
// ✅ Filter first
$activeUsers = array_filter($hugeDataset['users'], fn($u) => $u['active']);
$accessor = new DataAccessor(['users' => $activeUsers]);
$emails = $accessor->get('users.*.email');

Multiple wildcards can be expensive on large nested structures:

// Can be slow on large datasets
$accessor->get('departments.*.teams.*.users.*.email');
// Consider limiting depth or filtering

DataAccessor uses internal caching for path resolution, so repeated calls with the same path are fast:

$accessor = new DataAccessor($data);
// First call parses path
$value1 = $accessor->get('user.profile.name');
// Subsequent calls use cached path (fast)
$value2 = $accessor->get('user.profile.name');

The following working examples demonstrate DataAccessor in action:

All examples are fully tested and can be run directly:

Terminal window
php examples/main-classes/data-accessor/basic-usage.php
php examples/main-classes/data-accessor/structure-introspection.php

The DataAccessor functionality is thoroughly tested. Key test files:

Unit Tests:

Integration Tests:

Run the tests:

Terminal window
# Run all DataAccessor tests
task test:unit -- --filter=DataAccessor
# Run specific test file
vendor/bin/pest tests/Unit/DataAccessor/DataAccessorTest.php