Skip to main content

File Association Internal Details

Where The Files Are Stored

FileLocationResolverInterface decides where to store the file. It takes the entity instance and the name of the property holding the file and outputs a FilePointer describing where the file in that property will be stored. The default implementation DefaultFileLocationResolver stores files into the filesystem with the identifier 'default' and the key similar to the following:

entity/ffa87ef3fc5388bc8b666e2cec17d27cc493d0c1/image/e5/80/72/6d/31337
╰----╯ ╰--------------------------------------╯ ╰---╯ ╰---------╯ ╰---╯
A B C D E
  • A: Prefix, defaults to 'entity'.
  • B: SHA-1 hash of the entity's fully-qualified class name.
  • C: Property name.
  • D: Hashed directories of the entity's ID. The ID is hashed using SHA-1, then split by 2 characters each. Then, the first four of them are taken to form the directory structure.
  • E: The entity ID.

This default should be sufficient in most cases, for all entities, and all filesystems. It masks internal details (entity class names). It does not pile too many files in one directory (some filesystems struggle with a huge amount of files in a directory). The ordering is chosen to make it easier for manual administration tasks.

To obtain the entity's ID, DefaultFileLocationResolver calls ObjectIdResolverInterface. By default, it is DefaultObjectIdResolver which calls getId() of the entity.

To override this default behavior, you can create your own implementation of either FileLocationResolverInterface or ObjectIdResolverInterface. If you are using autoconfiguration, then you are good to go. Otherwise, you need to tag them in the service container:

services:
App\MyFileLocationResolver:
tags:
- { name: 'rekalogika.file.association.file_location_resolver' }
App\MyObjectIdResolver:
tags:
- { name: 'rekalogika.file.association.object_id_resolver' }

About File Names

Like modern key-value cloud storage services, this framework uses the concept of 'keys', not 'paths'. The file name is not part of the key but is stored in the metadata, along with other properties of the file. The original file name is never taken into consideration when determining where to store the file.

The metadata itself is stored in a sidecar file. Using the example above, the metadata will be stored in this location:

entity/ffa87ef3fc5388bc8b666e2cec17d27cc493d0c1/image/e5/80/72/6d/31337.metadata

The caller can obtain the file name using the appropriate methods:

$imageFilename = $entity->getImage()?->getName();

When possible, the framework should have copied the file name of the original file to the destination metadata when the file was first associated with the entity.

How It Works

The storage key of the file is deterministic. It is determined only by the object's class name, the object's ID, and the name of the property containing the file. As long as those don't change, the key will remain the same.

When persisting an entity, the framework will calculate the destination storage key of every applicable property of the entity, and compare it to the current file residing on each property:

  • If both are the same, the framework leaves it alone.
  • If they are different, the framework will copy the file from the entity to the storage destination.
  • If null, the framework will attempt to remove the file from the storage, irrespective of whether the file exists or not.

Architecture

In a nutshell: Doctrine Unit Of Work ➡️ Doctrine Events ➡️ rekalogika/reconstitutor ➡️ InterfaceReconstitutor & AttributeReconstitutor ➡️ FileAssociationManager ➡️ FileRepository (from rekalogika/file).

InterfaceReconstitutor & AttributeReconstitutor are the entry points of this package. They execute methods of FileAssociationManager which works with the entities and FileRepository to manage the association between the entities and files.

InterfaceReconstitutor & AttributeReconstitutor are registered to the service container so that they are called by our rekalogika/reconstitutor when the relevant events are being emitted by Doctrine. The service configuration is done by the package rekalogika/file-bundle.