diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..754617f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+/tests export-ignore
+/.gitignore export-ignore
+/.travis.yml export-ignore
+/phpunit.xml export-ignore
+/phpcs.xml export-ignore
+/PULL_REQUEST_TEMPLATE.md export-ignore
+/CODE_OF_CONDUCT.md export-ignore
+/CONTRIBUTING.md export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4f4acd3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+vendor/
+composer.lock
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e1e0d74
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: php
+php:
+ - '7.3'
+ - '7.4'
+
+before_script:
+ - composer install
+
+script:
+ - composer validate --strict
+ - vendor/bin/phpunit --coverage-text
+ - vendor/bin/phpcs src/ tests/
+
+cache:
+ directories:
+ - $HOME/.composer/cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..06f15bd
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 1.0.0 - 2020-11-10
+### Added
+- The initial implementation of the package.
+
+# Versions
+- [1.0.0 > Unreleased](https://github.com/ulrack/environment-extension/compare/1.0.0...HEAD)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..6dcd974
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at info@grizzit.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..7921a76
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# Contributing
+
+To contribute to this package, please keep to these guidelines.
+
+- Fork the package.
+- Create a branch per feature.
+- Commit your changes to these branches.
+- Create a pull request per feature to the master branch of the original repository.
+
+## Pull requests
+
+Pull request should follow these rules, before they can get accepted.
+
+- Follow the [pull request template](PULL_REQUEST_TEMPLATE.md).
+- Contains a short but complete description.
+- Has passed all test command listed bellow.
+
+## Running Tests
+
+``` bash
+$ vendor/bin/phpunit --coverage-text
+$ vendor/bin/phpcs src/ tests/
+```
+
+## Notes
+
+Multiple commits per feature are allowed, but please provide a good description in your pull request.
+This description will be used to squash your feature into the master branch.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c2833e1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) GrizzIT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..ce6ccf9
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,18 @@
+# (bugfix / feature / refactor / etc.): Short descriptive title
+
+## Description
+
+Fixes #
+
+Changes proposed in this pull request:
+-
+-
+-
+
+Make sure you can check the following boxes before submitting the pull request:
+- [] The proposed changes work with the dependencies from composer.
+- [] The proposed changes have been thoroughly tested.
+- [] The proposed changes don't alter the code in such a manner that it conflicts with the initial purpose of the package.
+
+Optional checkbox
+- [] Backwards incompatible
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7bf58aa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+[![Join us on Discord](https://img.shields.io/discord/753858953452191916.svg?label=Join+us&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/k9KVYqm)
+[![Build Status](https://travis-ci.com/ulrack/environment-extension.svg?branch=master)](https://travis-ci.com/ulrack/environment-extension)
+
+# Ulrack Environment Extension
+
+This extension adds the ability to configure environment specific configuration
+for projects using Ulrack.
+
+## Installation
+
+To install the package run the following command:
+
+```
+composer require ulrack/environment-extension
+```
+
+## Usage
+
+Please see the [documentation](docs/index.md) for information on the usage of this package.
+
+## Change log
+
+Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
+
+## Contributing
+
+Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details.
+
+## MIT License
+
+Copyright (c) GrizzIT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..95e22f3
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,61 @@
+{
+ "name": "ulrack/environment-extension",
+ "description": "The environment extension for Ulrack.",
+ "keywords": [
+ "services",
+ "environment",
+ "variables"
+ ],
+ "type": "library",
+ "license": "MIT",
+ "prefer-stable": true,
+ "minimum-stability": "stable",
+ "require": {
+ "php": "^7.3",
+ "grizz-it/configuration": "^1.2",
+ "grizz-it/storage": "^1.0",
+ "ulrack/command": "^2.0",
+ "ulrack/invocation-extension": "^1.1",
+ "ulrack/persistent-extension": "^1.1",
+ "ulrack/services": "^3.3"
+ },
+ "authors": [
+ {
+ "name": "Ulrack",
+ "homepage": "https://www.ulrack.com/",
+ "role": "Developer"
+ }
+ ],
+ "config": {
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Ulrack\\EnvironmentExtension\\": "src/"
+ },
+ "files": [
+ "locator.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Ulrack\\EnvironmentExtension\\Tests\\": "tests/"
+ }
+ },
+ "archive": {
+ "exclude": [
+ "/tests",
+ "/.gitignore",
+ "/.travis.yml",
+ "/phpunit.xml",
+ "/phpcs.xml",
+ "/PULL_REQUEST_TEMPLATE.md",
+ "/CODE_OF_CONDUCT.md",
+ "/CONTRIBUTING.md"
+ ]
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ }
+}
diff --git a/configuration/command/environment.command.json b/configuration/command/environment.command.json
new file mode 100644
index 0000000..92d944b
--- /dev/null
+++ b/configuration/command/environment.command.json
@@ -0,0 +1,4 @@
+{
+ "command": "environment",
+ "description": "Environment related commands."
+}
\ No newline at end of file
diff --git a/configuration/command/environment.get.command.json b/configuration/command/environment.get.command.json
new file mode 100644
index 0000000..6c046d0
--- /dev/null
+++ b/configuration/command/environment.get.command.json
@@ -0,0 +1,15 @@
+{
+ "parent": "environment",
+ "command": "get",
+ "description": "Retrieve the value of an environment variable.",
+ "service": "services.command.environment.get",
+ "parameters": [
+ {
+ "long": "key",
+ "short": "k",
+ "description": "The key of the environment variable.",
+ "type": "string",
+ "required": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/configuration/command/environment.list.command.json b/configuration/command/environment.list.command.json
new file mode 100644
index 0000000..40d2e89
--- /dev/null
+++ b/configuration/command/environment.list.command.json
@@ -0,0 +1,6 @@
+{
+ "parent": "environment",
+ "command": "list",
+ "description": "List all environment variable keys.",
+ "service": "services.command.environment.list"
+}
\ No newline at end of file
diff --git a/configuration/command/environment.set.command.json b/configuration/command/environment.set.command.json
new file mode 100644
index 0000000..7bb7208
--- /dev/null
+++ b/configuration/command/environment.set.command.json
@@ -0,0 +1,22 @@
+{
+ "parent": "environment",
+ "command": "set",
+ "description": "Set the value of an environment variable.",
+ "service": "services.command.environment.set",
+ "parameters": [
+ {
+ "long": "key",
+ "short": "k",
+ "description": "The key of the environment variable.",
+ "type": "string",
+ "required": true
+ },
+ {
+ "long": "value",
+ "short": "v",
+ "description": "The value of the environment variable.",
+ "type": "string",
+ "required": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/configuration/configuration/environment.configuration.json b/configuration/configuration/environment.configuration.json
new file mode 100644
index 0000000..861c16e
--- /dev/null
+++ b/configuration/configuration/environment.configuration.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "configuration.schema.json",
+ "key": "environment",
+ "location": "configuration/environment"
+}
diff --git a/configuration/invocations/environment.invocations.json b/configuration/invocations/environment.invocations.json
new file mode 100644
index 0000000..76f3cbe
--- /dev/null
+++ b/configuration/invocations/environment.invocations.json
@@ -0,0 +1,13 @@
+{
+ "environment.service.factory.extension": {
+ "service": "invocations.environment.service.factory",
+ "method": "getExtension",
+ "parameters": {
+ "key": "environment"
+ }
+ },
+ "environment.service.factory": {
+ "service": "services.core.service.manager",
+ "method": "getServiceFactory"
+ }
+}
\ No newline at end of file
diff --git a/configuration/persistent/environment.persistent.json b/configuration/persistent/environment.persistent.json
new file mode 100644
index 0000000..a9851fe
--- /dev/null
+++ b/configuration/persistent/environment.persistent.json
@@ -0,0 +1,3 @@
+{
+ "environment": {}
+}
\ No newline at end of file
diff --git a/configuration/service-compiler-extensions/environment.extension.json b/configuration/service-compiler-extensions/environment.extension.json
new file mode 100644
index 0000000..57b7e26
--- /dev/null
+++ b/configuration/service-compiler-extensions/environment.extension.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "service.compiler.extension.schema.json",
+ "class": "\\Ulrack\\Services\\Component\\Compiler\\Extension\\PassThroughCompiler",
+ "sortOrder": 100,
+ "key": "environment",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "default": {
+ "default": null
+ }
+ },
+ "required": ["default"]
+ }
+}
\ No newline at end of file
diff --git a/configuration/service-factory-extensions/environment.extension.json b/configuration/service-factory-extensions/environment.extension.json
new file mode 100644
index 0000000..5482401
--- /dev/null
+++ b/configuration/service-factory-extensions/environment.extension.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "service.factory.extension.schema.json",
+ "class": "\\Ulrack\\EnvironmentExtension\\Factory\\Extension\\EnvironmentFactory",
+ "key": "environment"
+}
\ No newline at end of file
diff --git a/configuration/services/environment.commands.json b/configuration/services/environment.commands.json
new file mode 100644
index 0000000..64e30f4
--- /dev/null
+++ b/configuration/services/environment.commands.json
@@ -0,0 +1,20 @@
+{
+ "command.environment.get": {
+ "class": "\\Ulrack\\EnvironmentExtension\\Command\\EnvironmentGetCommand",
+ "parameters": {
+ "serviceFactory": "@{invocations.environment.service.factory}"
+ }
+ },
+ "command.environment.set": {
+ "class": "\\Ulrack\\EnvironmentExtension\\Command\\EnvironmentSetCommand",
+ "parameters": {
+ "environmentStorage": "@{persistent.environment}"
+ }
+ },
+ "command.environment.list": {
+ "class": "\\Ulrack\\EnvironmentExtension\\Command\\EnvironmentListCommand",
+ "parameters": {
+ "environmentFactory": "@{invocations.environment.service.factory.extension}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..2a8c398
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,8 @@
+# Ulrack Environment Extension Documentation
+
+This documentation will provide you with information about working with, and extending this package.
+
+- [Main README](../README.md)
+- [Usage](usage/index.md)
+ - [Installation](usage/installation.md)
+ - [Create an environment variable](usage/create-an-environment-variable.md)
diff --git a/docs/usage/create-an-environment-variable.md b/docs/usage/create-an-environment-variable.md
new file mode 100644
index 0000000..a6d7aa8
--- /dev/null
+++ b/docs/usage/create-an-environment-variable.md
@@ -0,0 +1,45 @@
+# Ulrack Environment Extension - Create an environment variable
+
+This package add the ability to create and manage environment variables for
+projects. This is being managed through the services layer, so multiple
+declarations can be made per file. To create an environment variable create a
+file in the `configuration/environment` directory with the following content.
+
+```json
+{
+ "foo": {
+ "default": null
+ }
+}
+```
+
+A default value can be declared for an environment variable through the
+`default` field. The environment can now be accessed anywhere in the project
+through the services layer by calling it with `@{environment.foo}`. The
+variable can be managed with commands.
+
+## List command
+
+The list command can be used to retrieve a list of all availible environment
+variables. This command is `bin/application environment list`.
+
+## Get command
+
+The get command can be used to retrieve the JSON representation of a value of
+an environment variable. This command accepts one parameter which is `key` (or
+shorthand `k`). It can be invoked like this
+`bin/application environment get --key="foo"`.
+
+## Set command
+
+The set command can be used to alter the value of the environment variable in
+the storage. It accepts two parameters, the `key` variable (same as for the
+`get` command). And the `value` (or shorthand `v`) parameter which accepts a
+JSON string representation of the value. If the value parameter is omitted
+it can be added interactively.
+
+## Further reading
+
+[Back to usage index](index.md)
+
+[Installation](installation.md)
diff --git a/docs/usage/index.md b/docs/usage/index.md
new file mode 100644
index 0000000..d773b77
--- /dev/null
+++ b/docs/usage/index.md
@@ -0,0 +1,9 @@
+# Ulrack Environment Extension - Usage Documentation
+
+This part of the documentation will focus on the usage of this package.
+
+## Index
+
+- [Main index](../index.md)
+- [Installation](installation.md)
+- [Create an environment variable](create-an-environment-variable.md)
\ No newline at end of file
diff --git a/docs/usage/installation.md b/docs/usage/installation.md
new file mode 100644
index 0000000..adc08c9
--- /dev/null
+++ b/docs/usage/installation.md
@@ -0,0 +1,20 @@
+# Ulrack Environment Extension - Installation
+
+This package is meant as a plug-and-play extension to any Ulrack project.
+To start using this package, install it through composer by running:
+```
+composer require ulrack/environment-extension
+```
+
+After the package is installed, run the following command:
+```
+bin/application cache clear
+```
+
+After this is done, the extension should be up and running.
+
+## Further reading
+
+[Back to usage index](index.md)
+
+[Create an environment variable](create-an-environment-variable.md)
diff --git a/locator.php b/locator.php
new file mode 100644
index 0000000..29cef29
--- /dev/null
+++ b/locator.php
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..2b83b56
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ src
+
+
+
+
+ tests
+
+
+
diff --git a/src/Command/EnvironmentGetCommand.php b/src/Command/EnvironmentGetCommand.php
new file mode 100644
index 0000000..d2421db
--- /dev/null
+++ b/src/Command/EnvironmentGetCommand.php
@@ -0,0 +1,54 @@
+serviceFactory = $serviceFactory;
+ }
+
+ /**
+ * Executes the command.
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @return void
+ */
+ public function __invoke(
+ InputInterface $input,
+ OutputInterface $output
+ ): void {
+ $output->writeLine(
+ json_encode(
+ $this->serviceFactory->create(
+ 'environment.' . $input->getParameter('key')
+ )
+ )
+ );
+ }
+}
diff --git a/src/Command/EnvironmentListCommand.php b/src/Command/EnvironmentListCommand.php
new file mode 100644
index 0000000..19f473b
--- /dev/null
+++ b/src/Command/EnvironmentListCommand.php
@@ -0,0 +1,57 @@
+environmentFactory = $environmentFactory;
+ }
+
+ /**
+ * Executes the command.
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @return void
+ */
+ public function __invoke(
+ InputInterface $input,
+ OutputInterface $output
+ ): void {
+ $keys = $this->environmentFactory->getKeys();
+ if (count($keys) > 0) {
+ $output->outputList($keys);
+
+ return;
+ }
+
+ throw new Exception('No available keys.');
+ }
+}
diff --git a/src/Command/EnvironmentSetCommand.php b/src/Command/EnvironmentSetCommand.php
new file mode 100644
index 0000000..5471359
--- /dev/null
+++ b/src/Command/EnvironmentSetCommand.php
@@ -0,0 +1,58 @@
+environmentStorage = $environmentStorage;
+ }
+
+ /**
+ * Executes the command.
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @return void
+ */
+ public function __invoke(
+ InputInterface $input,
+ OutputInterface $output
+ ): void {
+ $value = $input->getParameter('value');
+
+ $jsonValue = json_decode($value, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $value = $jsonValue;
+ }
+
+ $this->environmentStorage->set(
+ $input->getParameter('key'),
+ $value
+ );
+ }
+}
diff --git a/src/Common/UlrackEnvironmentExtensionPackage.php b/src/Common/UlrackEnvironmentExtensionPackage.php
new file mode 100644
index 0000000..c6c2f6e
--- /dev/null
+++ b/src/Common/UlrackEnvironmentExtensionPackage.php
@@ -0,0 +1,16 @@
+environmentVariables[$serviceKey] = $value;
+ }
+
+ /**
+ * Retrieves the environment storage.
+ *
+ * @return StorageInterface
+ */
+ private function getEnvironmentStorage(): StorageInterface
+ {
+ if (is_null($this->environmentStorage)) {
+ $this->environmentStorage = $this->superCreate(
+ 'persistent.environment'
+ );
+ }
+
+ return $this->environmentStorage;
+ }
+
+ /**
+ * Retrieves a list of all environment keys.
+ *
+ * @return array
+ */
+ public function getKeys(): array
+ {
+ return array_keys($this->getServices()[$this->getKey()] ?? []);
+ }
+
+ /**
+ * Invoke the invocation and return the result.
+ *
+ * @param string $serviceKey
+ *
+ * @return mixed
+ *
+ * @throws DefinitionNotFoundException When the definition can not be found.
+ */
+ public function create(string $serviceKey)
+ {
+ $serviceKey = $this->preCreate(
+ $serviceKey,
+ $this->getParameters()
+ )['serviceKey'];
+
+ $internalKey = preg_replace(
+ sprintf('/^%s\\./', preg_quote($this->getKey())),
+ '',
+ $serviceKey,
+ 1
+ );
+
+ if (!isset($this->environmentVariables[$internalKey])) {
+ $services = $this->getServices()[$this->getKey()];
+ if (!isset($services[$internalKey])) {
+ throw new DefinitionNotFoundException($serviceKey);
+ }
+
+ $storage = $this->getEnvironmentStorage();
+ if (!$storage->has($internalKey)) {
+ $storage->set(
+ $internalKey,
+ $services[$internalKey]['default'] ?? null
+ );
+ }
+
+ $this->registerService(
+ $internalKey,
+ $storage->get($internalKey)
+ );
+ }
+
+ return $this->postCreate(
+ $serviceKey,
+ $this->environmentVariables[$internalKey],
+ $this->getParameters()
+ )['return'];
+ }
+}
diff --git a/tests/Command/EnvironmentGetCommandTest.php b/tests/Command/EnvironmentGetCommandTest.php
new file mode 100644
index 0000000..c383082
--- /dev/null
+++ b/tests/Command/EnvironmentGetCommandTest.php
@@ -0,0 +1,53 @@
+createMock(ServiceFactoryInterface::class);
+ $input = $this->createMock(InputInterface::class);
+ $output = $this->createMock(OutputInterface::class);
+ $subject = new EnvironmentGetCommand($serviceFactory);
+
+ $input->expects(static::once())
+ ->method('getParameter')
+ ->with('key')
+ ->willReturn('foo');
+
+ $serviceFactory->expects(static::once())
+ ->method('create')
+ ->with('environment.foo')
+ ->willReturn('bar');
+
+ $output->expects(static::once())
+ ->method('writeLine')
+ ->with('"bar"');
+
+ $subject->__invoke(
+ $input,
+ $output
+ );
+ }
+}
diff --git a/tests/Command/EnvironmentListCommandTest.php b/tests/Command/EnvironmentListCommandTest.php
new file mode 100644
index 0000000..b87d7ea
--- /dev/null
+++ b/tests/Command/EnvironmentListCommandTest.php
@@ -0,0 +1,67 @@
+createMock(EnvironmentFactory::class);
+ $output = $this->createMock(OutputInterface::class);
+ $subject = new EnvironmentListCommand($serviceFactory);
+ $serviceFactory->expects(static::once())
+ ->method('getKeys')
+ ->willReturn(['foo', 'bar']);
+
+ $output->expects(static::once())
+ ->method('outputList')
+ ->with(['foo', 'bar']);
+
+ $subject->__invoke(
+ $this->createMock(InputInterface::class),
+ $output
+ );
+ }
+
+ /**
+ * @covers ::__invoke
+ * @covers ::__construct
+ *
+ * @return void
+ */
+ public function testInvokeNoKeys(): void
+ {
+ $serviceFactory = $this->createMock(EnvironmentFactory::class);
+ $subject = new EnvironmentListCommand($serviceFactory);
+ $serviceFactory->expects(static::once())
+ ->method('getKeys')
+ ->willReturn([]);
+
+ $this->expectException(Exception::class);
+ $subject->__invoke(
+ $this->createMock(InputInterface::class),
+ $this->createMock(OutputInterface::class)
+ );
+ }
+}
diff --git a/tests/Command/EnvironmentSetCommandTest.php b/tests/Command/EnvironmentSetCommandTest.php
new file mode 100644
index 0000000..56167ed
--- /dev/null
+++ b/tests/Command/EnvironmentSetCommandTest.php
@@ -0,0 +1,74 @@
+createMock(StorageInterface::class);
+ $input = $this->createMock(InputInterface::class);
+ $subject = new EnvironmentSetCommand($storage);
+
+ $input->expects(static::exactly(2))
+ ->method('getParameter')
+ ->withConsecutive(['value'], ['key'])
+ ->willReturnOnConsecutiveCalls('bar', 'foo');
+
+ $storage->expects(static::once())
+ ->method('set')
+ ->with('foo', 'bar');
+
+ $subject->__invoke(
+ $input,
+ $this->createMock(OutputInterface::class)
+ );
+ }
+
+ /**
+ * @covers ::__invoke
+ * @covers ::__construct
+ *
+ * @return void
+ */
+ public function testInvokeJson(): void
+ {
+ $storage = $this->createMock(StorageInterface::class);
+ $input = $this->createMock(InputInterface::class);
+ $subject = new EnvironmentSetCommand($storage);
+
+ $input->expects(static::exactly(2))
+ ->method('getParameter')
+ ->withConsecutive(['value'], ['key'])
+ ->willReturnOnConsecutiveCalls('{"foo":"bar"}', 'foo');
+
+ $storage->expects(static::once())
+ ->method('set')
+ ->with('foo', ['foo' => 'bar']);
+
+ $subject->__invoke(
+ $input,
+ $this->createMock(OutputInterface::class)
+ );
+ }
+}
diff --git a/tests/Factory/Extension/EnvironmentFactoryTest.php b/tests/Factory/Extension/EnvironmentFactoryTest.php
new file mode 100644
index 0000000..64b97b0
--- /dev/null
+++ b/tests/Factory/Extension/EnvironmentFactoryTest.php
@@ -0,0 +1,54 @@
+createMock(ServiceFactoryInterface::class);
+ $serviceFactory->expects(static::once())
+ ->method('create')
+ ->with('persistent.environment')
+ ->willReturn($this->createMock(StorageInterface::class));
+
+ $subject = new EnvironmentFactory(
+ $serviceFactory,
+ 'environment',
+ [],
+ ['environment' => ['foo' => ['default' => null]]],
+ (function () {
+ return [];
+ }),
+ []
+ );
+
+ $this->assertEquals(['foo'], $subject->getKeys());
+ $this->assertEquals(null, $subject->create('environment.foo'));
+
+ $this->expectException(DefinitionNotFoundException::class);
+ $subject->create('environment.bar');
+ }
+}