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

Temporary Directory Laravel Package

spatie/temporary-directory

Spatie's PHP package quickly creates, uses, and deletes temporary directories in the system's temp folder. Chainable methods handle custom naming, path helpers, and automatic cleanup of every file inside when the directory is deleted.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (nothing to publish, no service provider, no config file):

composer install spatie/temporary-directory

Requires PHP 8.0+ (2.x dropped 7.x). Add it to require if it's part of runtime code, require-dev if it's only used in tests/seeders.

Look here first: the TemporaryDirectory class itself — it's ~150 lines, so read the source once instead of guessing. Then the two changelog entries that matter most for day-to-day safety: 2.3.1 (configurable permissions) and 2.2.0/2.2.1 (deleteWhenDestroyed plus the strict-mode deletion fix).

First use case — anything that needs a scratch directory that must not leak into storage/:

use Spatie\TemporaryDirectory\TemporaryDirectory;

$temp = TemporaryDirectory::make();          // /tmp/1719234567
$csv  = $temp->path('report.csv');           // string, file does NOT exist yet

file_put_contents($csv, $rows);

Storage::disk('local')->put('exports/report.csv', fopen($csv, 'r'));

$temp->delete();                             // whole tree gone, including report.csv

The mental model: TemporaryDirectory is a path factory plus a scoped cleanup handle. It does not write files, buffer streams, or wrap a filesystem disk. You still use file_put_contents, Storage, ZipArchive, or Symfony's Filesystem — you just stop hardcoding /tmp/foo.

Implementation Patterns

Export / report generation (the most common Laravel case). Build the artifact inside the temp dir, stream it to its real destination, then delete:

public function export(OrderQuery $query): string
{
    $temp = TemporaryDirectory::make();
    $file = $temp->path('orders.csv');

    $handle = fopen($file, 'w');
    foreach ($query->cursor() as $order) {
        fputcsv($handle, $order->toExportRow());
    }
    fclose($handle);

    Storage::disk('s3')->put($key = "exports/orders-{$query->hash()}.csv", fopen($file, 'r'));
    $temp->delete();

    return $key;
}

Wrap in try/finally if anything between make() and delete() can throw — see Gotchas.

Same-filesystem moves. Storage::put() copies the bytes. If the destination is on the same volume, put the temp dir there and rename() instead, which is O(1) regardless of file size:

$temp = TemporaryDirectory::make(storage_path('app/exports-tmp'));
// ...build $temp->path('big.zip')
rename($temp->path('big.zip'), storage_path('app/exports/big.zip'));
$temp->delete();

Processing inbound files. Uploaded archives, chunked imports, image conversion — extract/build into a temp dir rather than a timestamped folder under storage/app that nobody ever cleans up:

$temp = TemporaryDirectory::make();
(new ZipArchive)->open($upload->getRealPath())->extractTo($temp->path());
// walk $temp->path() with File::files(), import, then $temp->delete();

Scoped cleanup inside a service or job. deleteWhenDestroyed() makes the destructor do the work, which keeps the happy path readable:

public function handle(): void
{
    $temp = TemporaryDirectory::make()->deleteWhenDestroyed();

    foreach ($this->files as $file) {
        File::copy($file, $temp->path(basename($file)));
    }

    $this->process($temp->path());
    // no explicit delete needed
}

This is a good fit for services and short-lived jobs, not for anything you store on $this or return to a caller.

Testing. Replace Storage::fake() where the code under test uses real fopen/ZipArchive/Imagick:

protected function setUp(): void
{
    parent::setUp();
    $this->temp = TemporaryDirectory::make();
}

protected function tearDown(): void
{
    $this->temp->delete();
    parent::tearDown();
}

public function test_it_converts_the_image(): void
{
    $input = $this->temp->path('in.png');
    File::ensureDirectoryExists(dirname($input));
    copy(base_path('tests/fixtures/in.png'), $input);

    (new Converter)->convert($input, $this->temp->path('out.jpg'));

    $this->assertFileExists($this->temp->path('out.jpg'));
}

