Deleting Files with Laravel and PHP

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.

  1. Laravel: Use the Storage facade
    The documented way of deleting files is to use the Storage facade. The reason for this is that you get a consistent interface for several pre-defined file systems and can even create custom drivers for any of the adapters provided by Flysystem.

    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.

  2. Laravel: Use the File facade
    You may pass an absolute path or an array of absolute paths to the delete method of the File facade.

    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);
    
  3. PHP: Use unlink()
    Good old PHP has the venerable, log-filling 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.

  4. PHP: Use array_map()
    You can delete an array of files using 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'));
    
  5. PHP: Truncate the file
    Now we’re getting desperate. You can truncate the file to zero length which will empty it of contents but not delete the file itself:

    $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.

  6. Laravel: Use the Symfony Process Component
    When you want to run a process outside of PHP, you can reach for Symfony’s process component which is included in Laravel.

    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

  7. PHP: Use exec()
    Our old pal 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

Your email address will not be published. Required fields are marked *