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.