Skip to content

Dot Notation Access

SimpleDto provides powerful get() and set() methods that allow you to access and modify nested Dto properties using dot notation. This makes working with complex, nested data structures much easier.

The get() and set() methods support:

  • Dot notation for nested property access (user.address.city)
  • Wildcards for array operations (emails.*.email)
  • Multi-level nesting with wildcards (employees.*.orders.*.total)
  • Default values for missing properties
  • Immutability - set() returns a new Dto instance

The get() method allows you to retrieve values from your Dto using dot notation.

use event4u\DataHelpers\SimpleDto;
class UserDto extends SimpleDto
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly int $age,
) {}
}
$user = new UserDto(
name: 'John Doe',
email: 'john@example.com',
age: 30
);
// Get simple property
$name = $user->get('name'); // 'John Doe'
$email = $user->get('email'); // 'john@example.com'

You can provide a default value as the second parameter:

// Returns default if property doesn't exist
$phone = $user->get('phone', 'N/A'); // 'N/A'
$country = $user->get('address.country', 'Unknown'); // 'Unknown'

SimpleDto provides strict type-safe getter methods that automatically convert values to the expected type or throw a TypeMismatchException if conversion fails. These methods delegate to the underlying DataAccessor.

These methods return a single value with strict type conversion. They return null if the path doesn’t exist or the value is null.

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\Attributes\AutoCast;
#[AutoCast]
class UserDto extends SimpleDto
{
public function __construct(
public readonly string $name,
public readonly string $age, // stored as string
public readonly string $score, // stored as string
public readonly int $active, // stored as int
) {}
}
$user = UserDto::fromArray([
'name' => 'John Doe',
'age' => '30',
'score' => '95.5',
'active' => 1,
]);
// getString() - returns string or null
$name = $user->getString('name'); // 'John Doe'
$age = $user->getString('age'); // '30'
$missing = $user->getString('phone'); // null
$withDefault = $user->getString('phone', 'N/A'); // 'N/A'
// getInt() - converts to int or returns null
$age = $user->getInt('age'); // 30 (string → int)
$active = $user->getInt('active'); // 1
$missing = $user->getInt('salary'); // null
$withDefault = $user->getInt('salary', 0); // 0
// getFloat() - converts to float or returns null
$score = $user->getFloat('score'); // 95.5 (string → float)
$age = $user->getFloat('age'); // 30.0 (string → float)
// getBool() - converts to bool or returns null
$active = $user->getBool('active'); // true (1 → true)
$inactive = $user->getBool('inactive'); // null
// getArray() - returns array or null
$tags = $user->getArray('tags'); // null (doesn't exist)

Collection getters are designed for wildcard paths and return typed arrays. They work with nested Dtos and array properties.

use event4u\DataHelpers\SimpleDto;
use event4u\DataHelpers\Attributes\AutoCast;
#[AutoCast]
class EmailDto extends SimpleDto
{
public function __construct(
public readonly string $email,
public readonly string $type,
public readonly bool $verified,
) {}
}
#[AutoCast]
class UserDto extends SimpleDto
{
/**
* @param array<int, EmailDto> $emails
*/
public function __construct(
public readonly string $name,
public readonly array $emails,
) {}
}
$user = UserDto::fromArray([
'name' => 'John Doe',
'emails' => [
['email' => 'john@work.com', 'type' => 'work', 'verified' => true],
['email' => 'john@home.com', 'type' => 'home', 'verified' => false],
],
]);
// getStringCollection() - returns array of strings
$addresses = $user->getStringCollection('emails.*.email');
// ['emails.0.email' => 'john@work.com', 'emails.1.email' => 'john@home.com']
$types = $user->getStringCollection('emails.*.type');
// ['emails.0.type' => 'work', 'emails.1.type' => 'home']
// getBoolCollection() - returns array of booleans
$verified = $user->getBoolCollection('emails.*.verified');
// ['emails.0.verified' => true, 'emails.1.verified' => false]

Collection getters work with deeply nested structures:

