Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Parameterbag Laravel Package

juststeveking/parameterbag

A simple, framework-agnostic parameter bag for PHP. Wrap arrays of data in a typed object and access, validate, and manipulate values through a clean, readable API—ideal for request parameters, configuration, or DTO-style payloads.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (PHP 8.0+ required):

composer require juststeveking/parameterbag:^2.0

Pin the major line — do not float. The package has been quiet since mid-2021 and a floating constraint buys you nothing but surprise.

Look here first, in order:

  1. src/ParameterBag.php — the whole package is essentially this one class. Read it end-to-end (it's a page) before writing code against it. The README covers the happy path; the class tells you exactly what get, set, and has do with edge inputs.
  2. composer.json → the require block. Confirm whether it constrains illuminate/* or just php. That constraint, not the PHP floor, decides whether it resolves in your app. Run composer why-not juststeveking/parameterbag ^2.0 if it fights.
  3. CHANGELOG / release/2.0.0.md — two lines only. Read the actual diff before upgrading anything that already depends on 1.x.

First use case — replace a loose array at a seam. The canonical win is a class that currently accepts array $payload and does $payload['user']['email'] ?? null all over its body:

use JustSteveKing\ParameterBag\ParameterBag;

final class HandleWebhook
{
    public function __construct(private ParameterBag $payload) {}

    public function handle(): void
    {
        $email  = $this->payload->get('user.email');     // dot notation
        $source = $this->payload->get('meta.source', 'unknown'); // default
    }
}
$handler = new HandleWebhook(
    new ParameterBag($request->validated())
);

That is the entire first use case: one seam, one test, observe. Hours, not days.

Implementation Patterns

Wrap at the boundary, not in the middle. Construct the bag where data enters your domain — a controller, a job's handle(), a webhook listener, a service consuming a third-party API response — and pass the bag inward. Constructing it deep in a service defeats the point; you want the named type visible in signatures so callers know what they're handing over.

// In the controller/job
$params = new ParameterBag($response->json()['data']);

dispatch(new SyncContact($params));

Use dot notation as the shape seam. Everything that reads nested payload keys goes through get('a.b.c'). When a partner API revises its contract (v2 nests user under data, say), you change the construction site — or add a small normalizer that reshapes the array before constructing the bag — instead of hunting string keys across call sites.

Always supply a default where the value is optional. get($key) returning null vs throwing is the kind of ambiguity that produces scattered ?? fallbacks. Push the default to the call site: get('locale', 'en').

Keep an explicit serialization boundary. When you cross back out of the domain (queue payload, JSON response, Eloquent attribute), call all() and let Laravel serialize a plain array. Do not hand a ParameterBag to a queue or a toArray() that expects an array — you'll get an object-shaped payload.

Iterate/collect when you need Laravel ergonomics. Because the bag implements ArrayAccess and iteration, you can bridge to Collection at the edge:

$items = collect($params->get('line_items', []))
    ->map(fn ($row) => new LineItem(new ParameterBag($row)));

Keep the collect() call at the boundary; don't smuggle collections into the bag's contract.

Wrap the package behind a thin internal adapter — the single highest-leverage move. Because the class is small and the maintainer is a bus factor of one, depend on your interface, not the vendor class:

namespace App\Support;

interface Payload
{
    public function get(string $key, mixed $default = null): mixed;
    public function has(string $key): bool;
    public function all(): array;
}
final class ParameterBagPayload implements Payload
{
    public function __construct(private \JustSteveKing\ParameterBag\ParameterBag $bag) {}

    public function get(string $key, mixed $default = null): mixed { return $this->bag->get($key, $default); }
    public function has(string $key): bool { return $this->bag->has($key); }
    public function all(): array { return $this->bag->all(); }
}

If the package is abandoned, forks, or you later swap in a DTO, it's a one-file change. Do this on day one, not after the first incident.

Compose into value objects. Domain-ish containers (money, pagination, address) read cleanly as a bag plus typed accessors:

final class Pagination
{
    private ParameterBag $p;

    public function __construct(ParameterBag $p) { $this->p = $p; }

    public function page(): int  { return (int) $this->p->get('page', 1); }
    public function perPage(): int { return (int) $this->p->get('per_page', 25); }
}

The bag handles presence and defaults; your accessor handles typing. That split keeps the "what's inside this array" problem in one place without pretending the bag is a DTO.

Use it for test fixtures. Building a bag with a couple of set() calls reads better than a nested array literal in an assert*, and lets a test mutate one key without rebuilding the whole structure.

Gotchas and Tips

Mutation is aliased. The bag wraps an array; if you pass the same instance to two consumers and one calls set(), the other sees the change. That's a design choice, not a bug — but it's a footgun. Treat a bag as owned by exactly one scope. If a consumer must mutate, clone the bag (or construct a fresh one from all()) before handing it on. There is no readonly/immutable mode; you don't get one.

has() and ArrayAccess::offsetExists are not the same question. A key that exists with a null value is a classic divergence between "isset" checks and "key present" checks. Read the class to see which one has() answers, and prefer has() explicitly over isset($bag['x']) in your code so intent is unambiguous.

Dot notation collides with literal dotted keys. If your payload genuinely has a key like "user.name" (some integration payloads do), get('user.name') is ambiguous — the bag traverses. Know which interpretation the implementation chose before you rely on it for real data, and normalize incoming keys if the source is out of your control.

Returns mixed. No validation, no typing, no schema. This is a bag, not a DTO. It will not tell you a required key is missing, it will not coerce types, and it will not catch a nested array where you expected a string. If your requirement is actually validated/typed structured data, stop here and use a DTO (or Laravel's FormRequest). The most common failed adoption is reaching for this expecting a DTO library.

Verify it actually resolves against your Laravel version. The PHP 8.0 floor in the release notes is not a compatibility statement. Laravel 10 needs PHP 8.1+, Laravel 11 needs 8.2+. If the 2.0.0 require block pins illuminate/* to an older line, the one-line install becomes dependency archaeology. Run composer update --dry-run against your real composer.json and record the result — the next person will assume "PHP 8.0+" means "works everywhere".

No service provider, no config publishing, no auto-discovery. It's framework-agnostic and slots in silently. That also means there's no place to configure behaviour — anything you want to customize (normalization, immutability, logging on missing keys) goes in your adapter, not the package.

Don't serialize the bag directly. Queued jobs, JSON responses, and toArray()-style APIs expect arrays. Always call all() at the boundary. If a bag ends up in a queued job's serialized properties, expect an object payload and a failure the first time you run the queue worker.

Pin the release; don't assume upstream will patch it. No published support policy, no visible security process, no CI evidence against PHP 8.3/8.4 or Laravel 11/12. If a compatibility patch is needed, you are writing it. Budget for that as a small, unbounded-on-date item rather than a surprise.

Read the 2.0.0 diff before upgrading a 1.x install. A two-line changelog for a major version is either a genuinely narrow break or an incomplete record. Check what changed in get/set/has semantics and whether the Pest swap leaked into require rather than require-dev. Then run the full suite.

Extension points are limited by design. The class is small and likely not final-heavy, so subclassing works, but wrapping is safer — subclassing couples you to the vendor's protected internals. If you need to add behaviour (missing-key logging, coercion), put it in App\Support\ParameterBagPayload, not in a subclass of the vendor class.

Fork deliberately. If you do fork for a PHP/Laravel compatibility patch, contribute it upstream and document the fork in your dependency watchlist with an owner and a review date. An undiscussed private fork is a liability, not a fix.

When not to reach for this: request-path data (use FormRequest), config (use config()/Repository), request parameters (Laravel ships its own ParameterBag via Symfony), or anything on a critical path needing active support. The package's value is convention and readability at internal seams — keep it there.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle