Skip to content

Commit 4fbaa94

Browse files
committed
Fix null-safety bugs, drop mixed types, drop Laravel 10, add test coverage
- readFile() could return false from a failed file_get_contents() where string was promised; now guarded. update() silently wrote null to the .env file if preg_replace() failed; now falls back to the original content instead. - set(), update(), setOrUpdate(), get(), and formatValue() now type their value parameters as string|int|float|bool|null instead of mixed. - Drop Laravel 10 support (minimum is now Laravel 11); bump PHPStan to level 8 and rector.php to target the Laravel 11 floor. - Add tests for getEnvFile() and the service provider's container binding; expand ArchTest.php; rename ExampleTest.php to EnvEditorTest.php.
1 parent 333c607 commit 4fbaa94

8 files changed

Lines changed: 83 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@
22

33
All notable changes to `env-editor-laravel` will be documented in this file.
44

5-
## Unreleased
5+
## v3.0.0 - 2026-07-21
6+
7+
### Fixed
8+
- `readFile()` could return `false` from a failed `file_get_contents()` where `string` was promised; now guarded.
9+
- `update()` silently wrote `null` to the `.env` file if `preg_replace()` failed (e.g. a PCRE backtrack-limit error); it now falls back to the original content instead.
10+
11+
### Changed
12+
- `set()`, `update()`, `setOrUpdate()`, `get()`, and `formatValue()` now type their value parameters as `string|int|float|bool|null` instead of `mixed`, matching the actual set of values a `.env` value can hold.
13+
14+
### Removed
15+
- Dropped Laravel 10 support (minimum is now Laravel 11)
16+
17+
## v2.0.0 - 2026-01-03
618

719
### Added
820
- `get()` method to read individual .env values with default support

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
Simple and powerful ENV file editor for your Laravel application.
99

10+
## Requirements
11+
12+
- PHP 8.2, 8.3, 8.4, or 8.5
13+
- Laravel 11, 12, or 13
14+
1015
## Installation
1116

1217
You can install the package via composer:

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
],
1818
"require": {
1919
"php": "^8.2|^8.3|^8.4|^8.5",
20-
"illuminate/support": "^10.0|^11.0|^12.0|^13.0"
20+
"illuminate/support": "^11.0|^12.0|^13.0"
2121
},
2222
"require-dev": {
2323
"driftingly/rector-laravel": "^1.0|^2.0",
2424
"larastan/larastan": "^2.0|^3.0",
2525
"laravel/pint": "^1.26",
26-
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
26+
"orchestra/testbench": "^9.0|^10.0|^11.0",
2727
"pestphp/pest": "^2.0|^3.0|^4.0",
2828
"pestphp/pest-plugin-laravel": "^2.0|^3.0|^4.0"
2929
},

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ includes:
33
- vendor/nesbot/carbon/extension.neon
44

55
parameters:
6-
level: 4
6+
level: 8
77
paths:
88
- src
99
tmpDir: build/phpstan

