├── bin
├── interop-config
└── interop-config.php
├── .docheader
├── doc
├── book
│ ├── contribute
│ │ └── bookdown.json
│ ├── getting-started
│ │ └── bookdown.json
│ ├── reference
│ │ └── bookdown.json
│ ├── console-tools.md
│ ├── api.md
│ ├── intro.md
│ ├── tutorial.md
│ ├── quick-start.md
│ └── examples.md
└── bookdown.json
├── phpbench.json.dist
├── src
├── Exception
│ ├── ExceptionInterface.php
│ ├── RuntimeException.php
│ ├── InvalidArgumentException.php
│ ├── OutOfBoundsException.php
│ ├── MandatoryOptionNotFoundException.php
│ ├── UnexpectedValueException.php
│ └── OptionNotFoundException.php
├── RequiresMandatoryOptions.php
├── ProvidesDefaultOptions.php
├── Tool
│ ├── AbstractConfig.php
│ ├── ConfigReader.php
│ ├── ConfigReaderCommand.php
│ ├── ConfigDumperCommand.php
│ ├── AbstractCommand.php
│ ├── ConfigDumper.php
│ └── ConsoleHelper.php
├── RequiresConfigId.php
├── RequiresConfig.php
└── ConfigurationTrait.php
├── .scrutinizer.yml
├── benchmark
├── blackfire.php
├── PackageConfigBench.php
├── FlexibleConfigurationBench.php
├── ContainerIdBench.php
├── ProvidesDefaultOptionsBench.php
├── RequiresMandatoryOptionsBench.php
├── RequiresMandatoryOptionsContainerIdBench.php
├── ProvidesDefaultOptionsMandatoryBench.php
├── RequiresMandatoryOptionsRecursiveContainerIdBench.php
├── ProvidesDefaultOptionsMandatoryContainerIdBench.php
├── ArrayPerfBench.php
└── BaseCase.php
├── LICENSE.md
├── CONTRIBUTING.md
├── composer.json
├── README-GIT.md
├── README.md
└── CHANGELOG.md
/bin/interop-config:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | Sandro Keil"
17 | }
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 | Sandro Keil
Powered by Bookdown Bootswatch Templates"
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exception/RuntimeException.php:
--------------------------------------------------------------------------------
1 | classSetUp();
24 |
25 | $probe = BlackfireProbe::getMainInstance();
26 | $probe->enable();
27 | $benchmarkClass->options();
28 | $probe->disable();
29 |
--------------------------------------------------------------------------------
/src/RequiresMandatoryOptions.php:
--------------------------------------------------------------------------------
1 | GitHub
Copyright (c) 2015-2017 Sandro Keil
Powered by Bookdown Bootswatch Templates"
19 | }
20 |
--------------------------------------------------------------------------------
/src/Exception/MandatoryOptionNotFoundException.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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/src/Exception/UnexpectedValueException.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Exception/OptionNotFoundException.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/bin/interop-config.php:
--------------------------------------------------------------------------------
1 | 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/RequiresConfig.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Tool/AbstractCommand.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 |
--------------------------------------------------------------------------------
/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 | [](https://travis-ci.org/sandrokeil/interop-config)
16 | [](https://scrutinizer-ci.com/g/sandrokeil/interop-config/)
17 | [](https://coveralls.io/r/sandrokeil/interop-config?branch=master)
18 | [](https://packagist.org/packages/sandrokeil/interop-config)
19 | [](https://packagist.org/packages/sandrokeil/interop-config)
20 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://travis-ci.org/sandrokeil/interop-config)
16 | [](https://scrutinizer-ci.com/g/sandrokeil/interop-config/)
17 | [](https://coveralls.io/r/sandrokeil/interop-config?branch=master)
18 | [](http://hhvm.h4cc.de/package/sandrokeil/interop-config)
19 | [](https://travis-ci.org/sandrokeil/interop-config)
20 | [](https://packagist.org/packages/sandrokeil/interop-config)
21 | [](https://www.versioneye.com/php/sandrokeil:interop-config/0.3.1)
22 | [](https://packagist.org/packages/sandrokeil/interop-config)
23 | [](https://www.versioneye.com/php/sandrokeil:interop-config/references)
24 | [](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 |
--------------------------------------------------------------------------------
/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>#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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------