Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Env Laravel Package

m1/env

m1/env extends Laravel's environment handling, letting you load and cascade multiple .env files so shared, environment-specific and local values stay separate across local, staging and production setups.

View on GitHub
Deep Wiki
Context7

Getting Started

m1/env is a read-only parser: you hand it the contents of a .env file and it returns a PHP array with type casting applied. It does not load variables into getenv()/$_ENV, and it is not a replacement for Laravel's own vlucas/phpdotenv bootstrap. Use it when you need to inspect, validate, diff, or import .env files that Laravel itself does not own.

Install and first parse

composer require m1/env
use M1\Env\Parser;

$contents = file_get_contents(base_path('.env.example'));

$env = (new Parser($contents))->getContent();

// $env is an associative array
// APP_ENV=local         => 'local'
// APP_DEBUG=false       => false   (bool, not string)
// DB_PORT=3306          => 3306    (int, not string)

Where to look first

  1. The README examples — they show the casting rules, which are the main source of surprises.
  2. src/M1/Env/Parser.php — it is a small class; read the value-parsing method before trusting edge cases.
  3. The package tests — the fastest way to confirm how quoting, comments, and numbers actually behave in your installed version.
  4. M1\Env\Exception\ParseException — this is what malformed lines throw; catch it and add your file path, because the exception alone will not tell you which file failed.

First real use case: a deploy preflight check

Before php artisan config:cache runs in CI, parse the target environment's .env and fail fast if required keys are missing or empty. This catches the classic "deployed with .env.example" incident without booting the whole framework against that file.

$env = (new Parser(file_get_contents($path)))->getContent();

foreach (['APP_KEY', 'DB_HOST', 'DB_DATABASE', 'DB_PASSWORD'] as $key) {
    if (! array_key_exists($key, $env) || $env[$key] === null || $env[$key] === '') {
        throw new RuntimeException("Missing required env key: {$key}");
    }
}

Note the use of array_key_exists rather than empty(): parsed values can legitimately be false, 0, or '0', and empty() will reject them.

Version reality check

The last release was February 2020. The library is dependency-free and small, so it generally keeps working on modern PHP, but treat it as frozen: pin the major version in composer.json, run your test suite on your actual PHP version, and keep the parser behind a thin wrapper class so swapping to symfony/dotenv's parser or another implementation is a one-file change.

Implementation Patterns

Wrap the parser in a single service

Do not scatter new Parser(...) calls across commands and providers. A small wrapper gives you consistent error messages, path resolution, and a place to cache results.

namespace App\Support;

use M1\Env\Exception\ParseException;
use M1\Env\Parser;

class EnvFileReader
{
    /** @var array<string, array> */
    private array $cache = [];

    public function read(string $path): array
    {
        if (isset($this->cache[$path])) {
            return $this->cache[$path];
        }

        if (! is_readable($path)) {
            throw new \RuntimeException("Env file is not readable: {$path}");
        }

        // Strip a UTF-8 BOM; otherwise the first key silently becomes "\xEF\xBB\xBFAPP_NAME".
        $contents = ltrim(file_get_contents($path), "\xEF\xBB\xBF");

        try {
            return $this->cache[$path] = (new Parser($contents))->getContent();
        } catch (ParseException $e) {
            // Never include values in the message — they are secrets.
            throw new \RuntimeException("Could not parse env file: {$path}", 0, $e);
        }
    }
}

Register it as a singleton in a service provider. Because it is request-scoped in Laravel, the in-memory cache is usually enough; for long-running queue workers that read many tenant files, use a bounded cache or Cache::remember with a short TTL.

Diff .env files to catch configuration drift

This is the highest-value day-to-day pattern: compare production/staging files against .env.example or against each other.

$expected = $reader->read(base_path('.env.example'));
$actual   = $reader->read(base_path('.env'));

$missing = array_diff(array_keys($expected), array_keys($actual));
$extra   = array_diff(array_keys($actual), array_keys($expected));

// Normalize before comparing values, because auto-casting produces different types:
$normalize = fn (array $env) => array_map(
    fn ($v) => is_bool($v) ? ($v ? 'true' : 'false') : (string) $v,
    $env
);

$changed = array_keys(array_diff_assoc($normalize($expected), $normalize($actual)));

Expose it as php artisan env:diff {file?} so it can run in CI or as a deploy step. This turns a class of "works on my machine" incidents into a failing pipeline.

Import an external .env into Laravel config

