jbzoo/data
JBZoo Data is a lightweight PHP library that wraps arrays and objects in a convenient Data class. Access nested values with dot notation, convert between formats, and simplify configuration, JSON, and request data handling in any PHP project.
Install it like any other Composer library — there is no Laravel service provider, no config file, and no facade to publish:
composer require jbzoo/data
The package is a thin, opinionated extension of ArrayObject. The entry point you will use 95% of the time is JBZoo\Data\Data:
use JBZoo\Data\Data;
$config = new Data([
'app' => ['name' => 'Acme', 'debug' => false],
'queue' => ['driver' => 'redis', 'retries' => 3],
]);
$config->get('app.name'); // null - get() is a flat lookup
$config->find('app.name'); // 'Acme' - find() understands dot notation
$config->getInt('queue.retries'); // typed getters with defaults
$config->set('app.debug', true);
Where to look first:
src/Data.php — the core API (get, set, has, find, typed getters, getData, setData).src/File.php + src/JSON.php, src/PHPArray.php, src/INI.php, src/Yml.php — file-backed collections that read on construction and persist with save().tests/ — the fastest way to see the intended semantics of find(), read-only mode, and magic property access.The first real use case in a Laravel app: wrap third-party or per-tenant configuration that does not belong in config/ (because it changes at runtime, comes from a DB, or ships with a package). For example, a JSON file in storage/ that feature flags live in:
use JBZoo\Data\JSON;
$flags = new JSON(storage_path('app/features.json'), true); // read-only
if ($flags->getBool('new_ui', false)) {
// ...
}
Use it as a config value object, not a replacement for config(). Laravel's config() is already cached and env-aware. Reach for jbzoo/data when you need a mutable, serializable, self-validating config object scoped to a tenant, a job payload, or a package.
Defaults + user overrides. A common pattern is to merge package defaults with user config:
$defaults = new Data(['retries' => 3, 'timeout' => 10]);
$user = new Data($request->validated()['queue'] ?? []);
$defaults->setData(array_merge($defaults->getData(), $user->getData()));
$retries = $defaults->getInt('retries');
Note this is a shallow merge — for nested config, write a small recursive merge helper or keep the nesting one level deep.
Dot-notation lookups with find(). find('services.redis.host') is the ergonomic way to read nested arrays without null-checks:
$host = $config->find('services.redis.host', '127.0.0.1');
Typed getters as guards. getInt, getFloat, getString, getBool, getArray double as lightweight validation. In a job or controller, calling getInt('queue.retries') and getting a cast value is safer than trusting an untyped array offset.
Read-only config for singletons. Bind read-only instances in the container so accidental writes fail loudly:
$this->app->singleton('tenant.config', function ($app) {
return new JSON(storage_path('app/tenants/'.$app->id().'.json'), true);
});
Any set() or remove() will throw JBZoo\Data\Exception, which surfaces bugs at the call site instead of silently diverging config.
File-backed persistence for admin-editable settings. JSON, PHPArray, INI, and Yml all read on construction. Mutate, then save():
$settings = new JSON(storage_path('app/settings.json'));
$settings->set('mail.from', '[email protected]');
$settings->save();
PHPArray::save() writes <?php return [...];, which is opcache-friendly — a good choice for generated config that is read on every request.
Conversion for cache/API responses. Since Data implements JsonSerializable and __toString() returns JSON, you can json_encode($data) or (string) $data directly. toJSON() is there when you need the raw string explicitly. Use this when caching resolved config in Redis:
Cache::put('tenant.42.config', $data->toJSON(), now()->addHour());
Composition over inheritance. For repeated shapes (e.g., a payment gateway config), subclass Data and add domain getters. It stays ArrayObject-compatible while giving you type safety at the edges.
get() is flat; find() is dotted. This trips people up constantly. $data->get('a.b') looks for a literal key named a.b, while $data->find('a.b') walks the tree. Pick one convention per codebase.find() resolves dot notation by splitting on ., so a key that literally contains a dot becomes ambiguous. Keep keys snake_case or kebab-case and reserve dots for traversal.$data->app['debug'] = true will not persist, because the magic getter / offsetGet returns a copy. Copy out, mutate, copy back: $app = $data->getArray('app'); $app['debug'] = true; $data->set('app', $app);.getData() returns an array copy. Mutating it does nothing to the object. Use setData() / set() to write.save() will fail if the parent directory is missing or unwritable. Ensure storage_path('app') (or your chosen path) exists and check permissions in deployment scripts. Wrap save() in try/catch for JBZoo\Data\Exception.INI and Yml are notorious for returning "1", "true", and 1 inconsistently. Always go through typed getters (getBool, getInt) rather than trusting raw values.ArrayObject semantics, but nested nulls and empty objects ({} vs []) can surprise you on round-trip.new Data() copies the array; this is config-sized data, not a query result set or a per-request payload of thousands of rows.dd($data->getData()) and var_export($data->getData(), true) are your friends. Since __toString() returns JSON, (string) $data is a quick log-friendly dump.validate() method in a subclass. For serialization tweaks, override jsonSerialize(). For persistence, override save() in a file-backed subclass rather than wrapping it externally.config() for jbzoo/data globally — you'll lose config:cache benefits and env resolution. Use it for the dynamic layer: tenant settings, feature flags, and import/export jobs where the object's mutation and conversion API genuinely pays off.How can I help you explore Laravel packages today?