Configuration Strategy — One Rule That Prevents Chaos

Configuration Strategy — One Rule That Prevents Chaos

Reading Time: 4 minutes

Series: Every Laravel Project Should Have These Building Blocks 
Part: 2 of 35 | Level: Beginner | Prerequisites: Folder Structure


What You’ll Learn

  • The single rule that prevents environment-related bugs
  • How to organize config/ as your application grows
  • When to create new config files vs. extending services.php
  • How to properly expose third-party service config
  • How to validate that required config values are present

The One Rule

Never call env() outside of config/ files.

That’s it. That’s the entire configuration strategy.

If you follow this one rule consistently, you eliminate an entire category of bugs. If you break it even once, you’ll spend an afternoon wondering why your config is returning null after running php artisan config:cache.

Here’s why: when you run config:cache, Laravel compiles all your config files into a single cached file and stops reading .env entirely. If your code calls env('STRIPE_KEY') directly, it will return null in a cached environment — which is every production server.

// ❌ Wrong — breaks after config:cache
class StripePaymentService
{
    public function __construct()
    {
        $this->apiKey = env('STRIPE_SECRET_KEY'); // null in production!
    }
}

// ✅ Correct — always works
class StripePaymentService
{
    public function __construct()
    {
        $this->apiKey = config('services.stripe.secret'); // reads from cache
    }
}

The config/ Folder Structure

Laravel ships with a set of standard config files. The ones you’ll interact with most are:

config/
├── app.php          # Application name, timezone, locale, debug flag
├── auth.php         # Guards, providers, password brokers
├── cache.php        # Default cache store, TTLs
├── database.php     # Database connections
├── filesystems.php  # Disk definitions (local, s3, etc.)
├── logging.php      # Log channels — covered in a dedicated article
├── mail.php         # Mailer config, default from address
├── queue.php        # Queue connections and default queue
├── services.php     # ← Third-party service credentials
└── session.php      # Session driver and TTL

The most important one to understand as your app grows is services.php.


config/services.php — Your Third-Party Service Registry

services.php is Laravel’s designated place for external service credentials. It ships mostly empty:

// config/services.php (default)
return [
    'mailgun' => [...],
    'postmark' => [...],
    'ses' => [...],
];

Every third-party service your application uses — Stripe, Twilio, AWS, a custom API — should have its credentials registered here:

// config/services.php
return [
    'stripe' => [
        'key'    => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
        'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
    ],

    'whm' => [
        'host'     => env('WHM_HOST'),
        'username' => env('WHM_USERNAME'),
        'api_token' => env('WHM_API_TOKEN'),
        'verify_ssl' => env('WHM_VERIFY_SSL', true),
    ],

    'name_api' => [
        'api_key' => env('NAME_API_KEY'),
        'base_url' => env('NAME_API_URL', 'https://api.name.com/v4/'),
    ],

    'ollama' => [
        'api_url'          => env('OLLAMA_API_URL', 'http://localhost:11434/api/chat'),
        'model'            => env('OLLAMA_MODEL', 'llama3.2'),
        'timeout'          => env('OLLAMA_TIMEOUT', 90),
        'context_char_limit' => env('OLLAMA_CONTEXT_CHAR_LIMIT', 3000),
        'temperature'      => env('OLLAMA_TEMPERATURE', 0.1),
    ],

    'admin' => [
        'email' => env('ADMIN_EMAIL'),
    ],
];

Then access it in code:

config('services.stripe.secret')
config('services.whm.host')
config('services.ollama.model')

When to Create a New Config File

The rule is simple: create a new config file when the configuration for a feature is complex enough that it deserves its own namespace.

Don’t create a new file for simple key/value pairs. These belong in services.php:

// ❌ Overkill — this doesn't need its own file
// config/stripe.php
return ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')];

// ✅ Just add it to services.php
'stripe' => ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')],

Do create a new file when you have a domain with many settings that would clutter services.php. Good examples from real projects:

// config/logging.php — Laravel ships this by default; customize it for named channels
// config/error_mail.php — configuration for the ErrorReporter service
// config/knowledge.php — configuration for AI knowledge base feature

// config/error_mail.php
return [
    'sensitive_fields' => [
        'password',
        'password_confirmation',
        'token',
        'card_number',
        'cvv',
    ],
];

Validating Required Config at Boot

One underused feature: you can validate that required config values are present when the application boots, rather than getting cryptic errors at runtime.

In AppServiceProvider::boot():

public function boot(): void
{
    $this->validateConfig();
}

private function validateConfig(): void
{
    $required = [
        'services.stripe.secret'   => 'STRIPE_SECRET',
        'services.whm.api_token'   => 'WHM_API_TOKEN',
        'mail.support.address'     => 'MAIL_SUPPORT_ADDRESS',
    ];

    foreach ($required as $configKey => $envKey) {
        if (blank(config($configKey))) {
            throw new \RuntimeException(
                "Required configuration [{$configKey}] is missing. Set {$envKey} in your .env file."
            );
        }
    }
}

This is especially useful for services that your app simply cannot function without. You’ll get a clear error immediately instead of a null-pointer exception buried in a stack trace.


The .env.example as Documentation

Your .env.example file is documentation. Every variable your application can use should be in it, with a comment explaining what it does:

# Application
APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost

# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=

# Stripe Payment Gateway
# Get your keys from https://dashboard.stripe.com/apikeys
STRIPE_KEY=pk_test_
STRIPE_SECRET=sk_test_
STRIPE_WEBHOOK_SECRET=whsec_

# Error Reporting
# Comma-separated list of developer emails to receive exception alerts
MAIL_SUPPORT_ADDRESS=dev@yourcompany.com

A new developer should be able to read .env.example and understand every setting without asking anyone.


Environment-Specific Config Overrides

Sometimes you need a different configuration in testing vs. production. Use the config() helper within AppServiceProvider to override:

// In config/mail.php
'support' => [
    'address' => env('MAIL_SUPPORT_ADDRESS', ''),
],

// In .env
MAIL_SUPPORT_ADDRESS=dev@company.com,senior@company.com

// In ErrorReporter — reads it as a comma-separated list
$emails = config('mail.support.address', []);
if (is_string($emails)) {
    $emails = explode(',', $emails);
}

For test environments, use .env.testing:

# .env.testing
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
MAIL_SUPPORT_ADDRESS=  # Empty — don't send real emails during tests

Key Takeaways

  • Never call env() outside config/ files — this is non-negotiable. It will break after config:cache.
  • All third-party credentials go in config/services.php — don’t create a new file unless you have 5+ related settings.
  • Access config via config('key.subkey') everywhere in your application code.
  • Your .env.example is documentation — keep it up to date and well-commented.
  • Validate required config at boot for services your app can’t live without.

Tips and Gotchas

⚠️ Warning: env() called directly in application code (outside config/) breaks php artisan config:cache. When config is cached, env() always returns null. This is a silent production bug that works perfectly in development.

💡 Tip: Run php artisan config:cache locally occasionally to catch env() misuse before it reaches production.

🔥 Expert Note: Group all third-party service credentials in config/services.php — not separate per-service config files. One file to open, one place to look. Laravel’s built-in MailBroadcast, and Socialite integrations already follow this convention.

Further Reading


← Folder Structure | Next: AppServiceProvider — The Backbone →

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

One Comment