├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Arr.php ├── Drivers ├── DotEnv.php ├── Driver.php ├── Env.php └── Json.php ├── File.php └── Store.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | 4 | jobs: 5 | phpunit: 6 | name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} on ${{ matrix.os }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | php: [8.0, 8.1, 8.2, 8.3] 11 | os: [ubuntu-latest, windows-latest] 12 | dependency-version: [lowest, highest] 13 | steps: 14 | - uses: actions/checkout@v4.2.2 15 | 16 | - name: Configure PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | extensions: mbstring, fileinfo 21 | coverage: none 22 | 23 | - name: Install dependencies 24 | uses: ramsey/composer-install@v3 25 | with: 26 | dependency-versions: ${{ matrix.dependency-version }} 27 | composer-options: "--prefer-dist" 28 | 29 | - name: Execute tests 30 | run: vendor/bin/phpunit 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Sven Luijten 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![file-config](https://user-images.githubusercontent.com/11269635/35174536-129cc67e-fd70-11e7-8b87-d2ba8cc24ec8.jpg) 2 | 3 | # File Config 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | [![Software License][ico-license]](LICENSE.md) 8 | [![Build Status][ico-build]][link-build] 9 | [![StyleCI][ico-styleci]][link-styleci] 10 | 11 | This package provides a persistent config store as flat files with an easy 12 | to use and understand API. This is perfect if the config file should be 13 | stored in userland, or somewhere the user is allowed to edit it manually. 14 | 15 | ## Installation 16 | You'll have to follow a couple of simple steps to install this package. 17 | 18 | ### Downloading 19 | Via [composer](http://getcomposer.org): 20 | 21 | ```bash 22 | $ composer require sven/file-config:^3.1 23 | ``` 24 | 25 | Or add the package to your dependencies in `composer.json` and run 26 | `composer update sven/file-config` on the command line to download 27 | the package: 28 | 29 | ```json 30 | { 31 | "require": { 32 | "sven/file-config": "^3.1" 33 | } 34 | } 35 | ``` 36 | 37 | ## Available drivers 38 | - [`Json`](./src/Drivers/Json.php) - For `.json` files. 39 | - [`DotEnv`](./src/Drivers/DotEnv.php) - For `.env` files. 40 | - [`Env` (deprecated)](./src/Drivers/Env.php) 41 | 42 | You can also write your own driver to use in your own applications. To write your 43 | own, read [writing your own driver](#writing-your-own-driver) in this document. 44 | 45 | ## Usage 46 | To get started, construct a new instance of `\Sven\FileConfig\Store`, providing it with a `\Sven\FileConfig\File` 47 | object, and an implementation of the `\Sven\FileConfig\Drivers\Driver` interface. We'll use the pre-installed 48 | `Json` driver in the examples. 49 | 50 | ```php 51 | use Sven\FileConfig\File; 52 | use Sven\FileConfig\Store; 53 | use Sven\FileConfig\Drivers\Json; 54 | 55 | $file = new File('/path/to/file.json'); 56 | $config = new Store($file, new Json()); 57 | ``` 58 | 59 | You can interact with your newly created `$config` object via the `get`, `set`, and `delete` 60 | methods. 61 | 62 | ### Examples 63 | Let's take a look at some examples. 64 | 65 | #### Getting a value from the file 66 | To retrieve a value from the configuration file, use the `get` method. Let's assume our (prettified) 67 | JSON configuration file looks like this: 68 | 69 | ```json 70 | { 71 | "database": { 72 | "name": "test", 73 | "host": "localhost", 74 | "user": "admin", 75 | "password": "root" 76 | } 77 | } 78 | ``` 79 | 80 | We can get the entire `database` array: 81 | 82 | ```php 83 | $config->get('database'); 84 | // ~> ['name' => 'test', 'host' => 'localhost', 'user' => 'admin', 'password' => root'] 85 | ``` 86 | 87 | ... or get only the `database.host` property using dot-notation: 88 | 89 | ```php 90 | $config->get('database.host'); 91 | // ~> 'localhost' 92 | ``` 93 | 94 | If the given key can not be found, `null` is returned by default. You may override this by 95 | passing a second argument to `get`: 96 | 97 | ```php 98 | $config->get('database.does_not_exist', 'default'); 99 | // ~> 'default' 100 | ``` 101 | 102 | #### Setting a value in the file 103 | To add or change a value in the configuration file, you may use the `set` method. Note that 104 | you have to call the `persist` method to write the changes you made to the file. You may also 105 | use the `fresh` method to retrieve a "fresh" instance of the `Store`, where the values will be 106 | read from the file again. 107 | 108 | ```php 109 | $config->set('database.user', 'new-username'); 110 | $config->persist(); 111 | 112 | $freshConfig = $config->fresh(); 113 | $freshConfig->get('database.user'); 114 | // ~> 'new-username' 115 | ``` 116 | 117 | The file will end up looking like this after you've called the `persist` method: 118 | 119 | ```json 120 | { 121 | "database": { 122 | "name": "test", 123 | "host": "localhost", 124 | "user": "new-username", 125 | "password": "root" 126 | } 127 | } 128 | ``` 129 | 130 | #### Deleting an entry from the file 131 | To remove one of the configuration options from the file, use the `delete` method. Again, don't forget 132 | to call `persist` to write the new contents to the file! 133 | 134 | ```php 135 | $config->delete('database.user'); 136 | $config->persist(); 137 | ``` 138 | 139 | ```json 140 | { 141 | "database": { 142 | "name": "test", 143 | "host": "localhost", 144 | "password": "root" 145 | } 146 | } 147 | ``` 148 | 149 | ## Writing your own driver 150 | You may want to use a file format for your configuration that's not (yet) included in 151 | this package. Thankfully, writing a driver is as straightforward as turning your file's 152 | contents into a PHP array. 153 | 154 | To create a driver, create a class that implements the `\Sven\FileConfig\Drivers\Driver` 155 | interface. Then add 2 methods to your class: `import` and `export`. The `import` method 156 | will receive the contents of the file as an argument, and expects a PHP array to be returned. 157 | 158 | The `export` method is the exact reverse: it receives a PHP array, and expects the new contents 159 | of the file to be returned (as a string). To see how this works in more detail, take a look at 160 | [the pre-installed `json` driver](src/Drivers/Json.php). 161 | 162 | ## Contributing 163 | All contributions (pull requests, issues and feature requests) are 164 | welcome. Make sure to read through the [CONTRIBUTING.md](CONTRIBUTING.md) first, 165 | though. See the [contributors page](../../graphs/contributors) for all contributors. 166 | 167 | ## License 168 | `sven/file-config` is licensed under the MIT License (MIT). Please see the 169 | [license file](LICENSE.md) for more information. 170 | 171 | [ico-version]: https://img.shields.io/packagist/v/sven/file-config.svg?style=flat-square 172 | [ico-license]: https://img.shields.io/badge/license-MIT-green.svg?style=flat-square 173 | [ico-downloads]: https://img.shields.io/packagist/dt/sven/file-config.svg?style=flat-square 174 | [ico-build]: https://img.shields.io/github/actions/workflow/status/svenluijten/file-config/tests.yml?style=flat-square 175 | [ico-styleci]: https://styleci.io/repos/117891803/shield 176 | 177 | [link-packagist]: https://packagist.org/packages/sven/file-config 178 | [link-downloads]: https://packagist.org/packages/sven/file-config 179 | [link-build]: https://github.com/svenluijten/file-config/actions/workflows/tests.yml 180 | [link-styleci]: https://styleci.io/repos/117891803 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sven/file-config", 3 | "description": "Store and read configuration values using files on disk", 4 | "keywords": [ 5 | "configuration", 6 | "config", 7 | "flatfile", 8 | "flat", 9 | "file", 10 | "json", 11 | "env" 12 | ], 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Sven Luijten", 17 | "email": "contact@svenluijten.com", 18 | "homepage": "https://svenluijten.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.0", 23 | "ext-json": "*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9.0 | ^10.0", 27 | "league/flysystem": "^3.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Sven\\FileConfig\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Sven\\FileConfig\\Tests\\": "tests/" 37 | } 38 | }, 39 | "config": { 40 | "sort-packages": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Arr.php: -------------------------------------------------------------------------------- 1 | 1) { 64 | $key = array_shift($keys); 65 | 66 | // If the key doesn't exist at this depth, we will just create an empty array 67 | // to hold the next value, allowing us to create the arrays to hold final 68 | // values at the correct depth. Then we'll keep digging into the array. 69 | if (!isset($array[$key]) || !is_array($array[$key])) { 70 | $array[$key] = []; 71 | } 72 | 73 | $array = &$array[$key]; 74 | } 75 | 76 | $array[array_shift($keys)] = $value; 77 | 78 | return $array; 79 | } 80 | 81 | /** 82 | * @param array $array 83 | * @param array|string $keys 84 | * @return void 85 | */ 86 | public static function forget(&$array, $keys) 87 | { 88 | $original = &$array; 89 | 90 | $keys = (array) $keys; 91 | 92 | if (count($keys) === 0) { 93 | return; 94 | } 95 | 96 | foreach ($keys as $key) { 97 | // if the exact key exists in the top-level, remove it 98 | if (static::exists($array, $key)) { 99 | unset($array[$key]); 100 | continue; 101 | } 102 | 103 | $parts = explode('.', $key); 104 | 105 | // clean up before each pass 106 | $array = &$original; 107 | 108 | while (count($parts) > 1) { 109 | $part = array_shift($parts); 110 | if (isset($array[$part]) && is_array($array[$part])) { 111 | $array = &$array[$part]; 112 | } else { 113 | continue 2; 114 | } 115 | } 116 | 117 | unset($array[array_shift($parts)]); 118 | } 119 | } 120 | 121 | /** 122 | * @param mixed $value 123 | * @return bool 124 | */ 125 | public static function accessible($value): bool 126 | { 127 | return is_array($value) || $value instanceof ArrayAccess; 128 | } 129 | 130 | /** 131 | * @param \ArrayAccess|array $array 132 | * @param string|int $key 133 | * @return bool 134 | */ 135 | public static function exists($array, $key): bool 136 | { 137 | if ($array instanceof ArrayAccess) { 138 | return $array->offsetExists($key); 139 | } 140 | 141 | return array_key_exists($key, $array); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Drivers/DotEnv.php: -------------------------------------------------------------------------------- 1 | $part) { 17 | // [ 18 | // 0 => 'FOO="bar"', 19 | // 1 => 'FOO', 20 | // 2 => '"', 21 | // 3 => 'bar', 22 | // ] 23 | preg_match('/(\w+)=([\"\']?)(.*)\2/', $part, $matches); 24 | 25 | if ($this->isEmptyLine($part) || $this->isComment($part)) { 26 | $result[$key] = $part; 27 | } else { 28 | $result[$matches[1]] = $matches[3]; 29 | } 30 | } 31 | 32 | return $result; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function export(array $config): string 39 | { 40 | $result = ''; 41 | 42 | foreach ($config as $key => $value) { 43 | if ($this->isEmptyLine($value, $key)) { 44 | $result .= PHP_EOL; 45 | } elseif ($this->isComment($value)) { 46 | $result .= PHP_EOL.$value; 47 | } else { 48 | $result .= PHP_EOL.$key.'='.$this->quoteIfNecessary($value); 49 | } 50 | } 51 | 52 | return trim($result, PHP_EOL).PHP_EOL; 53 | } 54 | 55 | protected function isEmptyLine(string $value, $key = null): bool 56 | { 57 | return $value === '' && !is_string($key); 58 | } 59 | 60 | protected function isComment(string $value): bool 61 | { 62 | return str_starts_with($value, '#'); 63 | } 64 | 65 | protected function quoteIfNecessary(string $value): string 66 | { 67 | // If the string contains anything that is not a 68 | // regular "word" like special characters or 69 | // spaces, we quote the value and return. 70 | if (preg_match('/[\W]/', $value) === 1) { 71 | return '"'.$value.'"'; 72 | } 73 | 74 | return $value; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Drivers/Driver.php: -------------------------------------------------------------------------------- 1 | $part) { 22 | preg_match(self::REGEX, $part, $matches); 23 | 24 | if ($this->isEmptyLine($part) || $this->isComment($part)) { 25 | $result[$key] = $part; 26 | } else { 27 | $result[$matches[1]] = $matches[2] ?? ''; 28 | } 29 | } 30 | 31 | return $result; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function export(array $config): string 38 | { 39 | $result = ''; 40 | 41 | foreach ($config as $key => $value) { 42 | if ($this->isEmptyLine($value, $key)) { 43 | $result .= PHP_EOL; 44 | } elseif ($this->isComment($value)) { 45 | $result .= PHP_EOL.$value; 46 | } else { 47 | $result .= PHP_EOL.$key.'='.$value; 48 | } 49 | } 50 | 51 | return trim($result, PHP_EOL).PHP_EOL; 52 | } 53 | 54 | protected function isEmptyLine(string $value, $key = null): bool 55 | { 56 | return $value === '' && !is_string($key); 57 | } 58 | 59 | protected function isComment(string $value): bool 60 | { 61 | return mb_substr($value, 0, 1) === '#'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Drivers/Json.php: -------------------------------------------------------------------------------- 1 | path = $path; 16 | } 17 | 18 | public function contents(): string 19 | { 20 | return file_get_contents($this->path); 21 | } 22 | 23 | public function update(string $content): bool 24 | { 25 | return (bool) file_put_contents($this->path, $content); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Store.php: -------------------------------------------------------------------------------- 1 | config = $driver->import($file->contents()); 14 | } 15 | 16 | public function get($key, $default = null) 17 | { 18 | return Arr::get($this->config, $key, $default); 19 | } 20 | 21 | public function all(): array 22 | { 23 | return $this->config; 24 | } 25 | 26 | public function set($key, $value): void 27 | { 28 | Arr::set($this->config, $key, $value); 29 | } 30 | 31 | public function delete($key): void 32 | { 33 | Arr::forget($this->config, $key); 34 | } 35 | 36 | public function fresh(): Store 37 | { 38 | return new self($this->file, $this->driver); 39 | } 40 | 41 | public function persist(): bool 42 | { 43 | return $this->file->update( 44 | $this->driver->export($this->config) 45 | ); 46 | } 47 | } 48 | --------------------------------------------------------------------------------