I was perusing the Laracasts forum yesterday when I spotted a thread I recalled seeing a long time ago that someone had resurrected: [L5] How to delete a file using Filesystem. The original poster was having an issue using the File facade to delete a file. What followed was a dozen posts attempting to answer the poster’s question and I think overall the mission was accomplished. After the latest post, I even offered my own answer. But, that got me thinking, “how many ways are there to skin this cat?” For fun, here is my canonical list of ways to delete a file using Laravel and PHP.
By default, Storage’s base or “root” path is storage_path('app')
. You can change this in config/filesystems.php
:
'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], // other disks are also configured here ],
For example, you could change “root” to the base path of your application:
'disks' => [ 'local' => [ 'driver' => 'local', 'root' => base_path(), ], // other disks are also configured here ],
The default disk is local so if you don’t change it by setting a different driver, that is what will be used along with the “root” path defined as above. Here’s a usage example:
use Illuminate\Support\Facades\Storage; Storage::delete('file.txt'); // Deletes /path/to/yourapp/storage/app/file.txt
The method returns true for success and false for failure. Be aware that you can’t rely on the return value when passing an array of paths as you will only get the return value for the last item. To ensure the file has been deleted you can check with File::exists
(and, yes, it’s just file_exists()
under the hood):
use Illuminate\Support\Facades\Storage; // For a single file $myFile = '/path/to/my/file.txt'; if (! Storage::exists($myFile)) { // do something } // For an array of file paths $myFiles = ['/path/to/1.txt','/path/to/2.txt','/path/to/3.txt']; foreach($myFiles as $file) { if (! Storage::exists($file)) { // do something } }
Here is the the concrete implementation:
public function delete($paths) { $paths = is_array($paths) ? $paths : func_get_args(); $success = true; foreach ($paths as $path) { try { if (! $this->driver->delete($path)) { $success = false; } } catch (FileNotFoundException $e) { $success = false; } } return $success; }
As you can see, the abstraction calls the delete method for the given driver. The driver then uses its native method of deleting the file.
use Illuminate\Support\Facades\File; $myFile = '/path/to/my/file.txt'; File::delete($myFile);
The operation will return true if it was successful or false if it was not. If you pass an array of paths, it will return true or false for the success/failure of deleting the last item in the array. If you need to know for certain that a file was deleted, call File::exists($myFile)
. If you passed an array of paths, you’ll have to loop over it and check each one as we did with the example for Storage
above.
The underlying code for the delete method is:
public function delete($paths) { $paths = is_array($paths) ? $paths : func_get_args(); $success = true; foreach ($paths as $path) { try { if (! @unlink($path)) { $success = false; } } catch (ErrorException $e) { $success = false; } } return $success; }
Don’t freak out about @unlink. The @ suppresses any emitted errors. The error is ultimately only helpful if you need to handle it. If you don’t, you’ll just fill your log file with errors. Note, however, how Laravel deals with it for inspiration.
Why File
? The most obvious reason is that Storage
has a predefined “root” path whereas you must pass the full path to File
. The advantage is you can pass a file at any path to File
. For example, you may have files you need to delete outside of your project entirely or may simply wish to delete a file higher up in the tree. You could define separate local disk to get around this limitation and, in most, cases that would be advisable. For example we could define a project root:
'disks' => [ 'local' => [ 'driver' => 'local', 'root' => base_path('app'), ], 'project-root' => [ 'driver' => 'local', 'root' => base_path(), ], // other disks ],
You would then use it like this:
// If for ex. base_path() = /home/vagrant/project // The path to file.txt would be /home/vagrant/project/file.txt $myFile = 'file.txt'; Storage::disk('project-root')->delete($myFile);
unlink()
unlink()
function and it can get the job done.
unlink('/path/to/your/file.txt');
As you might guess, if the file doesn’t exist, PHP will emit and log an error if the argument you supply to unlink()
is a path to a file that doesn’t exist or if you don’t have permission to delete the file or if the file is open in another handle. So a better way to call this may be:
$myFile = '/path/to/your/file.txt'; if (is_file($myFile) { unlink($myFile); }
It’s up to you to decide whether you want to @ suppress the error or not. Unfortunately, there is no simple way of determining whether the PHP user has permission to delete a file and it’s up to you to figure out if it’s possible another process has a handle on the file. For example, you might run into an issue if you try to delete a file that is being written at the same time by another process. Choose your own adventure as to how you want to deal with those scenarios, but have a look at this on SO for guidance: http://stackoverflow.com/questions/1241728/can-i-try-catch-a-warning.
If you know you’ll have permission and it doesn’t matter if the file exists or not and will not be caught up in another handle, you can suppress it. I know, I know, errors are meant to be handled, but…:
@unlink('/i/dont/care/if/this/file/exists/or/not.txt');
But, seriously, handle the error. In Laravel you can catch ErrorException
as illustrated in the File::delete()
and Storage::delete()
methods.
array_map()
array_map()
and unlink()
.
$files = ['/path/to/1.txt', '/path/to/2.txt', '/path/to/3.txt']; array_map('unlink, $files);
In addition, you can match a pattern of files for deletion with glob()
. glob()
will return an array of files and their paths that match the pattern provided.
array_map('unlink', glob('/path/to/*.txt'));
$myFile = '/path/to/my/file.txt'; if (is_file($myFile)) { $fh = fopen($myFile, 'w'); fclose($fh); }
When you open a file for writing (“w” mode), the file’s contents are emptied automatically. PHP does have ftruncate()
but using it for this purpose requires more code and I believe less is more.
use Symfony\Component\Process\Process; $myFile = '/path/to/my/file.txt'; if (is_file($myFile) { $cmd = "rm $myFile.txt"; $process = new Process($cmd); $process->run(); // Get the exit code and do something useful if you want if (! $process->getExitCode() === 0) { // exited with 1 or returned null because it hasn't terminated // do something } }
You can learn more about this component in the Laracast: Discover Symfony Components: The Console Component
exec()
exec()
can be used directly rather than wrapping it all up in a fancy process library.
$myFile = '/path/to/my/file.txt'; if (is_file($myFile)) { $output = []; $return_var = 0; exec("rm $myFile",$output, $return_var); } if ($return_var) { // it exited with a code of 1 which in this case means it failed // do something }
Oh, heck, you can use another program entirely with exec()
. Have Python? Go nuts!
$myFile = '/path/to/my/file.txt'; exec("python os.remove('" . $myFile . "')");
Doing basic file operations should be easy and Laravel certainly provides a great abstraction to help us with these everyday tasks. Still it’s good to know both how Laravel accomplishes these tasks as well as the alternatives. When it comes to deleting files, I’d stick with the Storage
and File
facades if for nothing else than the fact they mitigate some of the drawbacks of other methods. Let me know if I missed anything in the comments!
Leave a Comment