├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── dependabot-auto-merge.yml
│ ├── php-cs-fixer.yml
│ ├── phpstan.yml
│ ├── phpunit.yml
│ └── rector.yaml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── config
└── env-editor.php
├── phpunit.xml
├── phpunit.xml.dist
├── rector.php
├── resources
├── lang
│ └── en
│ │ └── env-editor.php
└── views
│ ├── components
│ ├── _backup.blade.php
│ ├── _configActions.blade.php
│ ├── _currentEnv.blade.php
│ ├── _itemModal.blade.php
│ └── _upload.blade.php
│ ├── index.blade.php
│ └── layout.blade.php
├── routes
└── routes.php
├── ruleset-php_cs.php
├── ruleset-phpstan.neon
├── src
├── Controllers
│ └── EnvController.php
├── Dto
│ ├── BackupObj.php
│ └── EntryObj.php
├── EnvEditor.php
├── Exceptions
│ └── EnvException.php
├── Facades
│ └── EnvEditor.php
├── Helpers
│ ├── EnvFileContentManager.php
│ ├── EnvFilesManager.php
│ └── EnvKeysManager.php
└── ServiceProvider.php
└── tests
├── Feature
├── ConfigurationTest.php
└── UiTest.php
├── TestCase.php
├── Unit
├── Dto
│ ├── BackupObjTest.php
│ └── EntryObjTest.php
└── Helpers
│ ├── EnvFileContentManagerTest.php
│ ├── EnvKeysManagerTest.php
│ └── FilesManagerTest.php
└── fixtures
└── .env.example
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
14 | [*.{yml,yaml}]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "composer"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | day: tuesday
13 | time: "12:00"
14 | labels:
15 | - "dependencies"
16 | open-pull-requests-limit: 10
17 |
18 | - package-ecosystem: "github-actions"
19 | directory: "/"
20 | schedule:
21 | interval: weekly
22 | day: tuesday
23 | time: "12:00"
24 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: dependabot-auto-merge
2 | on: pull_request_target
3 |
4 | permissions:
5 | pull-requests: write
6 | contents: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' }}
12 | steps:
13 |
14 | - name: Dependabot metadata
15 | id: metadata
16 | uses: dependabot/fetch-metadata@v2.3.0
17 | with:
18 | github-token: "${{ secrets.GITHUB_TOKEN }}"
19 |
20 | - name: Auto-merge Dependabot PRs for semver-minor updates
21 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
22 | run: gh pr merge --auto --rebase "$PR_URL"
23 | env:
24 | PR_URL: ${{github.event.pull_request.html_url}}
25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
26 |
27 | - name: Auto-merge Dependabot PRs for semver-patch updates
28 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
29 | run: gh pr merge --auto --rebase "$PR_URL"
30 | env:
31 | PR_URL: ${{github.event.pull_request.html_url}}
32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
33 |
--------------------------------------------------------------------------------
/.github/workflows/php-cs-fixer.yml:
--------------------------------------------------------------------------------
1 | name: Check & fix styling
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | php-cs-fixer:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v4
16 | # with:
17 | # ref: ${{ github.head_ref }}
18 | # token: ${{ secrets.PAT }}
19 |
20 | - name: Run PHP CS Fixer
21 | uses: docker://oskarstark/php-cs-fixer-ga
22 | with:
23 | args: --config=ruleset-php_cs.php --allow-risky=yes --show-progress=dots --diff --dry-run
24 | #
25 | # - name: Commit changes
26 | # uses: stefanzweifel/git-auto-commit-action@v4
27 | # with:
28 | # commit_message: Apply php-cs-fixer changes
29 |
--------------------------------------------------------------------------------
/.github/workflows/phpstan.yml:
--------------------------------------------------------------------------------
1 | name: PHPStan
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - '**.php'
8 | - 'ruleset-phpstan.neon'
9 | pull_request:
10 | branches: [main]
11 | paths:
12 | - '**.php'
13 | - 'ruleset-phpstan.neon'
14 |
15 |
16 | jobs:
17 | phpstan:
18 | name: phpstan
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Setup PHP
24 | uses: shivammathur/setup-php@v2
25 | with:
26 | php-version: 8.2
27 | coverage: none
28 |
29 | - name: Install composer dependencies
30 | uses: ramsey/composer-install@v3
31 |
32 | - name: Run PHPStan
33 | run: ./vendor/bin/phpstan --error-format=github analyse -c ruleset-phpstan.neon -vvv
34 |
--------------------------------------------------------------------------------
/.github/workflows/phpunit.yml:
--------------------------------------------------------------------------------
1 | name: phpunit
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: true
14 | matrix:
15 | os: [ubuntu-latest]
16 | php: [8.2, 8.3, 8.4]
17 | laravel: [11.*, 12.*]
18 | # include:
19 | # - laravel: 10.*
20 | # testbench: 8.*
21 | # - laravel: 11.*
22 | # testbench: 9.*
23 | # exclude:
24 | # - laravel: 11.*
25 | # php: 8.1
26 |
27 |
28 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }}
29 |
30 | steps:
31 | - name: Checkout code
32 | uses: actions/checkout@v4
33 |
34 | - name: Setup PHP
35 | uses: shivammathur/setup-php@v2
36 | with:
37 | php-version: ${{ matrix.php }}
38 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
39 | coverage: pcov
40 |
41 | - name: Setup problem matchers
42 | run: |
43 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
44 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
45 |
46 | - name: Get composer cache directory
47 | id: composer-cache
48 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
49 |
50 | - name: Cache dependencies
51 | uses: actions/cache@v4
52 | with:
53 | path: ${{ steps.composer-cache.outputs.dir }}
54 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
55 | restore-keys: ${{ runner.os }}-composer-
56 |
57 |
58 |
59 | - name: Install composer dependencies
60 | run: |
61 | composer --version
62 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
63 | composer update --prefer-dist --no-interaction --no-suggest --dev
64 | composer dump
65 |
66 |
67 | - name: Execute tests
68 | run: vendor/bin/phpunit
69 |
70 | - name: Upload coverage results to Coveralls
71 | env:
72 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73 | run: |
74 | composer global require php-coveralls/php-coveralls
75 | php-coveralls --coverage_clover=build/logs/clover.xml -v
76 |
--------------------------------------------------------------------------------
/.github/workflows/rector.yaml:
--------------------------------------------------------------------------------
1 | name: Rector
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 |
10 | jobs:
11 | rector:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Rector Cache
17 | uses: actions/cache@v4
18 | with:
19 | path: /tmp/rector
20 | key: ${{ runner.os }}-rector-${{ github.run_id }}
21 | restore-keys: ${{ runner.os }}-rector-
22 |
23 | - run: mkdir -p /tmp/rector
24 |
25 | - name: Setup PHP
26 | uses: shivammathur/setup-php@v2
27 | with:
28 | php-version: 8.2
29 | coverage: none
30 |
31 | - name: Install composer dependencies
32 | uses: ramsey/composer-install@v3
33 |
34 | - name: Rector Dry Run
35 | run: php vendor/bin/rector process --dry-run --config=rector.php
36 |
37 |
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | **/*.cache
3 | /build
4 |
5 | .idea
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 GeoSot
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://packagist.org/packages/geo-sot/laravel-env-editor)
2 | [](https://packagist.org/packages/geo-sot/laravel-env-editor)
3 | [](https://coveralls.io/github/GeoSot/Laravel-EnvEditor)
4 | [](https://www.codacy.com/manual/geo.sotis/Laravel-EnvEditor?utm_source=github.com&utm_medium=referral&utm_content=GeoSot/Laravel-EnvEditor&utm_campaign=Badge_Grade)
5 | [](https://codeclimate.com/github/GeoSot/Laravel-EnvEditor/maintainability)
6 | [](https://packagist.org/packages/geo-sot/laravel-env-editor)
7 | # Laravel .env Editor (plus GUI)
8 | This Package allows to manage Laravel .env file values on the Fly (add, edit, delete keys), upload another .env or create backups
9 |
10 | Management can be done through the user interface, or programmatically by using the `EnvEditor` Facade, without breaking the files structure.
11 |
12 | The inspiration for this package was, [Brotzka/laravel-dotenv-editor](https://github.com/Brotzka/laravel-dotenv-editor).
13 |
14 | * [Installation](#installation)
15 | * [Available Methods](#available_methods)
16 | * [User Interface](#user_interface)
17 |
18 | ## Installation:
19 |
20 | 1. Install package
21 | ```bash
22 | composer require geo-sot/laravel-env-editor
23 | ```
24 | 2. Publish assets
25 | ```bash
26 | php artisan vendor:publish --provider=GeoSot\EnvEditor\ServiceProvider
27 | ```
28 | This will publish all files:
29 | * config -> env-editor.php
30 | * views -> resources/views/vendor/env-editor/..
31 | * lang -> resources/lang/vendor/env-editor.php
32 |
33 | Or publish specific tags
34 |
35 | ```bash
36 | //Publish specific tag
37 | php artisan vendor:publish --tag=config
38 | php artisan vendor:publish --tag=translations
39 | php artisan vendor:publish --tag=views
40 |
41 | //Publish specific Tag from this Vendor
42 | php artisan vendor:publish --provider=GeoSot\EnvEditor\ServiceProvider --tag=config
43 |
44 | ```
45 |
46 | ## Available Methods:
47 |
48 | >* getEnvFileContent
49 | >* keyExists
50 | >* getKey
51 | >* addKey
52 | >* editKey
53 | >* deleteKey
54 | >* getAllBackUps
55 | >* upload
56 | >* backUpCurrent
57 | >* getFilePath
58 | >* deleteBackup
59 | >* restoreBackUp
60 |
61 |
62 | Example
63 |
64 | ```php
65 |
66 | EnvEditor::getEnvFileContent($fileName='')
67 | // Return The .env Data as Collection.
68 | // If FileName Is provided it searches inside backups Directory and returns these results
69 |
70 | EnvEditor::keyExists($key)
71 | // Search key existance in .env
72 |
73 | EnvEditor::getKey(string $key, $default = null)
74 | // Get key value from .env,
75 |
76 | EnvEditor::addKey($key, $value, array $options = [])
77 | // Adds new Key in .env file
78 | // As options can pass ['index'=>'someNumericIndex'] in order to place the new key after an other and not in the end,
79 | // or ['group'=>'MAIL/APP etc'] to place the new key oat the end of the group
80 |
81 | EnvEditor::editKey($key, $value)
82 | // Edits existing key value
83 |
84 | EnvEditor::deleteKey($key)
85 |
86 | EnvEditor::getAllBackUps()
87 | // Returns all Backup files as collection with some info like, created_date, content etc.
88 |
89 | EnvEditor::upload(UploadedFile $uploadedFile, $replaceCurrentEnv)
90 | // Gets an UploadedFile and stores it as backup or as current .env
91 |
92 | EnvEditor::backUpCurrent()
93 | // Backups current .env
94 |
95 | EnvEditor::getFilePath($fileName = '')
96 | // Returns the full path of a backup file.
97 | // If $fileName is empty returns the full path of the .env file
98 |
99 | EnvEditor::deleteBackup($fileName)
100 |
101 |
102 | EnvEditor::restoreBackUp()
103 |
104 |
105 |
106 | ```
107 |
108 |
109 |
110 |
111 | ## User Interface
112 |
113 | **Note:** user interface is disabled be default. You can enable it by changing the configuration option `env-editor.route.enable`
114 |
115 | User Interface Contains three Tabs
116 |
117 | - [Current .env](#current_env)
118 | * [Add new Key](#add_key)
119 | * [Edit Key](#edit_key)
120 | * [Delete new Key](#delete_key)
121 | - [Backups](#backups)
122 | * [Backups Index](#backups_index)
123 | * [Backup file details](#backup_file_details)
124 | - [Upload](#upload)
125 |
126 |
127 |
128 | ### Current .env
129 | 
130 |
131 |
132 | #### Add new key
133 | 
134 |
135 |
136 | #### Edit key
137 | 
138 |
139 |
140 | #### Delete key
141 | 
142 |
143 |
144 | ### Backups
145 | #### Backups Index
146 | 
147 |
148 |
149 | #### Backup file details
150 | 
151 |
152 |
153 | ### Upload
154 | 
155 |
156 |
157 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "geo-sot/laravel-env-editor",
3 | "description": "A laravel Package that supports .Env File, editing and backup ",
4 | "keywords": [
5 | "geo-sot",
6 | "laravel",
7 | "laravel-env-editor",
8 | "EnvEditor"
9 | ],
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Geo Sot",
14 | "email": "geo.sotis@gmail.com"
15 | }
16 | ],
17 | "require": {
18 | "php": ">=8.1",
19 | "laravel/framework": ">=11.0"
20 | },
21 | "require-dev": {
22 | "friendsofphp/php-cs-fixer": "^3",
23 | "larastan/larastan": "^3",
24 | "orchestra/testbench": ">=9",
25 | "rector/rector": "^2"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "GeoSot\\EnvEditor\\": "src/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "GeoSot\\EnvEditor\\Tests\\": "tests"
35 | }
36 | },
37 | "scripts": {
38 | "phpstan": "php --version && php vendor/bin/phpstan --version && php -d memory_limit=1G vendor/bin/phpstan analyse -c ruleset-phpstan.neon -vvv",
39 | "cs": "./vendor/bin/php-cs-fixer fix -vvv --show-progress=dots --config=ruleset-php_cs.php",
40 | "test": "./vendor/bin/phpunit",
41 | "rector": "./vendor/bin/rector process --config=rector.php",
42 | "test-all": [
43 | "@cs",
44 | "@phpstan",
45 | "@rector",
46 | "@test"
47 | ]
48 | },
49 | "extra": {
50 | "laravel": {
51 | "providers": [
52 | "GeoSot\\EnvEditor\\ServiceProvider"
53 | ],
54 | "aliases": {
55 | "EnvEditor": "GeoSot\\EnvEditor\\Facades\\EnvEditor"
56 | }
57 | }
58 | },
59 | "minimum-stability": "dev",
60 | "prefer-stable": true,
61 | "config": {
62 | "sort-packages": true
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/config/env-editor.php:
--------------------------------------------------------------------------------
1 | [
10 | 'backupDirectory' => storage_path('env-editor'),
11 | ],
12 |
13 | /*
14 | |--------------------------------------------------------------------------
15 | | Routes group config
16 | |--------------------------------------------------------------------------
17 | |
18 | */
19 | 'route' => [
20 | 'enable' => false,
21 | // Prefix url for route Group
22 | 'prefix' => 'env-editor',
23 | // Routes base name
24 | 'name' => 'env-editor',
25 | // Middleware(s) applied on route Group
26 | 'middleware' => ['web'],
27 | ],
28 |
29 | /* ------------------------------------------------------------------------------------------------
30 | | Time Format for Views and parsed backups
31 | | ------------------------------------------------------------------------------------------------
32 | */
33 | 'timeFormat' => 'd/m/Y H:i:s',
34 |
35 | /* ------------------------------------------------------------------------------------------------
36 | | Set Views options
37 | | ------------------------------------------------------------------------------------------------
38 | | Here you can set The "extends" blade of index.blade.php
39 | */
40 | 'layout' => 'env-editor::layout',
41 | ];
42 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/Unit
14 |
15 |
16 | ./tests/Feature
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ./src
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/Unit
14 |
15 |
16 | ./tests/Feature
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ./src
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withParallel()
11 | ->withPaths([
12 | __DIR__.'/config',
13 | __DIR__.'/resources',
14 | __DIR__.'/routes',
15 | __DIR__.'/src',
16 | // __DIR__.'/tests',
17 | ])
18 | ->withPhpSets()
19 | ->withPreparedSets(
20 | deadCode: true,
21 | codeQuality: true,
22 | // codingStyle: true,
23 | typeDeclarations: true,
24 | privatization: true,
25 | naming: true,
26 | // instanceof: true,
27 | earlyReturn: true,
28 | // strictBooleans: true,
29 | )
30 | ->withCache(
31 | // ensure file system caching is used instead of in-memory
32 | cacheClass: FileCacheStorage::class,
33 | // specify a path that works locally as well as on CI job runners
34 | cacheDirectory: '/tmp/rector'
35 | );
36 |
--------------------------------------------------------------------------------
/resources/lang/en/env-editor.php:
--------------------------------------------------------------------------------
1 | '.env Editor',
5 | 'controllerMessages' => [
6 | 'backupWasCreated' => 'A new backup was created',
7 | 'fileWasRestored' => 'The backup file ":name", was restored as default .env',
8 | 'fileWasDeleted' => 'The backup file ":name", was deleted',
9 | 'currentEnvWasReplacedByTheUploadedFile' => 'File was uploaded and become the new .env file',
10 | 'uploadedFileSavedAsBackup' => 'File was uploaded as backup with the name ":name"',
11 | 'keyWasAdded' => 'Key ":name" was added',
12 | 'keyWasEdited' => 'Key ":name" has ben updated',
13 | 'keyWasDeleted' => 'Key ":name" was Deleted',
14 | ],
15 | 'views' => [
16 | 'tabTitles' => [
17 | 'upload' => 'Upload',
18 | 'backup' => 'Backups',
19 | 'currentEnv' => 'Current .env',
20 | ],
21 | 'currentEnv' => [
22 | 'title' => 'Current .env file Content',
23 | 'tableTitles' => [
24 | 'key' => 'Key',
25 | 'value' => 'Value',
26 | 'actions' => 'Actions',
27 | ],
28 | 'btn' => [
29 | 'edit' => 'Edit File',
30 | 'delete' => 'Delete Key',
31 | 'addAfterKey' => 'Add new key after this key',
32 | 'addNewKey' => 'Add New key',
33 | 'deleteConfigCache' => 'Clear config cache',
34 | 'deleteConfigCacheDesc' => 'On production environments changed values may not applied immediately cause of cached config. So you may try to un-cache it',
35 | ],
36 | 'modal' => [
37 | 'title' => [
38 | 'new' => 'New Key',
39 | 'edit' => 'Edit Key',
40 | 'delete' => 'Delete Key',
41 | ],
42 | 'input' => [
43 | 'key' => 'Key',
44 | 'value' => 'Value',
45 | ],
46 | 'btn' => [
47 | 'close' => 'Close',
48 | 'new' => 'Add Key',
49 | 'edit' => 'Update Key',
50 | 'delete' => 'Delete Key',
51 | ],
52 | ],
53 | ],
54 | 'upload' => [
55 | 'title' => 'Here You can upload a new ".env" file as a backup or to replace the current ".env"',
56 | 'selectFilePrompt' => 'Select File',
57 | 'btn' => [
58 | 'clearFile' => 'Cancel',
59 | 'uploadAsBackup' => 'Upload as backup',
60 | 'uploadAndReplace' => 'Upload and replace current .env',
61 | ],
62 | ],
63 | 'backup' => [
64 | 'title' => 'Here you can see a list of saved backup files (if you have), you can create a new one, or download the .env file',
65 | 'tableTitles' => [
66 | 'filename' => 'File Name',
67 | 'created_at' => 'Creation Date',
68 | 'actions' => 'Actions',
69 | ],
70 | 'noBackUpItems' => 'There are no backups on your chosen directory.
You can make your first backup by pressing the "Get a new BackUp" button',
71 | 'btn' => [
72 | 'backUpCurrentEnv' => 'Get a new BackUp',
73 | 'downloadCurrentEnv' => 'Download current .env',
74 | 'download' => 'Download File',
75 | 'delete' => 'Delete File',
76 | 'restore' => 'Restore File',
77 | 'viewContent' => 'View file Contents',
78 | ],
79 | ],
80 | ],
81 | 'exceptions' => [
82 | 'fileNotExists' => 'File ":name" does not Exists !!!',
83 | 'keyAlreadyExists' => 'Key ":name" already Exists !!!',
84 | 'keyNotExists' => 'Key ":name" does not Exists !!!',
85 | 'provideFileName' => 'You have to provide a FileName !!!',
86 | ],
87 | ];
88 |
--------------------------------------------------------------------------------
/resources/views/components/_backup.blade.php:
--------------------------------------------------------------------------------
1 | @php($translatePrefix='env-editor::env-editor.views.backup.')
2 |
3 |
4 |
{{__($translatePrefix.'title')}}
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{__($translatePrefix.'tableTitles.filename')}} |
16 | {{__($translatePrefix.'tableTitles.created_at')}} |
17 | {{__($translatePrefix.'tableTitles.actions')}} |
18 |
19 |
20 |
21 |
22 |
23 | @{{ item.name }} |
24 | @{{ item.created_at }} |
25 |
26 |
27 |
30 |
32 |
34 |
35 |
36 | |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | @{{ dt.key||' ' }} |
45 | @{{ dt.value }} |
46 |
47 |
48 |
49 |
50 |
51 | |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
{!! __($translatePrefix.'noBackUpItems') !!}
59 |
60 |
61 |
62 |
63 |
64 | @push('scripts')
65 |
66 |
118 | @endpush
119 |
--------------------------------------------------------------------------------
/resources/views/components/_configActions.blade.php:
--------------------------------------------------------------------------------
1 | @php($translatePrefix='env-editor::env-editor.views.currentEnv.')
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 | @push('scripts')
11 |
12 |
33 | @endpush
34 |
--------------------------------------------------------------------------------
/resources/views/components/_currentEnv.blade.php:
--------------------------------------------------------------------------------
1 | @php($translatePrefix='env-editor::env-editor.views.currentEnv.')
2 |
3 |
4 |
{{__($translatePrefix.'title')}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{__($translatePrefix.'tableTitles.key')}} |
13 | {{__($translatePrefix.'tableTitles.value')}} |
14 | {{__($translatePrefix.'tableTitles.actions')}} |
15 |
16 |
17 |
18 |
19 | @{{ item.key }} |
20 | @{{ item.value }} |
21 |
22 |
23 |
24 |
25 |
26 |
27 | |
28 |
29 |
30 | |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | @push('scripts')
41 |
42 |
43 |
87 | @endpush
88 |
--------------------------------------------------------------------------------
/resources/views/components/_itemModal.blade.php:
--------------------------------------------------------------------------------
1 | @php($translatePrefix='env-editor::env-editor.views.currentEnv.')
2 |
3 |
32 |
33 |
34 | @push('scripts')
35 |
36 |
152 | @endpush
153 |
--------------------------------------------------------------------------------
/resources/views/components/_upload.blade.php:
--------------------------------------------------------------------------------
1 | @php($translatePrefix='env-editor::env-editor.views.upload.')
2 |
3 |
4 |
{{__($translatePrefix.'title')}}
5 |
23 |
24 |
25 |
26 | @push('scripts')
27 |
28 |
92 | @endpush
93 |
--------------------------------------------------------------------------------
/resources/views/index.blade.php:
--------------------------------------------------------------------------------
1 | @php($package='env-editor')
2 | @php($translatePrefix='env-editor::env-editor.')
3 |
4 | @extends(config("$package.layout"))
5 | @push('documentTitle')
6 |
7 | {{trans('env-editor::env-editor.menuTitle')}}
8 | @endpush
9 |
10 | @section('content')
11 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | @stop
49 | @include('env-editor::components._itemModal')
50 | @include('env-editor::components._currentEnv')
51 | @include('env-editor::components._upload')
52 | @include('env-editor::components._backup')
53 | @include('env-editor::components._configActions')
54 | @push('scripts')
55 |
112 | @endpush
113 |
--------------------------------------------------------------------------------
/resources/views/layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | @lang('env-editor::env-editor.menuTitle')
10 |
11 |
12 |
13 |
14 |
18 |
19 |
23 |
24 | @stack('styles')
25 |
26 |
27 |
28 |
29 |
30 |
@stack('documentTitle')
31 |
32 | @yield('content')
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
47 |
48 |
49 | @stack('scripts')
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/routes/routes.php:
--------------------------------------------------------------------------------
1 | middleware(config('env-editor.route.middleware'))
11 | ->group(function () use ($routeMainName): void {
12 | Route::get('/', [EnvController::class, 'index'])->name($routeMainName.'.index');
13 |
14 | Route::post('key', [EnvController::class, 'addKey'])->name($routeMainName.'.key');
15 | Route::patch('key', [EnvController::class, 'editKey']);
16 | Route::delete('key', [EnvController::class, 'deleteKey']);
17 |
18 | Route::delete('clear-cache', [EnvController::class, 'clearConfigCache'])->name($routeMainName.'.clearConfigCache');
19 |
20 | Route::prefix('files')->group(function () use ($routeMainName): void {
21 | Route::get('/', [EnvController::class, 'getBackupFiles'])
22 | ->name($routeMainName.'.getBackups');
23 | Route::post('create-backup', [EnvController::class, 'createBackup'])
24 | ->name($routeMainName.'.createBackup');
25 | Route::post('restore-backup/{filename?}', [EnvController::class, 'restoreBackup'])
26 | ->name($routeMainName.'.restoreBackup');
27 | Route::delete('destroy-backup/{filename?}', [EnvController::class, 'destroyBackup'])
28 | ->name($routeMainName.'.destroyBackup');
29 |
30 | Route::get('download/{filename?}', [EnvController::class, 'download'])
31 | ->name($routeMainName.'.download');
32 | Route::post('upload', [EnvController::class, 'upload'])
33 | ->name($routeMainName.'.upload');
34 | });
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/ruleset-php_cs.php:
--------------------------------------------------------------------------------
1 | true,
8 | 'php_unit_method_casing' => ['case' => 'snake_case'],
9 | 'elseif' => true,
10 | 'phpdoc_align' => ['align' => 'left']
11 | ];
12 |
13 | $dirsToCheck = [
14 | __DIR__.'/src',
15 | __DIR__.'/config',
16 | __DIR__.'/database',
17 | __DIR__.'/resources',
18 | __DIR__.'/routes',
19 | __DIR__.'/tests'
20 | ];
21 |
22 | $finder = Finder::create()
23 | ->in(array_filter($dirsToCheck, 'file_exists'))
24 | ->exclude(['vendor'])
25 | ->name('*.php')
26 | ->notName('*.blade.php')
27 | ->ignoreDotFiles(true)
28 | ->ignoreVCS(true);
29 |
30 | return (new Config())
31 | ->setFinder($finder)
32 | ->setRules($rules)
33 | ->setRiskyAllowed(true)
34 | ->setUsingCache(true);
35 |
--------------------------------------------------------------------------------
/ruleset-phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/larastan/larastan/extension.neon
3 | parameters:
4 | paths:
5 | - src
6 | - tests
7 |
8 | level: 6
9 | reportStaticMethodSignatures: true
10 | noEnvCallsOutsideOfConfig: true
11 | viewDirectories:
12 | - resources/views
13 |
14 | parallel:
15 | jobSize: 20
16 | maximumNumberOfProcesses: 32
17 | minimumNumberOfJobsPerProcess: 2
18 |
--------------------------------------------------------------------------------
/src/Controllers/EnvController.php:
--------------------------------------------------------------------------------
1 | envEditor->getEnvFileContent();
35 | if ($request->wantsJson()) {
36 | return $this->returnGenericResponse(true, ['items' => $envFileContent]);
37 | }
38 |
39 | return view(static::INDEX_VIEW, ['envValues' => $envFileContent]);
40 | }
41 |
42 | /**
43 | * Add a new key on current .env file.
44 | */
45 | public function addKey(Request $request): JsonResponse
46 | {
47 | $result = $this->envEditor->addKey(
48 | $request->input('key'),
49 | $request->input('value'),
50 | $request->except(['key', 'value'])
51 | );
52 |
53 | return $this->returnGenericResponse($result, [], 'keyWasAdded', $request->input('key'));
54 | }
55 |
56 | /**
57 | * Edit a key of current .env file.
58 | */
59 | public function editKey(Request $request): JsonResponse
60 | {
61 | $result = $this->envEditor->editKey($request->input('key'), $request->input('value'));
62 |
63 | return $this->returnGenericResponse($result, [], 'keyWasEdited', $request->input('key'));
64 | }
65 |
66 | /**
67 | * Delete a key from current .env file.
68 | */
69 | public function deleteKey(Request $request): JsonResponse
70 | {
71 | $result = $this->envEditor->deleteKey($request->input('key'));
72 |
73 | return $this->returnGenericResponse($result, [], 'keyWasDeleted', $request->input('key'));
74 | }
75 |
76 | /**
77 | * Display a listing of the resource.
78 | */
79 | public function getBackupFiles(Request $request): View|JsonResponse
80 | {
81 | $allBackUps = $this->envEditor->getAllBackUps()->toArray();
82 | if ($request->wantsJson()) {
83 | return $this->returnGenericResponse(true, ['items' => $allBackUps]);
84 | }
85 |
86 | return view(static::INDEX_VIEW, ['backUpFiles' => $allBackUps]);
87 | }
88 |
89 | /**
90 | * Create BackUp of .env File.
91 | */
92 | public function createBackup(): JsonResponse
93 | {
94 | $result = $this->envEditor->backUpCurrent();
95 |
96 | return $this->returnGenericResponse($result, [], 'backupWasCreated');
97 | }
98 |
99 | /**
100 | * Restore Backup file.
101 | */
102 | public function restoreBackup(string $filename): JsonResponse
103 | {
104 | $result = $this->envEditor->restoreBackUp($filename);
105 |
106 | return $this->returnGenericResponse($result, [], 'fileWasRestored', $filename);
107 | }
108 |
109 | /**
110 | * Delete Backup file.
111 | */
112 | public function destroyBackup(string $filename): JsonResponse
113 | {
114 | $result = $this->envEditor->deleteBackup($filename);
115 |
116 | return $this->returnGenericResponse($result, [], 'fileWasDeleted', $filename);
117 | }
118 |
119 | /**
120 | * Get Files As Download.
121 | */
122 | public function download(string $filename = ''): BinaryFileResponse
123 | {
124 | $path = $this->envEditor->getFilePath($filename);
125 |
126 | return response()->download($path);
127 | }
128 |
129 | /**
130 | * Upload File As BackUp or replace Current .Env.
131 | */
132 | public function upload(Request $request): JsonResponse
133 | {
134 | $request->validate([
135 | 'file' => 'required|file|mimetypes:application/octet-stream,text/plain|mimes:txt,text,',
136 | ]);
137 | $replaceCurrentEnv = filter_var($request->input('replace_current'), FILTER_VALIDATE_BOOLEAN);
138 |
139 | /** @var UploadedFile $uploadedFile */
140 | $uploadedFile = $request->file('file');
141 | $file = $this->envEditor->upload($uploadedFile, $replaceCurrentEnv);
142 | $successMsg = ($replaceCurrentEnv) ? 'currentEnvWasReplacedByTheUploadedFile' : 'uploadedFileSavedAsBackup';
143 |
144 | return $this->returnGenericResponse(true, [], $successMsg, $file->getFilename());
145 | }
146 |
147 | /**
148 | * Clears Config cache to get new values.
149 | */
150 | public function clearConfigCache(): JsonResponse
151 | {
152 | Artisan::call('config:clear');
153 |
154 | return $this->returnGenericResponse(true, ['message' => Artisan::output()]);
155 | }
156 |
157 | /**
158 | * Generic ajax response.
159 | *
160 | * @param array $data
161 | */
162 | protected function returnGenericResponse(
163 | bool $success,
164 | array $data = [],
165 | ?string $translationWord = null,
166 | string $keyName = '',
167 | ): JsonResponse {
168 | if ($translationWord && $success) {
169 | $data = array_merge($data, [
170 | 'message' => __(
171 | ServiceProvider::TRANSLATE_PREFIX."controllerMessages.$translationWord",
172 | ['name' => $keyName]
173 | ),
174 | ]);
175 | }
176 |
177 | return response()->json(array_merge($data, [
178 | 'success' => $success,
179 | ]), $success ? 200 : 400);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/Dto/BackupObj.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class BackupObj implements \JsonSerializable, Arrayable
13 | {
14 | /**
15 | * @param Collection $entries
16 | */
17 | public function __construct(
18 | public readonly string $name,
19 | public readonly Carbon $createdAt,
20 | public readonly Carbon $modifiedAt,
21 | public readonly string $path,
22 | public readonly string $rawContent,
23 | public readonly Collection $entries,
24 | ) {
25 | }
26 |
27 | /**
28 | * @return array{real_name:string, name:string, created_at:string, modified_at:string, raw_content:string, path:string, entries:array}
29 | */
30 | public function toArray(): array
31 | {
32 | return [
33 | 'real_name' => $this->name,
34 | 'name' => $this->name,
35 | 'created_at' => $this->createdAt->format(config('env-editor.timeFormat')),
36 | 'modified_at' => $this->modifiedAt->format(config('env-editor.timeFormat')),
37 | 'raw_content' => $this->rawContent,
38 | 'path' => $this->path,
39 | 'entries' => $this->entries->toArray(),
40 | ];
41 | }
42 |
43 | /**
44 | * @return array{real_name:string, name:string, created_at:string, modified_at:string, raw_content:string, path:string, entries:array}
45 | */
46 | public function jsonSerialize(): array
47 | {
48 | return $this->toArray();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Dto/EntryObj.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class EntryObj implements \JsonSerializable, Arrayable
12 | {
13 | /**
14 | * @param int|string|null $value
15 | */
16 | public function __construct(
17 | public readonly string $key,
18 | protected mixed $value,
19 | public readonly int $group,
20 | public readonly int $index,
21 | protected bool $isSeparator = false,
22 | ) {
23 | }
24 |
25 | public static function parseEnvLine(string $line, int $group, int $index): self
26 | {
27 | $entry = explode('=', $line, 2);
28 | $isSeparator = 1 === count($entry);
29 |
30 | return new self(Arr::get($entry, 0), Arr::get($entry, 1), $group, $index, $isSeparator);
31 | }
32 |
33 | public static function makeKeysSeparator(int $groupIndex, int $index): self
34 | {
35 | return new self('', '', $groupIndex, $index, true);
36 | }
37 |
38 | public function getAsEnvLine(): string
39 | {
40 | return $this->isSeparator() ? '' : "$this->key=$this->value";
41 | }
42 |
43 | public function isSeparator(): bool
44 | {
45 | return $this->isSeparator;
46 | }
47 |
48 | /**
49 | * @return int|string|mixed|null
50 | */
51 | public function getValue(mixed $default = null): mixed
52 | {
53 | return $this->value ?: $default;
54 | }
55 |
56 | /**
57 | * @param int|string|mixed|null $value
58 | */
59 | public function setValue(mixed $value): void
60 | {
61 | $this->value = $value;
62 | }
63 |
64 | /**
65 | * @return array{key:string, value: int|string|null, group:int, index:int , isSeparator:bool}
66 | */
67 | public function toArray(): array
68 | {
69 | /** @var array{key:string, value: int|string|null, group:int, index:int , isSeparator:bool} $result */
70 | $result = get_object_vars($this);
71 |
72 | return $result;
73 | }
74 |
75 | /**
76 | * @return array{key:string, value: int|string|null, group:int, index:int , isSeparator:bool}
77 | */
78 | public function jsonSerialize(): array
79 | {
80 | return $this->toArray();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/EnvEditor.php:
--------------------------------------------------------------------------------
1 | keysManager = new EnvKeysManager($this);
31 | $this->filesManager = new EnvFilesManager($this, $filesystem);
32 | $this->fileContentManager = new EnvFileContentManager($this, $filesystem);
33 | }
34 |
35 | /**
36 | * Parse the .env Contents.
37 | *
38 | * @return Collection
39 | *
40 | * @throws EnvException
41 | */
42 | public function getEnvFileContent(string $fileName = ''): Collection
43 | {
44 | return $this->getFileContentManager()->getParsedFileContent($fileName);
45 | }
46 |
47 | /**
48 | * Check if key Exist in Current env.
49 | */
50 | public function keyExists(string $key): bool
51 | {
52 | return $this->getKeysManager()->has($key);
53 | }
54 |
55 | /**
56 | * Add the Key on the Current Env.
57 | */
58 | public function getKey(string $key, mixed $default = null): float|bool|int|string|null
59 | {
60 | return $this->getKeysManager()->get($key, $default);
61 | }
62 |
63 | /**
64 | * Add the Key on the Current Env.
65 | *
66 | * @param array $options
67 | *
68 | * @throws EnvException
69 | */
70 | public function addKey(string $key, mixed $value, array $options = []): bool
71 | {
72 | return $this->getKeysManager()->add($key, $value, $options);
73 | }
74 |
75 | /**
76 | * Edits the Given Key env.
77 | *
78 | * @throws EnvException
79 | */
80 | public function editKey(string $keyToChange, mixed $newValue): bool
81 | {
82 | return $this->getKeysManager()->edit($keyToChange, $newValue);
83 | }
84 |
85 | /**
86 | * Deletes the Given Key form env.
87 | *
88 | * @throws EnvException
89 | */
90 | public function deleteKey(string $key): bool
91 | {
92 | return $this->getKeysManager()->delete($key);
93 | }
94 |
95 | /**
96 | * Get all Backup Files.
97 | *
98 | * @return Collection
99 | */
100 | public function getAllBackUps(): Collection
101 | {
102 | return $this->getFilesManager()->getAllBackUps();
103 | }
104 |
105 | /**
106 | * upload Backup.
107 | */
108 | public function upload(UploadedFile $uploadedFile, bool $replaceCurrentEnv): File
109 | {
110 | return $this->getFilesManager()->upload($uploadedFile, $replaceCurrentEnv);
111 | }
112 |
113 | /**
114 | * Used to create a backup of the current .env.
115 | * Will be assigned with the current timestamp.
116 | *
117 | * @throws EnvException
118 | */
119 | public function backUpCurrent(): bool
120 | {
121 | return $this->getFilesManager()->backUpCurrentEnv();
122 | }
123 |
124 | /**
125 | * Returns the full path of a backup file. If $fileName is empty return the path of the .env file.
126 | *
127 | * @throws EnvException
128 | */
129 | public function getFilePath(string $fileName = ''): string
130 | {
131 | return $this->getFilesManager()->getFilePath($fileName);
132 | }
133 |
134 | /**
135 | * Delete the given backup-file.
136 | *
137 | * @throws EnvException
138 | */
139 | public function deleteBackup(string $fileName): bool
140 | {
141 | return $this->getFilesManager()->deleteBackup($fileName);
142 | }
143 |
144 | /**
145 | * Restore the given backup-file.
146 | *
147 | * @throws EnvException
148 | */
149 | public function restoreBackUp(string $fileName): bool
150 | {
151 | return $this->getFilesManager()->restoreBackup($fileName);
152 | }
153 |
154 | public function config(string $key, mixed $default = null): mixed
155 | {
156 | return $this->configRepository->get($key, $default);
157 | }
158 |
159 | public function getKeysManager(): EnvKeysManager
160 | {
161 | return $this->keysManager;
162 | }
163 |
164 | public function getFilesManager(): EnvFilesManager
165 | {
166 | return $this->filesManager;
167 | }
168 |
169 | public function getFileContentManager(): EnvFileContentManager
170 | {
171 | return $this->fileContentManager;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/Exceptions/EnvException.php:
--------------------------------------------------------------------------------
1 | code}]: {$this->message}\n";
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Facades/EnvEditor.php:
--------------------------------------------------------------------------------
1 | getEnvFileContent(string $fileName = '')
14 | * @method static bool keyExists(string $key)
15 | * @method static mixed getKey(string $key, $default = null)
16 | * @method static bool addKey(string $key, $value, array $options = [])
17 | * @method static bool editKey(string $keyToChange, $newValue)
18 | * @method static bool deleteKey(string $key)
19 | * @method static Collection getAllBackUps()
20 | * @method static File upload(UploadedFile $uploadedFile, bool $replaceCurrentEnv)
21 | * @method static bool backUpCurrent()
22 | * @method static string getFilePath(string $fileName = '')
23 | * @method static bool deleteBackup(string $fileName)
24 | * @method static bool restoreBackUp(string $fileName)
25 | * @method static mixed config(string $key, $default = null)
26 | *
27 | * @see \GeoSot\EnvEditor\EnvEditor
28 | */
29 | class EnvEditor extends Facade
30 | {
31 | /**
32 | * Get the registered name of the component.
33 | */
34 | protected static function getFacadeAccessor(): string
35 | {
36 | return \GeoSot\EnvEditor\EnvEditor::class;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Helpers/EnvFileContentManager.php:
--------------------------------------------------------------------------------
1 |
21 | *
22 | * @throws EnvException
23 | */
24 | public function getParsedFileContent(string $fileName = ''): Collection
25 | {
26 | /** @var list $content */
27 | $content = preg_split('/(\r\n|\r|\n)/', $this->getFileContents($fileName));
28 |
29 | $groupIndex = 1;
30 | /** @var Collection $collection */
31 | $collection = new Collection();
32 | foreach ($content as $index => $line) {
33 | $entryObj = EntryObj::parseEnvLine($line, $groupIndex, $index);
34 | $collection->push($entryObj);
35 |
36 | if ($entryObj->isSeparator()) {
37 | ++$groupIndex;
38 | }
39 | }
40 |
41 | return $collection->sortBy('index');
42 | }
43 |
44 | /**
45 | * Get The File Contents.
46 | *
47 | * @throws EnvException
48 | */
49 | protected function getFileContents(string $file = ''): string
50 | {
51 | $envFile = $this->envEditor->getFilesManager()->getFilePath($file);
52 |
53 | return $this->filesystem->get($envFile);
54 | }
55 |
56 | /**
57 | * Save the new collection on .env file.
58 | *
59 | * @param Collection $envValues
60 | *
61 | * @throws EnvException
62 | */
63 | public function save(Collection $envValues, string $fileName = ''): bool
64 | {
65 | $env = $envValues
66 | ->sortBy(fn (EntryObj $entryObj): int => $entryObj->index)
67 | ->map(fn (EntryObj $entryObj): string => $entryObj->getAsEnvLine());
68 |
69 | $content = implode(PHP_EOL, $env->toArray());
70 |
71 | $result = $this->filesystem->put(
72 | $this->envEditor->getFilesManager()->getFilePath($fileName),
73 | $content
74 | );
75 |
76 | return false !== $result;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Helpers/EnvFilesManager.php:
--------------------------------------------------------------------------------
1 | makeBackupsDirectory();
21 | }
22 |
23 | /**
24 | * Get all Backup Files.
25 | *
26 | * @return Collection
27 | */
28 | public function getAllBackUps(): Collection
29 | {
30 | $files = $this->filesystem->files($this->getBackupsDir());
31 |
32 | return (new Collection($files))
33 | ->map(fn (SplFileInfo $file): BackupObj => new BackupObj(
34 | $file->getFilename(),
35 | Carbon::parse($file->getCTime()),
36 | Carbon::parse($file->getMTime()),
37 | $file->getPath(),
38 | $file->getContents(),
39 | $this->envEditor->getFileContentManager()->getParsedFileContent($file->getFilename()),
40 | ))
41 | ->sortByDesc('createdAt');
42 | }
43 |
44 | /**
45 | * Used to create a backup of the current .env.
46 | * Will be assigned with the current timestamp.
47 | *
48 | * @throws EnvException
49 | */
50 | public function backUpCurrentEnv(): bool
51 | {
52 | return $this->filesystem->copy(
53 | $this->getFilePath(),
54 | $this->getBackupsDir($this->makeBackUpFileName())
55 | );
56 | }
57 |
58 | /**
59 | * Restore the given backup-file.
60 | *
61 | * @throws EnvException
62 | */
63 | public function restoreBackup(string $fileName): bool
64 | {
65 | if ('' === $fileName) {
66 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.provideFileName'), 1);
67 | }
68 | $file = $this->getBackupsDir($fileName);
69 |
70 | return $this->filesystem->copy($file, $this->getFilePath());
71 | }
72 |
73 | /**
74 | * uploadBackup.
75 | */
76 | public function upload(UploadedFile $uploadedFile, bool $replaceCurrentEnv): File
77 | {
78 | return $replaceCurrentEnv ?
79 | $uploadedFile->move($this->getEnvDir(), $this->getEnvFileName()) :
80 | $uploadedFile->move($this->getBackupsDir(), $this->makeBackUpFileName());
81 | }
82 |
83 | /**
84 | * Delete the given backup-file.
85 | *
86 | * @throws EnvException
87 | */
88 | public function deleteBackup(string $fileName): bool
89 | {
90 | if ('' === $fileName) {
91 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.provideFileName'), 1);
92 | }
93 | $file = $this->getFilePath($fileName);
94 |
95 | return $this->filesystem->delete($file);
96 | }
97 |
98 | /**
99 | * Returns the full path of a backup file. If $fileName is empty return the path of the .env file.
100 | *
101 | * @throws EnvException
102 | */
103 | public function getFilePath(string $fileName = ''): string
104 | {
105 | $path = ('' === $fileName)
106 | ? $this->getEnvFileName()
107 | : $this->getBackupsDir($fileName);
108 |
109 | if ($this->filesystem->exists($path)) {
110 | return $path;
111 | }
112 |
113 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.fileNotExists', ['name' => $path]), 0);
114 | }
115 |
116 | /**
117 | * Get the backup File Name.
118 | */
119 | protected function makeBackUpFileName(): string
120 | {
121 | return 'env_'.date('Y-m-d_His');
122 | }
123 |
124 | /**
125 | * Get the .env File Name.
126 | */
127 | protected function getEnvFileName(): string
128 | {
129 | return app()->environmentFilePath();
130 | }
131 |
132 | public function getBackupsDir(?string $path = null): string
133 | {
134 | return $this->envEditor->config('paths.backupDirectory').($path ? DIRECTORY_SEPARATOR.$path : $path);
135 | }
136 |
137 | public function getEnvDir(?string $path = null): string
138 | {
139 | return dirname($this->getEnvFileName()).($path ? DIRECTORY_SEPARATOR.$path : $path);
140 | }
141 |
142 | /**
143 | * Checks if Backups directory Exists and creates it.
144 | */
145 | public function makeBackupsDirectory(): void
146 | {
147 | $path = $this->getBackupsDir();
148 | if (!$this->filesystem->exists($path)) {
149 | $this->filesystem->makeDirectory($path, 0755, true, true);
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Helpers/EnvKeysManager.php:
--------------------------------------------------------------------------------
1 | getFirst($key) instanceof EntryObj;
24 | }
25 |
26 | /**
27 | * Add the Key on the Current Env.
28 | */
29 | public function get(string $key, mixed $default = null): float|bool|int|string|null
30 | {
31 | $result = $this->getFirst($key);
32 |
33 | return $result instanceof EntryObj ? $result->getValue($default) : $default;
34 | }
35 |
36 | /**
37 | * Add the Key on the Current Env.
38 | *
39 | * @param array{index?: int|string|null, group?: int|string|null} $options
40 | *
41 | * @throws EnvException
42 | */
43 | public function add(string $key, mixed $value, array $options = []): bool
44 | {
45 | if ($this->has($key)) {
46 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.keyAlreadyExists', ['name' => $key]), 0);
47 | }
48 | $envData = $this->getEnvData();
49 | $givenGroup = Arr::get($options, 'group', null);
50 |
51 | $groupIndex = $givenGroup ?? $envData->pluck('group')->unique()->sort()->last() + 1;
52 |
53 | if (!$givenGroup && !$envData->last()->isSeparator()) {
54 | $separator = EntryObj::makeKeysSeparator((int) $groupIndex, $envData->count() + 1);
55 | $envData->push($separator);
56 | }
57 |
58 | $lastSameGroupEntry = $envData->last(fn (EntryObj $entryObj): bool => explode('_', $entryObj->key, 2)[0] === strtoupper((string) $givenGroup) && $entryObj->isSeparator());
59 |
60 | $index = Arr::get(
61 | $options,
62 | 'index',
63 | $lastSameGroupEntry ? $lastSameGroupEntry->index + 0.1 : $envData->count() + 2
64 | );
65 |
66 | $entryObj = new EntryObj($key, $value, $groupIndex, $index);
67 |
68 | $envData->push($entryObj);
69 |
70 | return $this->envEditor->getFileContentManager()->save($envData);
71 | }
72 |
73 | /**
74 | * Deletes the Given Key form env.
75 | *
76 | * @throws EnvException
77 | */
78 | public function edit(string $keyToChange, mixed $newValue = null): bool
79 | {
80 | if (!$this->has($keyToChange)) {
81 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.keyNotExists', ['name' => $keyToChange]), 11);
82 | }
83 | $envData = $this->getEnvData();
84 | $newEnv = $envData->map(function (EntryObj $entryObj) use ($keyToChange, $newValue): EntryObj {
85 | if ($entryObj->key === $keyToChange) {
86 | $entryObj->setValue($newValue);
87 | }
88 |
89 | return $entryObj;
90 | });
91 |
92 | return $this->envEditor->getFileContentManager()->save($newEnv);
93 | }
94 |
95 | /**
96 | * Deletes the Given Key form env.
97 | *
98 | * @throws EnvException
99 | */
100 | public function delete(string $key): bool
101 | {
102 | if (!$this->has($key)) {
103 | throw new EnvException(__(ServiceProvider::TRANSLATE_PREFIX.'exceptions.keyNotExists', ['name' => $key]), 10);
104 | }
105 | $envData = $this->getEnvData();
106 | $newEnv = $envData->filter(fn (EntryObj $entryObj): bool => $entryObj->key !== $key);
107 |
108 | return $this->envEditor->getFileContentManager()->save($newEnv);
109 | }
110 |
111 | /**
112 | * @return Collection
113 | */
114 | protected function getEnvData(): Collection
115 | {
116 | return $this->envEditor->getFileContentManager()->getParsedFileContent();
117 | }
118 |
119 | protected function getFirst(string $key): ?EntryObj
120 | {
121 | return $this->getEnvData()
122 | ->reject(fn (EntryObj $entryObj): bool => $entryObj->isSeparator())
123 | ->firstWhere('key', '==', $key);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadResources();
20 | $this->publishResources();
21 | }
22 |
23 | /**
24 | * Register services.
25 | */
26 | public function register(): void
27 | {
28 | $this->mergeConfigFrom(
29 | __DIR__.'/../config/env-editor.php',
30 | static::PACKAGE
31 | );
32 |
33 | $this->app->singleton(EnvEditor::class, fn (): EnvEditor => new EnvEditor(
34 | new Repository(config(static::PACKAGE)),
35 | $this->app->make(Filesystem::class)
36 | ));
37 |
38 | $this->app->alias(EnvEditor::class, 'env-editor');
39 | }
40 |
41 | private function loadResources(): void
42 | {
43 | $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
44 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'env-editor');
45 | $this->loadTranslationsFrom(__DIR__.'/../resources/lang', static::PACKAGE);
46 | }
47 |
48 | private function publishResources(): void
49 | {
50 | $this->publishes([
51 | __DIR__.'/../config/env-editor.php' => config_path(static::PACKAGE.'.php'),
52 | ], 'config');
53 |
54 | $this->publishes([
55 | __DIR__.'/../resources/views' => resource_path('/views/vendor/'.static::PACKAGE),
56 | ], 'views');
57 |
58 | $this->publishes([
59 | __DIR__.'/../resources/lang/' => resource_path('lang/vendor/'.static::PACKAGE),
60 | ], 'translations');
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Feature/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | app->make(Repository::class)->set('env-editor.route.enable', $enableRoutes);
19 |
20 | $routeNames = [
21 | '.index',
22 | '.key',
23 | '.clearConfigCache',
24 | '.files.getBackups',
25 | '.files.createBackup',
26 | '.files.restoreBackup',
27 | '.files.destroyBackup',
28 | '.files.download',
29 | '.files.upload',
30 | ];
31 |
32 | foreach ($routeNames as $name) {
33 | $routeName = $this->app['config']['env-editor.route.name'].$name;
34 | $this->assertFalse(Route::has($routeName));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Feature/UiTest.php:
--------------------------------------------------------------------------------
1 | useEnvironmentPath(self::getTestPath());
17 | $app->loadEnvironmentFrom(self::getTestFile());
18 | $app['config']->set('env-editor.route.enable', true);
19 | }
20 |
21 | #[Test]
22 | public function can_see_dashboard(): void
23 | {
24 | $response = $this->get($this->makeRoute('index'));
25 | $response->assertStatus(200)
26 | ->assertSee(trans('env-editor::env-editor.menuTitle'));
27 | }
28 |
29 | #[Test]
30 | public function get_json_results(): void
31 | {
32 | $response = $this->getJson($this->makeRoute('index'));
33 | $response->assertStatus(200);
34 | /** @var array> $json */
35 | $json = $response->json('items');
36 | $jsonResponse = collect($json);
37 | $envData = EnvEditor::getEnvFileContent()->toJson();
38 |
39 | $this->assertEqualsCanonicalizing($envData, $jsonResponse);
40 | }
41 |
42 | #[Test]
43 | public function can_set_key_value(): void
44 | {
45 | $response = $this->postJson($this->makeRoute('key'), [
46 | 'key' => 'FOO',
47 | 'value' => 'bar',
48 | ]);
49 | $response->assertStatus(200);
50 | $this->assertSame('bar', EnvEditor::getKey('FOO'));
51 | }
52 |
53 | #[Test]
54 | public function can_edit_key_value(): void
55 | {
56 | $this->postJson($this->makeRoute('key'), [
57 | 'key' => 'FOO',
58 | 'value' => 'bar',
59 | ]);
60 |
61 | $response = $this->patchJson($this->makeRoute('key'), [
62 | 'key' => 'FOO',
63 | 'value' => 'foo-test',
64 | ]);
65 | $response->assertStatus(200);
66 | $this->assertSame('foo-test', EnvEditor::getKey('FOO'));
67 | }
68 |
69 | #[Test]
70 | public function can_delete_key_value(): void
71 | {
72 | $this->postJson($this->makeRoute('key'), [
73 | 'key' => 'FOO',
74 | 'value' => 'bar',
75 | ]);
76 |
77 | $response = $this->deleteJson($this->makeRoute('key'), [
78 | 'key' => 'FOO',
79 | ]);
80 | $response->assertStatus(200);
81 | $this->assertFalse(EnvEditor::keyExists('FOO'));
82 | }
83 |
84 | #[Test]
85 | public function can_see_backups(): void
86 | {
87 | $response = $this->get($this->makeRoute('getBackups'));
88 | $response->assertStatus(200)
89 | ->assertSee(trans('env-editor::env-editor.views.backup.title'));
90 | }
91 |
92 | #[Test]
93 | public function can_get_json_backups(): void
94 | {
95 | $response = $this->getJson($this->makeRoute('getBackups'));
96 | $response->assertStatus(200);
97 | /** @var array> $json */
98 | $json = $response->json('items');
99 | $jsonResponse = collect($json);
100 | $envData = EnvEditor::getAllBackUps()->toJson();
101 |
102 | $this->assertEqualsCanonicalizing($envData, $jsonResponse);
103 | }
104 |
105 | #[Test]
106 | public function can_create_backups(): void
107 | {
108 | $backupsDir = config('env-editor.paths.backupDirectory');
109 | File::deleteDirectory($backupsDir);
110 | $files = fn () => File::glob($backupsDir.'/env_*');
111 | $this->assertEmpty($files());
112 | $response = $this->postJson($this->makeRoute('createBackup'));
113 | $response->assertStatus(200);
114 | $this->assertCount(1, $files());
115 | }
116 |
117 | #[Test]
118 | public function can_restore_backups(): void
119 | {
120 | $backupsDir = config('env-editor.paths.backupDirectory');
121 | File::deleteDirectory($backupsDir);
122 |
123 | EnvEditor::addKey('FOO', 'bar');
124 | EnvEditor::backUpCurrent();
125 | EnvEditor::deleteKey('FOO');
126 | $this->assertNull(EnvEditor::getKey('FOO'));
127 | $file = EnvEditor::getAllBackUps()->first()->name;
128 | $this->postJson($this->makeRoute('restoreBackup').'/'.$file);
129 | $this->assertSame('bar', EnvEditor::getKey('FOO'));
130 | }
131 |
132 | #[Test]
133 | public function can_destroy_backups(): void
134 | {
135 | $backupsDir = config('env-editor.paths.backupDirectory');
136 | File::deleteDirectory($backupsDir);
137 | EnvEditor::backUpCurrent();
138 |
139 | $file = EnvEditor::getAllBackUps()->first()->name;
140 | $this->deleteJson($this->makeRoute('destroyBackup').'/'.$file);
141 | $this->assertCount(0, EnvEditor::getAllBackUps());
142 | }
143 |
144 | #[Test]
145 | public function can_download(): void
146 | {
147 | EnvEditor::shouldReceive('getFilePath')->once()->with('fooBar')->andReturns(self::getTestFile(true));
148 | $response = $this->get($this->makeRoute('download', ['filename' => 'fooBar']));
149 | $response->assertStatus(200);
150 | $response->assertDownload(self::getTestFile());
151 | }
152 |
153 | #[Test]
154 | public function can_upload_file(): void
155 | {
156 | $this->assertFalse(EnvEditor::keyExists('FOO'));
157 | $this->assertFalse(EnvEditor::keyExists('FOO2'));
158 | $fileContent = [
159 | 'FOO=bar',
160 | 'FOO2=bar2',
161 | ];
162 | $this->postJson($this->makeRoute('upload'), [
163 | 'replace_current' => true,
164 | 'file' => UploadedFile::fake()->createWithContent('test.txt', implode(PHP_EOL, $fileContent)),
165 | ]);
166 | $this->assertSame('bar', EnvEditor::getKey('FOO'));
167 | $this->assertSame('bar2', EnvEditor::getKey('FOO2'));
168 | }
169 |
170 | /**
171 | * @param array $parameters
172 | */
173 | protected function makeRoute(string $route, array $parameters = []): string
174 | {
175 | return route(config('env-editor.route.name').'.'.$route, $parameters);
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('app.key', $key);
32 | }
33 |
34 | protected function getPackageProviders($app): array
35 | {
36 | return [
37 | ServiceProvider::class,
38 | ];
39 | }
40 |
41 | protected function getPackageAliases($app): array
42 | {
43 | return [
44 | 'env-editor' => EnvEditor::class,
45 | ];
46 | }
47 |
48 | protected static function getTestPath(): string
49 | {
50 | return __DIR__.'/fixtures';
51 | }
52 |
53 | protected static function getTestFile(bool $fullPath = false): string
54 | {
55 | $file = '.env.example';
56 |
57 | return $fullPath
58 | ? static::getTestPath().DIRECTORY_SEPARATOR.$file
59 | : $file;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Unit/Dto/BackupObjTest.php:
--------------------------------------------------------------------------------
1 | app['config']->set('env-editor.timeFormat', 'd/m H:i');
17 | $this->freezeTime();
18 | $now = now();
19 |
20 | $data = [
21 | 'name' => 'foo-file',
22 | 'createdAt' => $now->clone()->subHour(),
23 | 'modifiedAt' => $now->clone(),
24 | 'rawContent' => 'dummy-content',
25 | 'path' => 'foo-path',
26 | 'entries' => new Collection([
27 | new EntryObj('key',
28 | 'value', 1, 1),
29 | ]),
30 | ];
31 |
32 | $dto = new BackupObj(...$data);
33 |
34 | $expected = [
35 | 'name' => 'foo-file',
36 | 'real_name' => 'foo-file',
37 | 'created_at' => $now->clone()->subHour()->format('d/m H:i'),
38 | 'modified_at' => $now->clone()->format('d/m H:i'),
39 | 'raw_content' => 'dummy-content',
40 | 'path' => 'foo-path',
41 | 'entries' => $data['entries']->toArray(),
42 | ];
43 | $this->assertEquals($expected, $dto->jsonSerialize());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Unit/Dto/EntryObjTest.php:
--------------------------------------------------------------------------------
1 | key);
19 | self::assertEquals($value, $entry->getValue());
20 | self::assertSame($isSeparator, $entry->isSeparator());
21 | }
22 |
23 | #[Test]
24 | public function creates_key_separator(): void
25 | {
26 | $entry = EntryObj::makeKeysSeparator(1, 2);
27 |
28 | self::assertSame(2, $entry->index);
29 | self::assertSame(1, $entry->group);
30 | self::assertSame('', $entry->key);
31 | self::assertEquals('', $entry->getValue());
32 | self::assertTrue($entry->isSeparator());
33 | }
34 |
35 | #[Test]
36 | #[DataProvider('getDummyData')]
37 | public function returns_env_lines(string $line, string $key, mixed $value, bool $isSeparator): void
38 | {
39 | $entry = EntryObj::parseEnvLine($line, 2, 8);
40 |
41 | self::assertSame($line, $entry->getAsEnvLine());
42 | }
43 |
44 | #[Test]
45 | #[DataProvider('getDummyData')]
46 | public function returns_value_or_default(string $line, string $key, mixed $value, bool $isSeparator): void
47 | {
48 | $entry = EntryObj::parseEnvLine($line, 2, 8);
49 | self::assertEquals($value ?: 'foobar', $entry->getValue('foobar'));
50 | }
51 |
52 | /**
53 | * @return array{array{string, string, mixed, bool}}
54 | */
55 | public static function getDummyData(): array
56 | {
57 | return [
58 | ['test=1', 'test', 1, false],
59 | ['test="foo"', 'test', '"foo"', false],
60 | ['test=null', 'test', 'null', false],
61 | ['test="null"', 'test', '"null"', false],
62 | ['test=""', 'test', '""', false],
63 | ['test=', 'test', null, false],
64 | ['', '', '', true],
65 | ];
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Unit/Helpers/EnvFileContentManagerTest.php:
--------------------------------------------------------------------------------
1 | app['config']->set('env-editor.paths.backupDirectory', self::getTestPath());
20 |
21 | $manager = $this->getEnvFileContentManager();
22 | $content = $manager->getParsedFileContent(self::getTestFile());
23 |
24 | $separators = $content->filter(fn (EntryObj $obj) => $obj->isSeparator());
25 | $groups = $content->groupBy(fn (EntryObj $obj) => $obj->group);
26 |
27 | self::assertCount(5, $separators);
28 | self::assertCount(5, $groups);
29 | self::assertCount(17, $content);
30 | }
31 |
32 | #[Test]
33 | public function wrong_file_throws_exception(): void
34 | {
35 | self::expectException(EnvException::class);
36 | $file = config('env-editor.paths.backupDirectory').DIRECTORY_SEPARATOR.'not-existed-file';
37 | self::expectExceptionMessage('File "'.$file.'" does not Exists !!!');
38 | \GeoSot\EnvEditor\Facades\EnvEditor::getFilePath('not-existed-file');
39 | }
40 |
41 | #[Test]
42 | public function fail_to_retrieve_file_contents(): void
43 | {
44 | $manager = $this->getEnvFileContentManager();
45 | $file = config('env-editor.paths.backupDirectory').DIRECTORY_SEPARATOR.'not-existed-file';
46 |
47 | self::expectException(EnvException::class);
48 | self::expectExceptionMessage('File "'.$file.'" does not Exists !!!');
49 | $manager->getParsedFileContent('not-existed-file');
50 | }
51 |
52 | #[Test]
53 | public function saves_file_contents(): void
54 | {
55 | $testPath = self::getTestPath();
56 | $this->app['config']->set('env-editor.paths.backupDirectory', $testPath);
57 | $baseFile = self::getTestFile();
58 | $manager = $this->getEnvFileContentManager();
59 | $content = $manager->getParsedFileContent($baseFile);
60 |
61 | $backUpFile = 'test.tmp';
62 | $backUpFileFullPath = $testPath.DIRECTORY_SEPARATOR.$backUpFile;
63 |
64 | file_put_contents($backUpFileFullPath, '');
65 | $manager->save($content, $backUpFile);
66 |
67 | self::assertFileEquals(self::getTestFile(true), $backUpFileFullPath);
68 | self::assertEqualsCanonicalizing($content->toArray(), $manager->getParsedFileContent($backUpFile)->toArray());
69 | unlink($backUpFileFullPath);
70 | }
71 |
72 | protected function getEnvFileContentManager(): EnvFileContentManager
73 | {
74 | $envEditor = new EnvEditor(
75 | new Repository($this->app['config']->get('env-editor')),
76 | new Filesystem()
77 | );
78 | $this->app->singleton(EnvEditor::class, fn () => $envEditor);
79 |
80 | return $envEditor->getFileContentManager();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/Unit/Helpers/EnvKeysManagerTest.php:
--------------------------------------------------------------------------------
1 | app->useEnvironmentPath(self::getTestPath());
20 | $this->app->loadEnvironmentFrom(self::getTestFile());
21 | }
22 |
23 | #[Test]
24 | public function check_key_existence(): void
25 | {
26 | self::assertTrue($this->getEnvKeysManager()->has('LOG_CHANNEL'));
27 | self::assertTrue($this->getEnvKeysManager()->has('DB_CONNECTION'));
28 | self::assertFalse($this->getEnvKeysManager()->has('FOO'));
29 | self::assertFalse($this->getEnvKeysManager()->has(''));
30 | self::assertFalse($this->getEnvKeysManager()->has('null'));
31 | }
32 |
33 | #[Test]
34 | public function returns_value_or_default(): void
35 | {
36 | self::assertEquals('stack', $this->getEnvKeysManager()->get('LOG_CHANNEL'));
37 | self::assertEquals('mysql', $this->getEnvKeysManager()->get('DB_CONNECTION'));
38 | self::assertEquals('3306', $this->getEnvKeysManager()->get('DB_PORT'));
39 | self::assertEquals('', $this->getEnvKeysManager()->get('BROADCAST_DRIVER'));
40 | self::assertEquals('foo', $this->getEnvKeysManager()->get('BROADCAST_DRIVER', 'foo'));
41 | self::assertEquals(null, $this->getEnvKeysManager()->get('FOO'));
42 | self::assertEquals('Bar', $this->getEnvKeysManager()->get('FOO', 'Bar'));
43 | }
44 |
45 | #[Test]
46 | public function deletes_keys(): void
47 | {
48 | $fileName = 'dummy.tmp';
49 | $fullPath = $this->createNewDummyFile($fileName);
50 | $this->app->loadEnvironmentFrom($fileName);
51 | $getContent = fn (): string => file_get_contents($fullPath) ?: throw new \RuntimeException("File {$fullPath}, not found");
52 |
53 | self::assertStringContainsString('LOG_CHANNEL', $getContent());
54 | self::assertTrue($this->getEnvKeysManager()->delete('LOG_CHANNEL'));
55 | self::assertStringNotContainsString('LOG_CHANNEL=stack', $getContent());
56 |
57 | self::assertStringContainsString('CACHE_DRIVER', $getContent());
58 | self::assertTrue($this->getEnvKeysManager()->delete('CACHE_DRIVER'));
59 | self::assertStringNotContainsString('CACHE_DRIVER="file"', $getContent());
60 |
61 | self::assertStringNotContainsString('CACHE_DRIVER', $getContent());
62 | try {
63 | $this->getEnvKeysManager()->delete('CACHE_DRIVER');
64 | } catch (\Exception $e) {
65 | self::assertInstanceOf(EnvException::class, $e);
66 | unlink($fullPath);
67 | }
68 | }
69 |
70 | #[Test]
71 | public function edits_keys(): void
72 | {
73 | $fileName = 'dummy.tmp';
74 | $fullPath = $this->createNewDummyFile($fileName);
75 | $this->app->loadEnvironmentFrom($fileName);
76 |
77 | $getContent = fn (): string => file_get_contents($fullPath) ?: throw new \RuntimeException("File {$fullPath}, not found");
78 |
79 | self::assertStringContainsString('LOG_CHANNEL=stack', $getContent());
80 | self::assertTrue($this->getEnvKeysManager()->edit('LOG_CHANNEL', 'foo'));
81 | self::assertStringContainsString('LOG_CHANNEL=foo', $getContent());
82 |
83 | self::assertStringContainsString('CACHE_DRIVER="file"', $getContent());
84 | self::assertTrue($this->getEnvKeysManager()->edit('CACHE_DRIVER', '"bar"'));
85 | self::assertStringContainsString('CACHE_DRIVER="bar"', $getContent());
86 |
87 | self::assertTrue($this->getEnvKeysManager()->edit('CACHE_DRIVER', ''));
88 | self::assertStringContainsString('CACHE_DRIVER=', $getContent());
89 |
90 | self::assertTrue($this->getEnvKeysManager()->edit('CACHE_DRIVER', null));
91 | self::assertStringContainsString('CACHE_DRIVER=', $getContent());
92 |
93 | self::assertStringNotContainsString('WRONG_KEY', $getContent());
94 | try {
95 | $this->getEnvKeysManager()->edit('WRONG_KEY', 'fail');
96 | } catch (\Exception $e) {
97 | self::assertInstanceOf(EnvException::class, $e);
98 | unlink($fullPath);
99 | }
100 | }
101 |
102 | #[Test]
103 | public function adds_keys(): void
104 | {
105 | $fileName = 'dummy.tmp';
106 | $fullPath = $this->createNewDummyFile($fileName);
107 | $this->app->loadEnvironmentFrom($fileName);
108 |
109 | EnvEditorFacade::addKey('FOO', 'bar');
110 | $this->assertSame('bar', EnvEditorFacade::getKey('FOO'));
111 | try {
112 | EnvEditorFacade::addKey('FOO', 'bar2');
113 | } catch (\Exception $e) {
114 | self::assertInstanceOf(EnvException::class, $e);
115 | $this->assertEquals('Key "FOO" already Exists !!!', $e->getMessage());
116 | unlink($fullPath);
117 | }
118 | }
119 |
120 | #[Test]
121 | public function adds_two_keys_in_different_group(): void
122 | {
123 | $fileName = 'dummy.tmp';
124 | $fullPath = $this->createNewDummyFile($fileName);
125 | $this->app->loadEnvironmentFrom($fileName);
126 |
127 | EnvEditorFacade::addKey('FOO1', 'bar');
128 | EnvEditorFacade::addKey('FOO2', 'bar');
129 |
130 | $envData = EnvEditorFacade::getEnvFileContent();
131 |
132 | $firstKey = $envData->firstWhere('key', 'FOO1');
133 | $secondKey = $envData->firstWhere('key', 'FOO2');
134 |
135 | $this->assertGreaterThan($firstKey->group, $secondKey->group);
136 | unlink($fullPath);
137 | }
138 |
139 | protected function getEnvKeysManager(): EnvKeysManager
140 | {
141 | $envEditor = new EnvEditor(
142 | new Repository($this->app['config']->get('env-editor')),
143 | new Filesystem()
144 | );
145 | $this->app->singleton(EnvEditor::class, fn () => $envEditor);
146 |
147 | return $envEditor->getKeysManager();
148 | }
149 |
150 | protected function createNewDummyFile(string $name = 'test.tmp'): string
151 | {
152 | $dummyFullPath = self::getTestPath().DIRECTORY_SEPARATOR.$name;
153 |
154 | copy(self::getTestFile(true), $dummyFullPath);
155 |
156 | return $dummyFullPath;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/Unit/Helpers/FilesManagerTest.php:
--------------------------------------------------------------------------------
1 | cleanBackUpDir();
22 | parent::tearDown();
23 | }
24 |
25 | /**
26 | * Test makeBackupsDirectory method.
27 | */
28 | #[Test]
29 | public function constructor_calls_make_backups_directory_method(): void
30 | {
31 | $classname = EnvFilesManager::class;
32 |
33 | // Get mock, without the constructor being called
34 | $mock = $this->getMockBuilder($classname)
35 | ->disableOriginalConstructor()
36 | ->getMock();
37 |
38 | // set expectations for constructor calls
39 | $mock->expects($this->once())
40 | ->method('makeBackupsDirectory');
41 |
42 | // now call the constructor
43 | $reflectedClass = new \ReflectionClass($classname);
44 | $constructor = $reflectedClass->getConstructor();
45 |
46 | $envEditorMock = \Mockery::mock(EnvEditor::class);
47 | $constructor->invoke($mock, $envEditorMock, $this->app->make(Filesystem::class));
48 | }
49 |
50 | /**
51 | * Test makeBackupsDirectory method.
52 | */
53 | #[Test]
54 | public function backup_dir_is_created(): void
55 | {
56 | $path = $this->getEnvFilesManager()->getBackupsDir();
57 | $this->createAndTestPath($path);
58 | }
59 |
60 | /**
61 | * Test makeBackupsDirectory method.
62 | */
63 | #[Test]
64 | public function get_env_dir_exists(): void
65 | {
66 | $path = $this->getEnvFilesManager()->getEnvDir();
67 | $this->createAndTestPath($path);
68 | }
69 |
70 | #[Test]
71 | public function get_backups_dir_can_return_file(): void
72 | {
73 | $path = $this->getEnvFilesManager()->getBackupsDir();
74 | $filename = 'test.tmp';
75 | $filePath = $path.DIRECTORY_SEPARATOR.$filename;
76 | file_put_contents($filePath, 'dummy');
77 |
78 | $filePath1 = $this->getEnvFilesManager()->getBackupsDir($filename);
79 | $this->assertTrue(file_exists($filePath1));
80 | unlink($filePath);
81 | }
82 |
83 | #[Test]
84 | public function get_env_dir_can_return_file(): void
85 | {
86 | $path = $this->getEnvFilesManager()->getEnvDir();
87 | $filename = 'test.tmp';
88 | $filePath = $path.DIRECTORY_SEPARATOR.$filename;
89 | file_put_contents($filePath, 'dummy');
90 |
91 | $filePath1 = $this->getEnvFilesManager()->getEnvDir($filename);
92 | $this->assertTrue(file_exists($filePath1));
93 | unlink($filePath);
94 | }
95 |
96 | #[Test]
97 | public function get_all_back_ups_returns_all_files(): void
98 | {
99 | $manager = $this->getEnvFilesManager();
100 | $file1 = $manager->getBackupsDir('test.tmp');
101 | $file2 = $manager->getBackupsDir('test2.tmp');
102 | file_put_contents($file1, 'dummy');
103 | file_put_contents($file2, 'dummy');
104 |
105 | $backUps = $manager->getAllBackUps();
106 | $this->assertEquals(2, $backUps->count());
107 |
108 | unlink($file1);
109 | unlink($file2);
110 | }
111 |
112 | #[Test]
113 | public function back_up_current_env_works_and_returns_bool(): void
114 | {
115 | $fileName = 'test.tmp';
116 | $this->app->loadEnvironmentFrom($fileName);
117 |
118 | $content = time().'_dummy';
119 | $manager = $this->getEnvFilesManager();
120 | $file = $manager->getEnvDir($fileName);
121 | file_put_contents($file, $content);
122 |
123 | // Check CurrentEnv
124 | $currentEnv = $manager->getFilePath();
125 |
126 | $this->assertTrue(file_exists($currentEnv));
127 | $this->assertEquals(file_get_contents($currentEnv), $content);
128 |
129 | $result = $manager->backUpCurrentEnv();
130 | $this->assertTrue($result);
131 |
132 | $backUps = $manager->getAllBackUps();
133 |
134 | $this->assertEquals(1, $backUps->count());
135 | $backup = $backUps->first();
136 | $this->assertInstanceOf(Collection::class, $backup->entries);
137 | $this->assertInstanceOf(EntryObj::class, $backup->entries->first());
138 | $this->assertEquals($backup->rawContent, $content);
139 |
140 | unlink($file);
141 | }
142 |
143 | #[Test]
144 | public function restore_backup_works_and_returns_bool(): void
145 | {
146 | $this->app->loadEnvironmentFrom('.env.example');
147 | $manager = $this->getEnvFilesManager();
148 | // place a dummy env file
149 | file_put_contents($this->app->environmentFile(), '');
150 |
151 | $fileName = time().'_test.tmp';
152 | $content = time().'_dummy';
153 | $file = $manager->getBackupsDir($fileName);
154 | file_put_contents($file, $content);
155 |
156 | $result = $manager->restoreBackup($fileName);
157 | $this->assertTrue($result);
158 |
159 | $currentEnv = $manager->getFilePath();
160 | $this->assertEquals(file_get_contents($currentEnv), $content);
161 |
162 | unlink($file);
163 | }
164 |
165 | #[Test]
166 | public function restore_backup_wrong_backup(): void
167 | {
168 | $manager = $this->getEnvFilesManager();
169 |
170 | self::expectException(EnvException::class);
171 | self::expectExceptionMessage('You have to provide a FileName !!!');
172 | $manager->restoreBackup('');
173 | }
174 |
175 | #[Test]
176 | public function delete_backup_works_and_returns_bool(): void
177 | {
178 | $fileName = time().'_test.tmp';
179 | $manager = $this->getEnvFilesManager();
180 | $file = $manager->getBackupsDir($fileName);
181 | file_put_contents($file, 'dummy');
182 |
183 | $result = $manager->deleteBackup($fileName);
184 | $this->assertTrue($result);
185 |
186 | $this->assertFalse(file_exists($file));
187 | }
188 |
189 | #[Test]
190 | public function delete_backup_throws_exception_if_empty_sting_given(): void
191 | {
192 | $manager = $this->getEnvFilesManager();
193 | $this->expectException(EnvException::class);
194 | $this->expectExceptionMessage('You have to provide a FileName !!!');
195 | $manager->deleteBackup('');
196 | }
197 |
198 | private function createAndTestPath(string $path): void
199 | {
200 | $path = realpath($path);
201 | $this->assertNotFalse($path);
202 | $filename = tempnam($path, 'test') ?: throw new \RuntimeException("Couldn't create file");
203 | $this->assertEquals($filename, realpath($filename));
204 | unlink($filename);
205 | }
206 |
207 | private function cleanBackUpDir(): void
208 | {
209 | (new Filesystem())->cleanDirectory($this->getEnvFilesManager()->getBackupsDir());
210 | }
211 |
212 | /**
213 | * @param array $config
214 | */
215 | protected function getEnvFilesManager(array $config = []): EnvFilesManager
216 | {
217 | $envEditor = new EnvEditor(
218 | new Repository($config ?: $this->app['config']->get('env-editor')),
219 | new Filesystem()
220 | );
221 |
222 | return $envEditor->getFilesManager();
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/tests/fixtures/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME="Test env"
2 | APP_ENV=local
3 | APP_URL=https://example.test
4 |
5 | LOG_CHANNEL=stack
6 | LOG_DEPRECATIONS_CHANNEL=null
7 | LOG_LEVEL=debug
8 |
9 | DB_CONNECTION=mysql
10 | DB_HOST=127.0.0.1
11 | DB_PORT=3306
12 |
13 |
14 | BROADCAST_DRIVER=
15 | CACHE_DRIVER="file"
16 | FILESYSTEM_DRIVER=local
17 |
--------------------------------------------------------------------------------