├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── envy.php ├── ecs.php ├── rector.php ├── src ├── Actions │ ├── AddEnvironmentVariablesToList.php │ ├── FilterEnvironmentCalls.php │ ├── FindEnvironmentCalls.php │ ├── FindEnvironmentVariablesToPrune.php │ ├── FormatEnvironmentCall.php │ ├── ParseFilterList.php │ ├── PruneEnvironmentFile.php │ ├── ReadEnvironmentFile.php │ └── UpdateEnvironmentFile.php ├── Commands │ ├── Concerns │ │ └── HasUsefulConsoleMethods.php │ ├── InstallCommand.php │ ├── PruneCommand.php │ └── SyncCommand.php ├── Contracts │ ├── Actions │ │ ├── AddsEnvironmentVariablesToList.php │ │ ├── FiltersEnvironmentCalls.php │ │ ├── FindsEnvironmentCalls.php │ │ ├── FindsEnvironmentVariablesToPrune.php │ │ ├── FormatsEnvironmentCall.php │ │ ├── ParsesFilterList.php │ │ ├── PrunesEnvironmentFile.php │ │ ├── ReadsEnvironmentFile.php │ │ └── UpdatesEnvironmentFile.php │ ├── Filter.php │ └── Finder.php ├── Envy.php ├── EnvyServiceProvider.php ├── Exceptions │ ├── ConfigFileNotFoundException.php │ └── EnvironmentFileNotFoundException.php └── Support │ ├── EnvironmentCall.php │ ├── EnvironmentVariable.php │ ├── Filters │ ├── EqualityFilter.php │ ├── Filter.php │ ├── RegexFilter.php │ └── WildcardFilter.php │ ├── LaravelFinder.php │ └── PhpParser │ ├── AppendEnvironmentVariablesNodeVisitor.php │ └── EnvCallNodeVisitor.php └── stubs ├── ArrayItem.stub └── Array_.stub /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `envsync` will be documented in this file. 4 | 5 | ## v1.1.0 - 2023-03-22 6 | 7 | ### Added 8 | 9 | - Migrated to Pest v2 [#34](https://github.com/worksome/envy/pull/34) 10 | 11 | ### Changed 12 | 13 | - Required Laravel 10 as minimum Laravel version [#34](https://github.com/worksome/envy/pull/34) 14 | 15 | ## v1.0.0 - 2023-02-01 16 | 17 | > Note: Whilst this is a major release, there are no breaking changes. We feel Envy is battle tested enough to be considered stable, 18 | > so we're moving to a 1.0.0 release 🥳 19 | 20 | ### Added 21 | - Added support for Laravel 10 [#30](https://github.com/worksome/envy/pull/30) 22 | 23 | ## v0.5.0 - 2022-07-21 24 | 25 | ### Changed 26 | - Improved layouts for console commands [#24](https://github.com/worksome/envy/pull/24) 27 | 28 | ## v0.4.0 - 2022-04-13 29 | 30 | ### Added 31 | - Missing .env files will now display a friendly error message in the console [#21](https://github.com/worksome/envy/pull/21) 32 | - Missing broadcast .env values added to default values [#20](https://github.com/worksome/envy/pull/20) 33 | - Missing AWS .env values added to default values [#19](https://github.com/worksome/envy/pull/19) 34 | - Illuminate/Contracts version has been updated to earliest that supports feature set [#18](https://github.com/worksome/envy/pull/18) 35 | - Github Actions checkout upgraded from v2 to v3 [#16](https://github.com/worksome/envy/pull/16) 36 | 37 | ## v0.3.1 - 2022-02-21 38 | 39 | ### Fixed 40 | - Two missing environment variables in the default configuration for Laravel 9 projects [#6](https://github.com/worksome/envy/pull/6) 41 | 42 | ## v0.3.0 - 2022-02-19 43 | 44 | ### Added 45 | - Support for `Filter` objects for more complex inclusions and exclusions [#5](https://github.com/worksome/envy/pull/5) 46 | 47 | ## v0.2.2 - 2022-02-14 48 | 49 | ### Fixed 50 | - Boolean values will now be copied over as defaults [#2](https://github.com/worksome/envy/pull/2) 51 | 52 | ## v0.2.1 - 2022-02-07 53 | 54 | ### Added 55 | - Added `MYSQL_ATTR_SSL_CA` to `exclusions` 56 | 57 | ## v0.2.0 - 2022-02-07 58 | 59 | ### Changed 60 | - Renamed `blacklist` and `whitelist` to `exclusions` and `inclusions` respectively [#1](https://github.com/worksome/envy/pull/1) 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) worksome 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 | # Envy 2 | 3 | Automate keeping your environment files in sync. 4 | 5 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/worksome/envy/run-tests.yml?branch=main)](https://github.com/worksome/envy/actions?query=workflow%3Arun-tests+branch%3Amain) 6 | [![PHPStan](https://github.com/worksome/envy/actions/workflows/phpstan.yml/badge.svg)](https://github.com/worksome/envy/actions/workflows/phpstan.yml) 7 | 8 | How many times have you onboarded a new dev onto your team, only to have to spend ages debugging with them because your project's `.env.example` file is wildly outdated? Too many to count, if you're anything like us. Wouldn't it be nice if there were a way to ensure your environment files stay up to date? That's why we created Envy! 9 | 10 | **With a simple Artisan command, you can sync your environment files with your project config to keep everything fresh.** 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | ```bash 17 | composer require worksome/envy --dev 18 | ``` 19 | 20 | We recommend publishing our config file, which allows you to fine-tune Envy to your project's requirements. You can do that using our install command: 21 | 22 | ```bash 23 | php artisan envy:install 24 | ``` 25 | 26 | That's it! You're now ready to start using Envy. 27 | 28 | ## Usage 29 | 30 | Envy provides two commands: `envy:sync` and `envy:prune`. Let's break down how to use each of them. 31 | 32 | ### `php artisan envy:sync` 33 | 34 | CleanShot 2022-02-01 at 16 18 27@2x 35 | 36 | This command combs through your project's config files for calls to Laravel's `env` function. After finding them all, it will compare them against your configured environment files (by default just your `.env.example`) for missing entries. If there are missing entries, you will be given the choice to either: 37 | 1. Add the missing keys to your environment file 38 | 2. Add the missing keys to Envy's exclusion list 39 | 40 | To learn more about configuring environment files, config files and exclusions, see the Configuration documentation. 41 | 42 | The `envy:sync` command provides several options you might find helpful. 43 | 44 | #### `--path` 45 | 46 | If you'd like to run the sync command against a certain environment file, rather than your configured environment files, you may pass the path to the specified environment file using this option. 47 | 48 | #### `--dry` 49 | 50 | The `--dry` option will prevent the command from actually performing any updates. This is useful if you want to run Envy as part of a CI check without actually making updates. If missing entries were found, the command will fail, which would in turn fail the check in CI. 51 | 52 | #### `--force` 53 | 54 | If you want to automatically make changes to your configured environment files without being asked to confirm, you may pass the `--force` option. This is useful for CI bots, where you want to automate changes to your `.env.example` file, as no user input will be requested. 55 | 56 | ### `php artisan envy:prune` 57 | 58 | CleanShot 2022-02-02 at 12 03 51@2x 59 | 60 | This command will search your project's configured environment files (by default just your `.env.example`) for entries that could not be found in any of the configured config files. If there are additional entries, you will be given the choice to either: 61 | 1. Remove the additional entries from your environment file 62 | 2. Add the missing keys to Envy's inclusion list 63 | 64 | To learn more about configuring environment files, config files and inclusions, see the Configuration documentation. 65 | 66 | The `envy:prune` command provides several options you might find helpful. 67 | 68 | #### `--path` 69 | 70 | If you'd like to run the prune command against a certain environment file, rather than your configured environment files, you may pass the path to the specified environment file using this option. 71 | 72 | #### `--dry` 73 | 74 | The `--dry` option will prevent the command from actually pruning any environment variables. This is useful if you want to run Envy as part of a CI check without actually making updates. If additional entries were found, the command will fail, which would in turn fail the check in CI. 75 | 76 | #### `--force` 77 | 78 | If you want to automatically make changes to your configured environment files without being asked to confirm, you may pass the `--force` option. This is useful for CI bots, where you want to automate changes to your `.env.example` file, as no user input will be requested. 79 | 80 | ## Configuration 81 | 82 | You can customise Envy to suit your project's requirements using our `envy.php` config file. Here is a breakdown of the available options. 83 | 84 | ### `environment_files` 85 | 86 | Out of the box, Envy will only make changes to your `.env.example` file. If you want to add additional `.env` files, such as `.env.testing` or `.env.dusk`, you can append them to this array. 87 | 88 | > ⚠️ We do not recommend adding your `.env` file to this array as it could cause unwanted side effects, particularly in CI. 89 | 90 | ### `config_files` 91 | 92 | By default, Envy will recursively scan all files in your project's `config` directory. For most projects, this will suffice. If your project makes `env` calls *outside* of config files (which is an anti-pattern), you should add the relevant files or directories to this array. 93 | 94 | This is also useful if you make use of a package for which you have *not* published its config file. You may instead add an entry to this array that points to the base config file in the `vendor` directory. 95 | 96 | Note that if you reference a directory instead of a file, it will include all files in that directory recursively. 97 | 98 | ### `display_comments` 99 | 100 | CleanShot 2022-02-01 at 16 28 42@2x 101 | 102 | Some config keys, such as the one pictured above, may include comments. If you set `display_comments` to `true`, we will convert the config comment to an environment comment and place it above the relevant environment variable when inserting it into your configured environment files. This can be helpful in certain projects for remembering the purpose of various environment variables. 103 | 104 | ### `display_location_hints` 105 | 106 | When combing your config files, we make a note of the file and line where you called `env`. If you set `display_location_hints` to `true`, we will create a comment with this location information and place it above the relevant environment variable when inserting it into your configured environment files. This can be helpful in certain projects for locating the usage of various environment variables. 107 | 108 | ### `display_default_values` 109 | 110 | When combing your config files, we make a note of any default parameter set in the `env` call. For example, given a call for `env('APP_NAME', 'Laravel')`, the default would be `'Laravel'`. If `display_default_values` is set to `true`, we will insert the default value as the value for the relevant environment variable when updating your configured environment files. 111 | 112 | For obvious reasons, we will only insert scalar (primitive) types when copying default values. 113 | 114 | > ⚠️ If you have `exclude_calls_with_defaults` set to `true` (which is the default), this option will have no effect because calls with defaults will be ignored. 115 | 116 | ### `exclude_calls_with_defaults` 117 | 118 | If you copy over every single call to `env`, your environment files will quickly become difficult to read. To help alleviate this, we provide the option to ignore calls to `env` that have default values provided. By default this is enabled, but setting this to `false` will let Envy sync environment variables that have defaults too. 119 | 120 | ### `exclusions` 121 | 122 | This array is a collection of environment keys that should never be synced to your environment files. By default, we include all Laravel environment variables that aren't included in the default `.env` file created when you first create a new Laravel project. Removing values from this array will cause them to be picked up again whilst syncing. Of course, if you have custom variables or variables provided by packages that you want to ignore, you may add them here. 123 | 124 | If you select the `Add to exclusions` option when running `php artisan envy:sync`, this array will be updated with the environment variables listed by that command. 125 | 126 | > 💡 You may still manually insert keys from your exclusions into your `.env` file. We won't remove them. 127 | 128 | ### `inclusions` 129 | 130 | This array is a collection of environment keys that we should never prune from your configured environment files, even if we cannot find reference to those variables in your configured config files. This can be useful for JS variables used by Laravel Mix, for example. 131 | 132 | If you select the `Add to inclusions` option when running `php artisan envy:prune`, this array will be updated with the environment variables listed by that command. 133 | 134 | ## Advanced 135 | 136 | Once you're familiar with the basics of Envy, you may find these advanced features useful. 137 | 138 | ### Filters 139 | 140 | Sometimes, you'll want a more powerful way to represent items in the `exclusions` and `inclusions` lists than basic strings. For example, imagine you want to add all environment variables beginning with `STRIPE_` to the 141 | exclusions list. Rather than manually inserting them all individually, you can use the `Worksome\Envy\Support\Filters\Filter` class. 142 | 143 | ```php 144 | /** 145 | * Any environment variables that are added to exclusions will never be inserted 146 | * into .env files. Our defaults are based on the base Laravel config files. 147 | * Feel free to add or remove variables as required by your project needs. 148 | */ 149 | 'exclusions' => [ 150 | Filter::wildcard('STRIPE_*'), 151 | ], 152 | ``` 153 | 154 | Now, any environment variable starting with `STRIPE_` will automatically be excluded when syncing to your configured environment files. We also offer `Filter::regex`, which is an even more powerful 155 | filter that allows you to match environment variables against regular expression you provide. In fact, the `exclusions` and `inclusions` lists will accept a `string` or *any* class which implements 156 | the `Worksome\Envy\Contracts\Filter` contract, so you can even implement your own filters if that's your style. 157 | 158 | ## Testing 159 | 160 | We pride ourselves on a thorough test suite and strict static analysis. You can run all of our checks via a composer script: 161 | 162 | ```bash 163 | composer test 164 | ``` 165 | 166 | To make it incredibly easy to contribute, we also provide a docker-compose file that will spin up a container 167 | with all the necessary dependencies installed. Assuming you have docker installed, just run: 168 | 169 | ```bash 170 | docker-compose run --rm composer install # Only needed the first time 171 | docker-compose run --rm composer test # Run tests and static analysis 172 | ``` 173 | 174 | Support for XDebug is baked into the Docker image, you just need to configure the `XDEBUG_MODE` environment variable: 175 | 176 | ```bash 177 | docker-compose run --rm -e XDEBUG_MODE=debug php 178 | ``` 179 | 180 | ## Changelog 181 | 182 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 183 | 184 | ## Credits 185 | 186 | - [Luke Downing](https://github.com/lukeraymonddowning) 187 | - [All Contributors](../../contributors) 188 | 189 | ## License 190 | 191 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 192 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worksome/envy", 3 | "description": "Automatically keep your .env files in sync.", 4 | "keywords": [ 5 | "worksome", 6 | "laravel", 7 | "envsync" 8 | ], 9 | "homepage": "https://github.com/worksome/envsync", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Luke Downing", 14 | "email": "luke@worksome.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/contracts": "^11.0 || ^12.0", 21 | "nikic/php-parser": "^4.19.1 || ^5.0.2", 22 | "nunomaduro/termwind": "^1.15 || ^2.0", 23 | "spatie/laravel-package-tools": "^1.16", 24 | "thecodingmachine/safe": "^2.5 || ^3.0" 25 | }, 26 | "require-dev": { 27 | "nunomaduro/collision": "^8.1", 28 | "larastan/larastan": "^3.0", 29 | "orchestra/testbench": "^9.0 || ^10.0", 30 | "pestphp/pest": "^3.7", 31 | "pestphp/pest-plugin-laravel": "^3.1", 32 | "worksome/coding-style": "^3.1" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Worksome\\Envy\\": "src", 37 | "Worksome\\Envy\\Database\\Factories\\": "database/factories" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Worksome\\Envy\\Tests\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "lint": "vendor/bin/ecs --fix", 47 | "test:unit": "vendor/bin/pest", 48 | "test:coverage": "vendor/bin/pest --coverage --min=95", 49 | "test:types": "vendor/bin/phpstan analyse --memory-limit=-1", 50 | "test:style": "vendor/bin/ecs", 51 | "test": [ 52 | "@test:style", 53 | "@test:types", 54 | "@test:unit" 55 | ] 56 | }, 57 | "config": { 58 | "sort-packages": true, 59 | "allow-plugins": { 60 | "dealerdirect/phpcodesniffer-composer-installer": true, 61 | "pestphp/pest-plugin": true, 62 | "worksome/coding-style": true 63 | } 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "Worksome\\Envy\\EnvyServiceProvider" 69 | ] 70 | } 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /config/envy.php: -------------------------------------------------------------------------------- 1 | [ 13 | base_path('.env.example'), 14 | ], 15 | 16 | /** 17 | * Here you should list any config files/directories that you want to be 18 | * included when looking for calls to `env`. Directories are searched 19 | * recursively. Feel free to include unpublished vendor configs too. 20 | */ 21 | 'config_files' => [ 22 | config_path(), 23 | ], 24 | 25 | /** 26 | * Comments like the one you're reading can be quite useful when trying 27 | * to remember what an environment variable is used for. When set to 28 | * true, we'll copy any comments we find in config over to .env. 29 | */ 30 | 'display_comments' => false, 31 | 32 | /** 33 | * Some developers find it useful to have reference to where an environment 34 | * variable is used. Enabling this option will display a comment above a 35 | * linked .env variable with reference to the correct config file. 36 | */ 37 | 'display_location_hints' => false, 38 | 39 | /** 40 | * Enabling this option will also insert any provided defaults in your .env file 41 | * when updating. Note that only scalar (primitive) types will be copied over. 42 | * Defaults that include spaces will be wrapped in quotes for you. 43 | * 44 | * Note that `exclude_calls_with_defaults` must be set to `false` for this 45 | * to take effect. 46 | */ 47 | 'display_default_values' => true, 48 | 49 | /** 50 | * When calling the `env` function, you can optionally provide a default as the 51 | * second parameter. Envy will ignore any calls with a set default if this 52 | * option is set to true. Otherwise, it will include them whilst syncing. 53 | */ 54 | 'exclude_calls_with_defaults' => true, 55 | 56 | /** 57 | * Any environment variables that are added to exclusions will never be inserted 58 | * into .env files. Our defaults are based on the base Laravel config files. 59 | * Feel free to add or remove variables as required by your project needs. 60 | */ 61 | 'exclusions' => [ 62 | // config/app.php 63 | 'ASSET_URL', 64 | 65 | // config/broadcasting.php 66 | 'ABLY_KEY', 67 | 'PUSHER_APP_KEY', 68 | 'PUSHER_APP_SECRET', 69 | 'PUSHER_APP_ID', 70 | 'PUSHER_APP_CLUSTER', 71 | 72 | // config/cache.php 73 | 'MEMCACHED_PERSISTENT_ID', 74 | 'MEMCACHED_USERNAME', 75 | 'MEMCACHED_PASSWORD', 76 | 'MEMCACHED_HOST', 77 | 'MEMCACHED_PORT', 78 | 'AWS_ACCESS_KEY_ID', 79 | 'AWS_SECRET_ACCESS_KEY', 80 | 'AWS_DEFAULT_REGION', 81 | 'DYNAMODB_CACHE_TABLE', 82 | 'DYNAMODB_ENDPOINT', 83 | 84 | // config/database.php 85 | 'DATABASE_URL', 86 | 'DB_SOCKET', 87 | 'REDIS_CLIENT', 88 | 'REDIS_CLUSTER', 89 | 'REDIS_PREFIX', 90 | 'REDIS_URL', 91 | 'REDIS_DB', 92 | 'REDIS_CACHE_DB', 93 | 'MYSQL_ATTR_SSL_CA', 94 | 95 | // config/filesystems.php 96 | 'AWS_ENDPOINT', 97 | 'AWS_URL', 98 | 'AWS_BUCKET', 99 | 100 | // config/hashing.php 101 | 'BCRYPT_ROUNDS', 102 | 103 | // config/logging.php 104 | 'PAPERTRAIL_URL', 105 | 'PAPERTRAIL_PORT', 106 | 'LOG_STDERR_FORMATTER', 107 | 'LOG_SLACK_WEBHOOK_URL', 108 | 109 | // config/mail.php 110 | 'MAIL_SENDMAIL_PATH', 111 | 'MAIL_LOG_CHANNEL', 112 | 'MAIL_FROM_ADDRESS', 113 | 'MAIL_FROM_NAME', 114 | 115 | // config/queue.php 116 | 'SQS_PREFIX', 117 | 'SQS_QUEUE', 118 | 'SQS_SUFFIX', 119 | 'REDIS_QUEUE', 120 | 'QUEUE_FAILED_DRIVER', 121 | 122 | // config/sanctum.php 123 | 'SANCTUM_STATEFUL_DOMAINS', 124 | 125 | // config/services.php 126 | 'MAILGUN_DOMAIN', 127 | 'MAILGUN_SECRET', 128 | 'MAILGUN_ENDPOINT', 129 | 'POSTMARK_TOKEN', 130 | 131 | // config/session.php 132 | 'SESSION_CONNECTION', 133 | 'SESSION_STORE', 134 | 'SESSION_COOKIE', 135 | 'SESSION_DOMAIN', 136 | 'SESSION_SECURE_COOKIE', 137 | 138 | // config/view.php 139 | 'VIEW_COMPILED_PATH', 140 | ], 141 | 142 | /** 143 | * Any environment variables that are added to inclusions will never be pruned from 144 | * your .env files. By default, we include Laravel Mix variables. Feel free to 145 | * add or remove environment variables to suit your project's requirements. 146 | */ 147 | 'inclusions' => [ 148 | 'MIX_PUSHER_APP_KEY', 149 | 'MIX_PUSHER_APP_CLUSTER', 150 | ], 151 | ]; 152 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | __DIR__ . '/config', 14 | ]); 15 | 16 | WorksomeEcsConfig::setup($ecsConfig); 17 | }; 18 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]); 15 | 16 | // Define extra rule sets to be applied 17 | $rectorConfig->sets([ 18 | // SetList::DEAD_CODE, 19 | ]); 20 | 21 | // Register extra a single rules 22 | // $rectorConfig->rule(ClassOnObjectRector::class); 23 | }; 24 | -------------------------------------------------------------------------------- /src/Actions/AddEnvironmentVariablesToList.php: -------------------------------------------------------------------------------- 1 | printer = new Standard(); 34 | } 35 | 36 | public function __invoke(Collection $updates, string $listKey): void 37 | { 38 | if ($this->finder->envyConfigFile() === null) { 39 | throw new ConfigFileNotFoundException(); 40 | } 41 | 42 | $statements = $this->parser->parse(file_get_contents($this->finder->envyConfigFile())); 43 | 44 | if ($statements === null) { 45 | return; 46 | } 47 | 48 | $visitor = new AppendEnvironmentVariablesNodeVisitor($updates, $listKey); 49 | $traverser = new NodeTraverser(); 50 | $traverser->addVisitor(new NodeConnectingVisitor()); 51 | $traverser->addVisitor($visitor); 52 | $ast = $traverser->traverse($statements); 53 | 54 | if (! $visitor->variablesWereAppended()) { 55 | throw new InvalidArgumentException("[$listKey] is not a supported key in the envy.php config file."); 56 | } 57 | 58 | file_put_contents($this->finder->envyConfigFile(), $this->printer->prettyPrintFile($ast)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Actions/FilterEnvironmentCalls.php: -------------------------------------------------------------------------------- 1 | $exclusions 19 | */ 20 | public function __construct( 21 | private ReadsEnvironmentFile $readEnvironmentFile, 22 | private ParsesFilterList $parseFilterList, 23 | private array $exclusions = [], 24 | ) { 25 | } 26 | 27 | public function __invoke(string $filePath, Collection $environmentCalls): Collection 28 | { 29 | $existingKeys = ($this->readEnvironmentFile)($filePath)->map( 30 | fn(EnvironmentVariable $variable) => $variable->getKey() 31 | ); 32 | 33 | return $environmentCalls 34 | ->unique(fn(EnvironmentCall $call) => $call->getKey()) 35 | ->reject(fn(EnvironmentCall $call) => $existingKeys->contains($call->getKey())) 36 | ->reject(fn(EnvironmentCall $call) => $this->exclusionsContainVariable($call->getKey())); 37 | } 38 | 39 | private function exclusionsContainVariable(string $environmentVariable): bool 40 | { 41 | return collect(($this->parseFilterList)($this->exclusions)) 42 | ->filter(fn (Filter $filter) => $filter->check($environmentVariable)) 43 | ->isNotEmpty(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Actions/FindEnvironmentCalls.php: -------------------------------------------------------------------------------- 1 | addVisitor(new NodeConnectingVisitor()); 29 | $traverser->addVisitor($envCallNodeVisitor); 30 | $statements = $this->parser->parse(file_get_contents($filePath)); 31 | 32 | if ($statements === null) { 33 | return $envCallNodeVisitor->getEnvironmentVariables(); 34 | } 35 | 36 | $traverser->traverse($statements); 37 | 38 | return $envCallNodeVisitor 39 | ->getEnvironmentVariables() 40 | ->when( 41 | $excludeVariablesWithDefaults, 42 | fn (Collection $variables) => $variables->reject( 43 | fn (EnvironmentCall $variable) => $variable->getDefault() !== null 44 | ) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Actions/FindEnvironmentVariablesToPrune.php: -------------------------------------------------------------------------------- 1 | $inclusions 19 | */ 20 | public function __construct( 21 | private ReadsEnvironmentFile $readEnvironmentFile, 22 | private ParsesFilterList $parsesFilterList, 23 | private array $inclusions = [], 24 | ) { 25 | } 26 | 27 | public function __invoke(string $filePath, Collection $environmentCalls): Collection 28 | { 29 | $variablesInEnvironmentCalls = $environmentCalls->map( 30 | fn(EnvironmentCall $environmentCall) => $environmentCall->getKey() 31 | ); 32 | 33 | return $this->environmentVariables($filePath) 34 | ->diff($variablesInEnvironmentCalls) 35 | ->reject(fn (string $variable) => $this->inclusionsContainVariable($variable)) 36 | ->unique() 37 | ->sort() 38 | ->values(); 39 | } 40 | 41 | /** 42 | * @return Collection 43 | */ 44 | private function environmentVariables(string $filePath): Collection 45 | { 46 | return ($this->readEnvironmentFile)($filePath) 47 | ->map(fn(EnvironmentVariable $environmentVariable) => $environmentVariable->getKey()); 48 | } 49 | 50 | private function inclusionsContainVariable(string $environmentVariable): bool 51 | { 52 | return collect(($this->parsesFilterList)($this->inclusions)) 53 | ->filter(fn (Filter $filter) => $filter->check($environmentVariable)) 54 | ->isNotEmpty(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Actions/FormatEnvironmentCall.php: -------------------------------------------------------------------------------- 1 | getKey()}="); 25 | 26 | if ($this->displayDefaultValue && $environmentCall->getDefault() !== null) { 27 | $defaultValue = $environmentCall->getDefault(); 28 | 29 | if (Str::match('/\s/', $defaultValue) !== '') { 30 | $defaultValue = "\"{$defaultValue}\""; 31 | } 32 | 33 | $value = $value->append($defaultValue); 34 | } 35 | 36 | if ($this->displayLocationHint) { 37 | $value = $value->start("# See {$environmentCall->getFile()}::{$environmentCall->getLine()}" . PHP_EOL); 38 | } 39 | 40 | if ($this->displayComment && $environmentCall->getComment() !== null) { 41 | $value = $value->start($this->formatComment($environmentCall->getComment())); 42 | } 43 | 44 | return $value->__toString(); 45 | } 46 | 47 | private function formatComment(string $phpComment): string 48 | { 49 | // Remove PHP docblock syntax, such as '//' and '/*' 50 | $commentWithoutPhpSyntax = preg_replace('/^[\/*]+|\/\//m', '', $phpComment); 51 | 52 | // @phpstan-ignore-next-line 53 | return collect(explode(PHP_EOL, $commentWithoutPhpSyntax)) 54 | ->map(fn (string $line) => "#{$line}") 55 | ->join(PHP_EOL) . PHP_EOL; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Actions/ParseFilterList.php: -------------------------------------------------------------------------------- 1 | $this->transformFilter($filter), $list); 16 | } 17 | 18 | private function transformFilter(string|Filter $filter): Filter 19 | { 20 | if ($filter instanceof Filter) { 21 | return $filter; 22 | } 23 | 24 | return new EqualityFilter($filter); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions/PruneEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | reduce(function (string $content, string $environmentVariable) { 20 | $environmentVariable = preg_quote($environmentVariable); 21 | return preg_replace("/(#.*|\r\n?|\n)*^{$environmentVariable}=.*$/m", '', $content); 22 | }, file_get_contents($filePath)); 23 | 24 | file_put_contents($filePath, $updatedContent); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions/ReadEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | parse($this->getFileContents($envFilePath)); 20 | 21 | return collect($entries)->map(fn (Entry $entry) => new EnvironmentVariable( 22 | $entry->getName(), 23 | $entry->getValue()->get()->getChars() 24 | )); 25 | } 26 | 27 | /** 28 | * @throws EnvironmentFileNotFoundException 29 | */ 30 | private function getFileContents(string $envFilePath): string 31 | { 32 | if (! file_exists($envFilePath)) { 33 | throw new EnvironmentFileNotFoundException($envFilePath); 34 | } 35 | 36 | $content = file_get_contents($envFilePath); 37 | 38 | assert($content !== false); 39 | 40 | return $content; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Actions/UpdateEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | map(fn (EnvironmentCall $call) => ($this->formatEnvironmentCall)($call)) 24 | ->join(PHP_EOL); 25 | 26 | file_put_contents($filePath, $content, FILE_APPEND); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/Concerns/HasUsefulConsoleMethods.php: -------------------------------------------------------------------------------- 1 | $message 20 | "); 21 | 22 | return $this; 23 | } 24 | 25 | private function warning(string $message): self 26 | { 27 | render(" 28 |
$message
29 | "); 30 | 31 | return $this; 32 | } 33 | 34 | private function information(string $message): self 35 | { 36 | render(" 37 |
$message
38 | "); 39 | 40 | return $this; 41 | } 42 | 43 | private function askUserToStarRepository(): void 44 | { 45 | render(' 46 | 49 | '); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', ['--tag' => 'envy-config']); 21 | 22 | $this->information( 23 | 'Alright, Envy is ready to rock! Get started with either `php artisan envy:sync` or `php artisan envy:prune`.' 24 | ); 25 | 26 | return self::SUCCESS; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/PruneCommand.php: -------------------------------------------------------------------------------- 1 | getPendingPrunes($envy); 36 | } catch (EnvironmentFileNotFoundException $exception) { 37 | $this->warning($exception->getMessage()); 38 | 39 | return self::INVALID; 40 | } 41 | 42 | if ($pendingPrunes->isEmpty()) { 43 | render('
There are no variables to prune!
'); 44 | return self::SUCCESS; 45 | } 46 | 47 | $this->printPendingPrunes($pendingPrunes, $blade); 48 | 49 | if ($this->option('dry')) { 50 | return self::FAILURE; 51 | } 52 | 53 | match ($this->askWhatWeShouldDoNext($envy->hasPublishedConfigFile())) { 54 | self::ACTION_ADD_TO_INCLUSIONS => $this->addPendingPrunesToInclusions($envy, $pendingPrunes), 55 | self::ACTION_PRUNE_ENVIRONMENT_FILE => $this->updateEnvironmentFiles($envy, $pendingPrunes), 56 | default => $this->warning('Prune cancelled'), 57 | }; 58 | 59 | $this->askUserToStarRepository(); 60 | 61 | return self::SUCCESS; 62 | } 63 | 64 | /** 65 | * @return Collection> 66 | * 67 | * @throws EnvironmentFileNotFoundException 68 | */ 69 | private function getPendingPrunes(Envy $envy): Collection 70 | { 71 | /** @var string|null $path */ 72 | $path = $this->option('path'); 73 | 74 | return $envy->pendingPrunes( 75 | $envy->environmentCalls(), 76 | $path ? [strval($path)] : null 77 | ); 78 | } 79 | 80 | /** 81 | * @param Collection> $pendingPrunes 82 | */ 83 | private function printPendingPrunes(Collection $pendingPrunes, BladeCompiler $blade): void 84 | { 85 | render($blade->render(<<<'HTML' 86 |
87 | @foreach ($pendingPrunes as $path => $environmentVariables) 88 |
89 |
90 | 91 | {{ $environmentVariables->count() }} {{ Str::plural("variable", $environmentVariables->count()) }} to remove for {{ Str::after($path, base_path()) }} 92 | 93 | 94 | {{ $loop->iteration }}/{{ $loop->count }} 95 | 96 |
97 |
98 | @foreach ($environmentVariables as $environmentVariable) 99 |
{{ $environmentVariable }}
100 | @endforeach 101 |
102 |
103 | @endforeach 104 |
105 | HTML, ['pendingPrunes' => $pendingPrunes])); 106 | } 107 | 108 | private function askWhatWeShouldDoNext(bool $configFileHasBeenPublished): string 109 | { 110 | $options = collect([ 111 | self::ACTION_PRUNE_ENVIRONMENT_FILE => true, 112 | self::ACTION_ADD_TO_INCLUSIONS => $configFileHasBeenPublished, 113 | self::ACTION_CANCEL => true, 114 | ])->filter()->keys()->all(); 115 | 116 | // @phpstan-ignore return.type 117 | return $this->option('force') 118 | ? self::ACTION_PRUNE_ENVIRONMENT_FILE 119 | : strval($this->choice( 120 | 'How would you like to handle pruning?', 121 | $options, 122 | self::ACTION_PRUNE_ENVIRONMENT_FILE 123 | )); 124 | } 125 | 126 | /** 127 | * @param Collection> $pendingPrunes 128 | */ 129 | private function addPendingPrunesToInclusions(Envy $envy, Collection $pendingPrunes): void 130 | { 131 | $envy->updateInclusionsWithPendingPrunes($pendingPrunes); 132 | $this->success('Inclusions updated!'); 133 | } 134 | 135 | /** 136 | * @param Collection> $pendingPrunes 137 | */ 138 | private function updateEnvironmentFiles(Envy $envy, Collection $pendingPrunes): void 139 | { 140 | $envy->pruneEnvironmentFiles($pendingPrunes); 141 | $this->success('Environment variables pruned!'); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Commands/SyncCommand.php: -------------------------------------------------------------------------------- 1 | getPendingPrunes($envy, $config); 38 | } catch (EnvironmentFileNotFoundException $exception) { 39 | $this->warning($exception->getMessage()); 40 | 41 | return self::INVALID; 42 | } 43 | 44 | if ($pendingUpdates->isEmpty()) { 45 | render('
There are no variables to sync!
'); 46 | return self::SUCCESS; 47 | } 48 | 49 | $this->printPendingUpdates($pendingUpdates, $blade); 50 | 51 | if ($this->option('dry')) { 52 | return self::FAILURE; 53 | } 54 | 55 | match ($this->askWhatWeShouldDoNext($envy->hasPublishedConfigFile())) { 56 | self::ACTION_ADD_TO_EXCLUSIONS => $this->addPendingUpdatesToExclusions($envy, $pendingUpdates), 57 | self::ACTION_ADD_TO_ENVIRONMENT_FILE => $this->updateEnvironmentFiles($envy, $pendingUpdates), 58 | default => $this->warning('Sync cancelled'), 59 | }; 60 | 61 | $this->askUserToStarRepository(); 62 | 63 | return self::SUCCESS; 64 | } 65 | 66 | /** 67 | * @return Collection> 68 | * 69 | * @throws EnvironmentFileNotFoundException 70 | */ 71 | private function getPendingPrunes(Envy $envy, Repository $config): Collection 72 | { 73 | /** @var string|null $path */ 74 | $path = $this->option('path'); 75 | 76 | return $envy->pendingUpdates( 77 | $envy->environmentCalls(boolval($config->get('envy.exclude_calls_with_defaults', false))), 78 | $path ? [strval($path)] : null, 79 | ); 80 | } 81 | 82 | /** 83 | * Outputs pending updates to the console. 84 | * 85 | * @param Collection> $pendingUpdates 86 | */ 87 | private function printPendingUpdates(Collection $pendingUpdates, BladeCompiler $blade): void 88 | { 89 | render($blade->render(<<<'HTML' 90 |
91 | @foreach ($pendingUpdates as $path => $environmentCalls) 92 |
93 |
94 | 95 | {{ $environmentCalls->count() }} {{ Str::plural("update", $environmentCalls->count()) }} for {{ Str::after($path, base_path()) }} 96 | 97 | 98 | {{ $loop->iteration }}/{{ $loop->count }} 99 | 100 |
101 |
102 | @foreach ($environmentCalls as $environmentCall) 103 |
{{ $environmentCall->getKey() }}
104 | @endforeach 105 |
106 |
107 | @endforeach 108 |
109 | HTML, ['pendingUpdates' => $pendingUpdates])); 110 | } 111 | 112 | private function askWhatWeShouldDoNext(bool $configFileHasBeenPublished): string 113 | { 114 | $options = collect([ 115 | self::ACTION_ADD_TO_ENVIRONMENT_FILE => true, 116 | self::ACTION_ADD_TO_EXCLUSIONS => $configFileHasBeenPublished, 117 | self::ACTION_CANCEL => true, 118 | ])->filter()->keys()->all(); 119 | 120 | // @phpstan-ignore return.type 121 | return $this->option('force') 122 | ? self::ACTION_ADD_TO_ENVIRONMENT_FILE 123 | : strval($this->choice( 124 | 'How would you like to handle these updates?', 125 | $options, 126 | self::ACTION_ADD_TO_ENVIRONMENT_FILE 127 | )); 128 | } 129 | 130 | /** 131 | * @param Collection> $pendingUpdates 132 | */ 133 | private function addPendingUpdatesToExclusions(Envy $envy, Collection $pendingUpdates): void 134 | { 135 | $envy->updateExclusionsWithPendingUpdates($pendingUpdates); 136 | $this->success('Exclusions updated!'); 137 | } 138 | 139 | /** 140 | * @param Collection> $pendingUpdates 141 | */ 142 | private function updateEnvironmentFiles(Envy $envy, Collection $pendingUpdates): void 143 | { 144 | $envy->updateEnvironmentFiles($pendingUpdates); 145 | $this->success('Environment variables added!'); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Contracts/Actions/AddsEnvironmentVariablesToList.php: -------------------------------------------------------------------------------- 1 | $updates 21 | * 22 | * @throws ConfigFileNotFoundException Thrown when the envy config file hasn't been published. 23 | * @throws InvalidArgumentException Thrown when the envy config file doesn't contain the given key. 24 | */ 25 | public function __invoke(Collection $updates, string $listKey): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/Actions/FiltersEnvironmentCalls.php: -------------------------------------------------------------------------------- 1 | $environmentCalls 15 | * 16 | * @return Collection 17 | * 18 | * @throws EnvironmentFileNotFoundException 19 | */ 20 | public function __invoke(string $filePath, Collection $environmentCalls): Collection; 21 | } 22 | -------------------------------------------------------------------------------- /src/Contracts/Actions/FindsEnvironmentCalls.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function __invoke(string $filePath, bool $excludeVariablesWithDefaults = false): Collection; 16 | } 17 | -------------------------------------------------------------------------------- /src/Contracts/Actions/FindsEnvironmentVariablesToPrune.php: -------------------------------------------------------------------------------- 1 | $environmentCalls 15 | * 16 | * @return Collection 17 | * 18 | * @throws EnvironmentFileNotFoundException 19 | */ 20 | public function __invoke(string $filePath, Collection $environmentCalls): Collection; 21 | } 22 | -------------------------------------------------------------------------------- /src/Contracts/Actions/FormatsEnvironmentCall.php: -------------------------------------------------------------------------------- 1 | $list 13 | * 14 | * @return array 15 | */ 16 | public function __invoke(array $list): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Contracts/Actions/PrunesEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | $pendingPrunes 13 | */ 14 | public function __invoke(string $filePath, Collection $pendingPrunes): void; 15 | } 16 | -------------------------------------------------------------------------------- /src/Contracts/Actions/ReadsEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * @throws EnvironmentFileNotFoundException 17 | */ 18 | public function __invoke(string $envFilePath): Collection; 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/Actions/UpdatesEnvironmentFile.php: -------------------------------------------------------------------------------- 1 | $environmentCalls 15 | */ 16 | public function __invoke(string $filePath, Collection $environmentCalls): void; 17 | } 18 | -------------------------------------------------------------------------------- /src/Contracts/Filter.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function configFilePaths(): array; 15 | 16 | /** 17 | * An array of environment file paths to be synced. 18 | * 19 | * @return array 20 | */ 21 | public function environmentFilePaths(): array; 22 | 23 | /** 24 | * The config file path for this package. If the config file 25 | * has not been published, this will return null instead. 26 | */ 27 | public function envyConfigFile(): string|null; 28 | } 29 | -------------------------------------------------------------------------------- /src/Envy.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public function environmentCalls(bool $excludeCallsWithDefaults = false): Collection 38 | { 39 | // @phpstan-ignore-next-line 40 | return collect($this->finder->configFilePaths()) 41 | ->map(fn (string $path) => ($this->findEnvironmentCalls)($path, $excludeCallsWithDefaults)) 42 | ->flatten() 43 | ->sortBy(fn (EnvironmentCall $call) => $call->getKey()); 44 | } 45 | 46 | /** 47 | * Map the given environment calls to each configured `.env` file. 48 | * 49 | * @see Envy::environmentCalls() 50 | * 51 | * @param Collection $environmentCalls 52 | * @param array|null $environmentFilePaths 53 | * 54 | * @return Collection> 55 | * 56 | * @throws EnvironmentFileNotFoundException 57 | */ 58 | public function pendingUpdates(Collection $environmentCalls, array|null $environmentFilePaths = null): Collection 59 | { 60 | return collect($environmentFilePaths ?? $this->finder->environmentFilePaths()) 61 | ->flip() 62 | ->map(fn (int $index, string $path) => ($this->filtersEnvironmentCalls)($path, $environmentCalls)) 63 | ->filter(fn (Collection $environmentCalls) => $environmentCalls->isNotEmpty()); 64 | } 65 | 66 | /** 67 | * Perform the given updates on the relevant environment files. 68 | * 69 | * @see Envy::pendingUpdates() 70 | * 71 | * @param Collection> $pendingUpdates 72 | */ 73 | public function updateEnvironmentFiles(Collection $pendingUpdates): void 74 | { 75 | $pendingUpdates->each(fn (Collection $environmentCalls, string $path) => ($this->updateEnvironmentFile)( 76 | $path, 77 | $environmentCalls 78 | )); 79 | } 80 | 81 | /** 82 | * Convert a collection of pending updates to a collection of 83 | * environment variables and update the config exclusions. 84 | * 85 | * @param Collection> $pendingUpdates 86 | * 87 | * @throws Exceptions\ConfigFileNotFoundException 88 | */ 89 | public function updateExclusionsWithPendingUpdates(Collection $pendingUpdates): void 90 | { 91 | /** @var Collection $updates */ 92 | $updates = $pendingUpdates 93 | ->flatten() 94 | ->unique(fn (EnvironmentCall $environmentCall) => $environmentCall->getKey()) 95 | ->map(fn (EnvironmentCall $environmentCall) => new EnvironmentVariable( 96 | $environmentCall->getKey(), 97 | $environmentCall->getDefault() ?? '' 98 | )); 99 | 100 | ($this->addEnvironmentVariablesToList)($updates, AddsEnvironmentVariablesToList::EXCLUSIONS); 101 | } 102 | 103 | /** 104 | * Determine if the envy.php config file has been published to the project. 105 | */ 106 | public function hasPublishedConfigFile(): bool 107 | { 108 | return $this->finder->envyConfigFile() !== null; 109 | } 110 | 111 | /** 112 | * Map the environment variables to be removed to each configured .env file. 113 | * 114 | * @see Envy::environmentCalls() 115 | * 116 | * @param Collection $environmentCalls 117 | * @param array|null $environmentFilePaths 118 | * 119 | * @return Collection> 120 | * 121 | * @throws EnvironmentFileNotFoundException 122 | */ 123 | public function pendingPrunes(Collection $environmentCalls, array|null $environmentFilePaths = null): Collection 124 | { 125 | return collect($environmentFilePaths ?? $this->finder->environmentFilePaths()) 126 | ->flip() 127 | ->map(fn (int $index, string $path) => ($this->findEnvironmentVariablesToPrune)($path, $environmentCalls)) 128 | ->filter(fn (Collection $environmentVariables) => $environmentVariables->isNotEmpty()); 129 | } 130 | 131 | /** 132 | * Prune the given variables from the mapped environment files. 133 | * 134 | * @see Envy::pendingPrunes() 135 | * 136 | * @param Collection> $pendingPrunes 137 | */ 138 | public function pruneEnvironmentFiles(Collection $pendingPrunes): void 139 | { 140 | $pendingPrunes->each(fn (Collection $environmentVariables, string $path) => ($this->pruneEnvironmentFile)( 141 | $path, 142 | $environmentVariables, 143 | )); 144 | } 145 | 146 | /** 147 | * Convert a collection of pending prunes to a collection of 148 | * environment variables and update the config inclusions. 149 | * 150 | * @see Envy::pendingPrunes() 151 | * 152 | * @param Collection> $pendingPrunes 153 | * 154 | * @throws Exceptions\ConfigFileNotFoundException 155 | */ 156 | public function updateInclusionsWithPendingPrunes(Collection $pendingPrunes): void 157 | { 158 | /** @var Collection $environmentVariables */ 159 | $environmentVariables = $pendingPrunes 160 | ->flatten() 161 | ->map(fn (string $key) => new EnvironmentVariable($key, '')); 162 | 163 | ($this->addEnvironmentVariablesToList)($environmentVariables, AddsEnvironmentVariablesToList::INCLUSIONS); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/EnvyServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(Finder::class, fn(Application $app) => new LaravelFinder( 40 | $app, 41 | $this->config()['config_files'] ?? [], 42 | $this->config()['environment_files'] ?? [], 43 | )); 44 | 45 | $this->app->bind( 46 | FindsEnvironmentCalls::class, 47 | fn(Application $app) => $app->make(FindEnvironmentCalls::class, [ 48 | 'parser' => (new ParserFactory())->createForNewestSupportedVersion(), 49 | ]) 50 | ); 51 | $this->app->bind(ReadsEnvironmentFile::class, ReadEnvironmentFile::class); 52 | $this->app->bind(ParsesFilterList::class, ParseFilterList::class); 53 | $this->app->bind( 54 | FiltersEnvironmentCalls::class, 55 | fn (Application $app) => $app->make(FilterEnvironmentCalls::class, [ 56 | 'exclusions' => $this->config()['exclusions'] ?? [] 57 | ]) 58 | ); 59 | $this->app->bind(UpdatesEnvironmentFile::class, UpdateEnvironmentFile::class); 60 | $this->app->bind(FormatsEnvironmentCall::class, fn() => new FormatEnvironmentCall( 61 | $this->config()['display_comments'] ?? false, 62 | $this->config()['display_location_hints'] ?? false, 63 | $this->config()['display_default_values'] ?? true, 64 | )); 65 | $this->app->bind( 66 | AddsEnvironmentVariablesToList::class, 67 | fn (Application $app) => $app->make(AddEnvironmentVariablesToList::class, [ 68 | 'parser' => (new ParserFactory())->createForNewestSupportedVersion(), 69 | ]) 70 | ); 71 | $this->app->bind( 72 | FindsEnvironmentVariablesToPrune::class, 73 | fn (Application $app) => $app->make(FindEnvironmentVariablesToPrune::class, [ 74 | 'inclusions' => $this->config()['inclusions'] ?? [], 75 | ]) 76 | ); 77 | $this->app->bind(PrunesEnvironmentFile::class, PruneEnvironmentFile::class); 78 | } 79 | 80 | /** 81 | * @return array{ 82 | * environment_files?: list, 83 | * config_files?: list, 84 | * display_comments?: bool, 85 | * display_location_hints?: bool, 86 | * display_default_values?: bool, 87 | * exclude_calls_with_defaults?: bool, 88 | * exclusions?: list, 89 | * inclusions?: list, 90 | * } 91 | */ 92 | private function config(): array 93 | { 94 | // @phpstan-ignore-next-line 95 | return config('envy'); 96 | } 97 | 98 | public function configurePackage(Package $package): void 99 | { 100 | $package 101 | ->name('envy') 102 | ->hasConfigFile() 103 | ->hasCommands( 104 | InstallCommand::class, 105 | SyncCommand::class, 106 | PruneCommand::class, 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigFileNotFoundException.php: -------------------------------------------------------------------------------- 1 | file; 28 | } 29 | 30 | public function getLine(): int 31 | { 32 | return $this->line; 33 | } 34 | 35 | public function getKey(): string 36 | { 37 | return $this->key; 38 | } 39 | 40 | public function getDefault(): string|null 41 | { 42 | return $this->default; 43 | } 44 | 45 | public function getComment(): string|null 46 | { 47 | return $this->comment; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Support/EnvironmentVariable.php: -------------------------------------------------------------------------------- 1 | key; 18 | } 19 | 20 | public function getValue(): string 21 | { 22 | return $this->value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Support/Filters/EqualityFilter.php: -------------------------------------------------------------------------------- 1 | comparison === $environmentVariable; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Support/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | pattern, $environmentVariable) === 1; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Support/Filters/WildcardFilter.php: -------------------------------------------------------------------------------- 1 | wildcard, $this->comparison)) 21 | ->map(fn (string $part) => preg_quote($part)) 22 | ->join('\S+'); 23 | 24 | return preg_match("/{$comparison}/", $environmentVariable) === 1; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/LaravelFinder.php: -------------------------------------------------------------------------------- 1 | $configFiles 18 | * @param array $environmentFiles 19 | */ 20 | public function __construct( 21 | private Application $app, 22 | private array $configFiles, 23 | private array $environmentFiles, 24 | ) { 25 | } 26 | 27 | public function configFilePaths(): array 28 | { 29 | // @phpstan-ignore-next-line 30 | return collect($this->configFiles) 31 | ->map(fn(string $path) => is_file($path) ? [$path] : $this->allFilesRecursively($path)) 32 | ->flatten() 33 | ->all(); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | private function allFilesRecursively(string $directory): array 40 | { 41 | // @phpstan-ignore-next-line 42 | return LazyCollection::make(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory))) 43 | ->filter(fn(mixed $file) => $file instanceof SplFileInfo) 44 | ->reject(fn(SplFileInfo $file) => $file->isDir()) 45 | ->map(fn(SplFileInfo $file) => $file->getPathname()) 46 | ->values() 47 | ->all(); 48 | } 49 | 50 | public function environmentFilePaths(): array 51 | { 52 | return $this->environmentFiles; 53 | } 54 | 55 | public function envyConfigFile(): string|null 56 | { 57 | if (! file_exists($this->app->configPath('envy.php'))) { 58 | return null; 59 | } 60 | 61 | return $this->app->configPath('envy.php'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Support/PhpParser/AppendEnvironmentVariablesNodeVisitor.php: -------------------------------------------------------------------------------- 1 | $updates 19 | */ 20 | public function __construct(private Collection $updates, private string $arrayKey) 21 | { 22 | } 23 | 24 | public function leaveNode(Node $node): Node 25 | { 26 | if (! $node instanceof Node\Expr\Array_) { 27 | return $node; 28 | } 29 | 30 | $arrayItem = $node->getAttribute('parent'); 31 | 32 | if (! $arrayItem instanceof Node\Expr\ArrayItem) { 33 | return $node; 34 | } 35 | 36 | if (! $arrayItem->key instanceof Node\Scalar\String_) { 37 | return $node; 38 | } 39 | 40 | if ($arrayItem->key->value !== $this->arrayKey) { 41 | return $node; 42 | } 43 | 44 | $this->updates->each(function (EnvironmentVariable $variable) use (&$node) { 45 | $item = new ArrayItem(new Node\Scalar\String_($variable->getKey())); 46 | 47 | $node->items[] = $item; 48 | }); 49 | 50 | $this->variablesWereAppended = true; 51 | 52 | return $node; 53 | } 54 | 55 | public function variablesWereAppended(): bool 56 | { 57 | return $this->variablesWereAppended; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/PhpParser/EnvCallNodeVisitor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private Collection $environmentVariables; 23 | 24 | /** 25 | * The printer to format env calls with. 26 | */ 27 | private PrettyPrinterAbstract $printer; 28 | 29 | public function __construct( 30 | private readonly string $filePath, 31 | private readonly bool $excludeVariablesWithDefaults = false, 32 | ) { 33 | $this->environmentVariables = new Collection(); 34 | $this->printer = new Standard(); 35 | } 36 | 37 | public function enterNode(Node $node): Node 38 | { 39 | if (! $node instanceof Node\Expr\FuncCall) { 40 | return $node; 41 | } 42 | 43 | if (! $this->isEnvCall($node)) { 44 | return $node; 45 | } 46 | 47 | $call = new EnvironmentCall( 48 | $this->filePath, 49 | $node->getStartLine(), 50 | $this->print($node->getArgs()[0]->value), 51 | $this->getDefaultValue($node), 52 | $this->getComment($node), 53 | ); 54 | 55 | if ($this->excludeVariablesWithDefaults && $call->getDefault() !== null) { 56 | return $node; 57 | } 58 | 59 | $this->environmentVariables->push($call); 60 | 61 | return $node; 62 | } 63 | 64 | /** 65 | * @return Collection 66 | */ 67 | public function getEnvironmentVariables(): Collection 68 | { 69 | return $this->environmentVariables; 70 | } 71 | 72 | private function isEnvCall(Node\Expr\FuncCall $node): bool 73 | { 74 | if (! $node->name instanceof Node\Name) { 75 | return false; 76 | } 77 | 78 | if (! $node->name->isUnqualified()) { 79 | return false; 80 | } 81 | 82 | if ($node->name->toLowerString() !== 'env') { 83 | return false; 84 | } 85 | 86 | if (count($node->getArgs()) === 0) { 87 | return false; 88 | } 89 | 90 | return true; 91 | } 92 | 93 | private function getDefaultValue(Node\Expr\FuncCall $node): string|null 94 | { 95 | if (count($node->getArgs()) < 2) { 96 | return null; 97 | } 98 | 99 | $providedDefault = $node->getArgs()[1]->value; 100 | 101 | // Because the value will be excluded, we should include it regardless. 102 | if ($this->excludeVariablesWithDefaults) { 103 | return $this->print($providedDefault); 104 | } 105 | 106 | if ($providedDefault instanceof Node\Scalar) { 107 | return $this->print($providedDefault); 108 | } 109 | 110 | if ($this->isBoolean($providedDefault)) { 111 | return $this->print($providedDefault); 112 | } 113 | 114 | return null; 115 | } 116 | 117 | private function getComment(Node\Expr\FuncCall $node): string|null 118 | { 119 | /** @var Node|null $previousNode */ 120 | $previousNode = $node->getAttribute('previous'); 121 | 122 | if ($previousNode === null) { 123 | return null; 124 | } 125 | 126 | /** @var \PhpParser\Node\Expr\ArrayItem $parent */ 127 | $parent = $previousNode->getAttribute('parent'); 128 | 129 | if ($parent->hasAttribute('comments')) { 130 | /** @var list|null $comments */ 131 | $comments = $parent->getAttribute('comments'); 132 | 133 | if ($comments === null || count($comments) === 0) { 134 | return null; 135 | } 136 | 137 | /** @var string $comment */ 138 | $comment = $comments[0]->getReformattedText(); 139 | 140 | return $comment; 141 | } 142 | 143 | return null; 144 | } 145 | 146 | private function print(Node $node): string 147 | { 148 | if ($node instanceof Node\Scalar\String_) { 149 | return $node->value; 150 | } 151 | 152 | return $this->printer->prettyPrint([$node]); 153 | } 154 | 155 | private function isBoolean(Node\Expr $providedDefault): bool 156 | { 157 | if (! $providedDefault instanceof Node\Expr\ConstFetch) { 158 | return false; 159 | } 160 | 161 | $name = $providedDefault->name->name ?? $providedDefault->name->getParts()[0]; 162 | 163 | return in_array($name, ['true', 'false'], true); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /stubs/ArrayItem.stub: -------------------------------------------------------------------------------- 1 |