├── .docheader ├── .scrutinizer.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README-GIT.md ├── README.md ├── benchmark ├── ArrayPerfBench.php ├── BaseCase.php ├── ContainerIdBench.php ├── FlexibleConfigurationBench.php ├── PackageConfigBench.php ├── ProvidesDefaultOptionsBench.php ├── ProvidesDefaultOptionsMandatoryBench.php ├── ProvidesDefaultOptionsMandatoryContainerIdBench.php ├── RequiresMandatoryOptionsBench.php ├── RequiresMandatoryOptionsContainerIdBench.php ├── RequiresMandatoryOptionsRecursiveContainerIdBench.php └── blackfire.php ├── bin ├── interop-config └── interop-config.php ├── composer.json ├── doc ├── book │ ├── api.md │ ├── console-tools.md │ ├── contribute │ │ └── bookdown.json │ ├── examples.md │ ├── getting-started │ │ └── bookdown.json │ ├── intro.md │ ├── quick-start.md │ ├── reference │ │ └── bookdown.json │ └── tutorial.md └── bookdown.json ├── phpbench.json.dist └── src ├── ConfigurationTrait.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── MandatoryOptionNotFoundException.php ├── OptionNotFoundException.php ├── OutOfBoundsException.php ├── RuntimeException.php └── UnexpectedValueException.php ├── ProvidesDefaultOptions.php ├── RequiresConfig.php ├── RequiresConfigId.php ├── RequiresMandatoryOptions.php └── Tool ├── AbstractCommand.php ├── AbstractConfig.php ├── ConfigDumper.php ├── ConfigDumperCommand.php ├── ConfigReader.php ├── ConfigReaderCommand.php └── ConsoleHelper.php /.docheader: -------------------------------------------------------------------------------- 1 | /** 2 | * Sandro Keil (https://sandro-keil.de) 3 | * 4 | * @link http://github.com/sandrokeil/interop-config for the canonical source repository 5 | * @copyright Copyright (c) 20%regexp:\d{2}%-%year% Sandro Keil 6 | * @license http://github.com/sandrokeil/interop-config/blob/master/LICENSE.md New BSD License 7 | */ 8 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | excluded_paths: [vendor/*, test/*] 4 | before_commands: 5 | - 'composer install --dev --prefer-source' 6 | tools: 7 | external_code_coverage: true 8 | php_mess_detector: true 9 | php_code_sniffer: true 10 | sensiolabs_security_checker: true 11 | php_code_coverage: true 12 | php_pdepend: true 13 | php_loc: 14 | enabled: true 15 | excluded_dirs: [vendor, test] 16 | php_cpd: 17 | enabled: true 18 | excluded_dirs: [vendor, test] 19 | build: 20 | nodes: 21 | analysis: 22 | tests: 23 | override: 24 | - php-scrutinizer-run 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.2.0 (2020-09-05) 6 | 7 | ### Added 8 | 9 | * PHP 8 support 10 | 11 | ### Deprecated 12 | 13 | * Nothing 14 | 15 | ### Removed 16 | 17 | * Nothing 18 | 19 | ### Fixed 20 | 21 | * Nothing 22 | 23 | ## 2.1.0 (2017-02-14) 24 | 25 | ### Added 26 | 27 | * [#36](https://github.com/sandrokeil/interop-config/issues/36): Create console tools to generate/display config based on factories. Read more in the [docs](https://sandrokeil.github.io/interop-config/reference/console-tools.html). 28 | * Composer suggestion of `psr/container` 29 | 30 | ### Deprecated 31 | 32 | * Nothing 33 | 34 | ### Removed 35 | 36 | * Composer suggestion of `container-interop/container-interop` 37 | 38 | ### Fixed 39 | 40 | * Nothing 41 | 42 | 43 | ## 2.0.1 (2016-12-09) 44 | This release contains **no** BC break. 45 | 46 | ### Added 47 | 48 | * More test cases for iterable return type and `\Iterator` objects 49 | 50 | ### Deprecated 51 | 52 | * Nothing 53 | 54 | ### Removed 55 | 56 | * Nothing 57 | 58 | ### Fixed 59 | 60 | * [#34](https://github.com/sandrokeil/interop-config/issue/34): Inconsistent return type in `defaultOptions()` 61 | * `defaultOptions()` method return type is `iterable` but `array` is still valid 62 | 63 | 64 | ## 2.0.0 (2016-12-06) 65 | To upgrade from version 1.x to version 2.x you have to add the PHP scalar types of the interfaces to your implemented 66 | factory methods. 67 | 68 | ### Added 69 | 70 | * [#33](https://github.com/sandrokeil/interop-config/pull/33): PHP 7.1 language features (return types, scalar type hints) 71 | * `dimensions()` method return type is `iterable` 72 | * `canRetrieveOptions()` method return type is `bool` 73 | * `mandatoryOptions()` method return type is `iterable` 74 | * `defaultOptions()` method return type is `array` 75 | * Minor performance improvements 76 | * More test cases 77 | 78 | ### Deprecated 79 | 80 | * Nothing 81 | 82 | ### Removed 83 | 84 | * PHP < 7.1 support 85 | 86 | ### Fixed 87 | 88 | * Nothing 89 | 90 | 91 | ## 1.0.0 (2016-03-05) 92 | 93 | > This release contains BC breaks, but upgrade path is simple. 94 | 95 | ### Added 96 | 97 | * [#26](https://github.com/sandrokeil/interop-config/pull/26): `dimensions()` method to `RequiresConfig` to make configuration depth flexible 98 | 99 | ### Deprecated 100 | 101 | * Nothing 102 | 103 | ### Removed 104 | 105 | * [#26](https://github.com/sandrokeil/interop-config/pull/26): `vendorName()` and `packageName()` method from `RequiresConfig`, replaced by `dimensions()` method 106 | * It's recommended to remove the methods and use the values directly in `dimensions()` to increase performance 107 | 108 | ```php 109 | public function dimensions() 110 | { 111 | return [$this->vendorName(), $this->packageName()]; 112 | } 113 | ``` 114 | 115 | * [#26](https://github.com/sandrokeil/interop-config/pull/26): `RequiresContainerId` interface is renamed to `RequiresConfigId` 116 | * use the container id as a second argument by `options()` method. 117 | 118 | ### Fixed 119 | 120 | * [#28](https://github.com/sandrokeil/interop-config/pull/28): Throws exception if dimensions are set but default options are available and no mandatory options configured 121 | 122 | ## 0.3.1 (2015-10-21) 123 | 124 | ### Added 125 | 126 | * Nothing 127 | 128 | ### Deprecated 129 | 130 | * Nothing 131 | 132 | ### Removed 133 | 134 | * Nothing 135 | 136 | ### Fixed 137 | 138 | * Fixed *Illegal offset type in isset or empty* if options are empty and recursive mandatory options are used 139 | 140 | ## 0.3.0 (2015-10-18) 141 | 142 | ### Added 143 | 144 | * [#9](https://github.com/sandrokeil/interop-config/issues/9): Introducing ProvidesDefaultOptions interface 145 | * [#13](https://github.com/sandrokeil/interop-config/issues/13): Support for recursive mandatory options check 146 | * `canRetrieveOptions()` to `ConfigurationTrait` to perform the options check without throwing an exception 147 | * `optionsWithFallback()` to `ConfigurationTrait` which uses default options if config can not be retrieved 148 | * OptionNotFoundException and MandatoryOptionNotFoundException extends OutOfBoundsException instead of RuntimeException 149 | * Check if retrieved options are an array or an instance of ArrayAccess 150 | * Benchmark suite 151 | * Updated documentation 152 | 153 | ### Deprecated 154 | 155 | * Nothing 156 | 157 | ### Removed 158 | 159 | * `HasConfig` interface, was renamed to `RequiresConfig` 160 | * `HasContainer` interface, was renamed to `RequiresContainerId` 161 | * `HasMandatoryOptions` interface, was renamed to `RequiresMandatoryOptions` 162 | * `HasDefaultOptions` interface, was renamed to `ProvidesDefaultOptions` 163 | * `ObtainsOptions` interface, was merged in `RequiresConfig` 164 | * `OptionalOptions` interface, can be achieved via `ProvidesDefaultOptions` 165 | 166 | ### Fixed 167 | 168 | * fixed wrong function name in documentation 169 | 170 | ## 0.2.0 (2015-09-20) 171 | 172 | ### Added 173 | 174 | * [#5](https://github.com/sandrokeil/interop-config/issues/5): replaced `componentName` function with `packageName` ([PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-meta.md#package-oriented-autoloading)) 175 | 176 | ### Deprecated 177 | 178 | * Nothing 179 | 180 | ### Removed 181 | 182 | * [#5](https://github.com/sandrokeil/interop-config/issues/5): `componentName` function from `HasConfig` interface (BC break) 183 | 184 | ### Fixed 185 | 186 | * Nothing 187 | 188 | ## 0.1.0 (2015-09-05) 189 | 190 | ### Added 191 | * Initial release 192 | * Added interfaces 193 | * [#2](https://github.com/sandrokeil/interop-config/issues/2): Added trait implementation 194 | 195 | ### Deprecated 196 | 197 | * Nothing 198 | 199 | ### Removed 200 | 201 | * Nothing 202 | 203 | ### Fixed 204 | 205 | * Nothing 206 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Visit [github.com/sandrokeil/interop-config/](https://github.com/sandrokeil/interop-config/ "Project Website") for the project website. 4 | 5 | - Make sure you have execute `composer install` 6 | - Be sure you are in the root directory 7 | 8 | ## Resources 9 | 10 | If you wish to contribute to `interop-config`, please be sure to read to the following resources: 11 | 12 | - Coding Standards: [PSR-0/1/2/4](https://github.com/php-fig/fig-standards/tree/master/accepted) 13 | - Git Guide: [README-GIT.md](https://github.com/sandrokeil/interop-config/blob/master/README-GIT.md) 14 | 15 | If you are working on new features, or refactoring an existing component, please create an issue first, so we can discuss 16 | it. 17 | 18 | ## Running tests 19 | 20 | To run tests execute *phpunit*: 21 | 22 | ```sh 23 | $ ./vendor/bin/phpunit 24 | ``` 25 | 26 | You can turn on conditional tests with the TestConfig.php file. 27 | To do so: 28 | 29 | - Enter the `test/` subdirectory. 30 | - Copy `TestConfig.php.dist` file to `TestConfig.php` 31 | - Edit `TestConfig.php` to enable any specific functionality you want to test, as well as to provide test values to 32 | utilize. 33 | 34 | ## Running PHPCodeSniffer 35 | 36 | To check coding standards execute *phpcs*: 37 | 38 | ```sh 39 | $ ./vendor/bin/phpcs 40 | ``` 41 | 42 | To auto fix coding standard issues execute: 43 | 44 | ```sh 45 | $ ./vendor/bin/phpcbf 46 | ``` 47 | 48 | ## Running benchmarks 49 | 50 | To run benchmarks execute *phpbench*: 51 | 52 | ```sh 53 | $ ./vendor/bin/phpbench run --report=aggregate 54 | ``` 55 | 56 | ## Generate documentation 57 | 58 | To generate the documentation execute *bookdown*: 59 | 60 | ```sh 61 | $ ./vendor/bin/bookdown doc/bookdown.json 62 | ``` 63 | 64 | ## Composer shortcuts 65 | 66 | For every program above there are shortcuts defined in the `composer.json` file. 67 | 68 | * `check`: Executes PHPCodeSniffer and PHPUnit 69 | * `cs`: Executes PHPCodeSniffer 70 | * `cs-fix`: Executes PHPCodeSniffer and auto fixes issues 71 | * `test`: Executes PHPUnit 72 | * `test-coverage`: Executes PHPUnit with code coverage 73 | * `docs`: Generates awesome Bookdown.io docs 74 | * `benchmark`: Executes PHPBench 75 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | New BSD License 2 | =============== 3 | 4 | Copyright (c) 2015-2019, Sandro Keil 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the names of the copyright holders nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README-GIT.md: -------------------------------------------------------------------------------- 1 | # Using GIT 2 | 3 | ## Setup your own public GitHub repository 4 | 5 | Your first step is to establish a public repository from which you can pull your work into the master repository. 6 | 7 | 1. Setup a [GitHub account](https://github.com/), if you haven't yet 8 | 2. Fork the [interop-config repository](https://github.com/sandrokeil/interop-config) 9 | 3. Clone your fork locally and enter it (use your own GitHub username in the statement below) 10 | 11 | ```sh 12 | $ git clone git@github.com:[your username]/interop-config.git 13 | $ cd interop-config 14 | ``` 15 | 16 | 4. Add a remote to the canonical `interop-config` repository, so you can keep your fork up-to-date: 17 | 18 | ```sh 19 | $ git remote add upstream https://github.com/sandrokeil/interop-config.git 20 | $ git fetch upstream 21 | ``` 22 | 23 | ## Keeping Up-to-Date 24 | 25 | Periodically, you should update your fork to match the canonical `interop-config` repository. we have 26 | added a remote to the `interop-config` repository, which allows you to do the following: 27 | 28 | ```sh 29 | $ git checkout master 30 | $ git pull upstream master 31 | - OPTIONALLY, to keep your remote up-to-date - 32 | $ git push origin 33 | ``` 34 | 35 | If you're tracking other branches -- for example, the *develop* branch, where new feature development occurs -- 36 | you'll want to do the same operations for that branch; simply substitute "develop" for "master". 37 | 38 | ## Working on interop-config 39 | 40 | When working on `interop-config`, we recommend you do each new feature or bugfix in a new branch. This simplifies the 41 | task of code review as well as of merging your changes into the canonical repository. 42 | 43 | A typical work flow will then consist of the following: 44 | 45 | 1. Create a new local branch based off your master branch. 46 | 2. Switch to your new local branch. (This step can be combined with the previous step with the use of `git checkout -b`.) 47 | 3. Do some work, commit, repeat as necessary. 48 | 4. Push the local branch to your remote repository. 49 | 5. Send a pull request. 50 | 51 | The mechanics of this process are actually quite trivial. Below, we will create a branch for fixing an issue in the tracker. 52 | 53 | ```sh 54 | $ git checkout -b 3452 55 | Switched to a new branch '3452' 56 | ``` 57 | ... do some work ... 58 | 59 | ```sh 60 | $ git commit 61 | ``` 62 | ... write your log message ... 63 | 64 | ```sh 65 | $ git push origin HEAD:3452 66 | Counting objects: 38, done. 67 | Delta compression using up to 2 threads. 68 | Compression objects: 100$ (18/18), done. 69 | Writing objects: 100$ (20/20), 8.19KiB, done. 70 | Total 20 (delta 12), reused 0 (delta 0) 71 | To ssh://git@github.com/sandrokeil/interop-config.git 72 | g5342..9k3532 HEAD -> master 73 | ``` 74 | 75 | You can do the pull request from GitHub. Navigate to your repository, select the branch you just created, and then 76 | select the "Pull Request" button in the upper right. Select the user *sandrokeil* as the recipient. 77 | 78 | ### Branch to issue the pull request 79 | 80 | Which branch should you issue a pull request against? 81 | 82 | - For fixes against the stable release, issue the pull request against the *master* branch. 83 | - For new features, or fixes that introduce new elements to the public API 84 | (such as new public methods or properties), issue the pull request against the *develop* branch. 85 | 86 | ## Branch Cleanup 87 | 88 | As you might imagine, if you are a frequent contributor, you'll start to get a ton of branches both locally and on 89 | your remote. 90 | 91 | Once you know that your changes have been accepted to the master repository, we suggest doing some cleanup of these 92 | branches. 93 | 94 | - Local branch cleanup 95 | 96 | ```sh 97 | $ git branch -d 98 | ``` 99 | 100 | - Remote branch removal 101 | 102 | ```sh 103 | $ git push origin : 104 | ``` 105 | 106 | 107 | ## Feed and emails 108 | 109 | RSS feeds may be found at: 110 | 111 | `https://github.com/sandrokeil/interop-config/commits/.atom` 112 | 113 | where <branch> is a branch in the repository. 114 | 115 | To subscribe to git email notifications, simply watch or fork the `interop-config` repository on GitHub. 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interoperability Configuration 2 | 3 | > You want to configure your factories? 4 | 5 | > You want to reduce your factory boilerplate code? 6 | 7 | > You want to check automatically for mandatory options or merge default options? 8 | 9 | > You want to have a valid config structure? 10 | 11 | > You want to generate your configuration files from factory classes? 12 | 13 | > This library comes to the rescue! 14 | 15 | [![Build Status](https://travis-ci.org/sandrokeil/interop-config.png?branch=master)](https://travis-ci.org/sandrokeil/interop-config) 16 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/sandrokeil/interop-config/badges/quality-score.png?s=cdef161c14156e3e36ed0ce3d6fd7979d38d916c)](https://scrutinizer-ci.com/g/sandrokeil/interop-config/) 17 | [![Coverage Status](https://coveralls.io/repos/sandrokeil/interop-config/badge.svg?branch=master)](https://coveralls.io/r/sandrokeil/interop-config?branch=master) 18 | [![Latest Stable Version](https://poser.pugx.org/sandrokeil/interop-config/v/stable.png)](https://packagist.org/packages/sandrokeil/interop-config) 19 | [![Total Downloads](https://poser.pugx.org/sandrokeil/interop-config/downloads.png)](https://packagist.org/packages/sandrokeil/interop-config) 20 | [![License](https://poser.pugx.org/sandrokeil/interop-config/license.png)](https://packagist.org/packages/sandrokeil/interop-config) 21 | 22 | `interop-config` provides interfaces and a concrete implementation to create instances depending on configuration via 23 | factory classes and ensures a valid config structure. It can also be used to auto discover factories 24 | and to create configuration files. 25 | 26 | * **Well tested.** Besides unit test and continuous integration/inspection this solution is also ready for production use. 27 | * **Framework agnostic** This PHP library does not depends on any framework but you can use it with your favourite framework. 28 | * **Every change is tracked**. Want to know whats new? Take a look at [CHANGELOG.md](https://github.com/sandrokeil/interop-config/blob/master/CHANGELOG.md) 29 | * **Listen to your ideas.** Have a great idea? Bring your tested pull request or open a new issue. See [CONTRIBUTING.md](CONTRIBUTING.md) 30 | 31 | You should have coding conventions and you should have config conventions. If not, you should think about that. 32 | `interop-config` is universally applicable! See further [documentation](http://sandrokeil.github.io/interop-config/ "Latest interop-config documentation") for more details. 33 | 34 | ## Installation 35 | 36 | The suggested installation method is via composer. For composer documentation, please refer to 37 | [getcomposer.org](http://getcomposer.org/). 38 | 39 | Run `composer require sandrokeil/interop-config` to install interop-config. Version `1.x` is for PHP < 7.1 and Version `2.x` is for PHP >= 7.1. 40 | 41 | ## Documentation 42 | For the latest online documentation visit [http://sandrokeil.github.io/interop-config/](http://sandrokeil.github.io/interop-config/ "Latest interop-config documentation"). 43 | Refer the *Quick Start* section for a detailed explanation. 44 | 45 | Documentation is [in the doc tree](doc/), and can be compiled using [bookdown](http://bookdown.io) or [Docker](https://www.docker.com/) 46 | 47 | ```console 48 | $ docker run -it --rm -v $(pwd):/app sandrokeil/bookdown doc/bookdown.json 49 | $ docker run -it --rm -p 8080:8080 -v $(pwd):/app php:7.1-cli php -S 0.0.0.0:8080 -t /app/doc/html 50 | ``` 51 | 52 | or run *bookdown* 53 | 54 | ```console 55 | $ ./vendor/bin/bookdown doc/bookdown.json 56 | $ php -S 0.0.0.0:8080 -t doc/html/ 57 | ``` 58 | 59 | Then browse to [http://localhost:8080/](http://localhost:8080/) 60 | 61 | ## Projects 62 | This is a list of projects who are using `interop-config` interfaces ([incomplete](https://packagist.org/packages/sandrokeil/interop-config/dependents)). 63 | 64 | * [prooph/service-bus](https://github.com/prooph/service-bus) - PHP Lightweight Message Bus supporting CQRS 65 | * [prooph/event-store](https://github.com/prooph/event-store) - PHP EventStore Implementation 66 | * [prooph/snapshot-store](https://github.com/prooph/snapshot-store) - Simple and lightweight snapshot store 67 | * [prooph/psr7-middleware](https://github.com/prooph/psr7-middleware) - PSR-7 Middleware for prooph components 68 | 69 | ## Benchmarks 70 | The benchmarks uses [PHPBench](http://phpbench.readthedocs.org/en/latest/) and can be started by the following command: 71 | 72 | ```console 73 | $ ./vendor/bin/phpbench run -v --report=table 74 | ``` 75 | 76 | or with [Docker](https://www.docker.com/) 77 | 78 | ```console 79 | $ docker run --rm -it --volume $(pwd):/app prooph/php:7.1-cli-opcache php ./vendor/bin/phpbench run --report=table 80 | ``` 81 | 82 | You can use the `group` and `filter` argument to get only results for a specific group/filter. 83 | 84 | These groups are available: `perf`, `config`, `configId`, `mandatory`, `mandatoryRev` and `default` 85 | 86 | These filters are available: `can`, `options` and `fallback` 87 | -------------------------------------------------------------------------------- /benchmark/ArrayPerfBench.php: -------------------------------------------------------------------------------- 1 | config = $this->getTestConfig(); 33 | } 34 | 35 | /** 36 | * @Subject 37 | */ 38 | public function isArray(): bool 39 | { 40 | return is_array($this->config); 41 | } 42 | 43 | /** 44 | * @Subject 45 | */ 46 | public function castArray(): bool 47 | { 48 | return (array)$this->config === $this->config; 49 | } 50 | 51 | /** 52 | * Returns test config 53 | * 54 | * @return array 55 | */ 56 | private function getTestConfig(): array 57 | { 58 | // Load the user-defined test configuration file, if it exists; otherwise, load default 59 | if (is_readable('test/TestConfig.php')) { 60 | $testConfig = require 'test/testing.config.php'; 61 | } else { 62 | $testConfig = require 'test/testing.config.php.dist'; 63 | } 64 | 65 | return $testConfig; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /benchmark/BaseCase.php: -------------------------------------------------------------------------------- 1 | config = $this->getTestConfig(); 51 | $this->factory = $this->getFactoryClass(); 52 | $this->configId = $this->factory instanceof RequiresConfigId ? 'orm_default' : null; 53 | } 54 | 55 | /** 56 | * Returns test config 57 | * 58 | * @return array 59 | */ 60 | private function getTestConfig(): array 61 | { 62 | // Load the user-defined test configuration file, if it exists; otherwise, load default 63 | if (is_readable('test/TestConfig.php')) { 64 | $testConfig = require 'test/testing.config.php'; 65 | } else { 66 | $testConfig = require 'test/testing.config.php.dist'; 67 | } 68 | 69 | return $testConfig; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /benchmark/ContainerIdBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"configId"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"configId"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/FlexibleConfigurationBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"config"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"config"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/PackageConfigBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"config"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"config"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/ProvidesDefaultOptionsBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"default", "config"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"default", "config"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/ProvidesDefaultOptionsMandatoryBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"default", "config", "mandatory"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"default", "config", "mandatory"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/ProvidesDefaultOptionsMandatoryContainerIdBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"default", "configId", "mandatory"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"default", "configId", "mandatory"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/RequiresMandatoryOptionsBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"mandatory", "config"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"mandatory", "config"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/RequiresMandatoryOptionsContainerIdBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"configId", "mandatory"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"configId", "mandatory"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/RequiresMandatoryOptionsRecursiveContainerIdBench.php: -------------------------------------------------------------------------------- 1 | factory->options($this->config, $this->configId); 29 | } 30 | 31 | /** 32 | * @Subject 33 | * @Groups({"configId", "mandatoryRec"}) 34 | */ 35 | public function can(): void 36 | { 37 | $this->factory->canRetrieveOptions($this->config, $this->configId); 38 | } 39 | 40 | /** 41 | * @Subject 42 | * @Groups({"configId", "mandatoryRec"}) 43 | */ 44 | public function fallback(): void 45 | { 46 | $this->factory->optionsWithFallback($this->config, $this->configId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmark/blackfire.php: -------------------------------------------------------------------------------- 1 | classSetUp(); 24 | 25 | $probe = BlackfireProbe::getMainInstance(); 26 | $probe->enable(); 27 | $benchmarkClass->options(); 28 | $probe->disable(); 29 | -------------------------------------------------------------------------------- /bin/interop-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | Usage: 32 | command [options] [arguments] 33 | 34 | Options: 35 | -h, --help, help Display this help message 36 | 37 | Available commands: 38 | generate-config Generates options for the provided class name 39 | display-config Displays current options for the provided class name 40 | EOF; 41 | 42 | try { 43 | switch ($command) { 44 | case 'display-config': 45 | $command = new Tool\ConfigReaderCommand(); 46 | $status = $command($argv); 47 | exit($status); 48 | case 'generate-config': 49 | $command = new Tool\ConfigDumperCommand(); 50 | $status = $command($argv); 51 | exit($status); 52 | case '-h': 53 | case '--help': 54 | case 'help': 55 | $consoleHelper = new ConsoleHelper(); 56 | $consoleHelper->writeLine($help); 57 | exit(0); 58 | default: 59 | $consoleHelper = new ConsoleHelper(); 60 | $consoleHelper->writeErrorMessage(strip_tags($help)); 61 | exit(1); 62 | } 63 | } catch (\Throwable $e) { 64 | $consoleHelper = new ConsoleHelper(); 65 | $consoleHelper->writeErrorMessage($e->getMessage()); 66 | $consoleHelper->writeLine($help); 67 | exit(1); 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandrokeil/interop-config", 3 | "description": "Provides interfaces and a concrete implementation to create instances depending on configuration via factory classes and ensures a valid config structure. It can also be used to auto discover factories and to create configuration files.", 4 | "homepage": "https://github.com/sandrokeil/interop-config", 5 | "license": "BSD-3-Clause", 6 | "type": "library", 7 | "keywords": [ 8 | "php", 9 | "config", 10 | "interop", 11 | "service", 12 | "factory", 13 | "options", 14 | "interfaces", 15 | "standard", 16 | "configuration" 17 | ], 18 | "config": { 19 | "sort-packages": true 20 | }, 21 | "authors": [ 22 | { 23 | "name": "Sandro Keil", 24 | "homepage": "https://sandro-keil.de", 25 | "role": "maintainer" 26 | } 27 | ], 28 | "support": { 29 | "issues": "https://github.com/sandrokeil/interop-config/issues", 30 | "source": "https://github.com/sandrokeil/interop-config", 31 | "docs": "http://sandrokeil.github.io/interop-config/" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Interop\\Config\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "InteropTest\\Config\\": "test/", 41 | "InteropBench\\Config\\": "benchmark/" 42 | } 43 | }, 44 | "require": { 45 | "php": "^7.1 || ^8.0" 46 | }, 47 | "require-dev": { 48 | "bookdown/bookdown": "^1.1.0", 49 | "malukenho/docheader": "^0.1.7", 50 | "php-coveralls/php-coveralls": "^2.1", 51 | "phpbench/phpbench": "^0.15", 52 | "phpunit/phpunit": "^7.0.1 || ^9.3.8", 53 | "squizlabs/php_codesniffer": "^3.0", 54 | "webuni/commonmark-attributes-extension": "^0.5.0", 55 | "webuni/commonmark-table-extension": "^0.6.1" 56 | }, 57 | "suggest": { 58 | "psr/container": "To retrieve config in your factories" 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-master": "2.1-dev", 65 | "dev-develop": "2.2-dev" 66 | } 67 | }, 68 | "bin": [ 69 | "bin/interop-config" 70 | ], 71 | "scripts": { 72 | "check": [ 73 | "@cs", 74 | "@docheader", 75 | "@test" 76 | ], 77 | "cs": "phpcs", 78 | "cs-fix": "phpcbf", 79 | "test": "phpunit --no-coverage", 80 | "test-coverage": "phpunit", 81 | "docheader": "docheader check src/ test/ benchmark/", 82 | "docs": "bookdown doc/bookdown.json", 83 | "benchmark": "phpbench run -v --report=table" 84 | }, 85 | "archive": { 86 | "exclude": [ 87 | ".coveralls.yml", 88 | ".scrutinizer.yml", 89 | ".travis.yml", 90 | "benchmark", 91 | "build", 92 | "humbug.json.dist", 93 | "phpbench.json.dist", 94 | "phpunit.xml*", 95 | "test" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /doc/book/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | This file describes the classes of this package. All interfaces can be combined. Don't panic, the interfaces are 4 | quite easy and the `ConfigurationTrait`, which is a concrete implementation, has full support of those interfaces. You 5 | need only one interface called `RequiresConfig` to start and then you can implement the others if they are needed. 6 | 7 | * `RequiresConfig` Interface 8 | * `RequiresConfigId` Interface 9 | * `RequiresMandatoryOptions` Interface 10 | * `ProvidesDefaultOptions` Interface 11 | 12 | ## RequiresConfig Interface 13 | 14 | The `RequiresConfig` interface exposes three methods: `dimensions`, `canRetrieveOptions` and `options`. 15 | 16 | ### dimensions() 17 | ```php 18 | public function dimensions() : iterable 19 | ``` 20 | 21 | The `dimensions` method has no parameters and MUST return an array. The values (used as key names) of the array are used 22 | as the depth of the configuration to retrieve options. Two values means a configuration depth of two. An empty array is 23 | valid. 24 | 25 | ### canRetrieveOptions() 26 | ```php 27 | public function canRetrieveOptions($config) : bool 28 | ``` 29 | Checks if options are available depending on provided dimensions and checks that the retrieved options are an array or 30 | have implemented \ArrayAccess. 31 | 32 | ### options() 33 | ```php 34 | public function options($config) : [] 35 | ``` 36 | The `options` method takes one mandatory parameter: a configuration array. It MUST be an array or an object which implements the 37 | `ArrayAccess` interface. A call to `options` returns the configuration depending on provided dimensions of the 38 | class or throws an exception if the parameter is invalid or if the configuration is missing or if a mandatory option is missing. 39 | 40 | If the `ProvidesDefaultOptions` interface is implemented, these options MUST be overridden by the provided config. 41 | 42 | #### Exceptions 43 | Exceptions directly thrown by the `options` method MUST implement the `Interop\Config\Exception\ExceptionInterface`. 44 | 45 | If the configuration parameter is not an array or not an object which implements the `ArrayAccess` interface the method 46 | SHOULD throw a `Interop\Config\Exception\InvalidArgumentException`. 47 | 48 | If a key which is returned from `dimensions` is not set under the previous dimensions key in the configuration parameter, 49 | the method SHOULD throw a `Interop\Config\Exception\OptionNotFoundException`. 50 | 51 | If a value from the configuration based on dimensions is not an array or an object which has `\ArrayAccess` implemented, 52 | the method SHOULD throw a `Interop\Config\Exception\UnexpectedValueException`. 53 | 54 | If the class implements the `RequiresMandatoryOptions` interface and if a mandatory option from `mandatoryOptions` is not set 55 | in the options array which was retrieved from the configuration parameter before, the method SHOULD throw a 56 | `Interop\Config\Exception\MandatoryOptionNotFoundException`. 57 | 58 | If the retrieved options are not of type array or \ArrayAccess the method SHOULD throw a `Interop\Config\Exception\UnexpectedValueException`. 59 | 60 | ## RequiresMandatoryOptions Interface 61 | The `RequiresMandatoryOptions` interface exposes one method: `mandatoryOptions` 62 | 63 | ### mandatoryOptions() 64 | ```php 65 | public function mandatoryOptions() : iterable 66 | ``` 67 | The `mandatoryOptions` method has no parameters and MUST return an array of strings which represents the list of mandatory 68 | options. This array can have a multiple depth. 69 | 70 | ## ProvidesDefaultOptions Interface 71 | The `DefaultOptions` interface exposes one method: `defaultOptions` 72 | 73 | ### defaultOptions() 74 | ```php 75 | public function defaultOptions() : iterable 76 | ``` 77 | The `defaultOptions` method has no parameters and MUST return an key-value array where the key is the option name and 78 | the value is the default value for this option. This array can have a multiple depth. 79 | The return value MUST be compatible with the PHP function `array_replace_recursive`. 80 | 81 | ## RequiresConfigId 82 | The `RequiresConfigId` is only a marker interface and has no methods. It marks the factory that multiple instances are 83 | supported. The `ConfigurationTrait` has an optional parameter `$configId` implemented for the methods of `RequiresConfig`. 84 | So it is full supported. 85 | 86 | ## ConfigurationTrait 87 | The `ConfigurationTrait` implements the functions of `RequiresConfig` interface and has support for 88 | `ProvidesDefaultOptions`, `RequiresMandatoryOptions` and `RequiresConfigId` interfaces if the the class has they implemented. 89 | 90 | Additional it has one more method `optionsWithFallback` to reduce boilerplate code. 91 | 92 | ### optionsWithFallback() 93 | ```php 94 | public function optionsWithFallback($config) : [] 95 | ``` 96 | Checks if options can be retrieved from config and if not, default options (`ProvidesDefaultOptions` interface) or an empty array will be returned. 97 | -------------------------------------------------------------------------------- /doc/book/console-tools.md: -------------------------------------------------------------------------------- 1 | # Console Tools 2 | Starting in 2.1.0, interop-config began shipping with console tools. 3 | 4 | To get an overview of available commands run in your CLI `./vendor/bin/interop-config help`. This displays the following help message. 5 | 6 | ```bash 7 | Usage: 8 | command [options] [arguments] 9 | 10 | Options: 11 | -h, --help, help Display this help message 12 | 13 | Available commands: 14 | generate-config Generates options for the provided class name 15 | display-config Displays current options for the provided class name 16 | ``` 17 | 18 | ## generate-config 19 | The `generate-config` command is pretty handy. It has never been so easy to create the configuration for a class which 20 | uses one of the `Interop\Config` interfaces. Depending on implemented interfaces, a wizard will ask you for the option values. 21 | It is also possible to update your current configuration. The value in brackets is used, if input is blank. 22 | 23 | ```bash 24 | Usage: 25 | generate-config [options] [] [] 26 | 27 | Options: 28 | -h, --help, help Display this help message 29 | 30 | Arguments: 31 | configFile Path to a config file or php://stdout for which to generate options. 32 | className Name of the class to reflect and for which to generate options. 33 | 34 | Reads the provided configuration file (creating it if it does not exist), and injects it with options for the provided 35 | class name, writing the changes back to the file. 36 | ``` 37 | 38 | If your PHP config file is in the folder `config/global.php` and you have a class `My\AwesomeFactory` then you run 39 | 40 | ```bash 41 | $ ./vendor/bin/interop-config generate-config config/global.php "My\AwesomeFactory" 42 | ``` 43 | 44 | ## display-config 45 | You can also see which options are set in the configuration file for a factory. If multiple configurations are supported 46 | through the `Interop\Config\RequiresConfigId` you can enter a *config id* or leave it blank to display all configurations. 47 | 48 | ```bash 49 | Usage: 50 | display-config [options] [] [] 51 | 52 | Options: 53 | -h, --help, help Display this help message 54 | 55 | Arguments: 56 | configFile Path to a config file for which to display options. It must return an array / ArrayObject. 57 | className Name of the class to reflect and for which to display options. 58 | 59 | Reads the provided configuration file and displays options for the provided class name. 60 | ``` 61 | 62 | If your PHP config file is in the folder `config/global.php` and you have a class `My\AwesomeFactory` then you run 63 | 64 | ```bash 65 | $ ./vendor/bin/interop-config display-config config/global.php "My\AwesomeFactory" 66 | ``` 67 | -------------------------------------------------------------------------------- /doc/book/contribute/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Contribute", 3 | "content": [ 4 | { 5 | "contributing": "../../../CONTRIBUTING.md" 6 | }, 7 | { 8 | "readme-git": "../../../README-GIT.md" 9 | }, 10 | { 11 | "changelog": "../../../CHANGELOG.md" 12 | } 13 | ], 14 | "target": "./html", 15 | "tocDepth": 2, 16 | "copyright": "Copyright (c) 2015-2016 Sandro Keil" 17 | } 18 | -------------------------------------------------------------------------------- /doc/book/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This files contains examples for each interface. The factory class uses the `ConfigurationTrait` to retrieve options 4 | from a configuration and optional to perform a mandatory option check or merge default options. There is also an 5 | example for a independent config structure of the Zend Expressive TwigRendererFactory. The 6 | [psr-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md "Container Interoperability") specification 7 | is used, so it's framework agnostic. 8 | 9 | ## Use a vendor.package.id config structure 10 | 11 | Let's assume we have the following module configuration: 12 | 13 | ```php 14 | // interop config example 15 | return [ 16 | // vendor name 17 | 'doctrine' => [ 18 | // package name 19 | 'connection' => [ 20 | // container id 21 | 'orm_default' => [ 22 | // mandatory params 23 | 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', 24 | 'params' => [ 25 | 'host' => 'localhost', 26 | 'port' => '3306', 27 | 'user' => 'username', 28 | 'password' => 'password', 29 | 'dbname' => 'database', 30 | ], 31 | ], 32 | ], 33 | ], 34 | ]; 35 | ``` 36 | 37 | > Note that the configuration above is injected as `$config` in `options()` 38 | 39 | ### Retrieving options 40 | Then you have easily access to the `orm_default` options in your method with `ConfigurationTrait`. 41 | 42 | ```php 43 | use Interop\Config\ConfigurationTrait; 44 | use Interop\Config\RequiresConfigId; 45 | use Psr\Container\ContainerInterface; 46 | 47 | class MyDBALConnectionFactory implements RequiresConfigId 48 | { 49 | use ConfigurationTrait; 50 | 51 | public function __invoke(ContainerInterface $container) 52 | { 53 | // get options for doctrine.connection.orm_default 54 | $options = $this->options($container->get('config'), 'orm_default'); 55 | 56 | // check if mandatory options are available or use \Interop\Config\RequiresMandatoryOptions, see below 57 | if (empty($options['driverClass'])) { 58 | throw new Exception\RuntimeException( 59 | sprintf( 60 | 'Driver class was not set for configuration %s.%s.%s', 61 | 'doctrine', 62 | 'connection', 63 | 'orm_default' 64 | ) 65 | ); 66 | } 67 | 68 | if (empty($options['params'])) { 69 | throw new Exception\RuntimeException( 70 | sprintf( 71 | 'Params was not set for configuration %s.%s.%s', 72 | 'doctrine', 73 | 'connection', 74 | 'orm_default' 75 | ) 76 | ); 77 | } 78 | 79 | $driverClass = $options['driverClass']; 80 | $params = $options['params']; 81 | 82 | // create your instance and set options 83 | 84 | return $instance; 85 | } 86 | 87 | /** 88 | * Is used to retrieve options from the configuration array ['doctrine' => ['connection' => [...]]]. 89 | * 90 | * @return iterable 91 | */ 92 | public function dimensions() : iterable 93 | { 94 | return ['doctrine', 'connection']; 95 | } 96 | } 97 | ``` 98 | 99 | ### Mandatory options check 100 | You can also check for mandatory options automatically with `MandatoryOptionsInterface`. Now we want also check that 101 | option `driverClass` and `params` are available. So we also implement in the example above the interface 102 | `RequiresMandatoryOptions`. If one of these options are missing, an exception is raised. 103 | 104 | ```php 105 | use Interop\Config\ConfigurationTrait; 106 | use Interop\Config\RequiresMandatoryOptions; 107 | use Interop\Config\RequiresConfigId; 108 | use Psr\Container\ContainerInterface; 109 | 110 | class MyDBALConnectionFactory implements RequiresConfigId, RequiresMandatoryOptions 111 | { 112 | use ConfigurationTrait; 113 | 114 | public function __invoke(ContainerInterface $container) 115 | { 116 | // get options for doctrine.connection.orm_default 117 | $options = $this->options($container->get('config'), 'orm_default'); 118 | 119 | // mandatory options check is automatically done by RequiresMandatoryOptions 120 | 121 | $driverClass = $options['driverClass']; 122 | $params = $options['params']; 123 | 124 | // create your instance and set options 125 | 126 | return $instance; 127 | } 128 | 129 | /** 130 | * Returns a list of mandatory options which must be available 131 | * 132 | * @return string[] List with mandatory options 133 | */ 134 | public function mandatoryOptions() : iterable 135 | { 136 | return [ 137 | 'driverClass', 138 | 'params', 139 | ]; 140 | } 141 | 142 | /** 143 | * Is used to retrieve options from the configuration array ['doctrine' => ['connection' => [...]]]. 144 | * 145 | * @return [] 146 | */ 147 | public function dimensions() : iterable 148 | { 149 | return ['doctrine', 'connection']; 150 | } 151 | } 152 | ``` 153 | 154 | ## Use a static factory 155 | Creation of a new instance of a specific config key is really easy by using the static variants of the factory. With 156 | this you don't have to instantiate a factory to use another config id in your config, which is really awesome. Remember, 157 | config files should only contain scalar values, so you can cache it in production. We have to add some lines of code 158 | to our factory. The magic is done via the `__callStatic()` method. 159 | 160 | ```php 161 | use Interop\Config\ConfigurationTrait; 162 | use Interop\Config\RequiresMandatoryOptions; 163 | use Interop\Config\RequiresConfigId; 164 | use Psr\Container\ContainerInterface; 165 | 166 | class MyDBALConnectionFactory implements RequiresConfigId, RequiresMandatoryOptions 167 | { 168 | use ConfigurationTrait; 169 | 170 | /** 171 | * @var string 172 | */ 173 | private $configId; 174 | 175 | /** 176 | * Creates a new instance from a specified config, specifically meant to be used as static factory. 177 | * 178 | * In case you want to use another config key than provided by the factories, you can add the following factory to 179 | * your config: 180 | * 181 | * 182 | * [MyDBALConnectionFactory::class, 'orm_second'], 185 | * ]; 186 | * 187 | * 188 | * @param string $name 189 | * @param array $arguments 190 | * @return mixed 191 | * @throws \InvalidArgumentException 192 | */ 193 | public static function __callStatic($name, array $arguments) 194 | { 195 | if (!isset($arguments[0]) || !$arguments[0] instanceof ContainerInterface) { 196 | throw new \InvalidArgumentException( 197 | sprintf('The first argument must be of type %s', ContainerInterface::class) 198 | ); 199 | } 200 | return (new static($name))->__invoke($arguments[0]); 201 | } 202 | 203 | /** 204 | * @param string $configId 205 | */ 206 | public function __construct(string $configId) 207 | { 208 | $this->configId = $configId; 209 | } 210 | 211 | public function __invoke(ContainerInterface $container) 212 | { 213 | // get options for doctrine.connection.[config id] 214 | $options = $this->options($container->get('config'), $this->configId); 215 | 216 | // mandatory options check is automatically done by RequiresMandatoryOptions 217 | 218 | $driverClass = $options['driverClass']; 219 | $params = $options['params']; 220 | 221 | // create your instance and set options 222 | 223 | return $instance; 224 | } 225 | 226 | /** 227 | * Returns a list of mandatory options which must be available 228 | * 229 | * @return iterable List with mandatory options 230 | */ 231 | public function mandatoryOptions() : iterable 232 | { 233 | return [ 234 | 'driverClass', 235 | 'params', 236 | ]; 237 | } 238 | 239 | /** 240 | * Is used to retrieve options from the configuration array ['doctrine' => ['connection' => [...]]]. 241 | * 242 | * @return iterable 243 | */ 244 | public function dimensions() : iterable 245 | { 246 | return ['doctrine', 'connection']; 247 | } 248 | } 249 | ``` 250 | 251 | ### Default options 252 | Use the `ProvidesDefaultOptions` interface if you have default options. These options are merged with the provided options in 253 | `\Interop\Config\RequiresConfig::options()`. Let's look at this example from 254 | [DoctrineORMModule](https://github.com/doctrine/DoctrineORMModule/blob/master/docs/configuration.md#how-to-use-two-connections). 255 | All the options under the key *orm_crawler* are optional, but it's not visible in the factory. 256 | 257 | ```php 258 | return [ 259 | 'doctrine' => [ 260 | 'configuration' => [ 261 | 'orm_crawler' => [ 262 | 'metadata_cache' => 'array', 263 | 'query_cache' => 'array', 264 | 'result_cache' => 'array', 265 | 'hydration_cache' => 'array', 266 | ], 267 | ], 268 | ], 269 | ]; 270 | ``` 271 | 272 | ```php 273 | use Interop\Config\ConfigurationTrait; 274 | use Interop\Config\ProvidesDefaultOptions; 275 | use Interop\Config\RequiresConfigId; 276 | 277 | class ConfigurationFactory implements RequiresConfigId, ProvidesDefaultOptions 278 | { 279 | use ConfigurationTrait; 280 | 281 | public function __invoke(ContainerInterface $container) 282 | { 283 | // get options for doctrine.configuration.orm_crawler 284 | $options = $this->options($container->get('config'), 'orm_crawler'); 285 | 286 | # these keys are always available now 287 | $options['metadata_cache']; 288 | $options['query_cache']; 289 | $options['result_cache']; 290 | $options['hydration_cache']; 291 | 292 | // create your instance and set options 293 | 294 | return $instance; 295 | } 296 | 297 | /** 298 | * Returns a list of default options, which are merged in \Interop\Config\RequiresConfig::options 299 | * 300 | * @return iterable List with default options and values 301 | */ 302 | public function defaultOptions() : iterable 303 | { 304 | return [ 305 | 'metadata_cache' => 'array', 306 | 'query_cache' => 'array', 307 | 'result_cache' => 'array', 308 | 'hydration_cache' => 'array', 309 | ]; 310 | } 311 | 312 | /** 313 | * Is used to retrieve options from the configuration array 314 | * ['doctrine' => ['configuration' => []]]. 315 | * 316 | * @return iterable 317 | */ 318 | public function dimensions() : iterable 319 | { 320 | return ['doctrine', 'configuration']; 321 | } 322 | } 323 | ``` 324 | 325 | ## Use arbitrary configuration structure 326 | Whatever configuration structure you use, `interop-config` can handle it. You can use a three-dimensional array with 327 | `vendor.package.id` like the examples above or you don't care of it and organize your configuration by behavior or 328 | nature (db, cache, ... or sale, admin). 329 | 330 | The following example demonstrates how to replace the [Zend Expressive TwigRendererFactory](https://github.com/zendframework/zend-expressive-twigrenderer/blob/e1dd1744bf9ba5ec364fc1320566699d04f407c4/src/TwigRendererFactory.php). 331 | The factory uses optionally the following config structure: 332 | 333 | ```php 334 | return [ 335 | 'debug' => true, 336 | 'templates' => [ 337 | 'cache_dir' => 'path to cached templates', 338 | 'assets_url' => 'base URL for assets', 339 | 'assets_version' => 'base version for assets', 340 | 'extension' => 'file extension used by templates; defaults to html.twig', 341 | 'paths' => [ 342 | // namespace / path pairs 343 | // 344 | // Numeric namespaces imply the default/main namespace. Paths may be 345 | // strings or arrays of string paths to associate with the namespace. 346 | ], 347 | ], 348 | 'twig' => [ 349 | 'cache_dir' => 'path to cached templates', 350 | 'assets_url' => 'base URL for assets', 351 | 'assets_version' => 'base version for assets', 352 | 'extensions' => [ 353 | // extension service names or instances 354 | ], 355 | ], 356 | ]; 357 | ``` 358 | 359 | You can see that the factory uses different keys (debug, templates, twig) of the config array on the same level. This 360 | configuration is maybe used by other factories too like the `debug` setting. `interop-config` reduces the checks in the 361 | factory and gives the user the possibility to find out the config structure. More than that, it is possible to create the 362 | configuration file from the factory. 363 | 364 | ```php 365 | namespace Zend\Expressive\Twig; 366 | 367 | use Psr\Container\ContainerInterface; 368 | use Twig_Environment as TwigEnvironment; 369 | use Twig_Extension_Debug as TwigExtensionDebug; 370 | use Twig_ExtensionInterface; 371 | use Twig_Loader_Filesystem as TwigLoader; 372 | use Zend\Expressive\Router\RouterInterface; 373 | 374 | // interop-config 375 | use Interop\Config\ConfigurationTrait; 376 | use Interop\Config\RequiresConfig; 377 | use Interop\Config\ProvidesDefaultOptions; 378 | 379 | class TwigRendererFactory implements RequiresConfig, ProvidesDefaultOptions 380 | { 381 | use ConfigurationTrait; 382 | 383 | /** 384 | * Uses root config to retrieve several options 385 | * 386 | * @return iterable 387 | */ 388 | public function dimensions() : iterable 389 | { 390 | return []; 391 | } 392 | 393 | /** 394 | * This is the whole config structure with default settings for this factory 395 | */ 396 | public function defaultOptions() : iterable 397 | { 398 | return [ 399 | 'debug' => false, 400 | 'templates' => [ 401 | 'extension' => 'html.twig', 402 | 'paths' => [], 403 | ], 404 | 'twig' => [ 405 | 'cache_dir' => false, 406 | 'assets_url' => '', 407 | 'assets_version' => '', 408 | 'extensions' => [], 409 | ], 410 | ]; 411 | } 412 | 413 | /** 414 | * @param ContainerInterface $container 415 | * @return TwigRenderer 416 | */ 417 | public function __invoke(ContainerInterface $container) 418 | { 419 | $config = $container->has('config') ? $container->get('config') : []; 420 | 421 | // no OptionNotFoundException is thrown from ConfigurationTrait, because there are no config dimensions 422 | $config = $this->options($config); 423 | 424 | $debug = (bool) $config['debug']; 425 | 426 | // Create the engine instance 427 | $loader = new TwigLoader(); 428 | $environment = new TwigEnvironment($loader, [ 429 | 'cache' => $debug ? false : $config['twig']['cache_dir'], 430 | 'debug' => $debug, 431 | 'strict_variables' => $debug, 432 | 'auto_reload' => $debug 433 | ]); 434 | // Add extensions 435 | if ($container->has(RouterInterface::class)) { 436 | $environment->addExtension(new TwigExtension( 437 | $container->get(RouterInterface::class), 438 | $config['twig']['assets_url'], 439 | $config['twig']['assets_version'] 440 | )); 441 | } 442 | if ($debug) { 443 | $environment->addExtension(new TwigExtensionDebug()); 444 | } 445 | // Add user defined extensions 446 | $this->injectExtensions($environment, $container, $config['twig']['extensions']); 447 | // Inject environment 448 | $twig = new TwigRenderer($environment, $config['templates']['extension']); 449 | // Add template paths 450 | foreach ($config['templates']['paths'] as $namespace => $paths) { 451 | $namespace = is_numeric($namespace) ? null : $namespace; 452 | foreach ((array) $paths as $path) { 453 | $twig->addPath($path, $namespace); 454 | } 455 | } 456 | return $twig; 457 | } 458 | /** 459 | * Inject extensions into the TwigEnvironment instance. 460 | * 461 | * @param TwigEnvironment $environment 462 | * @param ContainerInterface $container 463 | * @param array $extensions 464 | * @throws Exception\InvalidExtensionException 465 | */ 466 | private function injectExtensions(TwigEnvironment $environment, ContainerInterface $container, array $extensions) 467 | { 468 | foreach ($extensions as $extension) { 469 | // Load the extension from the container 470 | if (is_string($extension) && $container->has($extension)) { 471 | $extension = $container->get($extension); 472 | } 473 | if (! $extension instanceof Twig_ExtensionInterface) { 474 | throw new Exception\InvalidExtensionException(sprintf( 475 | 'Twig extension must be an instance of Twig_ExtensionInterface; "%s" given,', 476 | is_object($extension) ? get_class($extension) : gettype($extension) 477 | )); 478 | } 479 | if ($environment->hasExtension($extension->getName())) { 480 | continue; 481 | } 482 | $environment->addExtension($extension); 483 | } 484 | } 485 | // The mergeConfig function is not needed anymore 486 | // private function mergeConfig($config) 487 | } 488 | ``` 489 | -------------------------------------------------------------------------------- /doc/book/getting-started/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Getting Started", 3 | "content": [ 4 | { 5 | "overview": "../intro.md" 6 | }, 7 | { 8 | "quick-Start": "../quick-start.md" 9 | } 10 | ], 11 | "target": "./html", 12 | "tocDepth": 2, 13 | "copyright": "Copyright (c) 2015-2016 Sandro Keil
Powered by Bookdown Bootswatch Templates" 14 | } 15 | -------------------------------------------------------------------------------- /doc/book/intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | > You want to configure your factories? 4 | 5 | > You want to reduce your factory boilerplate code? 6 | 7 | > You want to check automatically for mandatory options or merge default options? 8 | 9 | > You want to have a valid config structure? 10 | 11 | > You want to generate your configuration files from factory classes? 12 | 13 | > This library comes to the rescue! 14 | 15 | [![Build Status](https://travis-ci.org/sandrokeil/interop-config.png?branch=master)](https://travis-ci.org/sandrokeil/interop-config) 16 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/sandrokeil/interop-config/badges/quality-score.png?s=cdef161c14156e3e36ed0ce3d6fd7979d38d916c)](https://scrutinizer-ci.com/g/sandrokeil/interop-config/) 17 | [![Coverage Status](https://coveralls.io/repos/sandrokeil/interop-config/badge.svg?branch=master)](https://coveralls.io/r/sandrokeil/interop-config?branch=master) 18 | [![HHVM Status](http://hhvm.h4cc.de/badge/sandrokeil/interop-config.svg?style=flat)](http://hhvm.h4cc.de/package/sandrokeil/interop-config) 19 | [![PHP 7 ready](http://php7ready.timesplinter.ch/sandrokeil/interop-config/badge.svg)](https://travis-ci.org/sandrokeil/interop-config) 20 | [![Latest Stable Version](https://poser.pugx.org/sandrokeil/interop-config/v/stable.png)](https://packagist.org/packages/sandrokeil/interop-config) 21 | [![Dependency Status](https://www.versioneye.com/user/projects/53615c75fe0d0720eb00009e/badge.png)](https://www.versioneye.com/php/sandrokeil:interop-config/0.3.1) 22 | [![Total Downloads](https://poser.pugx.org/sandrokeil/interop-config/downloads.png)](https://packagist.org/packages/sandrokeil/interop-config) 23 | [![Reference Status](https://www.versioneye.com/php/sandrokeil:interop-config/reference_badge.svg?style=flat)](https://www.versioneye.com/php/sandrokeil:interop-config/references) 24 | [![License](https://poser.pugx.org/sandrokeil/interop-config/license.png)](https://packagist.org/packages/sandrokeil/interop-config) 25 | 26 | `intop-config` provides interfaces and a concrete implementation to create instances depending on configuration via 27 | factory classes and ensures a valid config structure. It can also be used to auto discover factories and to create 28 | configuration files. 29 | 30 | * **Well tested.** Besides unit test and continuous integration/inspection this solution is also ready for production use. 31 | * **Framework agnostic** This PHP library does not depends on any framework but you can use it with your favourite framework. 32 | * **Every change is tracked**. Want to know whats new? Take a look at the changelog section. 33 | * **Listen to your ideas.** Have a great idea? Bring your tested pull request or open a new issue. See contributing section. 34 | 35 | You should have coding conventions and you should have config conventions. If not, you should think about that. 36 | `interop-config` is universally applicable! See further documentation for more details. 37 | 38 | ## Installation 39 | The suggested installation method is via composer. For composer documentation, please refer to 40 | [getcomposer.org](http://getcomposer.org/). 41 | 42 | Run `composer require sandrokeil/interop-config` to install interop-config. 43 | 44 | It is recommended to use [psr-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md) to retrieve the 45 | configuration in your factories. 46 | 47 | ## Config Structure 48 | > The following example is a common practice for libraries. You are free to use another config structure. See examples. 49 | 50 | The config keys should have the following structure `vendor.package.container_id`. The `container_id` is optional and is 51 | only necessary if you have different instances of the same class e.g. database connection. 52 | 53 | A common configuration looks like that: 54 | 55 | ```php 56 | // interop config example 57 | return [ 58 | // vendor name 59 | 'doctrine' => [ 60 | // package name 61 | 'connection' => [ 62 | // container id 63 | 'orm_default' => [ 64 | // mandatory options 65 | 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', 66 | 'params' => [ 67 | 'host' => 'localhost', 68 | 'port' => '3306', 69 | 'user' => 'username', 70 | 'password' => 'password', 71 | 'dbname' => 'database', 72 | ], 73 | ], 74 | ], 75 | ], 76 | ]; 77 | ``` 78 | 79 | So `doctrine` is the vendor, `connection` is the package and `orm_default` is the container id. After that the specified 80 | instance options follow. The following example uses `ConfigurationTrait` which implements the logic to retrieve the 81 | options from a configuration. `RequiresConfigId` interface ensures support for more than one instance. 82 | 83 | > Note that the configuration above is injected as `$config` in `options()` and 84 | [psr-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md) is used to retrieve the application configuration. 85 | 86 | ```php 87 | use Interop\Config\ConfigurationTrait; 88 | use Interop\Config\RequiresConfigId; 89 | use Interop\Config\RequiresMandatoryOptions; 90 | use Psr\Container\ContainerInterface; 91 | 92 | class MyDBALConnectionFactory implements RequiresConfigId, RequiresMandatoryOptions 93 | { 94 | use ConfigurationTrait; 95 | 96 | public function __invoke(ContainerInterface $container) 97 | { 98 | // get options for doctrine.connection.orm_default 99 | $options = $this->options($container->get('config'), 'orm_default'); 100 | 101 | // mandatory options check is automatically done by RequiresMandatoryOptions 102 | 103 | $driverClass = $options['driverClass']; 104 | $params = $options['params']; 105 | 106 | // create your instance and set options 107 | 108 | return $instance; 109 | } 110 | 111 | /** 112 | * Is used to retrieve options from the configuration array ['doctrine' => ['connection' => []]]. 113 | * 114 | * @return iterable 115 | */ 116 | public function dimensions() : iterable 117 | { 118 | return ['doctrine', 'connection']; 119 | } 120 | 121 | /** 122 | * Returns a list of mandatory options which must be available 123 | * 124 | * @return iterable List with mandatory options 125 | */ 126 | public function mandatoryOptions() : iterable 127 | { 128 | return ['params' => ['user', 'password', 'dbname']]; 129 | } 130 | } 131 | ``` 132 | -------------------------------------------------------------------------------- /doc/book/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | Typically you will have a factory which creates a concrete instance depending on some options (dependencies). 3 | 4 | ## RequiresConfig interface 5 | Let's say *My factory requires a configuration* so you will implement the `RequiresConfig` interface. 6 | 7 | ```php 8 | use Interop\Config\RequiresConfig; 9 | 10 | class MyAwesomeFactory implements RequiresConfig 11 | { 12 | public function dimensions() : iterable 13 | { 14 | return ['vendor-package']; 15 | } 16 | 17 | public function canRetrieveOptions($config) : bool 18 | { 19 | // custom implementation depending on specifications 20 | } 21 | 22 | public function options($config) 23 | { 24 | // custom implementation depending on specifications 25 | } 26 | } 27 | ``` 28 | 29 | ### Need configuration per container id 30 | If you support more than one instance with different configuration then you simply use the `RequiresConfigId` interface. 31 | 32 | > Don't use the dimensions() method for container id configuration 33 | 34 | ```php 35 | use Interop\Config\RequiresConfigId; 36 | 37 | class MyAwesomeFactory implements RequiresConfigId 38 | { 39 | public function dimensions() : iterable 40 | { 41 | return ['vendor-package']; 42 | } 43 | 44 | public function canRetrieveOptions($config, string $configId = null) : bool 45 | { 46 | // custom implementation depending on specifications 47 | } 48 | 49 | public function options($config, string $configId = null) 50 | { 51 | // custom implementation depending on specifications 52 | } 53 | } 54 | ``` 55 | 56 | Ok you have now a factory which says that the factory supports a configuration and you have a PHP file which contains 57 | the configuration as a PHP array, but how is the configuration used? 58 | 59 | Depending on the implemented interface `RequiresConfigId` above our configuration PHP file looks like that: 60 | 61 | ```php 62 | // interop config example 63 | return [ 64 | // vendor/package name 65 | 'vendor-package' => [ 66 | // container id 67 | 'container-id' => [ 68 | // some options ... 69 | ], 70 | ], 71 | ]; 72 | ``` 73 | 74 | As you can see that you have to implement the functionality of `canRetrieveOptions()` and `options()` method. Good news, 75 | this is not necessary. See `ConfigurationTrait`. 76 | 77 | ## ConfigurationTrait 78 | The `ConfigurationTrait` is a concrete implementation of the `RequiresConfig` interface and has full support of 79 | `ProvidesDefaultOptions`, `RequiresMandatoryOptions` and `RequiresConfigId`interfaces. It's a 80 | [PHP Trait](http://php.net/manual/en/language.oop5.traits.php "PHP Trait Documentation") so you can extend your factory 81 | from a class. 82 | 83 | Your factory looks now like that: 84 | 85 | ```php 86 | use Interop\Config\RequiresConfigId; 87 | use Interop\Config\ConfigurationTrait; 88 | 89 | class MyAwesomeFactory implements RequiresConfigId 90 | { 91 | use ConfigurationTrait; 92 | 93 | public function dimensions() : iterable 94 | { 95 | return ['vendor-package']; 96 | } 97 | } 98 | ``` 99 | 100 | Now you have all the ingredients to create multiple different instances depending on configuration. 101 | 102 | ## Create an instance 103 | Factories are often implemented as a [callable](http://php.net/manual/en/language.oop5.magic.php#object.invoke "PHP __invoke() Documentation"). 104 | This means that your factory instance can be called like a function. You can also use a `create` method or something else. 105 | 106 | The factory gets a `ContainerInterface` ([Container PSR](https://github.com/php-fig/fig-standards/blob/master/proposed/container-meta.md)) 107 | provided to retrieve the configuration. 108 | 109 | > Note that the configuration above is injected as `$config` in `options()` and 110 | [psr-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md) is used to retrieve the application configuration. 111 | 112 | ```php 113 | use Interop\Config\RequiresConfigId; 114 | use Interop\Config\ConfigurationTrait; 115 | use Psr\Container\ContainerInterface; 116 | 117 | class MyAwesomeFactory implements RequiresConfigId 118 | { 119 | use ConfigurationTrait; 120 | 121 | public function dimensions() : iterable 122 | { 123 | return ['vendor-package']; 124 | } 125 | 126 | public function __invoke(ContainerInterface $container) 127 | { 128 | // get options for vendor-package.container-id 129 | // method options() is implemented in ConfigurationTrait 130 | $options = $this->options($container->get('config'), 'orm_default'); 131 | 132 | return new Awesome($options); 133 | } 134 | } 135 | ``` 136 | The `ConfigurationTrait` does the job to check and retrieve options depending on implemented interfaces. *Nice, but what is 137 | if I have mandatory options?* See `RequiresMandatoryOptions` interface. 138 | 139 | ## RequiresMandatoryOptions interface 140 | The `RequiresConfig::options()` interface specification says that it MUST support mandatory options check. Let's say that we need 141 | params for a db connection. Our config *should* looks like that: 142 | 143 | ```php 144 | // interop config example 145 | return [ 146 | // vendor/package name 147 | 'vendor-package' => [ 148 | // container id 149 | 'container-id' => [ 150 | 'params' => [ 151 | 'user' => 'username', 152 | 'password' => 'password', 153 | 'dbname' => 'database', 154 | ], 155 | ], 156 | ], 157 | ]; 158 | ``` 159 | 160 | Remember our factory sentence. *My factory requires a configuration and requires a container id along with mandatory options*. 161 | The `ConfigurationTrait` ensures that these options are available, otherwise an exception is thrown. This is great, because 162 | the developer gets an exact exception message with what is wrong. This is useful for developers who use your factory the first time. 163 | 164 | ```php 165 | use Interop\Config\RequiresConfigId; 166 | use Interop\Config\RequiresMandatoryOptions; 167 | use Interop\Config\ConfigurationTrait; 168 | use Psr\Container\ContainerInterface; 169 | 170 | class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions 171 | { 172 | use ConfigurationTrait; 173 | 174 | public function dimensions() : iterable 175 | { 176 | return ['vendor-package']; 177 | } 178 | 179 | public function mandatoryOptions() : iterable 180 | { 181 | return ['params' => ['user', 'password', 'dbname']]; 182 | } 183 | 184 | public function __invoke(ContainerInterface $container) 185 | { 186 | // get options for vendor-package.container-id 187 | // method options() is implemented in ConfigurationTrait 188 | // an exception is raised when a mandatory option is missing 189 | $options = $this->options($container->get('config'), 'container-id'); 190 | 191 | return new Awesome($options); 192 | } 193 | } 194 | ``` 195 | 196 | *Hey, the database port and host is missing.* That's right, but the default value of the port is *3306* and the host is 197 | *localhost*. It makes no sense to set it in the configuration. *So I make the database port/host not configurable?* No, you 198 | use the `ProvidesDefaultOptions` interface. 199 | 200 | ## ProvidesDefaultOptions interface 201 | The `ProvidesDefaultOptions` interface defines default options for your instance. These options are merged with the provided 202 | options. 203 | 204 | Remember: *My factory requires configuration, requires a container id along with mandatory options and it provides default options.* 205 | 206 | ```php 207 | use Interop\Config\RequiresConfigId; 208 | use Interop\Config\RequiresMandatoryOptions; 209 | use Interop\Config\ProvidesDefaultOptions; 210 | use Interop\Config\ConfigurationTrait; 211 | use Psr\Container\ContainerInterface; 212 | 213 | class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions 214 | { 215 | use ConfigurationTrait; 216 | 217 | public function dimensions() : iterable 218 | { 219 | return ['vendor-package']; 220 | } 221 | 222 | public function mandatoryOptions() : iterable 223 | { 224 | return ['params' => ['user', 'password', 'dbname']]; 225 | } 226 | 227 | public function defaultOptions() : iterable 228 | { 229 | return [ 230 | 'params' => [ 231 | 'host' => 'localhost', 232 | 'port' => '3306', 233 | ], 234 | ]; 235 | } 236 | 237 | public function __invoke(ContainerInterface $container) 238 | { 239 | // get options for vendor-package.container-id 240 | // method options() is implemented in ConfigurationTrait 241 | // an exception is raised when a mandatory option is missing 242 | // if host/port is missing, default options will be used 243 | $options = $this->options($container->get('config'), 'container-id'); 244 | 245 | return new Awesome($options); 246 | } 247 | } 248 | ``` 249 | 250 | Now you have a bullet proof factory class which throws meaningful exceptions if something goes wrong. *This is cool, but 251 | I don't want to use exceptions.* No problem, see next. 252 | 253 | ## Avoid exceptions 254 | The `RequiresConfig` interface provides a method `canRetrieveOptions()`. This method checks if options are available depending on 255 | implemented interfaces and checks that the retrieved options are an array or have implemented `\ArrayAccess`. 256 | 257 | > `canRetrieveOptions()` returning true does not mean that `options($config)` will not throw an exception. 258 | It does however mean that `options()` will not throw an `OptionNotFoundException`. Mandatory options are not checked. 259 | 260 | You can call this function and if it returns false, you can use the default options. 261 | 262 | 263 | ```php 264 | use Interop\Config\RequiresConfigId; 265 | use Interop\Config\RequiresMandatoryOptions; 266 | use Interop\Config\ProvidesDefaultOptions; 267 | use Interop\Config\ConfigurationTrait; 268 | use Psr\Container\ContainerInterface; 269 | 270 | class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions 271 | { 272 | use ConfigurationTrait; 273 | 274 | // other functions see above 275 | 276 | public function __invoke(ContainerInterface $container) 277 | { 278 | $config = $container->get('config'); 279 | 280 | $options = []; 281 | 282 | if ($this->canRetrieveOptions($config, 'container-id')) { 283 | // get options for vendor-package.container-id 284 | // method options() is implemented in ConfigurationTrait 285 | // if host/port is missing, default options will be used 286 | $options = $this->options($config, 'container-id'); 287 | } elseif ($this instanceof ProvidesDefaultOptions) { 288 | $options = $this->defaultOptions(); 289 | } 290 | 291 | return new Awesome($options); 292 | } 293 | } 294 | ``` 295 | 296 | *Nice, is there a one-liner?* Of course. You can use the `optionsWithFallback()` method. This function is not a part 297 | of the specification but is implemented in `ConfigurationTrait` to reduce some boilerplate code. 298 | 299 | ```php 300 | use Interop\Config\RequiresConfigId; 301 | use Interop\Config\RequiresMandatoryOptions; 302 | use Interop\Config\ProvidesDefaultOptions; 303 | use Interop\Config\ConfigurationTrait; 304 | use Psr\Container\ContainerInterface; 305 | 306 | class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions 307 | { 308 | use ConfigurationTrait; 309 | 310 | // other functions see above 311 | 312 | public function __invoke(ContainerInterface $container) 313 | { 314 | // get options for vendor-package.container-id 315 | // method options() is implemented in ConfigurationTrait 316 | // if configuration is not available, default options will be used 317 | $options = $this->optionsWithFallback($container->get('config'), 'container-id'); 318 | 319 | return new Awesome($options); 320 | } 321 | } 322 | ``` 323 | 324 | *Using `optionsWithFallback()` method and the `RequiresMandatoryOptions` is ambiguous or?* Yes, so it's up to you to implement 325 | the interfaces in a sense order. 326 | 327 | Take a look at the examples section for more use cases. `interop-config` is universally applicable. 328 | -------------------------------------------------------------------------------- /doc/book/reference/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Reference", 3 | "content": [ 4 | { 5 | "api": "../api.md" 6 | }, 7 | { 8 | "examples": "../examples.md" 9 | }, 10 | { 11 | "tutorial": "../tutorial.md" 12 | }, 13 | { 14 | "console-tools": "../console-tools.md" 15 | } 16 | ], 17 | "target": "./html", 18 | "tocDepth": 2, 19 | "copyright": "Copyright (c) 2015-2016 Sandro Keil
Powered by Bookdown Bootswatch Templates" 20 | } 21 | -------------------------------------------------------------------------------- /doc/book/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ## Overview 4 | 5 | In this short tutorial we will implement a simple example of interop-config. This example tries to give you a better 6 | understanding of what interop-config is and why you should use it. 7 | 8 | ### Components 9 | 10 | To implement interop-config we need a few components to glue it together. 11 | 12 | * Configuration 13 | * This file return an array with the configuration of our application 14 | * ServiceLocator 15 | * This class holds the root configuration of our application 16 | * This class holds some plugin manager classes (invokables, factories, ...) 17 | * ExampleFactory 18 | * This class represent the factory for a simple component and implements some interfaces from interop-config 19 | 20 | ### Folders 21 | 22 | We use the following simple folder structure for the example application: 23 | 24 | ``` 25 | ─── application_root/ 26 | ├── composer.json 27 | ├── config/ 28 | │   └── main.php 29 | ├── public/ 30 | │   └── index.php 31 | └── src/ 32 | └── SomVendorName/ 33 | ├── Exception/ 34 | │   └── RuntimeException.php 35 | └── MyComponent/ 36 | ├── MyComponentFactory.php 37 | └── MyFirstComponent.php 38 | ``` 39 | 40 | ## Install Composer and interop-config 41 | 42 | Since we have Composer it is really easy to manage our php packages. First of all we need Composer run. For that go to 43 | your `application_root` directory and setup a Composer.json file with the following listing. 44 | 45 | ```json 46 | { 47 | "name": "test", 48 | "autoload": { 49 | "psr-4": { 50 | "SomeVendorName\\": "src/" 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | To use Composer we need to download the `composer.phar` from [getcomposer.org](https://getcomposer.org/download/). 57 | If you have installed Composer, you can get the PHP packages we setup in the composer.json via `php composer.phar require sandrokeil/interop-config` 58 | 59 | If you need more help look at [getcomposer.org](https://getcomposer.org/doc/00-intro.md). 60 | 61 | ### Setup Composer autoloading 62 | 63 | Put the following code to the `public/index.php` to get the class auto loading, so we do not have to `include/require` classes before using them. 64 | 65 | ```php 66 | 67 | // this makes our life easier, everything is relative to application root now 68 | chdir(dirname(__DIR__)); 69 | 70 | require 'vendor/autoload.php'; 71 | 72 | // ... 73 | ``` 74 | 75 | ## Setup config and classes 76 | 77 | The following code is located in `config/main.php` and holds the main configuration `array` of the project. 78 | 79 | ```php 80 | 81 | return [ 82 | 'some_vendor_name' => [ 83 | 'my_component_configuration' => [ 84 | 'debug' => false, 85 | 'routes' => [ 86 | 'key1' => 'value1', 87 | 'key2' => 'value2', 88 | 'key3' => 'value3', 89 | ] 90 | ] 91 | ] 92 | ]; 93 | ``` 94 | 95 | ### ServiceLocator 96 | 97 | The `ServiceLocator` holds the application configuration and some plugin manager classes. 98 | The `ServiceLocator` is the main entry point if you need to instantiate classes in your application. 99 | 100 | ```php 101 | 102 | namespace SomeVendorName; 103 | 104 | class ServiceLocator 105 | { 106 | 107 | /** 108 | * Some factory classes 109 | * 110 | * @var array 111 | */ 112 | 113 | private $factories = array( 114 | 'config' => array(), 115 | 'my_component_factory' => 'SomeVendorName\MyComponent\MyComponentFactory' 116 | // ... 117 | ); 118 | 119 | public function __construct(array $config) 120 | { 121 | $this->factories['config'] = $config; 122 | } 123 | 124 | /** 125 | * Get some simple factories 126 | * 127 | * @param $name 128 | * @return mixed 129 | */ 130 | public function get($name) 131 | { 132 | // return config simple 133 | if ($name === 'config' && isset($this->factories['config'])) { 134 | return $this->factories['config']; 135 | } 136 | 137 | if (isset($this->factories[$name])) { 138 | $FactoryClass = new $this->factories[$name](); 139 | // if your factory implements a factory interface you should check it at this place 140 | return $FactoryClass->__invoke($this); 141 | } 142 | 143 | } 144 | 145 | /** 146 | * Check if factory exists 147 | * 148 | * @param $name 149 | * @return bool 150 | */ 151 | public function has($name): bool 152 | { 153 | return isset($this->factories[$name]); 154 | } 155 | 156 | } 157 | ``` 158 | 159 | ### Runtime Exception 160 | 161 | To throw useful exceptions we have a `RuntimeException` in our namespace. There are many more exception types, but for 162 | this simple example we satisfied by the `RuntimeException`. The following code is located in 163 | `src/SomeVendorName/Exception/RuntimeException.php`. 164 | 165 | ```php 166 | 167 | namespace SomeVendorName\Exception; 168 | 169 | class RuntimeException extends \Exception { 170 | 171 | } 172 | 173 | ``` 174 | 175 | ### The component factory class 176 | 177 | The factory class implements `OptainOptions` and uses the `ConfigurationTrait`. The factory class creates components 178 | with the configuration we explicitly setup before. Every component created by this factory will get the same configuration parameters. 179 | 180 | ```php 181 | 182 | namespace SomeVendorName\MyComponent; 183 | 184 | use SomeVendorName\ServiceLocator; 185 | use SomeVendorName\Exception\RuntimeException; 186 | use SomeVendorName\MyComponent\MyFirstComponent; 187 | 188 | use Interop\Config\RequiresConfig; 189 | use Interop\Config\ConfigurationTrait; 190 | 191 | class MyComponentFactory implements ObtainsOptions 192 | { 193 | use ConfigurationTrait; 194 | 195 | public function dimensions(): iterable 196 | { 197 | return ['some_vendor_name', 'my_component_configuration']; 198 | } 199 | 200 | public function __invoke(ServiceLocator $ServiceLocator): MyFirstComponent 201 | { 202 | 203 | $options = $this->options($ServiceLocator->get('config')); 204 | 205 | // check if mandatory options are available or use \Interop\Config\RequiresMandatoryOptions 206 | if (empty($options['routes'])) { 207 | throw new RuntimeException('routes not defined'); 208 | } 209 | if (empty($options['debug'])) { 210 | throw new RuntimeException('debug not defined'); 211 | } 212 | 213 | return new MyFirstComponent($options['routes'], $options['debug']); 214 | } 215 | } 216 | ``` 217 | 218 | ### The first component class 219 | 220 | Finally we have the component class where we need the configuration. 221 | 222 | ```php 223 | 224 | namespace SomeVendorName\MyComponent; 225 | 226 | use SomeVendorName\Exception\RuntimeException; 227 | 228 | class MyFirstComponent 229 | { 230 | public function __construct($routes, $debug){ 231 | var_dump(routes, debug); 232 | } 233 | } 234 | ``` 235 | 236 | ## Run the example application 237 | 238 | To run the example application we just need the following few lines in the public/index.php: 239 | 240 | ```php 241 | 242 | // this makes our life easier, everything is relative to application root now 243 | chdir(dirname(__DIR__)); 244 | 245 | require 'vendor/autoload.php'; 246 | 247 | $ServiceLocator = new SomeVendorName\ServiceLocator('config/main.php'); 248 | $MyComponentFactory = $ServiceLocator->get('my_component_factory'); 249 | 250 | ``` 251 | 252 | If you see some output different from fatal error you have successfully implemented interop-config. Take a look at the 253 | Quick-Start section to see what interop-config can do for you. 254 | 255 | 256 | 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /doc/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "interop-config - configurable factories", 3 | "content": [ 4 | {"getting-started": "book/getting-started/bookdown.json"}, 5 | {"reference": "book/reference/bookdown.json"}, 6 | {"contribute": "book/contribute/bookdown.json"} 7 | ], 8 | "target": "./html", 9 | "tocDepth": 2, 10 | "numbering": false, 11 | "template": "bookdown/themes", 12 | "extensions": { 13 | "commonmark": [ 14 | "Webuni\\CommonMark\\TableExtension\\TableExtension", 15 | "Webuni\\CommonMark\\AttributesExtension\\AttributesExtension" 16 | ] 17 | }, 18 | "copyright": "Visit interop-config on GitHub
Copyright (c) 2015-2017 Sandro Keil
Powered by Bookdown Bootswatch Templates" 19 | } 20 | -------------------------------------------------------------------------------- /phpbench.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap": "vendor/autoload.php", 3 | "path": "benchmark", 4 | "retry_threshold": 4, 5 | "xml_storage_path": "build/logs/bench", 6 | "reports": { 7 | "table": { 8 | "generator": "table", 9 | "cols": [ 10 | "benchmark", 11 | "subject", 12 | "mem_peak", 13 | "mean", 14 | "best", 15 | "diff" 16 | ], 17 | "sort": { 18 | "benchmark": "asc", 19 | "mean": "desc" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ConfigurationTrait.php: -------------------------------------------------------------------------------- 1 | dimensions(); 46 | $dimensions = $dimensions instanceof Iterator ? iterator_to_array($dimensions) : $dimensions; 47 | 48 | if ($this instanceof RequiresConfigId) { 49 | $dimensions[] = $configId; 50 | } 51 | 52 | foreach ($dimensions as $dimension) { 53 | if (((array)$config !== $config && !$config instanceof ArrayAccess) 54 | || (!isset($config[$dimension]) && $this instanceof RequiresMandatoryOptions) 55 | || (!isset($config[$dimension]) && !$this instanceof ProvidesDefaultOptions) 56 | ) { 57 | return false; 58 | } 59 | if ($this instanceof ProvidesDefaultOptions && !isset($config[$dimension])) { 60 | return true; 61 | } 62 | 63 | $config = $config[$dimension]; 64 | } 65 | return (array)$config === $config || $config instanceof ArrayAccess; 66 | } 67 | 68 | /** 69 | * Returns options based on dimensions() like [vendor][package] and can perform mandatory option checks if 70 | * class implements RequiresMandatoryOptions. If the ProvidesDefaultOptions interface is implemented, these options 71 | * must be overridden by the provided config. If you want to allow configurations for more then one instance use 72 | * RequiresConfigId interface. 73 | * 74 | * The RequiresConfigId interface is supported. 75 | * 76 | * @param array|ArrayAccess $config Configuration 77 | * @param string $configId Config name, must be provided if factory uses RequiresConfigId interface 78 | * @return array|ArrayAccess 79 | * @throws Exception\InvalidArgumentException If the $configId parameter is provided but factory does not support it 80 | * @throws Exception\UnexpectedValueException If the $config parameter has the wrong type 81 | * @throws Exception\OptionNotFoundException If no options are available 82 | * @throws Exception\MandatoryOptionNotFoundException If a mandatory option is missing 83 | */ 84 | public function options($config, string $configId = null) 85 | { 86 | $dimensions = $this->dimensions(); 87 | $dimensions = $dimensions instanceof Iterator ? iterator_to_array($dimensions) : $dimensions; 88 | 89 | if ($this instanceof RequiresConfigId) { 90 | $dimensions[] = $configId; 91 | } elseif ($configId !== null) { 92 | throw new Exception\InvalidArgumentException( 93 | sprintf('The factory "%s" does not support multiple instances.', __CLASS__) 94 | ); 95 | } 96 | 97 | // get configuration for provided dimensions 98 | foreach ($dimensions as $dimension) { 99 | if ((array)$config !== $config && !$config instanceof ArrayAccess) { 100 | throw Exception\UnexpectedValueException::invalidOptions($dimensions, $dimension); 101 | } 102 | 103 | if (!isset($config[$dimension])) { 104 | if (!$this instanceof RequiresMandatoryOptions && $this instanceof ProvidesDefaultOptions) { 105 | break; 106 | } 107 | throw Exception\OptionNotFoundException::missingOptions($this, $dimension, $configId); 108 | } 109 | $config = $config[$dimension]; 110 | } 111 | 112 | if ((array)$config !== $config && !$config instanceof ArrayAccess) { 113 | throw Exception\UnexpectedValueException::invalidOptions($this->dimensions()); 114 | } 115 | 116 | if ($this instanceof RequiresMandatoryOptions) { 117 | $this->checkMandatoryOptions($this->mandatoryOptions(), $config); 118 | } 119 | 120 | if ($this instanceof ProvidesDefaultOptions) { 121 | $options = $this->defaultOptions(); 122 | 123 | $config = array_replace_recursive( 124 | $options instanceof Iterator ? iterator_to_array($options) : (array)$options, 125 | (array)$config 126 | ); 127 | } 128 | return $config; 129 | } 130 | 131 | /** 132 | * Checks if options can be retrieved from config and if not, default options (ProvidesDefaultOptions interface) or 133 | * an empty array will be returned. 134 | * 135 | * @param array|ArrayAccess $config Configuration 136 | * @param string $configId Config name, must be provided if factory uses RequiresConfigId interface 137 | * @return array|ArrayAccess options Default options or an empty array 138 | * @throws Exception\MandatoryOptionNotFoundException If a mandatory option is missing 139 | */ 140 | public function optionsWithFallback($config, string $configId = null) 141 | { 142 | $options = []; 143 | 144 | if ($this->canRetrieveOptions($config, $configId)) { 145 | $options = $this->options($config, $configId); 146 | } elseif ($this instanceof ProvidesDefaultOptions) { 147 | $options = $this->defaultOptions(); 148 | } 149 | return $options; 150 | } 151 | 152 | /** 153 | * Checks if a mandatory param is missing, supports recursion 154 | * 155 | * @param iterable $mandatoryOptions 156 | * @param array|ArrayAccess $config 157 | * @throws Exception\MandatoryOptionNotFoundException 158 | */ 159 | private function checkMandatoryOptions(iterable $mandatoryOptions, $config): void 160 | { 161 | foreach ($mandatoryOptions as $key => $mandatoryOption) { 162 | $useRecursion = !is_scalar($mandatoryOption); 163 | 164 | if (!$useRecursion && isset($config[$mandatoryOption])) { 165 | continue; 166 | } 167 | 168 | if ($useRecursion && isset($config[$key])) { 169 | $this->checkMandatoryOptions($mandatoryOption, $config[$key]); 170 | return; 171 | } 172 | 173 | throw Exception\MandatoryOptionNotFoundException::missingOption( 174 | $this->dimensions(), 175 | $useRecursion ? $key : $mandatoryOption 176 | ); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | dimensions(); 35 | 36 | if ($factory instanceof RequiresConfigId) { 37 | $dimensions[] = $configId; 38 | } 39 | 40 | foreach ($dimensions as $dimension) { 41 | $position[] = $dimension; 42 | 43 | if ($dimension === $currentDimension) { 44 | break; 45 | } 46 | } 47 | 48 | if ($factory instanceof RequiresConfigId && $configId === null && count($dimensions) === count($position)) { 49 | return new static( 50 | rtrim( 51 | sprintf('The configuration "%s" needs a config id.', implode('.', $position)), 52 | '.' 53 | ) 54 | ); 55 | } 56 | 57 | return new static(sprintf('No options set for configuration "%s"', implode('.', $position))); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exception/OutOfBoundsException.php: -------------------------------------------------------------------------------- 1 | 30 | * return ['prooph', 'service_bus', 'command_bus']; 31 | * 32 | * 33 | * @return iterable 34 | */ 35 | public function dimensions(): iterable; 36 | 37 | /** 38 | * Returns options based on dimensions() like [vendor][package] and can perform mandatory option checks if 39 | * class implements RequiresMandatoryOptions. If the ProvidesDefaultOptions interface is implemented, these options 40 | * must be overridden by the provided config. If you want to allow configurations for more then one instance use 41 | * RequiresConfigId interface and add an optional second parameter named $configId. The ConfigurationTrait supports 42 | * RequiresConfigId interface. 43 | * 44 | * 45 | * return [ 46 | * // vendor name 47 | * 'prooph' => [ 48 | * // package name 49 | * 'service_bus' => [ 50 | * // only one instance possible 51 | * 'command_bus' => [ 52 | * // command bus factory options 53 | * 'router' => [ 54 | * 'routes' => [], 55 | * ], 56 | * ], 57 | * ], 58 | * ], 59 | * ]; 60 | * 61 | * 62 | * @param array|ArrayAccess $config Configuration 63 | * @return array|ArrayAccess options 64 | * @throws Exception\InvalidArgumentException If the $configId parameter is provided but factory does not support it 65 | * @throws Exception\UnexpectedValueException If the $config parameter has the wrong type 66 | * @throws Exception\OptionNotFoundException If no options are available 67 | * @throws Exception\MandatoryOptionNotFoundException If a mandatory option is missing 68 | */ 69 | public function options($config); 70 | 71 | /** 72 | * Checks if options are available depending on implemented interfaces and checks that the retrieved options are an 73 | * array or have implemented \ArrayAccess. The ConfigurationTrait supports RequiresConfigId interface. 74 | * 75 | * `canRetrieveOptions()` returning true does not mean that `options($config)` will not throw an exception. 76 | * It does however mean that `options()` will not throw an `OptionNotFoundException`. Mandatory options are 77 | * not checked. 78 | * 79 | * @param array|ArrayAccess $config Configuration 80 | * @return bool True if options are available, otherwise false 81 | */ 82 | public function canRetrieveOptions($config): bool; 83 | } 84 | -------------------------------------------------------------------------------- /src/RequiresConfigId.php: -------------------------------------------------------------------------------- 1 | 26 | * return ['doctrine', 'connection']; 27 | * 28 | * 29 | * The configuration looks like this 30 | * 31 | * 32 | * return [ 33 | * // vendor name 34 | * 'doctrine' => [ 35 | * // package name 36 | * 'connection' => [ 37 | * // config id 38 | * 'orm_default' => [ 39 | * // mandatory options, is optional 40 | * 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', 41 | * 'params' => [ 42 | * // default options, is optional 43 | * 'host' => 'localhost', 44 | * 'port' => '3306', 45 | * ], 46 | * ], 47 | * ], 48 | * ], 49 | * ]; 50 | * 51 | */ 52 | interface RequiresConfigId extends RequiresConfig 53 | { 54 | } 55 | -------------------------------------------------------------------------------- /src/RequiresMandatoryOptions.php: -------------------------------------------------------------------------------- 1 | helper = $helper ?: new ConsoleHelper(); 28 | } 29 | 30 | protected function help($resource = STDOUT): void 31 | { 32 | $this->helper->writeErrorMessage(sprintf(static::HELP_TEMPLATE, static::COMMAND_CLI_NAME)); 33 | } 34 | 35 | /** 36 | * @param string $command 37 | * @param string $configFile File from which config originates, and to 38 | * which it will be written. 39 | * @param array $config Parsed configuration. 40 | * @param string $class Name of class to reflect. 41 | * @return \stdClass 42 | */ 43 | protected function createArguments(string $command, string $configFile, array $config, string $class): \stdClass 44 | { 45 | return (object)[ 46 | 'command' => $command, 47 | 'configFile' => $configFile, 48 | 'config' => $config, 49 | 'class' => $class, 50 | ]; 51 | } 52 | 53 | protected function createErrorArgument(string $message): \stdClass 54 | { 55 | return (object)[ 56 | 'command' => static::COMMAND_ERROR, 57 | 'message' => $message, 58 | ]; 59 | } 60 | 61 | protected function createHelpArgument(): \stdClass 62 | { 63 | return (object)[ 64 | 'command' => static::COMMAND_HELP, 65 | ]; 66 | } 67 | 68 | protected function parseArgs(array $args): \stdClass 69 | { 70 | if (!count($args)) { 71 | return $this->createHelpArgument(); 72 | } 73 | 74 | $arg1 = array_shift($args); 75 | 76 | if (in_array($arg1, ['-h', '--help', 'help'], true)) { 77 | return $this->createHelpArgument(); 78 | } 79 | 80 | if (!count($args)) { 81 | return $this->createErrorArgument('Missing class name'); 82 | } 83 | 84 | $class = array_shift($args); 85 | 86 | if (!class_exists($class)) { 87 | return $this->createErrorArgument( 88 | sprintf('Class "%s" does not exist or could not be autoloaded.', $class) 89 | ); 90 | } 91 | 92 | $reflectionClass = new \ReflectionClass($class); 93 | 94 | if (!in_array(RequiresConfig::class, $reflectionClass->getInterfaceNames(), true)) { 95 | return $this->createErrorArgument( 96 | sprintf('Class "%s" does not implement "%s".', $class, RequiresConfig::class) 97 | ); 98 | } 99 | 100 | $configFile = $arg1; 101 | switch (file_exists($configFile)) { 102 | case true: 103 | $config = require $configFile; 104 | 105 | if ($config instanceof \Iterator) { 106 | $config = iterator_to_array($config); 107 | } elseif ($config instanceof \IteratorAggregate) { 108 | $config = iterator_to_array($config->getIterator()); 109 | } 110 | 111 | if (!is_array($config)) { 112 | return $this->createErrorArgument( 113 | sprintf('Configuration at path "%s" does not return an array.', $configFile) 114 | ); 115 | } 116 | 117 | break; 118 | case false: 119 | // fall-through 120 | default: 121 | if ($command = $this->checkFile($configFile)) { 122 | return $command; 123 | } 124 | 125 | $config = []; 126 | break; 127 | } 128 | 129 | return $this->createArguments(self::COMMAND_DUMP, $configFile, $config, $class); 130 | } 131 | 132 | abstract protected function checkFile(string $configFile): ?\stdClass; 133 | } 134 | -------------------------------------------------------------------------------- /src/Tool/AbstractConfig.php: -------------------------------------------------------------------------------- 1 | $value) { 20 | $key = $this->createConfigKey($key); 21 | $entries[] = sprintf( 22 | '%s%s%s,', 23 | $indent, 24 | $key ? sprintf('%s => ', $key) : '', 25 | $this->createConfigValue($value, $indentLevel) 26 | ); 27 | } 28 | 29 | $outerIndent = str_repeat(' ', ($indentLevel - 1) * 4); 30 | 31 | return sprintf("[\n%s\n%s]", implode("\n", $entries), $outerIndent); 32 | } 33 | 34 | private function createConfigKey($key): string 35 | { 36 | return sprintf("'%s'", $key); 37 | } 38 | 39 | private function createConfigValue($value, int $indentLevel): string 40 | { 41 | if (is_array($value) || $value instanceof \Traversable) { 42 | return $this->prepareConfig($value, $indentLevel + 1); 43 | } 44 | 45 | if (is_string($value) && class_exists($value)) { 46 | return sprintf('\\%s::class', ltrim($value, '\\')); 47 | } 48 | 49 | return var_export($value, true); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tool/ConfigDumper.php: -------------------------------------------------------------------------------- 1 | helper = $helper ?: new ConsoleHelper(); 44 | } 45 | 46 | /** 47 | * @param array $config 48 | * @param string $className 49 | * @return array 50 | */ 51 | public function createConfig(array $config, string $className): array 52 | { 53 | $reflectionClass = new \ReflectionClass($className); 54 | 55 | $interfaces = $reflectionClass->getInterfaceNames(); 56 | 57 | $factory = $reflectionClass->newInstanceWithoutConstructor(); 58 | $dimensions = []; 59 | $mandatoryOptions = []; 60 | $defaultOptions = []; 61 | 62 | if (in_array(RequiresConfig::class, $interfaces, true)) { 63 | $dimensions = $factory->dimensions(); 64 | } 65 | 66 | if (in_array(RequiresConfigId::class, $interfaces, true)) { 67 | $configId = $this->helper->readLine( 68 | 'config id (default)', 69 | 'Multiple instances are supported, please enter a' 70 | ); 71 | if ('' === $configId) { 72 | $configId = 'default'; 73 | } 74 | $dimensions[] = $configId; 75 | } 76 | 77 | $parent = &$config; 78 | 79 | foreach ($dimensions as $dimension) { 80 | if (empty($parent[$dimension])) { 81 | $parent[$dimension] = []; 82 | } 83 | $parent = &$parent[$dimension]; 84 | } 85 | 86 | if (in_array(RequiresMandatoryOptions::class, $interfaces, true)) { 87 | $mandatoryOptions = $this->readMandatoryOption($factory->mandatoryOptions(), $parent); 88 | } 89 | 90 | if (in_array(ProvidesDefaultOptions::class, $interfaces)) { 91 | $defaultOptions = $this->readDefaultOption($factory->defaultOptions(), $parent); 92 | } 93 | 94 | $options = array_replace_recursive( 95 | $defaultOptions instanceof \Iterator ? iterator_to_array($defaultOptions) : (array)$defaultOptions, 96 | (array)$mandatoryOptions 97 | ); 98 | 99 | $parent = array_replace_recursive($parent, $options); 100 | 101 | return $config; 102 | } 103 | 104 | private function readMandatoryOption(iterable $mandatoryOptions, array $config, string $path = ''): array 105 | { 106 | $options = []; 107 | 108 | foreach ($mandatoryOptions as $key => $mandatoryOption) { 109 | if (!is_scalar($mandatoryOption)) { 110 | $options[$key] = $this->readMandatoryOption( 111 | $mandatoryOptions[$key], 112 | $config[$key] ?? [], 113 | trim($path . '.' . $key, '.') 114 | ); 115 | continue; 116 | } 117 | $previousValue = isset($config[$mandatoryOption]) ? ' (' . $config[$mandatoryOption] . ')' : ''; 118 | 119 | $options[$mandatoryOption] = $this->helper->readLine( 120 | trim($path . '.' . $mandatoryOption, '.') . $previousValue 121 | ); 122 | 123 | if ('' === $options[$mandatoryOption] && isset($config[$mandatoryOption])) { 124 | $options[$mandatoryOption] = $config[$mandatoryOption]; 125 | } 126 | } 127 | return $options; 128 | } 129 | 130 | private function readDefaultOption(iterable $defaultOptions, array $config, string $path = ''): array 131 | { 132 | $options = []; 133 | 134 | foreach ($defaultOptions as $key => $defaultOption) { 135 | if (!is_scalar($defaultOption)) { 136 | $options[$key] = $this->readDefaultOption( 137 | $defaultOptions[$key], 138 | $config[$key] ?? [], 139 | trim($path . '.' . $key, '.') 140 | ); 141 | continue; 142 | } 143 | $previousValue = isset($config[$key]) 144 | ? ' (' . $config[$key] . '), current value ' . $defaultOption . '' 145 | : ' (' . $defaultOption . ')'; 146 | 147 | $options[$key] = $this->helper->readLine(trim($path . '.' . $key, '.') . $previousValue); 148 | 149 | if ('' === $options[$key]) { 150 | $options[$key] = $config[$key] ?? $defaultOption; 151 | } else { 152 | $options[$key] = $this->convertToType($options[$key], $defaultOption); 153 | } 154 | } 155 | return $options; 156 | } 157 | 158 | private function convertToType($value, $originValue) 159 | { 160 | switch (gettype($originValue)) { 161 | case 'boolean': 162 | return (bool)$value; 163 | case 'integer': 164 | return (int)$value; 165 | case 'double': 166 | return (float)$value; 167 | case 'string': 168 | default: 169 | return $value; 170 | } 171 | } 172 | 173 | public function dumpConfigFile(iterable $config): string 174 | { 175 | return 'return ' . $this->prepareConfig($config) . ';'; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Tool/ConfigDumperCommand.php: -------------------------------------------------------------------------------- 1 | Usage: 25 | %s [options] [] [] 26 | 27 | Options: 28 | -h, --help, help Display this help message 29 | 30 | Arguments: 31 | configFile Path to a config file or php://stdout for which to generate options. 32 | className Name of the class to reflect and for which to generate options. 33 | 34 | Reads the provided configuration file (creating it if it does not exist), and injects it with options for the provided 35 | class name, writing the changes back to the file. 36 | EOH; 37 | 38 | /** 39 | * @var ConfigDumper 40 | */ 41 | private $configDumper; 42 | 43 | public function __construct(ConsoleHelper $helper = null, ConfigDumper $configReader = null) 44 | { 45 | parent::__construct($helper); 46 | $this->configDumper = $configReader ?: new ConfigDumper($this->helper); 47 | } 48 | 49 | /** 50 | * @param array $args Argument list, minus script name 51 | * @return int Exit status 52 | */ 53 | public function __invoke(array $args): int 54 | { 55 | $arguments = $this->parseArgs($args); 56 | 57 | switch ($arguments->command) { 58 | case self::COMMAND_HELP: 59 | $this->help(); 60 | return 0; 61 | case self::COMMAND_ERROR: 62 | $this->helper->writeErrorLine($arguments->message); 63 | $this->help(); 64 | return 1; 65 | case self::COMMAND_DUMP: 66 | // fall-through 67 | default: 68 | break; 69 | } 70 | 71 | $config = $this->configDumper->createConfig($arguments->config, $arguments->class); 72 | 73 | $fileHeader = ''; 74 | 75 | if (file_exists($arguments->configFile)) { 76 | foreach (token_get_all(file_get_contents($arguments->configFile)) as $token) { 77 | if (is_array($token)) { 78 | if (isset($token[1]) && 'return' === $token[1]) { 79 | break; 80 | } 81 | $fileHeader .= $token[1]; 82 | continue; 83 | } 84 | $fileHeader .= $token; 85 | } 86 | } 87 | 88 | if (empty($fileHeader)) { 89 | $fileHeader = ConfigDumper::CONFIG_TEMPLATE; 90 | } 91 | 92 | file_put_contents($arguments->configFile, $fileHeader . $this->configDumper->dumpConfigFile($config) . PHP_EOL); 93 | 94 | $this->helper->writeLine(sprintf('[DONE] Changes written to %s', $arguments->configFile)); 95 | return 0; 96 | } 97 | 98 | protected function checkFile(string $configFile): ?\stdClass 99 | { 100 | if (!is_writable(dirname($configFile)) && 'php://stdout' !== $configFile) { 101 | return $this->createErrorArgument(sprintf( 102 | 'Cannot create configuration at path "%s"; not writable.', 103 | $configFile 104 | )); 105 | } 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Tool/ConfigReader.php: -------------------------------------------------------------------------------- 1 | helper = $helper ?: new ConsoleHelper(); 28 | } 29 | 30 | public function readConfig(array $config, string $className): array 31 | { 32 | $reflectionClass = new \ReflectionClass($className); 33 | 34 | $interfaces = $reflectionClass->getInterfaceNames(); 35 | 36 | $factory = $reflectionClass->newInstanceWithoutConstructor(); 37 | $dimensions = []; 38 | 39 | if (in_array(RequiresConfig::class, $interfaces, true)) { 40 | $dimensions = $factory->dimensions(); 41 | } 42 | 43 | foreach ($dimensions as $dimension) { 44 | if (!isset($config[$dimension])) { 45 | throw OptionNotFoundException::missingOptions($factory, $dimension, null); 46 | } 47 | $config = $config[$dimension]; 48 | } 49 | 50 | if (in_array(RequiresConfigId::class, $interfaces, true)) { 51 | while (true) { 52 | $configId = $this->helper->readLine(implode(', ', array_keys($config)), 'For which config id'); 53 | 54 | if ('' !== $configId) { 55 | if (isset($config[$configId])) { 56 | return $config[$configId]; 57 | } 58 | $this->helper->writeErrorMessage(sprintf('No config id with name "%s" exists.', $configId)); 59 | continue; 60 | } 61 | break; 62 | } 63 | } 64 | 65 | return $config; 66 | } 67 | 68 | public function dumpConfigFile(iterable $config): string 69 | { 70 | return $this->prepareConfig($config); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Tool/ConfigReaderCommand.php: -------------------------------------------------------------------------------- 1 | Usage: 28 | %s [options] [] [] 29 | 30 | Options: 31 | -h, --help, help Display this help message 32 | 33 | Arguments: 34 | configFile Path to a config file for which to display options. It must return an array / ArrayObject. 35 | className Name of the class to reflect and for which to display options. 36 | 37 | Reads the provided configuration file and displays options for the provided class name. 38 | EOH; 39 | // @codingStandardsIgnoreEnd 40 | 41 | /** 42 | * @var ConfigReader 43 | */ 44 | private $configReader; 45 | 46 | public function __construct(ConsoleHelper $helper = null, ConfigReader $configReader = null) 47 | { 48 | parent::__construct($helper); 49 | $this->configReader = $configReader ?: new ConfigReader($this->helper); 50 | } 51 | 52 | /** 53 | * @param array $args Argument list, minus script name 54 | * @return int Exit status 55 | */ 56 | public function __invoke(array $args): int 57 | { 58 | $arguments = $this->parseArgs($args); 59 | 60 | switch ($arguments->command) { 61 | case self::COMMAND_HELP: 62 | $this->help(); 63 | return 0; 64 | case self::COMMAND_ERROR: 65 | $this->helper->writeErrorLine($arguments->message); 66 | $this->help(); 67 | return 1; 68 | case self::COMMAND_DUMP: 69 | // fall-through 70 | default: 71 | break; 72 | } 73 | 74 | $config = $this->configReader->readConfig($arguments->config, $arguments->class); 75 | 76 | $this->helper->write($this->configReader->dumpConfigFile($config) . PHP_EOL); 77 | 78 | return 0; 79 | } 80 | 81 | protected function checkFile(string $configFile): ?\stdClass 82 | { 83 | if (!is_readable(dirname($configFile))) { 84 | return $this->createErrorArgument(sprintf( 85 | 'Cannot read configuration at path "%s".', 86 | $configFile 87 | )); 88 | } 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Tool/ConsoleHelper.php: -------------------------------------------------------------------------------- 1 | message`, 20 | * `message`) 21 | * - Write output to a specified stream, optionally with colorization. 22 | * - Write a line of output to a specified stream, optionally with 23 | * colorization, using the system EOL sequence.. 24 | * - Write an error message to STDERR. 25 | * - Reads a single line from the user 26 | * 27 | * Colorization will only occur when expected sequences are discovered and then, only if the console terminal allows it. 28 | * 29 | * Essentially, provides the bare minimum to allow you to provide messages to the current console. 30 | * 31 | * @copyright Copyright (c) 2016 Zend Technologies USA Inc. (http://www.zend.com) 32 | */ 33 | class ConsoleHelper 34 | { 35 | const COLOR_GREEN = "\033[32m"; 36 | const COLOR_YELLOW = "\033[33m"; 37 | const COLOR_RED = "\033[31m"; 38 | const COLOR_RESET = "\033[0m"; 39 | 40 | const HIGHLIGHT_INFO = 'info'; 41 | const HIGHLIGHT_VALUE = 'value'; 42 | const HIGHLIGHT_ERROR = 'error'; 43 | 44 | private $highlightMap = [ 45 | self::HIGHLIGHT_VALUE => self::COLOR_GREEN, 46 | self::HIGHLIGHT_INFO => self::COLOR_YELLOW, 47 | self::HIGHLIGHT_ERROR => self::COLOR_RED, 48 | ]; 49 | 50 | /** 51 | * @var string Exists only for testing. 52 | */ 53 | private $eol = PHP_EOL; 54 | 55 | /** 56 | * @var bool 57 | */ 58 | private $supportsColor; 59 | 60 | /** 61 | * @var resource Exists only for testing. 62 | */ 63 | private $errorStream = STDERR; 64 | 65 | /** 66 | * Input stream 67 | * 68 | * @var resource 69 | */ 70 | private $inputStream; 71 | 72 | /** 73 | * Output stream 74 | * 75 | * @var resource 76 | */ 77 | private $outputStream; 78 | 79 | public function __construct($inputStream = STDIN, $outputStream = STDOUT, $errorStream = STDERR) 80 | { 81 | $this->inputStream = $inputStream; 82 | $this->outputStream = $outputStream; 83 | $this->errorStream = $errorStream; 84 | $this->supportsColor = $this->detectColorCapabilities($outputStream); 85 | } 86 | 87 | public function readLine(string $name, string $text = 'Please enter a value for'): string 88 | { 89 | fwrite($this->outputStream, $this->colorize(sprintf('%s %s: ', $text, $name))); 90 | return trim(fread($this->inputStream, 1024), "\n"); 91 | } 92 | 93 | /** 94 | * Colorize a string for use with the terminal. 95 | * 96 | * Takes strings formatted as `string` and formats them per the 97 | * $highlightMap; if color support is disabled, simply removes the formatting 98 | * tags. 99 | * 100 | * @param string $string 101 | * @return string 102 | */ 103 | public function colorize(string $string): string 104 | { 105 | $reset = $this->supportsColor ? self::COLOR_RESET : ''; 106 | foreach ($this->highlightMap as $key => $color) { 107 | $pattern = sprintf('#<%s>(.*?)#s', $key, $key); 108 | $color = $this->supportsColor ? $color : ''; 109 | $string = preg_replace($pattern, $color . '$1' . $reset, $string); 110 | } 111 | return $string; 112 | } 113 | 114 | /** 115 | * @param string $string 116 | * @param bool $colorize Whether or not to colorize the string 117 | * @return void 118 | */ 119 | public function write(string $string, bool $colorize = true): void 120 | { 121 | if ($colorize) { 122 | $string = $this->colorize($string); 123 | } 124 | $string = $this->formatNewlines($string); 125 | 126 | fwrite($this->outputStream, $string); 127 | } 128 | 129 | /** 130 | * @param string $string 131 | * @param bool $colorize Whether or not to colorize the line 132 | * @return void 133 | */ 134 | public function writeLine(string $string, bool $colorize = true): void 135 | { 136 | $this->write($string . $this->eol, $colorize); 137 | } 138 | 139 | /** 140 | * @param string $string 141 | * @param bool $colorize Whether or not to colorize the line 142 | * @return void 143 | */ 144 | public function writeErrorLine(string $string, bool $colorize = true): void 145 | { 146 | $string .= $this->eol; 147 | 148 | if ($colorize) { 149 | $string = $this->colorize($string); 150 | } 151 | $string = $this->formatNewlines($string); 152 | 153 | fwrite($this->errorStream, $string); 154 | } 155 | 156 | /** 157 | * Emit an error message. 158 | * 159 | * Wraps the message in ``, and passes it to `writeLine()`, 160 | * using STDERR as the resource; emits an additional empty line when done, 161 | * also to STDERR. 162 | * 163 | * @param string $message 164 | * @return void 165 | */ 166 | public function writeErrorMessage(string $message): void 167 | { 168 | $this->writeErrorLine(sprintf('%s', $message), true); 169 | $this->writeErrorLine('', false); 170 | } 171 | 172 | /** 173 | * @param resource $resource 174 | * @return bool 175 | */ 176 | private function detectColorCapabilities($resource = STDOUT): bool 177 | { 178 | if ('\\' === DIRECTORY_SEPARATOR) { 179 | // Windows 180 | return false !== getenv('ANSICON') 181 | || 'ON' === getenv('ConEmuANSI') 182 | || 'xterm' === getenv('TERM'); 183 | } 184 | 185 | try { 186 | return function_exists('posix_isatty') && posix_isatty($resource); 187 | } catch (\Throwable $e) { 188 | return false; 189 | } 190 | } 191 | 192 | /** 193 | * Ensure newlines are appropriate for the current terminal. 194 | * 195 | * @param string 196 | * @return string 197 | */ 198 | private function formatNewlines(string $string): string 199 | { 200 | $string = str_replace($this->eol, "\0PHP_EOL\0", $string); 201 | $string = preg_replace("/(\r\n|\n|\r)/", $this->eol, $string); 202 | return str_replace("\0PHP_EOL\0", $this->eol, $string); 203 | } 204 | } 205 | --------------------------------------------------------------------------------