constlab/data-transfer-object
A lightweight data transfer object package for Laravel and PHP. Define typed DTOs, hydrate them from arrays or requests, and access properties safely with casting and validation support for cleaner, more predictable application code.
Install and confirm the version
composer require <vendor>/<package>:^0.0.2
v0.0.2 is the first release with boolean support, so pin to ^0.0.2 rather than * — anything below this will silently fail to hydrate bool properties, and v0.0.x makes no semver guarantees about patch-level behaviour.
Where to look first
README.md — the supported-types table is the contract you code against. Confirm exactly which types are listed now that bool has been added.tests/ — for a 0.0.x package, the test suite is a more reliable spec than the docs. Look for the boolean fixtures added in this release; they will show you which input literals (true, "true", 1, "1", "on") are treated as truthy.src/ type-resolution class — the place a bool branch was added. Skim it once so you know where to hook in a custom type later.First use case
The canonical day-one use case is a settings or DTO object whose flags come from a source that is not a PHP boolean — a config array, an environment variable, a JSON payload, or a request body:
class NotificationSettings
{
public bool $emailEnabled = true;
public bool $smsEnabled = false;
public ?bool $digestEnabled = null;
}
Before v0.0.2 you would have had to declare these as int/string and cast by hand, or skip typing entirely. Now you can declare the real type and let the package do the coercion.
Start with a single object, hydrate it from config(), and assert the result in a test before rolling it out across the codebase.
Hydrating flags from config and env
Config values and env() results are strings far more often than developers expect. Treat the package as the boundary layer that normalises them:
// config/features.php
return [
'new_dashboard' => env('FEATURE_NEW_DASHBOARD', false),
];
$features = FeatureFlags::from(config('features'));
$features->newDashboard; // real bool, not "false" string
This is the highest-value pattern: it removes a class of if ($flag === 'false') bugs that appear when an env var is set as a string.
Request payloads and checkboxes
HTML checkboxes are the classic trap — an unchecked box submits nothing, not false. Pair the package with a defaulted property and a normalising step:
class UpdatePreferencesRequest extends FormRequest
{
public function rules(): array
{
return ['newsletter' => ['sometimes', 'boolean']];
}
protected function prepareForValidation(): void
{
$this->merge(['newsletter' => $this->boolean('newsletter')]);
}
}
Then declare public bool $newsletter = false; on the DTO. The default covers the absent-key case, and the validation rule covers the malformed-value case. Do the normalisation in one of those places, not both — double-coercion makes failures hard to trace.
Nested and nullable flags
Use ?bool when "not specified" is semantically different from "off" — for example, a PATCH endpoint where null means "leave unchanged" and false means "explicitly disable". Do not use ?bool as a lazy default; it pushes a null check onto every consumer.
class PatchUser
{
public ?bool $isActive = null; // null = untouched
}
JSON / API boundary objects
When decoding third-party JSON, decode to arrays first (not objects), then map. Booleans arriving as 1/0 from a legacy service are the exact case this release targets; assert the mapping in a test with a real captured payload rather than a hand-written fixture.
Testing pattern
Build a small data provider per boolean literal you accept. This is cheap insurance against a patch release changing coercion semantics:
public static function truthy(): array
{
return [[true], [1], ['1'], ['true'], ['on']];
}
(bool) "false" is true. If the package's boolean handling uses a naive PHP cast anywhere, the string "false" (env var, query string, form field) becomes true. The release notes do not specify which literals are supported, so verify this explicitly in a test before trusting it in production. If it fails, wrap the input with filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) before handing it over.
Missing key vs. null vs. false. These are three different states. A typed non-nullable bool with no default will either throw or silently become false depending on implementation — decide which you want per property and set defaults accordingly. declare(strict_types=1) in your own files does not protect you here, because the coercion happens inside the package.
Config caching. If the package reads configuration at construction time, php artisan config:cache can freeze a value you expected to change at runtime. Clear the cache after deploying changes, and never call env() outside config/ in code that feeds this package.
v0.0.x stability. Two releases in, expect the type-resolution rules to shift. Anything you depend on — which literals are truthy, what happens on a missing key — belongs in your own test suite, not just in your head. Consider a thin wrapper class around the package so a future breaking change is a one-file fix.
Debugging a bad cast. Reach for tinker and hydrate the object with a single known-bad value. If the failure is opaque, check whether the package validates types before assignment or relies on PHP's coercion; the error message will differ substantially between the two.
Extension point. If you need Carbon, enums, or backed enums for flags, the type-resolution class is the place to add them — but register custom handling in a service provider rather than editing vendor code, and re-check it against the changelog on every bump through 0.1.0.
How can I help you explore Laravel packages today?