#[AutoCast]
class OrderDto extends SimpleDto
{
public function __construct(
public readonly int $id,
public readonly string $total, // stored as string
public readonly string $status,
) {}
}
#[AutoCast]
class EmployeeDto extends SimpleDto
{
/**
* @param array<int, OrderDto> $orders
*/
public function __construct(
public readonly string $name,
public readonly array $orders,
) {}
}
#[AutoCast]
class DepartmentDto extends SimpleDto
{
/**
* @param array<int, EmployeeDto> $employees
*/
public function __construct(
public readonly string $name,
public readonly array $employees,
) {}
}
$department = DepartmentDto::fromArray([
'name' => 'Engineering',
'employees' => [
[
'name' => 'Alice',
'orders' => [
['id' => 1, 'total' => '100.50', 'status' => 'pending'],
['id' => 2, 'total' => '250.00', 'status' => 'shipped'],
],
],
[
'name' => 'Bob',
'orders' => [
['id' => 3, 'total' => '75.25', 'status' => 'pending'],
],
],
],
]);
// Get all order totals as floats
$totals = $department->getFloatCollection('employees.*.orders.*.total');
// ['employees.0.orders.0.total' => 100.5, 'employees.0.orders.1.total' => 250.0, 'employees.1.orders.0.total' => 75.25]
// Get all order IDs as integers
$ids = $department->getIntCollection('employees.*.orders.*.id');
// ['employees.0.orders.0.id' => 1, 'employees.0.orders.1.id' => 2, 'employees.1.orders.0.id' => 3]
// Get all order statuses as strings
$statuses = $department->getStringCollection('employees.*.orders.*.status');
// ['employees.0.orders.0.status' => 'pending', 'employees.0.orders.1.status' => 'shipped', 'employees.1.orders.0.status' => 'pending']

Type-safe getters throw TypeMismatchException when:

  1. Single value getters receive an array (use collection getters instead)
  2. Collection getters are used without wildcards in the path
  3. Any getter receives a value that cannot be converted to the expected type
use event4u\DataHelpers\Exceptions\TypeMismatchException;
$user = UserDto::fromArray([
'name' => 'John',
'age' => 'invalid',
'emails' => [
['email' => 'john@work.com'],
['email' => 'john@home.com'],
],
]);
// ❌ Throws TypeMismatchException - array returned for single value getter
try {
$emails = $user->getInt('emails.*.email');
} catch (TypeMismatchException $e) {
// Use collection getter instead
$emails = $user->getStringCollection('emails.*.email');
}
// ❌ Throws TypeMismatchException - no wildcard in path
try {
$emails = $user->getStringCollection('emails.0.email');
} catch (TypeMismatchException $e) {
// Use single value getter instead
$email = $user->getString('emails.0.email');
}
// ❌ Throws TypeMismatchException - cannot convert to int
try {
$age = $user->getInt('age'); // 'invalid' → int fails
} catch (TypeMismatchException $e) {
// Handle error or use default
$age = $user->getInt('age', 0); // Still throws!
// Better: check value first or use get()
$age = $user->get('age', 0);
}

Access nested Dto properties using dot notation:

class AddressDto extends SimpleDto
{
public function __construct(
public readonly string $street,
public readonly string $city,
public readonly string $country,
) {}
}
class UserDto extends SimpleDto
{
public function __construct(
public readonly string $name,
public readonly AddressDto $address,
) {}
}
$user = new UserDto(
name: 'John Doe',
address: new AddressDto(
street: 'Main St',
city: 'New York',
country: 'USA'
)
);
// Access nested properties
$city = $user->get('address.city'); // 'New York'
$country = $user->get('address.country'); // 'USA'

Use wildcards (*) to access values from arrays:

class EmailDto extends SimpleDto
{
public function __construct(
public readonly string $email,
public readonly string $type,
public readonly bool $verified = false,
) {}
}
class UserDto extends SimpleDto
{
/**
* @param array<int, EmailDto> $emails
*/
public function __construct(
public readonly string $name,
public readonly array $emails,
) {}
}
$user = new UserDto(
name: 'John Doe',
emails: [
new EmailDto(email: 'john@work.com', type: 'work', verified: true),
new EmailDto(email: 'john@home.com', type: 'home', verified: false),
]
);
// Get all email addresses
$addresses = $user->get('emails.*.email');
// ['john@work.com', 'john@home.com']
// Get all verified flags
$verified = $user->get('emails.*.verified');
// [true, false]

Combine multiple wildcards for deeply nested structures:

class OrderDto extends SimpleDto
{
public function __construct(
public readonly int $id,
public readonly float $total,
public readonly string $status,
) {}
}
class EmployeeDto extends SimpleDto
{
/**
* @param array<int, EmailDto> $emails
* @param array<int, OrderDto> $orders
*/
public function __construct(
public readonly string $name,
public readonly array $emails,
public readonly array $orders,
) {}
}
class DepartmentDto extends SimpleDto
{
/**
* @param array<int, EmployeeDto> $employees
*/
public function __construct(
public readonly string $name,
public readonly array $employees,
) {}
}
$department = new DepartmentDto(
name: 'Engineering',
employees: [
new EmployeeDto(
name: 'Alice',
emails: [
new EmailDto(email: 'alice@work.com', type: 'work', verified: true),
],
orders: [
new OrderDto(id: 1, total: 100.50, status: 'pending'),
new OrderDto(id: 2, total: 250.00, status: 'shipped'),
]
),
new EmployeeDto(
name: 'Bob',
emails: [
new EmailDto(email: 'bob@work.com', type: 'work', verified: false),
],
orders: [
new OrderDto(id: 3, total: 75.25, status: 'pending'),
]
),
]
);
// Get all employee emails
$allEmails = $department->get('employees.*.emails.*.email');
// ['alice@work.com', 'bob@work.com']
// Get all order totals
$allTotals = $department->get('employees.*.orders.*.total');
// [100.50, 250.00, 75.25]
// Get all order statuses
$allStatuses = $department->get('employees.*.orders.*.status');
// ['pending', 'shipped', 'pending']

The set() method allows you to update Dto properties using dot notation.

Important: set() only works with mutable (non-readonly) properties. It modifies the instance in-place and returns void.

use Tests\Utils\Docu\Dtos\UserDto;
$user = new UserDto(
name: 'John Doe',
email: 'john@example.com',
age: 30
);
// Set simple property - modifies in-place
$user->set('name', 'Jane Doe');
echo $user->name; // 'Jane Doe' (modified in-place)

Update nested Dto properties:

use Tests\Utils\Docu\Dtos\UserDto;
use Tests\Utils\Docu\Dtos\AddressDto;
$user = new UserDto(
name: 'John Doe',
address: new AddressDto(
street: 'Main St',
city: 'New York',
country: 'USA'
)
);
// Update nested property - modifies in-place
$user->set('address.city', 'Los Angeles');
echo $user->get('address.city'); // 'Los Angeles' (modified)

Update all items in an array using wildcards:

$user = new UserWithEmailsDto(
name: 'John Doe',
emails: [
new EmailDto(email: 'john@work.com', type: 'work', verified: false),
new EmailDto(email: 'john@home.com', type: 'home', verified: false),
]
);
// Verify all emails at once - modifies in-place
$user->set('emails.*.verified', true);
$verified = $user->get('emails.*.verified');
// Result: [true, true]

Update deeply nested values:

class OrderDto extends SimpleDto
{
public function __construct(
public readonly int $id,
public readonly float $total,
public readonly string $status,
) {}
}
class EmployeeDto extends SimpleDto
{
/**
* @param array<int, OrderDto> $orders
*/
public function __construct(
public readonly string $name,
public readonly array $emails,
public readonly array $orders,
) {}
}
class DepartmentDto extends SimpleDto
{
/**
* @param array<int, EmployeeDto> $employees
*/
public function __construct(
public readonly string $name,
public readonly array $employees,
) {}
}
$department = new DepartmentDto(
name: 'Sales',
employees: [
new EmployeeDto(
name: 'Charlie',
emails: [],
orders: [
new OrderDto(id: 1, total: 100.50, status: 'pending'),
new OrderDto(id: 2, total: 250.00, status: 'pending'),
]
),
new EmployeeDto(
name: 'Diana',
emails: [],
orders: [
new OrderDto(id: 3, total: 75.25, status: 'pending'),
]
),
]
);
// Ship all orders at once - modifies in-place
$department->set('employees.*.orders.*.status', 'shipped');
$statuses = $department->get('employees.*.orders.*.status');
// ['shipped', 'shipped', 'shipped'] - all orders shipped

Since set() modifies in-place and returns void, you need to call it multiple times:

use Tests\Utils\Docu\Dtos\UserDto;
$user = new UserDto(
name: 'John Doe',
email: 'john@example.com',
age: 30
);
// Multiple updates - each modifies in-place
$user->set('name', 'Jane Doe');
$user->set('age', 25);
$user->set('email', 'jane@example.com');
// All properties modified
echo $user->name; // 'Jane Doe'
echo $user->age; // 25
echo $user->email; // 'jane@example.com'
use Tests\Utils\Docu\Dtos\UserDto;
$user = new UserDto(name: 'John', email: 'john@example.com', age: 30);
// Returns null for non-existent paths
$result = $user->get('nonexistent'); // null
// Use default value
$result = $user->get('nonexistent', 'default'); // 'default'
$user = new UserWithEmailsDto(name: 'John', emails: []);
// Wildcard on empty array returns empty array
$emails = $user->get('emails.*.email');
// Result: []
// Set on empty array returns unchanged Dto
$updated = $user->set('emails.*.verified', true);
// $updated->emails is still []

You can access array elements by numeric index:

$user = new UserWithEmailsDto(
name: 'John',
emails: [
new EmailDto(email: 'first@example.com', type: 'work', verified: false),
new EmailDto(email: 'second@example.com', type: 'home', verified: false),
]
);
// Access by index
$first = $user->get('emails.0.email'); // 'first@example.com'
$second = $user->get('emails.1.email'); // 'second@example.com'
// Update by index
$updated = $user->set('emails.0.verified', true);
// Only first email is verified
  • get() and set() use the underlying DataAccessor and DataMutator classes
  • Both methods convert the Dto to an array recursively
  • set() creates a new Dto instance (immutability)
  • For bulk operations, consider using DataMapper or DataMutator directly