Below are 17 problems this package exists to solve. Each one shows what you live with today on the left, and what takes its place on the right.
Select the ones you have hit in your own projects. If hardly any of them land, you probably do not need this package β that is a perfectly good outcome.
Nothing is sent anywhere β the counter in the bottom corner just keeps track as you scroll, and sums up at the end.
The file was meant for secrets. It filled up with everything else.
# .env β 40 lines, 3 of them actually secret
STRIPE_SECRET=sk_live_51H8xR2... # secret
AI_PROVIDER=openai # not a secret
AI_TEXT_MODEL=gpt-4o # not a secret
BILLING_URL=https://billing.internal # not a secret
# all four are equally hidden from review
# .env β only what must stay hidden
STRIPE_SECRET=sk_live_51H8xR2...
// app/Settings/AiSettings.php β reviewed in every PR
provider: 'openai',
text_model: 'gpt-4o',
// 1 line to protect instead of 40
The point is the split. Secrets keep their hiding place; the other 37 lines become code your team can read and review.
DB_HOST=prod-db.internal
DB_PASSWORD=hunter2
# knowing the host alone gets you nothing β
# it is behind the VPN either way
# .env
DB_PASSWORD=hunter2
// app/Settings/DatabaseSettings.php
host: 'prod-db.internal',
// now everyone can see which host each
// environment talks to. Nobody gains access.
A hostname is useless without credentials. Treating them the same means hiding information your team needs for no security gain.
$ git log -p .env
# nothing. .env is gitignored.
# Who raised the token limit? When? Why?
# Nobody can answer without asking around.
$ git log -p app/Settings/AiSettings.php
commit a1b2c3d β Sara, 2 weeks ago
Raise token limit for the summariser
- max_tokens: 4000,
+ max_tokens: 8000,
This is the capability you cannot get from a gitignored file: history, blame, and a pull request to discuss it in.
# .env.local CURRENCY=EUR
# .env.staging CURRENCY=EUR
# .env.production CURRENCY=EUR
# .env.qa CURRENCY=EUR
# four files, one value. Change it and you change
# four things. Miss one and they drift apart.
public function __construct(
public string $provider, // varies
public string $currency = 'EUR', // written once
) {}
// the one environment that differs just passes it:
// new static(provider: 'openai', currency: 'GBP')
A plain constructor default. The value stays typed, still shows up in show, diff and toArray(), and there is exactly one place to change it.
Generate one with --shared# .env on your laptop
AI_TEXT_MODEL=llama3.2
PAYMENT_MODE=sandbox
# What does staging use? production?
# Not in this file. Ask someone.
development(): 'llama3.2', 'sandbox'
staging(): 'gpt-4o-mini', 'sandbox'
production(): 'gpt-4o', 'live'
// one file answers every environment,
// so a new developer never has to guess
A settings class holds all environments side by side. A .env file physically cannot β it only ever describes the machine it is on. env-settings:show --all prints the whole table.
See show --allConfiguration is data your editor could understand β if you let it.
config('services.auth.domian')
// β null (typo in the key)
// no error, no warning. The bug surfaces
// three layers away as "host cannot be empty".
envSettings(AuthSettings::class)->domian
// β PHP error, and your editor greyed it out
// before you saved the file
->domain // string β autocompleted, renameable
A stringly-typed key can be wrong in silence. A property name cannot β the editor knows every one that exists.
$mode = 'sandbx';
// ships, deploys, runs.
// Fails the first time a real payment
// is taken. At 3 a.m.
$mode = PaymentMode::Sandbx;
// PHP Fatal error: undefined constant
// You never get to commit this.
match ($mode) { ... } // and match is exhaustive
Type the property as an enum and the valid set becomes part of the signature, checked before the code runs.
See enums in actionconfig('services.payment.mode')
config('services.ai.text_model')
config('services.sms.from')
// three unrelated strings.
// Want them all as JSON? Write it by hand.
$app = envSettings(AppSettings::class);
$app->payment->mode;
$app->ai->text_model;
$app->toArray(); // the whole tree, JSON-ready
Compose many settings classes into a single typed entry point you can navigate, pass around and serialise.
See the nested objectThe failures that only appear once you cache, or once you deploy.
// app/Services/Ai.php
$provider = env('AI_PROVIDER');
// local: 'openai' β
// production: null β
// env() stops working once config is cached
$provider = envSettings(AiSettings::class)->provider;
// local: 'ollama' β
// production: 'openai' β
// resolved from the container, not from the
// cached config file
Settings resolve at container-resolve time, never at config-load time, so caching cannot hollow them out.
// AuthSettings::production()
domain: '', // TODO: set production value
timeout: 0, // TODO: set production value
$ git push # green
# discovered in production, three days later
$ php artisan env-settings:check --env=production
β App\Settings\AuthSettings
domain empty string, but set in development()
timeout 0, but set in development()
$ echo $?
1 # CI stops here
The one command that exits non-zero, so the pipeline stops the deploy instead of production discovering it.
See the gate// config/env-settings.php
'environment_map' => ['qa' => 'staging'],
// app/Settings/AuthSettings.php
public static function staging()
// reading the class tells you nothing about qa
#[Environment('qa', 'uat')]
public static function qualityAssurance()
// no config entry at all β and it resolves
// the same way in every app that installs it
The method name no longer has to match the environment, and a class shipped in a package carries its own mapping.
See the mappingConfig is a team artefact. It gets read, shared and pasted around.
$ ssh prod-web-01
$ cat .env | grep AI_
# needs access, needs the box to be up,
# and puts secrets on your screen
# to answer a question about a model name
$ php artisan env-settings:diff AiSettings staging production
| text_model * | gpt-4o-mini | gpt-4o |
| max_tokens * | 2000 | 8000 |
# no access needed. Both are described in code.
Every environment is written down, so comparing two of them never requires reaching one of them.
All four commands| webhook_url | https://app.example.com/hook?t=s3cr3t |
# now pasted into a ticket, a chat thread
# and a CI log, permanently
| webhook_url * | ******** | ******** |
# the * still tells you staging and production
# disagree β the diff compares real values,
# then masks them for printing
Mark the property and it never reaches the terminal, while your application still receives the real value.
See what gets hiddennew AuthSettings(
domain: 'test.example.com',
redirect_url: 'http://test.example.com/cb',
timeout: 5, // the only one this test is about
mfa_enabled: false,
);
// add a 5th property -> every such test breaks at once
AuthSettings::fake(['timeout' => 5]);
// the rest keep their real values, and a property
// added next month changes nothing here
fake(), from() and with() return the real class, fully typed β so a test states its subject and stops paying for the rest.
See the doubles// edited AiSettings.php to test a longer timeout
- timeout: 30,
+ timeout: 300,
$ git commit -am 'fix bug'
# β¦and shipped your local tweak to everyone
// app/Settings/Overrides/AiSettings.php
// gitignored β yours alone
timeout: 300,
# .env
ENV_SETTINGS_OVERRIDE=true
Each developer can override any class locally without touching committed code or anyone elseβs setup.
One place it shines, and one place it deliberately does not go.
AI_TEXT_MODEL=gpt-4o
AI_MAX_TOKENS=8000
AI_TEMPERATURE=0.2
# these changed last sprint.
# No commit, no author, no discussion.
$ git blame app/Settings/AiSettings.php
a1b2c3d (Sara 3 weeks ago) text_model: 'gpt-4o',
9f8e7d6 (Marco 5 days ago) max_tokens: 8000,
# each line traceable to the PR that raised it
Providers, models and token limits differ per environment and change often β exactly the values that benefit from review.
// config/database.php
'host' => DatabaseSettings::resolve()->host,
// PHP Fatal error:
// Class "env" does not exist
// config files run before the container exists
# .env β read by config/*.php at bootstrap
APP_KEY=β¦ DB_HOST=β¦ MAIL_MAILER=β¦
SENTRY_DSN=β¦ # third-party packages too
// app/Settings β what your app adds on top
provider: 'openai',
Laravelβs config files, and every vendorβs, read env() at bootstrap. This package targets the configuration your application adds on top.
Read the boundarymatch your project