Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chillerlan/php-settings-container

Repository files navigation

chillerlan/php-settings-container

A container class for settings objects - decouple configuration logic from your application! Not a DI container.

Documentation

Installation

requires composer

composer.json (note: replace dev-main with a version constraint, e.g. ^3.0 - see releases for valid versions)

{
"require": {
"php": "^8.1",
"chillerlan/php-settings-container": "dev-main"
}
}

Profit!

Usage

The SettingsContainerInterface (wrapped inSettingsContainerAbstract) provides plug-in functionality for immutable object properties and adds some fancy, like loading/saving JSON, arrays etc. It takes an iterable as the only constructor argument and calls a method with the trait's name on invocation (MyTrait::MyTrait()) for each used trait.

A PHPStan ruleset to exclude errors generated by accessing magic properties on SettingsContainerInterface can be found in rules-magic-access.neon.

Simple usage

class MyContainer extends SettingsContainerAbstract{
protected string $foo;
protected string $bar;
}
bar = 'foo'; // which is equivalent to $container = new MyContainer(['bar' => 'foo', 'foo' => 'what']); // ...or try $container->fromJSON('{"foo": "what", "bar": "foo"}'); // fetch all properties as array $container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo'] // or JSON $container->toJSON(); // -> {"foo": "what", "bar": "foo"} // JSON via JsonSerializable $json = json_encode($container); // -> {"foo": "what", "bar": "foo"}">// use it just like a \stdClass (except the properties are fixed)
$container = new MyContainer;
$container->foo = 'what';
$container->bar = 'foo';

// which is equivalent to
$container = new MyContainer(['bar' => 'foo', 'foo' => 'what']);
// ...or try
$container->fromJSON('{"foo": "what", "bar": "foo"}');


// fetch all properties as array
$container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo']
// or JSON
$container->toJSON(); // -> {"foo": "what", "bar": "foo"}
// JSON via JsonSerializable
$json = json_encode($container); // -> {"foo": "what", "bar": "foo"}

By default, non-existing properties will be ignored and return null:

nope); // -> null">$container->nope = 'what';

var_dump($container->nope); // -> null

You can change this behaviour by adding the attribute ThrowOnInvalidProperty to your container class:

throws: attempt to write invalid property: "$nope"">#[ThrowOnInvalidProperty(true)]
class MyContainer extends SettingsContainerAbstract{
// ...
}

$container->nope = 'what'; // -> throws: attempt to write invalid property: "$nope"

Advanced usage

Suppose the following trait from library 1:

what; } }">trait SomeOptions{
protected string $foo;
protected string $what;

// this method will be called in SettingsContainerAbstract::construct()
// after the properties have been set
protected function SomeOptions():void{
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}

/*
* special prefixed magic setters & getters ("set_"/"get_" + property name)
*/

// this method will be called from __set() when property $what is set
protected function set_what(string $value):void{
$this->what = md5($value);
}

// this method is called on __get() for the property $what
protected function get_what():string{
return 'hash: '.$this->what;
}
}

And another trait from library 2:

trait MoreOptions{
protected string $bar = 'whatever'; // provide default values
}

We can now plug the several library options together to a single class/object:

'whatever', // MoreOptions 'bar' => 'nothing', ]; $container = new class ($commonOptions) extends SettingsContainerAbstract{ use SomeOptions, MoreOptions; }; var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value) var_dump($container->bar); // -> nothing $container->what = 'some value'; var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ")">$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];

$container = new class ($commonOptions) extends SettingsContainerAbstract{
use SomeOptions, MoreOptions;
};

var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value)
var_dump($container->bar); // -> nothing

$container->what = 'some value';
var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ")

A note on property hooks (PHP 8.4+)

Property hooks are called whenever a property is accessed (except from within the hook itself of course), which means that the custom get/set methods this library allows would conflict when a custom method is defined for a property that also has a hook defined. To prevent double method calls, the internal methods hasSetHook() and hasGetHook() have been introduced, and are called whenever the magic get/set methods are called: when both, a custom method and a property hook exist, only the property hook will be called.
Public properties will never call the magic get/set, however, their hooks will be called. (un)serializing a SettingsContainerInterface instance will bypass magic get/set and existing property hooks, while JSON de/encode as will call magic get/set or existing hooks explicitly via the toArray() and fromIterable() methods.

class PropertyHooksContainer extends SettingsContainerAbstract{

protected string $someValue{
set => doStuff($value);
}

// this method will be ignored in magic calls as a "set" hook on the property exists
protected function set_someValue(string $value):void{
$this->someValue = doOtherStuff($value);
}

// this custom method will be called as the property has no "get" hook
protected function get_someValue():string{
return doWhatever($this->someValue);
}

// this property will never trigger the magic get/set and associated methods
public string $otherValue{
set => doStuff($value);
get => $this->otherValue;
}

}

API

method return info
__construct(iterable $properties = null) - calls construct() internally after the properties have been set
__get(string $property) mixed calls $this->{'get_'.$property}() if such a method exists
__set(string $property, $value) void calls $this->{'set_'.$property}($value) if such a method exists
__isset(string $property) bool
__unset(string $property) void
__toString() string a JSON string
toArray() array
fromIterable(iterable $properties) SettingsContainerInterface
toJSON(int $jsonOptions = null) string accepts JSON options constants
fromJSON(string $json) SettingsContainerInterface
jsonSerialize() mixed implements the JsonSerializable interface
serialize() string implements the Serializable interface
unserialize(string $data) void implements the Serializable interface
__serialize() array implements the Serializable interface
__unserialize(array $data) void implements the Serializable interface

Internal (protected) methods

method return info
construct() void calls a method with trait name as replacement constructor for each used trait
isPrivate(string $property) bool private properties are excluded from magic calls
hasSetHook(string $property) bool
hasGetHook(string $property) bool

Disclaimer

This might be either an absolutely brilliant or completely stupid idea - you decide (in hindsight it was a great idea I guess - property hooks made their way into PHP 8.4).
Also, this is not a dependency injection container. Stop using DI containers FFS.