Search by

hpwebdeveloper / laravel-env-settings

panjeh

Environment-aware, type-safe configuration classes for Laravel. Move non-secret values out of .env and into typed, IDE-friendly PHP classes.

Package info

github.com/HPWebdeveloper/laravel-env-settings

pkg:composer/hpwebdeveloper/laravel-env-settings

Statistics

Installs: 82

Dependents: 0

Suggesters: 0

Stars: 17

Open Issues: 0

v1.7.1 2026-09-07 01:44 UTC

This package is auto-updated.

Last update: 2026-09-07 02:38:40 UTC


README

A typed configuration layer for non-secret values that differ between environments.

Laravel Env Settings demo — resolved settings for the current environment

Laravel Env Settings demo — comparing values across environments

by Hamed Panjeh

🚀 See how this package works in practice — try the live demo

A real Laravel application with worked examples: settings classes, per-environment values, local overrides, and the Artisan commands in action. The fastest way to understand the package before installing it.

💡 Why this exists — the problem it solves, and whether it's for you

Fifteen reasons, each one a before and an after: what belongs in .env, types instead of strings, staying correct at runtime, working with other people, and where this fits alongside other packages. Tick the ones you recognise — it reads as a diagnosis rather than a feature list.

Contents

Latest Version on Packagist GitHub Tests Action Status Total Downloads

Most of a typical .env holds no secrets at all — API URLs, model names, timeouts, queue names, feature modes. Those belong in version control, where they are typed, reviewable in pull requests, and visible to your whole team. This package keeps .env for secrets and the stock Laravel keys, and puts everything your application adds on top into app/Settings.

// Before: scattered, untyped, invisible to code review
$domain = config('services.auth0.domain');   // typo? runtime surprise
$model  = env('OPENAI_TEXT_MODEL');          // string? null? who knows
$mode   = env('PAYMENT_MODE', 'test');       // what's production's value? check the server

// After: typed, environment-aware, in version control
envSettings(AuthSettings::class)->domain     // string, IDE autocomplete
envSettings(AiSettings::class)->text_model   // defined per environment
envSettings(PaymentSettings::class)->mode    // visible in git, reviewable in PRs

AI Assistants

A published agent skill teaches Claude Code, Cursor, Codex and others how to work with this package — and the rule that matters most: secrets never go in a settings class. It is listed at skills.laravel.cloud:

npx skills add HPWebdeveloper/laravel-env-settings-skills        # Skills CLI
php artisan boost:add-skill HPWebdeveloper/laravel-env-settings-skills   # Laravel Boost

The skill stands alone, so your agent can learn the package before it is installed. The same guidance also ships bundled, so php artisan boost:install offers it once the package is in your project.

Requirements

Laravel PHP
13.x 8.3 – 8.5
12.x 8.2 – 8.5

Every combination above is covered by the CI test matrix. The only runtime dependency is illuminate/support.

Installation

composer require hpwebdeveloper/laravel-env-settings
php artisan vendor:publish --tag="env-settings-config"

Tip

📘 Follow the step-by-step setup guide on the demo → The same installation walked through in a real application, with the generated files shown at each step.

Quick Start

1. Generate

php artisan env-settings:make AuthSettings \
    --properties="domain:string,timeout:int,mfa_enabled:bool"

That writes app/Settings/AuthSettings.php with the structure in place and // TODO placeholders. Fill them in:

namespace App\Settings;

use HpWebDeveloper\LaravelEnvSettings\EnvironmentSettings;

class AuthSettings extends EnvironmentSettings
{
    public function __construct(
        public string $domain,
        public int $timeout,
        public bool $mfa_enabled,
    ) {}

    public static function development(): static
    {
        return new static(domain: 'dev.auth.example.com', timeout: 30, mfa_enabled: false);
    }

    public static function production(): static
    {
        return new static(domain: 'auth.example.com', timeout: 10, mfa_enabled: true);
    }
}

development() and production() are required. staging() and testing() fall back to development() — override them only when the values genuinely differ.

Property types: string, int, float, bool, array, or any enum.

A value that is the same everywhere goes in the signature, not in every factory. Give it a constructor default and write it once:

public function __construct(
    public string $provider,             // varies by environment
    public float $temperature = 0.2,     // the same everywhere — written once
) {}

public static function development(): static { return new static(provider: 'ollama'); }
public static function production(): static  { return new static(provider: 'openai'); }

