phpmd.xml 0000644 00000002417 13737345140 0006411 0 ustar 00
codemasher/php-settings-container PMD ruleset
*/examples/*
*/tests/*
*/vendor/*
1
.scrutinizer.yml 0000644 00000000246 13737345140 0007737 0 ustar 00 build:
nodes:
analysis:
tests:
override:
- php-scrutinizer-run
filter:
excluded_paths:
- examples/*
- tests/*
- vendor/*
LICENSE 0000644 00000002111 13737345140 0005553 0 ustar 00 The MIT License (MIT)
Copyright (c) 2018 Smiley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
README.md 0000644 00000014351 13737345140 0006036 0 ustar 00 # chillerlan/php-settings-container
A container class for immutable settings objects. Not a DI container. PHP 7.2+
- [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) provides immutable properties with magic getter & setter and some fancy
[![version][packagist-badge]][packagist]
[![license][license-badge]][license]
[![Travis][travis-badge]][travis]
[![Coverage][coverage-badge]][coverage]
[![Scrunitizer][scrutinizer-badge]][scrutinizer]
[![Packagist downloads][downloads-badge]][downloads]
[![PayPal donate][donate-badge]][donate]
[![Continuous Integration][gh-action-badge]][gh-action]
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-settings-container.svg?style=flat-square
[packagist]: https://packagist.org/packages/chillerlan/php-settings-container
[license-badge]: https://img.shields.io/github/license/chillerlan/php-settings-container.svg?style=flat-square
[license]: https://github.com/chillerlan/php-settings-container/blob/main/LICENSE
[travis-badge]: https://img.shields.io/travis/chillerlan/php-settings-container.svg?style=flat-square
[travis]: https://travis-ci.org/chillerlan/php-settings-container
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-settings-container.svg?style=flat-square
[coverage]: https://codecov.io/github/chillerlan/php-settings-container
[scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-settings-container.svg?style=flat-square
[scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-settings-container
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-settings-container.svg?style=flat-square
[downloads]: https://packagist.org/packages/chillerlan/php-settings-container/stats
[donate-badge]: https://img.shields.io/badge/donate-paypal-ff33aa.svg?style=flat-square
[donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WLYUNAT9ZTJZ4
[gh-action-badge]: https://github.com/chillerlan/php-settings-container/workflows/Continuous%20Integration/badge.svg
[gh-action]: https://github.com/chillerlan/php-settings-container/actions?query=workflow%3A%22Continuous+Integration%22
## Documentation
### Installation
**requires [composer](https://getcomposer.org)**
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^2.0` - see [releases](https://github.com/chillerlan/php-settings-container/releases) for valid versions)
```json
{
"require": {
"php": "^7.4",
"chillerlan/php-settings-container": "dev-main"
}
}
```
Profit!
## Usage
The `SettingsContainerInterface` (wrapped in`SettingsContainerAbstract` ) 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.
### Simple usage
```php
class MyContainer extends SettingsContainerAbstract{
protected $foo;
protected $bar;
}
```
Typed properties in PHP 7.4+:
```php
class MyContainer extends SettingsContainerAbstract{
protected string $foo;
protected string $bar;
}
```
```php
// use it just like a \stdClass
$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"}
//non-existing properties will be ignored:
$container->nope = 'what';
var_dump($container->nope); // -> null
```
### Advanced usage
```php
trait SomeOptions{
protected $foo;
protected $what;
// this method will be called in SettingsContainerAbstract::construct()
// after the properties have been set
protected function SomeOptions(){
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}
// this method will be called from __set() when property $what is set
protected function set_what(string $value){
$this->what = md5($value);
}
}
trait MoreOptions{
protected $bar = 'whatever'; // provide default values
}
```
```php
$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];
// now plug the several library options together to a single object
$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); // -> md5 hash of "some value"
```
### API
#### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php)
method | return | info
-------- | ---- | -----------
`__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set
(protected) `construct()` | void | calls a method with trait name as replacement constructor for each used trait
`__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](http://php.net/manual/json.constants.php)
`fromJSON(string $json)` | `SettingsContainerInterface` |
`jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface
## Disclaimer
This might be either an utterly genius or completely stupid idea - you decide. However, i like it and it works.
Also, this is not a dependency injection container. Stop using DI containers FFS.
examples/simple.php 0000644 00000001323 13737345140 0010372 0 ustar 00
* @copyright 2018 smiley
* @license MIT
*/
namespace chillerlan\SettingsExamples;
use chillerlan\Settings\SettingsContainerAbstract;
require_once __DIR__.'/../vendor/autoload.php';
class MyContainer extends SettingsContainerAbstract{
protected $foo;
protected $bar;
}
/** @var \chillerlan\Settings\SettingsContainerInterface $container */
$container = new MyContainer(['foo' => 'what']);
$container->bar = 'foo';
var_dump($container->toJSON()); // -> {"foo":"what","bar":"foo"}
// non-existing properties will be ignored:
$container->nope = 'what';
var_dump($container->nope); // -> NULL
examples/advanced.php 0000644 00000002260 13737345140 0010647 0 ustar 00
* @copyright 2018 smiley
* @license MIT
*/
namespace chillerlan\SettingsExamples;
use chillerlan\Settings\SettingsContainerAbstract;
require_once __DIR__.'/../vendor/autoload.php';
// from library #1
trait SomeOptions{
protected string $foo = '';
// this method will be called in SettingsContainerAbstract::__construct() after the properties have been set
protected function SomeOptions(){
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}
}
// from library #2
trait MoreOptions{
protected string $bar = 'whatever'; // provide default values
}
$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];
// now plug the several library options together to a single object
/**
* @property string $foo
* @property string $bar
*/
class MySettings extends SettingsContainerAbstract{
use SomeOptions, MoreOptions; // ...
};
$container = new MySettings($commonOptions);
var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value)
var_dump($container->bar); // -> nothing
.github/FUNDING.yml 0000644 00000000022 13737345140 0007722 0 ustar 00 ko_fi: codemasher
.github/workflows/tests.yml 0000644 00000002235 13737345140 0012037 0 ustar 00 # https://help.github.com/en/categories/automating-your-workflow-with-github-actions
# https://github.com/ergebnis/php-library-template
# https://github.com/sebastianbergmann/phpunit/blob/master/.github/workflows/ci.yml
on:
pull_request:
push:
branches:
- main
tags:
- "**"
name: "Continuous Integration"
jobs:
tests:
name: "Unit Tests"
runs-on: ubuntu-latest
strategy:
matrix:
php-binary:
- php7.4
dependencies:
- lowest
- highest
steps:
- name: "Checkout"
uses: actions/checkout@v2
- name: "Install lowest dependencies with composer"
if: matrix.dependencies == 'lowest'
run: ${{ matrix.php-binary }} $(which composer) update --no-interaction --no-progress --no-suggest --prefer-lowest
- name: "Install highest dependencies with composer"
if: matrix.dependencies == 'highest'
run: ${{ matrix.php-binary }} $(which composer) update --no-interaction --no-progress --no-suggest
- name: "Run unit tests with phpunit"
run: ${{ matrix.php-binary }} vendor/bin/phpunit --configuration=phpunit.xml --no-coverage
phpunit.xml 0000644 00000001372 13737345140 0006767 0 ustar 00
./src
./tests/
src/SettingsContainerAbstract.php 0000644 00000006216 13737345140 0013207 0 ustar 00
* @copyright 2018 Smiley
* @license MIT
*/
namespace chillerlan\Settings;
use ReflectionClass, ReflectionProperty;
use function call_user_func, call_user_func_array, get_object_vars, json_decode, json_encode, method_exists, property_exists;
abstract class SettingsContainerAbstract implements SettingsContainerInterface{
/**
* SettingsContainerAbstract constructor.
*
* @param iterable|null $properties
*/
public function __construct(iterable $properties = null){
if(!empty($properties)){
$this->fromIterable($properties);
}
$this->construct();
}
/**
* calls a method with trait name as replacement constructor for each used trait
* (remember pre-php5 classname constructors? yeah, basically this.)
*
* @return void
*/
protected function construct():void{
$traits = (new ReflectionClass($this))->getTraits();
foreach($traits as $trait){
$method = $trait->getShortName();
if(method_exists($this, $method)){
call_user_func([$this, $method]);
}
}
}
/**
* @inheritdoc
*/
public function __get(string $property){
if(property_exists($this, $property) && !$this->isPrivate($property)){
if(method_exists($this, 'get_'.$property)){
return call_user_func([$this, 'get_'.$property]);
}
return $this->{$property};
}
return null;
}
/**
* @inheritdoc
*/
public function __set(string $property, $value):void{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return;
}
if(method_exists($this, 'set_'.$property)){
call_user_func_array([$this, 'set_'.$property], [$value]);
return;
}
$this->{$property} = $value;
}
/**
* @inheritdoc
*/
public function __isset(string $property):bool{
return isset($this->{$property}) && !$this->isPrivate($property);
}
/**
* @internal Checks if a property is private
*
* @param string $property
*
* @return bool
*/
protected function isPrivate(string $property):bool{
return (new ReflectionProperty($this, $property))->isPrivate();
}
/**
* @inheritdoc
*/
public function __unset(string $property):void{
if($this->__isset($property)){
unset($this->{$property});
}
}
/**
* @inheritdoc
*/
public function __toString():string{
return $this->toJSON();
}
/**
* @inheritdoc
*/
public function toArray():array{
return get_object_vars($this);
}
/**
* @inheritdoc
*/
public function fromIterable(iterable $properties):SettingsContainerInterface{
foreach($properties as $key => $value){
$this->__set($key, $value);
}
return $this;
}
/**
* @inheritdoc
*/
public function toJSON(int $jsonOptions = null):string{
return json_encode($this, $jsonOptions ?? 0);
}
/**
* @inheritdoc
*/
public function fromJSON(string $json):SettingsContainerInterface{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return $this->fromIterable($data);
}
/**
* @inheritdoc
*/
public function jsonSerialize(){
return $this->toArray();
}
}
src/SettingsContainerInterface.php 0000644 00000004143 13737345140 0013341 0 ustar 00
* @copyright 2018 Smiley
* @license MIT
*/
namespace chillerlan\Settings;
use JsonSerializable;
/**
* a generic container with magic getter and setter
*/
interface SettingsContainerInterface extends JsonSerializable{
/**
* Retrieve the value of $property
*
* @param string $property
*
* @return mixed
*/
public function __get(string $property);
/**
* Set $property to $value while avoiding private and non-existing properties
*
* @param string $property
* @param mixed $value
*
* @return void
*/
public function __set(string $property, $value):void;
/**
* Checks if $property is set (aka. not null), excluding private properties
*
* @param string $property
*
* @return bool
*/
public function __isset(string $property):bool;
/**
* Unsets $property while avoiding private and non-existing properties
*
* @param string $property
*
* @return void
*/
public function __unset(string $property):void;
/**
* @see SettingsContainerInterface::toJSON()
*
* @return string
*/
public function __toString():string;
/**
* Returns an array representation of the settings object
*
* @return array
*/
public function toArray():array;
/**
* Sets properties from a given iterable
*
* @param iterable $properties
*
* @return \chillerlan\Settings\SettingsContainerInterface
*/
public function fromIterable(iterable $properties):SettingsContainerInterface;
/**
* Returns a JSON representation of the settings object
* @see \json_encode()
*
* @param int|null $jsonOptions
*
* @return string
*/
public function toJSON(int $jsonOptions = null):string;
/**
* Sets properties from a given JSON string
*
* @param string $json
*
* @return \chillerlan\Settings\SettingsContainerInterface
*
* @throws \Exception
* @throws \JsonException
*/
public function fromJSON(string $json):SettingsContainerInterface;
}
composer.json 0000644 00000001724 13737345140 0007301 0 ustar 00 {
"name": "chillerlan/php-settings-container",
"description": "A container class for immutable settings objects. Not a DI container. PHP 7.4+",
"homepage": "https://github.com/chillerlan/php-settings-container",
"license": "MIT",
"type": "library",
"minimum-stability": "stable",
"keywords": [
"php7", "helper", "container", "settings"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"require": {
"php": "^7.4 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"phan/phan": "^3.2.2",
"phpunit/phpunit": "9.4"
},
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\SettingsTest\\": "tests/",
"chillerlan\\SettingsExamples\\": "examples/"
}
}
}
.travis.yml 0000644 00000000763 13737345140 0006672 0 ustar 00 language: php
env:
global:
- PHAN_ALLOW_XDEBUG=0 PHAN_DISABLE_XDEBUG_WARN=1
matrix:
include:
- php: 7.4
- php: nightly
allow_failures:
- php: nightly
before_install:
- pecl channel-update pecl.php.net
- pecl install ast
install:
- composer install --no-interaction --prefer-source
- composer validate
script:
- vendor/bin/phpunit --configuration phpunit.xml --coverage-clover clover.xml
- vendor/bin/phan
after_script: bash <(curl -s https://codecov.io/bash)
tests/TestOptionsTrait.php 0000644 00000001415 13737345140 0011726 0 ustar 00
* @copyright 2018 smiley
* @license MIT
*/
namespace chillerlan\SettingsTest;
trait TestOptionsTrait{
protected string $test1 = 'foo';
protected ?bool $test2 = null;
protected string $testConstruct;
protected ?string $test4 = null;
protected ?string $test5 = null;
protected ?string $test6 = null;
protected function TestOptionsTrait(){
$this->testConstruct = 'success';
}
protected function set_test5($value){
$this->test5 = $value.'_test5';
}
protected function get_test6(){
return $this->test6 === null
? 'null'
: sha1($this->test6);
}
}
tests/ContainerTest.php 0000644 00000005641 13737345140 0011216 0 ustar 00
* @copyright 2018 Smiley
* @license MIT
*/
namespace chillerlan\SettingsTest;
use PHPUnit\Framework\TestCase;
use JsonException, TypeError;
class ContainerTest extends TestCase{
public function testConstruct(){
$container = new TestContainer([
'test1' => 'test1',
'test2' => true,
'test3' => 'test3',
'test4' => 'test4',
]);
$this->assertSame('test1', $container->test1);
$this->assertSame(true, $container->test2);
$this->assertNull($container->test3);
$this->assertSame('test4', $container->test4);
$this->assertSame('success', $container->testConstruct);
}
public function testGet(){
$container = new TestContainer;
$this->assertSame('foo', $container->test1);
$this->assertNull($container->test2);
$this->assertNull($container->test3);
$this->assertNull($container->test4);
$this->assertNull($container->foo);
// isset test
$this->assertTrue(isset($container->test1));
$this->assertFalse(isset($container->test2));
$this->assertFalse(isset($container->test3));
$this->assertFalse(isset($container->test4));
$this->assertFalse(isset($container->foo));
// custom getter
$container->test6 = 'foo';
$this->assertSame(sha1('foo'), $container->test6);
// nullable/isset test
$container->test6 = null;
$this->assertFalse(isset($container->test6));
$this->assertSame('null', $container->test6);
}
public function testSet(){
$container = new TestContainer;
$container->test1 = 'bar';
$container->test2 = false;
$container->test3 = 'nope';
$this->assertSame('bar', $container->test1);
$this->assertSame(false, $container->test2);
$this->assertNull($container->test3);
// unset
unset($container->test1);
$this->assertFalse(isset($container->test1));
// custom setter
$container->test5 = 'bar';
$this->assertSame('bar_test5', $container->test5);
}
public function testToArray(){
$container = new TestContainer(['test1' => 'no', 'test2' => true, 'testConstruct' => 'success']);
$this->assertSame(['test1' => 'no', 'test2' => true, 'testConstruct' => 'success', 'test4' => null, 'test5' => null, 'test6' => null], $container->toArray());
}
public function testToJSON(){
$container = (new TestContainer)->fromJSON('{"test1":"no","test2":true,"testConstruct":"success"}');
$expected = '{"test1":"no","test2":true,"testConstruct":"success","test4":null,"test5":null,"test6":null}';
$this->assertSame($expected, $container->toJSON());
$this->assertSame($expected, (string)$container);
}
public function testFromJsonException(){
$this->expectException(JsonException::class);
(new TestContainer)->fromJSON('-');
}
public function testFromJsonTypeError(){
$this->expectException(TypeError::class);
(new TestContainer)->fromJSON('2');
}
}
tests/TestContainer.php 0000644 00000001075 13737345140 0011213 0 ustar 00
* @copyright 2018 Smiley
* @license MIT
*/
namespace chillerlan\SettingsTest;
use chillerlan\Settings\SettingsContainerAbstract;
/**
* @property $test1
* @property $test2
* @property $test3
* @property $test4
* @property $test5
* @property $test6
*/
class TestContainer extends SettingsContainerAbstract{
use TestOptionsTrait;
private string $test3 = 'what';
}
.gitignore 0000644 00000000045 13737345140 0006542 0 ustar 00 /.build
/.idea
/vendor
composer.lock
.phan/config.php 0000644 00000003524 13737345140 0007541 0 ustar 00 '7.4',
// A list of directories that should be parsed for class and
// method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors.
//
// Thus, both first-party and third-party code being used by
// your application should be included in this list.
'directory_list' => [
'examples',
'src',
'tests',
'vendor',
],
// A regex used to match every file name that you want to
// exclude from parsing. Actual value will exclude every
// "test", "tests", "Test" and "Tests" folders found in
// "vendor/" directory.
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
// A directory list that defines files that will be excluded
// from static analysis, but whose class and method
// information should be included.
//
// Generally, you'll want to include the directories for
// third-party code (such as "vendor/") in this list.
//
// n.b.: If you'd like to parse but not analyze 3rd
// party code, directories containing that code
// should be added to both the `directory_list`
// and `exclude_analysis_directory_list` arrays.
'exclude_analysis_directory_list' => [
'tests',
'vendor',
],
];