pixelfed/dotenv-editor
A PHP package from PixelFed for editing .env files programmatically, letting you read, update, and manage environment variables from application code instead of handling the file manually.
This is a tiny wrapper around vlucas/phpdotenv's parser plus a line-buffer writer. It lets you read, mutate, and persist .env values from PHP. Treat it as ~300 lines of vendored code rather than a framework dependency — that framing determines how you should adopt it.
Install
composer require pixelfed/dotenv-editor
The pixelfed fork exists mainly to keep the original sven/laravel-dotenv-editor working on newer phpdotenv constraints. Before installing, open the fork's composer.json and confirm the illuminate/support and vlucas/phpdotenv ranges against your Laravel version. If they conflict, you're better off copying the source into app/Support/ and owning it — the surface area is small enough.
Registration
On Laravel 5.5+ package discovery should pick up the service provider. If not, add it to config/app.php:
'providers' => [
Sven\DotenvEditor\DotenvEditorServiceProvider::class,
],
'aliases' => [
'DotenvEditor' => Sven\DotenvEditor\Facades\DotenvEditor::class,
],
The fork kept the upstream namespace in most releases; confirm the FQCN by listing vendor/pixelfed/dotenv-editor/src before you write imports. That one check saves fifteen minutes of "class not found".
First use case
The canonical reason to install this is a first-run installer or an admin settings page that has to persist credentials the app can't already know. Start there:
DotenvEditor::load()
->setKey('DB_DATABASE', $request->database)
->setKey('DB_USERNAME', $request->username)
->setKey('DB_PASSWORD', $request->password)
->save();
Artisan::call('config:clear');
Read the source of DotenvEditor::class once, top to bottom, before you build on it. Note which methods return $this (chaining) and which return raw values — the API is not perfectly consistent across versions.
The three-call contract
Almost every correct usage is load() → mutate → save(). load() parses the file into an in-memory buffer of lines; mutations edit that buffer; save() serializes it back with file_put_contents. If you skip load(), the buffer is empty and save() will truncate your .env to nothing. Wrap it in a service so nobody on the team calls it raw:
class EnvFile
{
public function set(array $pairs): void
{
DotenvEditor::load();
foreach ($pairs as $key => $value) {
DotenvEditor::setKey($key, (string) $value);
}
DotenvEditor::save();
}
}
Installer / setup wizard
The most defensible use. Collect DB and mail settings, write them, clear config, then redirect to a fresh request so the new values are read by a clean process:
$this->envFile->set($validated);
Artisan::call('config:clear');
Artisan::call('cache:clear');
return redirect('/login'); // new request reads the new .env
Do not try to reconfigure the current request's DB connection from the newly written file — set config() in memory if you must, but the redirect approach avoids a class of confusing bugs.
Admin settings panel
Split the page into "editable keys" and "everything else". Maintain an allowlist:
const EDITABLE = ['MAIL_HOST', 'MAIL_PORT', 'MAIL_USERNAME', 'MAIL_PASSWORD', 'MAIL_ENCRYPTION'];
Iterate the allowlist, skip keys whose submitted value is the placeholder mask (••••••••), so a password field left untouched doesn't get overwritten with literal bullets. This is the single most common bug in these panels.
Artisan command wrapper
Expose it to ops so nobody hand-edits production .env files:
// php artisan env:set MAIL_HOST smtp.example.com
DotenvEditor::load()->setKey($key, $value)->save();
$this->info("Set {$key}. Run `php artisan config:clear` on all nodes.");
Make the command dry-run by default (--force to write) and print a diff.
Backups
Before any write, copy .env yourself — don't assume the package does it:
File::copy(base_path('.env'), storage_path('app/env-backups/'.now()->timestamp.'.env'));
Keep a restore command. You will need it.
Testing
Point the editor at a fixture file rather than the real .env:
DotenvEditor::load(base_path('tests/fixtures/.env.testing'));
If the version you're on hardcodes base_path('.env'), bind the config or refactor the class to accept a path — worth the ten minutes.
Changes don't take effect in-process. env() reads the loaded repository and config() is populated at boot. Writing the file does nothing to the running app. You need config:clear (or config:cache if you're cached) plus a process restart for queue workers and Octane. If config:cache is in play in production, .env is not read at all — your writes are inert until you recache.
Quoting and escaping. Values with spaces, #, =, quotes, or newlines need to be wrapped. The package handles common cases but not all; validate at the boundary (reject whitespace-only, strip control characters) and after writing, reload the file and assert getKey() returns what you wrote. A silent round-trip mismatch is worse than a crash.
Comments and ordering. setKey rewrites the matching line in place and appends new keys at the end, preserving comments and order. setContent()/full-content writes will not. Prefer setKey/deleteKey over content rewrites, and don't be surprised when save() reflows a hand-formatted file.
Duplicate keys. If .env has MAIL_HOST twice, getKey returns the first and setKey updates the first; the second still wins at parse time in some loaders. Deduplicate as part of your write path or you'll chase a "my change didn't apply" ghost.
Concurrency. There's no file locking. Two simultaneous admin saves can interleave and corrupt the file. Serialize writes (a queue job with WithoutOverlapping, or a cache lock) and keep the operation rare — this is not a per-request write path.
Permissions. The PHP-FPM user must own or be able to write .env. Deploy tooling often sets 600 deploy:deploy, which silently fails for www-data. Check is_writable() and fail loudly with a message that names the file and user.
Security. An .env write endpoint is remote code execution waiting to happen (imagine APP_KEY, DB_HOST pointed at an attacker's MySQL, or a crafted value). Put it behind admin auth, CSRF, an allowlist, rate limiting, and audit logging of key + old/new value (redact secrets in the log). Never expose it in a public-facing app.
Package health. Last release 2018, zero dependents listed, and it's a fork. Budget for it to break on the next phpdotenv major. The mitigation is cheap: it's small enough to vendor, and if it ever blocks you, replacing it with a ~40-line class around Dotenv\Parser plus file_put_contents is an afternoon. Do that proactively if you're on Laravel 10+ and hitting constraint conflicts rather than pinning your way out of it.
How can I help you explore Laravel packages today?