// and the one environment that genuinely differs just passes it
public static function staging(): static { return new static(provider: 'openai', temperature: 0.9); }

Generate that shape directly with --shared. PHP requires parameters with defaults to come last, so they collect at the end of the constructor.

2. Register

A settings class stays inert until it is listed in config/env-settings.php:

'register' => [
    \App\Settings\AuthSettings::class,
],

env-settings:make appends this line for you when the config has been published; if it can't, it prints exactly what to add. Each registered class becomes a container singleton, resolved once per request.

3. Use it anywhere

// The helper — template-typed, so your IDE autocompletes the result
$domain = envSettings(AuthSettings::class)->domain;

// Constructor injection
public function __construct(private AuthSettings $auth) {}

// The container
app(AuthSettings::class)->timeout;   // 10 in production, 30 in development

That's it. The correct environment is resolved automatically.

Tip

▶️ Try it yourself in the browser → Switch environments and watch the resolved values change, without installing anything.

How resolution works

For the current APP_ENV, the package picks a factory in this order:

  1. A method marked #[Environment] for that APP_ENV
  2. The environment_map config (localdevelopment, prodproduction, …), then the raw APP_ENV value as a method name
  3. fallback_environment (default: development)

It reads app()->environment() and config() — never env() — so it is fully compatible with php artisan config:cache.

Tip

🔎 See this resolution happening live on the demo → The demo prints the current APP_ENV, the method it maps to, and the resulting instance.

Features

Each of these is covered in depth in the full guide.

Fixed-value properties (enums)

When a property only ever holds one of a few values, type it as an enum. A typo becomes a parse error, and the valid set is documented by the type:

enum PaymentMode: string
{
    case Live = 'live';
    case Sandbox = 'sandbox';
}

public function __construct(
    public PaymentMode $mode,
) {}

// development(): mode: PaymentMode::Sandbox
// production():  mode: PaymentMode::Live

Reading it gives you the case itself, so calling code branches exhaustively instead of comparing strings:

$client = match (envSettings(PaymentSettings::class)->mode) {
    PaymentMode::Live => $gateway->live(),
    PaymentMode::Sandbox => $gateway->sandbox(),
};

The enum defines which values are possible; the factories choose which one each environment uses. toArray() unwraps enums (backed → value, pure → case name), so JSON output keeps working. → details

Declaring environments on the class

environment_map lives in the application's config, so the same class can resolve differently in two applications. Mark the factory instead and the answer sits beside the code:

use HpWebDeveloper\LaravelEnvSettings\Attributes\Environment;

#[Environment('production', 'prod')]
#[Environment('demo')]        // a second environment sharing production values
public static function production(): static { ... }

#[Environment('qa', 'uat')]   // the method name need not match the environment
public static function qualityAssurance(): static { ... }

APP_ENV=demo, qa or uat now resolve with no config edit. → details

Masking sensitive output

Mark a property #[Sensitive] and both env-settings:show and env-settings:diff print ******** instead of its value:

use HpWebDeveloper\LaravelEnvSettings\Attributes\Sensitive;

public function __construct(
    #[Sensitive] public string $passphrase,
) {}

Masking affects display only — toArray() still returns real values, and diff still flags the property with * when it differs. This is a safety net, not permission to store secrets here. → details

Local developer overrides

Individual developers can override values without touching committed code: set ENV_SETTINGS_OVERRIDE=true, add app/Settings/Overrides/AuthSettings.php extending the base class, and gitignore the directory. When overrides are off or the file is missing, the base class is used as normal. → details

Composing settings into a root object

A root class whose properties are other settings classes gives the whole configuration tree one entry point:

envSettings(AppSettings::class)->auth->domain;
envSettings(AppSettings::class)->payment->mode;

Register only the root. toArray() expands nested settings recursively, which makes a debug endpoint or health-check payload a one-liner. → details

Artisan Commands

env-settings:make

php artisan env-settings:make NotificationSettings \
    --properties="sms_provider:string,rate_limit_per_minute:int,sandbox_mode:bool"

# Custom path — the namespace follows the directory
php artisan env-settings:make NotificationSettings --path=app/Settings/Infrastructure

# Explicit namespace, for directories outside the application root
php artisan env-settings:make NotificationSettings \
    --path=packages/billing/src/Settings --namespace="Acme\\Billing\\Settings"

# Mark properties #[Sensitive] as they are generated
php artisan env-settings:make VaultSettings \
    --properties="endpoint:string,passphrase:string" --sensitive=passphrase

# Values that are the same in every environment become constructor defaults
php artisan env-settings:make AiSettings \
    --properties="provider:string,temperature:float,currency:string" \
    --shared="temperature=0.2,currency=EUR"

--shared takes name=value pairs. Each named property gets the value as a constructor default and is left out of the factories entirely, so it is written once instead of once per environment. Scalars only — an array or enum default cannot be expressed as one comma-separated argument, so those are refused rather than mangled into a class that will not parse.

A generated class only autoloads if its namespace matches where the file was written, so the namespace is resolved in this order: --namespace if given, otherwise derived from --path when that directory sits under the application root, otherwise config('env-settings.class_namespace'). A path outside the application root has no PSR-4 mapping to read, so it falls back to the default and warns — pass --namespace in that case. → full table

env-settings:check

Reports settings left at their generated placeholder, so a factory nobody finished cannot reach production:

php artisan env-settings:check --env=production   # a specific environment
php artisan env-settings:check                    # the current APP_ENV
php artisan env-settings:check "App\Settings\AuthSettings"
✗ App\Settings\AuthSettings
    domain                   empty string, but set in development()
    timeout                  0, but set in development()
    webhook_url              still contains "TODO"

1 of 3 classes incomplete for [production]: 3 values to fill in.

Run it in CI, on the branch that deploys. It exits non-zero when anything is incomplete, which makes it a gate rather than a report:

- name: Check production settings are complete
  run: php artisan env-settings:check --env=production

It is a static check — it resolves your classes, touches no network and needs no production credentials — so it belongs in the build, alongside your tests, not in a deploy hook where a failure is already too late.

What counts as incomplete. A value equal to its generated placeholder ('', 0, 0.0, false, [], null) when another environment supplies a real one — the shape of a class where one factory was filled in and another forgotten. A value that is empty in every environment is deliberate and never reported. Any string containing TODO is always reported.

Mark a property #[AllowEmpty] when its empty value is intentional:

use HpWebDeveloper\LaravelEnvSettings\Attributes\AllowEmpty;

public function __construct(
    #[AllowEmpty] public string $path_prefix,
) {}

env-settings:show

php artisan env-settings:show                                # all registered classes
php artisan env-settings:show "App\Settings\AuthSettings"    # one class
php artisan env-settings:show --all                          # every environment side by side
[ AuthSettings ] — Environment: production
+-------------+--------+------------------+
| Property    | Type   | Value            |
+-------------+--------+------------------+
| domain      | string | auth.example.com |
| timeout     | int    | 10               |
| mfa_enabled | bool   | true             |
+-------------+--------+------------------+

--all: every environment at once

A settings class states every environment in one file — but show prints only the current one and diff only ever two, so four environments took six diff runs to compare. --all puts them in one table:

[ AuthSettings ] — development, staging, production
+---------------+----------------------+--------------------------+------------------+
| Property      | development          | staging                  | production       |
+---------------+----------------------+--------------------------+------------------+
| domain *      | dev.auth.example.com | staging.auth.example.com | auth.example.com |
| timeout *     | 30                   | 20                       | 10               |
| mfa_enabled * | false                | true                     | true             |
| currency      | EUR                  | EUR                      | EUR              |
+---------------+----------------------+--------------------------+------------------+
* = differs somewhere
1 unmarked property is the same everywhere.

An unmarked row does not vary by environment, so it may not need to be a setting at all — that trailing count is the one thing this view tells you that no other command can.

Values are compared before masking, so a #[Sensitive] property is still marked * when it differs without being revealed.

env-settings:diff

php artisan env-settings:diff "App\Settings\AuthSettings" development production

# Omit any argument and you'll be prompted for it
php artisan env-settings:diff
[ AuthSettings ] — Comparing development vs production
+---------------+----------------------+------------------+
| Property      | development          | production       |
+---------------+----------------------+------------------+
| domain *      | dev.auth.example.com | auth.example.com |
| timeout *     | 30                   | 10               |
| mfa_enabled * | false                | true             |
+---------------+----------------------+------------------+
* = values differ between environments

Full documentation

📖 GUIDE.md — the complete reference, including:

Changelog

See Releases for recent changes.

Contributing

Issues and pull requests are welcome on GitHub.

Security

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). See LICENSE for details.