nette/safe-stream
Nette SafeStream provides a stream wrapper for atomic file operations, ensuring files are never left partially written or corrupted. It supports file locking and safe concurrent access, making it ideal for writing configs, caches, and logs reliably in PHP applications.
composer require nette/safe-stream
safe:// operation:
use Nette\Utils\SafeStream;
SafeStream::register();
In Laravel this belongs in AppServiceProvider::boot() or in bootstrap/app.php. Registration is process-wide; verify with:
in_array('safe', stream_get_wrappers(), true);
$realPath = storage_path('app/settings.json');
$url = 'safe://' . str_replace('\\', '/', $realPath); // safe:///var/www/... on Unix
file_put_contents($url, $json);
$contents = file_get_contents($url);
src/SafeStream.php — the class is small; read it before relying on any stream function.README.md and CHANGELOG.md — confirm supported PHP versions and recent behavioral changes.SafeStream does not introduce a new API. It registers a safe:// stream wrapper so existing native functions (file_get_contents, file_put_contents, fopen, unlink, rename, etc.) operate on the underlying file with atomic write semantics.Register once in bootstrap, not per call.
Calling SafeStream::register() repeatedly is harmless, but scattering it through the codebase makes failures harder to trace. Register it in one bootstrap location and treat safe:// as an application capability.
Wrap URL construction in a helper. The leading-slash behavior is easy to get wrong. Centralize it:
function safe_path(string $absolutePath): string
{
if (!str_starts_with($absolutePath, '/') && !preg_match('#^[A-Za-z]:/#', $absolutePath)) {
throw new InvalidArgumentException('Absolute path required');
}
return 'safe://' . str_replace('\\', '/', $absolutePath);
}
Use it for whole-file atomic replacement. The canonical pattern is write-then-rename under the hood; you do not need to implement that yourself:
final class AtomicJsonStore
{
public function __construct(private string $path) {}
public function read(): array
{
$raw = @file_get_contents($this->url());
return $raw === false ? [] : json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
}
public function write(array $data): void
{
$json = json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
if (file_put_contents($this->url(), $json) === false) {
throw new RuntimeException("Unable to write {$this->path}");
}
}
private function url(): string
{
return safe_path($this->path);
}
}
This is the highest-value pattern: configuration files, small state files, generated manifests, and cache metadata that must never be observed half-written.
Use fopen for large files.
file_get_contents loads everything into memory. For larger payloads, stream through the wrapper:
$in = fopen(safe_path($source), 'rb');
$out = fopen($destination, 'wb');
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);
Read and write through the wrapper consistently where correctness matters. Atomic rename means a plain reader usually sees either the old or the new complete file, which is a major benefit. However, if any writer bypasses the wrapper and writes directly, wrapper-based readers lose the guarantee. Standardize writers first, then readers.
Perform stat operations on the real path.
Use file_exists, filesize, filemtime, is_dir, glob, and scandir against the real filesystem path, not the safe:// URL. The wrapper is intended for stream I/O, not as a full replacement for filesystem metadata APIs.
Integrate as a small infrastructure service.
In Laravel, bind an AtomicFileStore or AtomicJsonStore in the container and inject it into the services that need it. This keeps safe:// string manipulation out of controllers and jobs, and gives you one place to add logging, permissions handling, or fallbacks.
Use it behind queue jobs and scheduled tasks. A common day-to-day use is writing the result of a scheduled command or queue job to a status file while another request reads it. Atomic replacement prevents readers from consuming truncated JSON or half-written CSV.
Test the behavior, not the wrapper. Write a test that writes a large payload and asserts the file is either absent or fully valid, then a test that readers never observe a partial JSON document. The wrapper is an implementation detail; your repository/store contract is what matters.
'safe://' . '/var/www/file' becomes safe:///var/www/file. 'safe://' . 'relative/file' resolves relative to the current working directory, which is fragile in long-running workers.safe://C:/path/file. Avoid parse_url-style parsing of safe:// paths.SafeStream provides atomicity, not path confinement. User-supplied paths still need validation against traversal and against your allowed directories.flock() around the read/modify/write sequence.FILE_APPEND, partial writes, and random-access updates are not the contract; use a lock or a real database for those.safe:// are unreliable. file_exists, filesize, filemtime, realpath, glob, and scandir should use the real path. Stat caching and wrapper limitations can produce misleading results.mkdir the real directory first; the wrapper will not create missing directories.0600 may become 0644. If permissions matter, chmod the destination after the write or manage umask deliberately.error_get_last().include or require safe:// paths. Opcache and __DIR__ behavior become confusing. Include the real path; use SafeStream for data files, not PHP code.safe protocol is process-wide; another library may register the same name. register() typically skips if already present, so verify which wrapper is active if behavior looks wrong.safe prefix and is not designed for subclassing or registering under another protocol name. If you need custom behavior, wrap it in your own service rather than extending the wrapper.safe is in stream_get_wrappers(), confirm the real parent directory is writable, run the same operation against the real path to isolate wrapper issues, and inspect error_get_last() when a native function returns false.How can I help you explore Laravel packages today?