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

Php Settings Container Laravel Package

chillerlan/php-settings-container

A lightweight PHP settings container that decouples configuration logic from your application. It is not a DI container, offering typed settings objects, property hooks for PHP before 8.4, and straightforward access, merging, and serialization.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require chillerlan/php-settings-container

Pin a version constraint — ^4.0 for the current line. Be aware 4.x requires PHP 8.4+; if your Laravel app still runs 8.2/8.3, use the ^3.3 line (which also backports the property-hook conflict handling). composer why-not chillerlan/php-settings-container ^4.0 is the fastest way to see if another dependency is holding you back.

Where to look first

  1. src/SettingsContainerAbstract.php — the whole package is one file; read the __get/__set/toArray/fromIterable methods and the protected SET_PREFIX/GET_PREFIX constants.
  2. src/SettingsContainerInterface.php — the contract you should type-hint against in your own code.
  3. The README's "Advanced usage" trait example — this is the pattern you will actually use day to day.
  4. rules-magic-access.neon — the bundled PHPStan ruleset, if you run static analysis.

First use case: a typed config object

Stop passing config('my-package') arrays around as array. Wrap the array once and inject the object:

use chillerlan\Settings\SettingsContainerAbstract;

class MailSettings extends SettingsContainerAbstract
{
    protected string $host = 'smtp.mailgun.org';
    protected int    $port = 587;
    protected string $driver = 'smtp';
}

// in a service provider
$this->app->singleton(MailSettings::class, fn () => new MailSettings(config('mail')));

Every consumer now gets $settings->port with a default, and a fatal typo ($settings->prot) instead of a silent null from an array lookup — once you enable the attribute below.

Implementation Patterns

Option bags per package, composed with traits

The intended architecture is one trait per concern/library, glued into a single container class. This is how the chillerlan packages themselves do it, and it maps cleanly onto Laravel when you have several subsystems contributing to one settings object:

trait CacheOptions
{
    protected int $ttl = 3600;
    protected bool $enabled = true;
}

trait LogOptions
{
    protected string $channel = 'stack';

    // called automatically by construct() after properties are set
    protected function LogOptions(): void
    {
        $this->channel = strtolower($this->channel);
    }
}

class AppSettings extends SettingsContainerAbstract
{
    use CacheOptions, LogOptions;
}

The trait-named method is a per-trait constructor hook — use it for normalization, validation, or deriving one value from another, not for side effects like I/O.

Custom setters/getters for invariants

Prefix a method with set_/get_ and it intercepts magic access. This is the lightweight alternative to a full property-hook or a mutator trait:

protected function set_port(int $value): void
{
    if ($value < 1 || $value > 65535) {
        throw new InvalidArgumentException("invalid port: {$value}");
    }
    $this->port = $value;
}

protected function get_host(): string
{
    return rtrim($this->host, '/');
}

Validation belongs here, because it covers both constructor hydration (new MailSettings(['port' => 0])) and later mutation. Note that toArray() in 3.2+ routes through the magic getter, so getters also affect what you serialize.

Hydration paths and which ones bypass what

Entry point Runs custom set_*/get_*? Runs PHP 8.4 hooks?
Constructor / fromIterable() yes (via __set) yes
fromJSON() yes yes
toArray() / toJSON() / json_encode() yes (via __get) yes
serialize() / unserialize() no no

That last row is the important one: cache-backed settings (Laravel's Cache::remember on a serialized container) will round-trip raw property values, skipping your validation and transforms. If you need the invariants enforced, don't cache the object — cache $settings->toArray() and rehydrate with new AppSettings($cached).

Binding in Laravel

Register the container as a singleton built from config, and type-hint the concrete class or SettingsContainerInterface in constructors. It integrates naturally with constructor injection because it takes an iterable and nothing else:

$this->app->singleton(MyPackageSettings::class, fn ($app) =>
    new MyPackageSettings($app['config']->get('my-package', []))
);

Because it accepts any iterable, you can feed it a Collection, a generator of defaults merged with user config, or an ArrayObject without conversion.

Passing settings into a library driver

When a third-party driver wants a flat array, call toArray() at the boundary and keep the container inside your own code:

$client = new SomeApiClient($settings->toArray());

Exposing settings to the frontend / API

jsonSerialize() is implemented, so a settings object can go straight into a response()->json([...]) payload. Combine with toJSON(JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) when you need a stable string for hashing or snapshot tests.

Gotchas and Tips

"Immutable" means fixed shape, not read-only. The README's tagline is misleading: properties are settable after construction. What you get is a closed set of keys. If you need true immutability, mark properties readonly (8.4) or expose only a getter-shaped facade.

Invalid properties silently return null by default. $container->typo = 'x' is discarded with no error, and reading it yields null — the classic source of "why is my config empty" bugs. Annotate your container:

use chillerlan\Settings\ThrowOnInvalidProperty;

#[ThrowOnInvalidProperty(true)]
class AppSettings extends SettingsContainerAbstract { /* ... */ }

Do this on every container class in your codebase; the default behavior is a footgun.

It is not a DI container. The README says so bluntly. Do not try to resolve services through it, do not make it your app() replacement, and do not let a settings class acquire dependencies. If you find yourself wanting a factory inside a container class, you want a service provider instead.

Static analysis will complain. Magic __get/__set means PHPStan/Psalm see property access as undefined. Two mitigations: copy rules-magic-access.neon into your PHPStan config to ignore those errors, or (better) declare the properties publicly-typed in docblocks/@property annotations so IDEs and analyzers agree with runtime.

Private properties are excluded from magic calls. isPrivate() is checked, so a private property will not be settable via the constructor and will not appear in toArray(). Always use protected, as every example does — this trips people up when they "tighten" visibility for cleanliness.

Property hooks vs. set_/get_ methods conflict (PHP 8.4). If a property has a set hook, the matching set_* method is ignored during magic access; if it has a get hook, the get_* method is used. Mixing both in one property is a silent behavior change, and public properties never trigger magic access at all while still triggering hooks. Pick one mechanism per property and stay consistent.

toJSON() throws JsonException regardless of options as of 3.2.1. Do not rely on JSON_PARTIAL_OUTPUT_ON_ERROR or a false return; wrap in try/catch if the payload can contain non-UTF-8 (e.g. binary or user-supplied strings).

Unserialization skips everything. unserialize() writes raw properties, bypassing setters, getters, and hooks. Treat any serialize() output as untrusted/short-lived and prefer toArray() + constructor rehydration for anything persisted.

Debugging inside the container is awkward. Because __get intercepts access, var_dump($container) shows raw protected properties but a debugger's "evaluate" pane may invoke your getters. When behavior looks wrong, dump $container->toArray() — it reflects the getter-processed state and is the fastest way to see what the rest of the app actually receives.

Defaults live on the property declaration, not in construct(). Set defaults inline (protected int $ttl = 3600;) so new Container and new Container([]) behave identically; doing it in the trait constructor means an explicitly-passed null can clobber your default.

Version drift. The 3.2.0 change to toArray() (now running values through the getter) and 3.3.0's hook handling are both behavioral, not just cosmetic. If you upgrade across those lines, add a test that asserts toArray() output for containers with custom getters.

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.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc