Back

Is this package for you?

🧭

How many of these sound familiar?

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.

What belongs in .env

5

The file was meant for secrets. It filled up with everything else.

βœ• One file, everything equally invisible
# .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
βœ“ Each value where it belongs
# .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.

βœ• Both treated as equally dangerous
DB_HOST=prod-db.internal
DB_PASSWORD=hunter2

# knowing the host alone gets you nothing β€”
# it is behind the VPN either way
βœ“ Only the credential stays secret
# .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.

βœ• No history, no author, no reason
$ git log -p .env

# nothing. .env is gitignored.
# Who raised the token limit? When? Why?
# Nobody can answer without asking around.
βœ“ Every change has an author and a reason
$ 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.

βœ• One value, copied into every environment
# .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.
βœ“ Written once, in the signature
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
βœ• Your .env holds today, and nothing else
# .env on your laptop
AI_TEXT_MODEL=llama3.2
PAYMENT_MODE=sandbox

# What does staging use? production?
# Not in this file. Ask someone.
βœ“ All environments visible at once
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 --all

Types instead of strings

3

Configuration is data your editor could understand β€” if you let it.

βœ• Wrong key, no complaint
config('services.auth.domian')
// β†’ null   (typo in the key)

// no error, no warning. The bug surfaces
// three layers away as "host cannot be empty".
βœ“ Wrong name, immediate error
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.

βœ• Any string is accepted
$mode = 'sandbx';

// ships, deploys, runs.
// Fails the first time a real payment
// is taken. At 3 a.m.
βœ“ Only the real options exist
$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 action
βœ• No shared shape, nothing to export
config('services.payment.mode')
config('services.ai.text_model')
config('services.sms.from')

// three unrelated strings.
// Want them all as JSON? Write it by hand.
βœ“ One typed object, exportable
$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 object

Correct at runtime

3

The failures that only appear once you cache, or once you deploy.

βœ• Works locally, empty after config:cache
// app/Services/Ai.php
$provider = env('AI_PROVIDER');

// local:      'openai'   βœ“
// production: null       βœ—
// env() stops working once config is cached
βœ“ Same value everywhere
$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.

βœ• Nothing checks the placeholder
// AuthSettings::production()
domain: '',    // TODO: set production value
timeout: 0,    // TODO: set production value

$ git push        # green
# discovered in production, three days later
βœ“ The build refuses to pass
$ 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
βœ• The answer lives in another file
// config/env-settings.php
'environment_map' => ['qa' => 'staging'],

// app/Settings/AuthSettings.php
public static function staging()

// reading the class tells you nothing about qa
βœ“ The class says it itself
#[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 mapping

Working with other people

4

Config is a team artefact. It gets read, shared and pasted around.

βœ• SSH, grep, and hope
$ 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
βœ“ A local command answers it
$ 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
βœ• The token goes with it
| webhook_url | https://app.example.com/hook?t=s3cr3t |

# now pasted into a ticket, a chat thread
# and a CI log, permanently
βœ“ Masked, and still useful
| 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 hidden
βœ• Every argument, or ArgumentCountError
new 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
βœ“ Name only what the test is about
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
βœ• Edit the shared file and hope
// edited AiSettings.php to test a longer timeout
-    timeout: 30,
+    timeout: 300,

$ git commit -am 'fix bug'
# …and shipped your local tweak to everyone
βœ“ Your machine only
// 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.

Where it fits

2

One place it shines, and one place it deliberately does not go.

βœ• Changed by someone, sometime, for some reason
AI_TEXT_MODEL=gpt-4o
AI_MAX_TOKENS=8000
AI_TEMPERATURE=0.2

# these changed last sprint.
# No commit, no author, no discussion.
βœ“ Every change argued for in a PR
$ 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.

βœ• It genuinely cannot do this
// config/database.php
'host' => DatabaseSettings::resolve()->host,

// PHP Fatal error:
// Class "env" does not exist
// config files run before the container exists
βœ“ Those keys stay where they are
# .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 boundary

of

match your project

what this means
Get the package Getting started All four commands Back to the demo