rector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
)
2323
->withPhpSets(php82: true)
2424
->withImportNames(importDocBlockNames: false, removeUnusedImports: true)->withSets([
25-
LaravelLevelSetList::UP_TO_LARAVEL_100,
25+
LaravelLevelSetList::UP_TO_LARAVEL_110,
2626
LaravelSetList::LARAVEL_ARRAYACCESS_TO_METHOD_CALL,
2727
LaravelSetList::LARAVEL_ARRAY_STR_FUNCTION_TO_STATIC_CALL,
2828
LaravelSetList::LARAVEL_CODE_QUALITY,

src/EnvEditor.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public function __construct(?string $envFile = null)
1818
$this->backupDir = dirname($this->envFile).'/.env.backup';
1919
}
2020

21-
public function set(string $key, $value): bool
21+
public function set(string $key, string|int|float|bool|null $value): bool
2222
{
2323
$formattedValue = $this->formatValue($value);
2424

@@ -29,19 +29,19 @@ public function set(string $key, $value): bool
2929
return $this->append($key, $formattedValue);
3030
}
3131

32-
public function update(string $key, $value): bool
32+
public function update(string $key, string|int|float|bool|null $value): bool
3333
{
3434
throw_unless($this->keyExists($key), Exception::class, sprintf("Key '%s' does not exist in .env file", $key));
3535

3636
$formattedValue = $this->formatValue($value);
3737
$envContent = $this->readFile();
3838

39-
$envContent = preg_replace(sprintf('/^%s=.*$/m', $this->escapeKey($key)), sprintf('%s=%s', $key, $formattedValue), $envContent);
39+
$replaced = preg_replace(sprintf('/^%s=.*$/m', $this->escapeKey($key)), sprintf('%s=%s', $key, $formattedValue), $envContent);
4040

41-
return $this->writeFile($envContent);
41+
return $this->writeFile($replaced ?? $envContent);
4242
}
4343

44-
public function setOrUpdate(string $key, $value): bool
44+
public function setOrUpdate(string $key, string|int|float|bool|null $value): bool
4545
{
4646
if ($this->keyExists($key)) {
4747
return $this->update($key, $value);
@@ -65,7 +65,7 @@ public function remove(string $key): bool
6565
return $this->writeFile(trim((string) $envContent).PHP_EOL);
6666
}
6767

68-
public function get(string $key, $default = null)
68+
public function get(string $key, string|int|float|bool|null $default = null): string|int|float|bool|null
6969
{
7070
$envContent = $this->readFile();
7171
$pattern = sprintf('/^%s=(.*)$/m', $this->escapeKey($key));
@@ -77,6 +77,9 @@ public function get(string $key, $default = null)
7777
return $default;
7878
}
7979

80+
/**
81+
* @return array<string, string>
82+
*/
8083
public function getAll(): array
8184
{
8285
$envContent = $this->readFile();
@@ -132,6 +135,9 @@ public function restore(string $backupFile): bool
132135
return true;
133136
}
134137

138+
/**
139+
* @return array<int, string>
140+
*/
135141
public function listBackups(): array
136142
{
137143
if (! is_dir($this->backupDir)) {
@@ -170,15 +176,17 @@ protected function readFile(): string
170176
return '';
171177
}
172178

173-
return file_get_contents($this->envFile);
179+
$content = file_get_contents($this->envFile);
180+
181+
return $content === false ? '' : $content;
174182
}
175183

176184
protected function writeFile(string $content): bool
177185
{
178186
return file_put_contents($this->envFile, $content) !== false;
179187
}
180188

181-
protected function formatValue($value): string
189+
protected function formatValue(string|int|float|bool|null $value): string
182190
{
183191
$value = strval($value);
184192

tests/ArchTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
declare(strict_types=1);
44

5+
arch()->preset()->php();
6+
arch()->preset()->security();
7+
58
it('will not use debugging functions')
69
->expect(['dd', 'dump', 'ray'])
710
->each->not->toBeUsed();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
}
2424
});
2525

26+
it('exposes the path to the underlying env file', function (): void {
27+
expect($this->editor->getEnvFile())->toBe($this->envFile);
28+
});
29+
30+
it('resolves to a singleton bound to the application base path .env file', function (): void {
31+
$editor = $this->app->make(EnvEditor::class);
32+
33+
expect($editor)->toBeInstanceOf(EnvEditor::class)
34+
->and($editor->getEnvFile())->toBe($this->app->basePath('.env'))
35+
->and($this->app->make(EnvEditor::class))->toBe($editor);
36+
});
37+
2638
it('can get a value from env file', function (): void {
2739
expect($this->editor->get('APP_NAME'))->toBe('Laravel');
2840
expect($this->editor->get('APP_ENV'))->toBe('local');
@@ -175,3 +187,33 @@
175187
expect($this->editor->get('APP_ENV'))->toBe('local')
176188
->and($this->editor->get('APP_DEBUG'))->toBe('true');
177189
});
190+
191+
it('can set boolean values', function (): void {
192+
$this->editor->set('FEATURE_ON', true);
193+
$this->editor->set('FEATURE_OFF', false);
194+
195+
expect($this->editor->get('FEATURE_ON'))->toBe('1')
196+
->and($this->editor->get('FEATURE_OFF'))->toBe('');
197+
});
198+
199+
it('returns default and an empty list when the env file does not exist at all', function (): void {
200+
$missingPath = sys_get_temp_dir().'/env_editor_missing_'.uniqid().'.env';
201+
$editor = new EnvEditor($missingPath);
202+
203+
expect($editor->get('ANYTHING', 'fallback'))->toBe('fallback')
204+
->and($editor->getAll())->toBe([])
205+
->and($editor->has('ANYTHING'))->toBeFalse();
206+
});
207+
208+
it('can create a new env file by setting a key when none exists yet', function (): void {
209+
$missingPath = sys_get_temp_dir().'/env_editor_new_'.uniqid().'.env';
210+
$editor = new EnvEditor($missingPath);
211+
212+
$result = $editor->set('FRESH_KEY', 'fresh_value');
213+
214+
expect($result)->toBeTrue()
215+
->and(file_exists($missingPath))->toBeTrue()
216+
->and($editor->get('FRESH_KEY'))->toBe('fresh_value');
217+
218+
unlink($missingPath);
219+
});

0 commit comments

Comments
 (0)