When migrating a legacy app or integrating a service that ships its own .env, parse it and merge it under a namespaced config key rather than mutating the environment.

$legacy = app(EnvFileReader::class)->read(storage_path('legacy/.env'));

config([
    'services.legacy' => [
        'host'     => $legacy['DB_HOST'] ?? null,
        'port'     => (int) ($legacy['DB_PORT'] ?? 3306),
        'database' => $legacy['DB_DATABASE'] ?? null,
    ],
]);

Important: parsing does not make env('DB_HOST') return anything new. env() reads the repository loaded at bootstrap. Consume the array directly or via config(). Also remember that php artisan config:cache snapshots config at build time — if this file changes at runtime, you must re-cache or read it dynamically.

Use fixtures in tests

The parser is ideal for testing configuration-reading logic without touching the filesystem or the process environment:

$env = (new Parser("APP_DEBUG=false\nDB_PORT=3306\nAPP_NAME=\"My App\""))->getContent();

$this->assertFalse($env['APP_DEBUG']);
$this->assertSame(3306, $env['DB_PORT']);
$this->assertSame('My App', $env['APP_NAME']);

Write fixtures that cover quoted numbers, # inside values, empty values, and base64 keys. Those are the cases that break in production.

Preserve strings when types matter

If downstream code expects strings (ports, IDs, version numbers), either quote in the file (DB_PORT="3306") or normalize immediately after parsing. Do the normalization once in the wrapper, document it, and never rely on the raw parser output being uniformly typed.

Gotchas and Tips

Auto-casting is the number one source of bugs. Unquoted true/false become booleans, null becomes null, and numeric-looking values become int/float. That means APP_VERSION=1.0 becomes the float 1.0, which string-concatenates to "1". DB_PORT=3306 becomes an int, so a strict === '3306' comparison fails. Phone numbers, ZIP codes, and zero-padded IDs lose their leading zeros. Quoted values are the escape hatch — wrap the value in single or double quotes to keep it a string.

Single vs double quotes are not equivalent. Single quotes are literal; double quotes interpret escape sequences (\n, \t, \", \\). For Windows paths, regexes, and anything containing backslashes, prefer single quotes — an unrecognized escape in double quotes may drop the backslash.

Unquoted # starts a comment. PASSWORD=abc#123 will likely parse as abc. Quote any value containing #, spaces, or shell-significant characters.

Values are split on the first =. Base64 values with trailing = padding usually survive, but when in doubt, quote them. Values containing spaces must be quoted.

Empty values are ambiguous. KEY= may resolve to null under auto-casting rather than ''. Use KEY="" when you need a genuine empty string, and test this against your installed version.

BOM and line endings. A UTF-8 BOM will corrupt the first key, producing an "undefined index" mystery. Strip it in your wrapper. CRLF is generally tolerated, but normalize if you see phantom parse failures.

Key naming. Stick to [A-Z0-9_]. Keys containing dots or dashes may not parse the way you expect; if you must represent nested config, encode it and expand in code.

No export prefix. Shell-style export KEY=value lines are not part of the format. Do not assume compatibility with files generated for source.

Interpolation: verify, don't assume. Do not assume phpdotenv-style ${VAR} expansion. Treat values as literal unless your installed version's tests prove otherwise, and resolve dependencies explicitly in PHP after parsing. This is safer anyway, because it makes the resolution order visible.

Duplicates: last one wins. Do not depend on it; a duplicate key is almost always a merge accident. Your diff command should flag duplicates by comparing against a line-based scan if this matters.

Do not putenv() the parsed values. It pollutes the process, breaks test isolation, and can shadow Laravel's own environment repository. Keep the parsed array as data and pass it through config() or method arguments.

Handle ParseException with context. Catch it in the wrapper, attach the file path, and log the line number and key if available — never the value. A malformed line in a 200-line production .env is otherwise painful to locate; a quick preg_split('/\R/', $contents) and a per-line trial parse will find the offender.

Security. These files contain credentials. Keep them outside public/, use 0600 permissions, never expose parsed arrays through a route or log, and be careful with dd()/dump() in commands that run in CI (CI logs are often broadly readable).

Extension points are limited. The parser is essentially a single class, and its internals may be private, so subclassing to customize casting is unreliable. Post-process the returned array in your wrapper instead, and keep the third-party class behind that wrapper so a future swap (e.g., to symfony/dotenv) does not ripple through your codebase.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings