Wrapping Up With File DELETE


In this video we are going to cover off how to DELETE a File resource from our API. This is a very similar process to that which we have seen before with the difference this time being that we also need to ensure the file data is removed from the storage system.

There are two Behat scenarios that cover the deletion of a File resource, which are:

# src/AppBundle/Features/file.feature

    Scenario: User can DELETE a File
      When I send a "DELETE" request to "/accounts/a1/files/f1"
      Then the response code should be 204
       And the "File" with id: f1 should have been deleted
       And the file with internal name: "intfile1" should have been deleted

    Scenario: User cannot DELETE a File they do not own
      When I send a "DELETE" request to "/accounts/a3/files/f2"
      Then the response code should be 403
      And the file with internal name: "intfile2" should not have been deleted

And as these tests cover a couple of new steps (And the file with internal name: "intfile1" should (not) have been deleted) it would make sense to quickly cover the code behind that actually runs these steps:

<?php

// src/AppBundle/Features/Context/FileStorageContext.php

namespace AppBundle\Features\Context;

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use League\Flysystem\Filesystem;

class FileStorageContext implements Context, SnippetAcceptingContext
{
    use \Behat\Symfony2Extension\Context\KernelDictionary;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * FileStorageContext constructor.
     * @param Filesystem $filesystem
     */
    public function __construct(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;
    }

    /**
     * @Then the file with internal name: :internalName should have been deleted
     */
    public function theFileWithInternalNameShouldHaveBeenDeleted($internalName)
    {
        $this->filesystem->assertAbsent($internalName);
    }

    /**
     * @Then the file with internal name: :internalName should not have been deleted
     */
    public function theFileWithInternalNameShouldNotHaveBeenDeleted($internalName)
    {
        $this->filesystem->assertPresent($internalName);
    }
}

Again, it is worth pointing out that I am not following Behat's best practices for the use of a Context here, but it works for me.

What's really nice here is that FlySystem comes with a couple of very handy methods for aiding us in our tests:

assertAbsent and assertPresent. You can see the code for these by clicking here.

As we [covered in the POST video], we are making use of multiple file system abstractions, depending on our Symfony environment. This means we can use local storage during our dev and test runs, but use Amazon S3, DropBox, or some other supported service in production.

The assertAbsent and assertPresent methods will work on any of the supported storage systems. In our case we are only calling these methods in the test environment, but it's still really cool that this functionality is available too us for free, with no special requirements needed from us.

File Deletion

I have chosen to delete the file in the FileHandler::delete method. This could be made more robust.

There are two approaches to this that I can think of. The first is the simpler way, but is potentially more risky. This is the approach I have opted for. In my case, an orphaned file on disk is not the end of the world.

// src/AppBundle/Handler/FileHandler.php

    /**
     * @param  FileInterface        $file
     * @return bool
     */
    public function delete($file)
    {
        $this->guardFileImplementsInterface($file);

        $isDeleted = $this->filesystem->delete($file->getInternalFileName());

        $this->repository->delete($file);

        return $isDeleted;
    }

The risk here is that the file deletes just fine, but the entity does not. Or, the entity deletes but the file does not.

Now we have a partially deleted situation. Not ideal. And worst, the return statement is going to lie to you in one of those cases. Ack. A rethink and refactor is required here for sure.

Notice though, that we don't care how the file is removed - whether it is removed from Amazon S3, or local disk, or wherever it may be stored, the process is abstracted away from us. Beautiful.

The alternative approach is to use an entity lifecycle callback to listen for the specific events thrown during deletion of the File entity, and only then remove the file. This would be more suitable in most cases.

Code For This Course

Get the code for this course.

Episodes