Deterministic names for debugging. name('my-app-export')->create() gives you a predictable path so you can tail -f it or inspect it after a failure, instead of hunting a timestamp dir. Pair with force() only when you've scoped the location yourself.

Fluent chains. Since 2.4.0 the builder methods return static rather than self, so subclassing and IDE inference work through location()->name()->permission()->create().

Gotchas and Tips

path() creates nothing. $temp->path('dumps/datadump.dat') returns a string; the dumps directory does not exist and file_put_contents will fail. Call File::ensureDirectoryExists(dirname($path)) or mkdir() first. This is the single most common first-hour bug.

->name() + ->force() is destructive, and ->location() decides what it destroys. force() removes an existing directory at that path. (new TemporaryDirectory(storage_path('app')))->name('app')->force()->create() will delete real application data. Only use force() with a location you exclusively own (e.g. storage_path('app/tmp')), never with a user-supplied or config-derived path.

Default permissions are 0777 (umask-adjusted). Since 2.3.1 you can pass ->permission(0755). In hardened/production containers, 0777 temp dirs are often flagged by security scans — set 0755 explicitly, or verify your umask makes it moot. Files created inside inherit your own fopen/Storage behavior, not this setting.

/tmp is usually a different filesystem. rename() across devices throws a warning/fails, Storage::put() silently copies gigabytes, and tmpfs contents count against RAM. If you're producing large artifacts, create the temp dir next to the final destination (TemporaryDirectory::make(storage_path('app/tmp'))).

Explicit names collide; default names don't. The package guarantees unique timestamped directories even under heavy parallel load (fixed in 1.1.5), but name('export') is entirely your responsibility — the second concurrent request throws PathAlreadyExists. For queued/parallel work, skip name() or suffix it with a UUID.

Cleanup is not guaranteed by destructors. deleteWhenDestroyed() won't run on exit, fatal errors, SIGKILL, OOM kills, or PHP's shutdown ordering. For anything holding sensitive data or large payloads, prefer explicit delete() in a finally:

$temp = TemporaryDirectory::make();
try {
    // ...
} finally {
    $temp->delete();
}

Long-running queue workers accumulate. horizon/queue:work processes live for hours; a destructor-based temp dir in a looping job can pile up until GC runs. Also watch gc_collect_cycles — the package forces a GC cycle on cleanup, and holding the object inside a circular reference delays deletion.

Broken symlinks and directory iteration. Deleting trees with broken symlinks failed pre-1.2.3; empty() uses FilesystemIterator and is recursive since 1.2.2. If you're on an older 1.x pin, upgrade rather than working around it.

Don't interpolate user input into path(). $temp->path($request->input('filename')) with ../../etc/passwd escapes the directory entirely — the package does no path sanitization. Always basename() or use Str::random() for the filename and keep the extension mapping in your own code.

Check existence instead of catching exceptions. PathAlreadyExists is a reasonable control-flow signal, but 2.1.1 added a direct existence check — prefer it over try/catch for "is this already there?" logic, and prefer try/catch (PathAlreadyExists $e) over catching \Exception now that 2.0.0 introduced dedicated exception classes.

empty() vs delete(). empty() removes the contents but leaves the directory (and the object) usable; delete() removes the directory itself. Deleting twice, or calling empty() on a directory that was removed out from under you, is worth guarding in maintenance commands.

On Windows / network mounts, open file handles block deletion. Close every fopen handle and ZipArchive before delete(), or you'll get a partially-deleted tree and a confusing warning.

Extension points are thin, by design. There's no config, no facade, and no filesystem abstraction to swap. If you need more (auto-registered cleanup, named pools, S3-backed temp storage), write a small TempDirectoryFactory in your app that wraps this package — don't fork it, since the whole value here is that it's a 150-line dependency with a stable API.

Pin sensibly. ^2.4 gets you static return types and permission support. Anything below 2.2.1 has the strict-mode deletion bug — if your app runs with strict_types or you saw temp dirs survive delete(), that's the cause.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log