diff --git a/README.md b/README.md index 0b22f912..53c947da 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ FileStorage Plugin for CakePHP **If you're upgrading from CakePHP 2.x please read [the migration guide](docs/Documentation/Migrating-from-CakePHP-2.md).** -The **File Storage** plugin is giving you the possibility to upload and store files in virtually any kind of storage backend. The plugin features the [Gaufrette](https://github.com/KnpLabs/Gaufrette) **and** [FlySystem](https://github.com/thephpleague/flysystem) library in a CakePHP fashion and provides a simple way to use the storage adapters through the [StorageManager](src/Storage/StorageManager.php) class. +The **File Storage** plugin is giving you the possibility to upload and store files in virtually any kind of storage backend. The plugin features the [FlySystem](https://github.com/thephpleague/flysystem) library in a CakePHP fashion and provides a simple way to use the storage adapters. Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored where. You can always write your own adapter or extend and overload existing ones. @@ -17,47 +17,22 @@ How it works The whole plugin is build with clear [Separation of Concerns (SoC)](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. Storing the path to a file inside an arbitrary table along other data is considered as *bad practice* because it doesn't respect SoC from an architecture perspective but many people do it this way for some reason. -You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events, the listeners listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a path builder class. +You associate the `file_storage` table with your model using the FileStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events, the listeners listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a path builder class. -List of supported Adapters --------------------------- - - * Apc - * Amazon S3 - * ACL Aware Amazon S3 - * Azure - * Doctrine DBAL - * Dropbox - * Ftp - * Grid FS - * In Memory - * Local File System - * MogileFS - * Open Cloud - * Rackspace Cloudfiles - * Sftp - * Zip File Supported CakePHP Versions -------------------------- - * CakePHP 4.x -> 3.0 Branch + * CakePHP 4.x -> 4.0 Branch (Rewritten almost from scratch) + * CakePHP 4.x -> 3.0 Branch (Old codebase) * CakePHP 3.x -> 2.0 Branch * CakePHP 2.x -> 1.0 Branch Requirements ------------ - * PHP 7.2+ + * PHP 7.4+ * CakePHP 4.x - * Gaufrette Storage Library 0.7.x - -Optional but required if you want image processing out of the box: - - * The [Imagine Image processing plugin](https://github.com/burzum/cakephp-imagine-plugin) if you want to process and store images. - * [FlySystem](https://github.com/thephpleague/flysystem) as alternative library over Gaufrette - -You can still implement whatever file processing you want very easy. It's not tied to Imagine. Documentation ------------- diff --git a/composer.json b/composer.json index 1ba4f837..70726225 100644 --- a/composer.json +++ b/composer.json @@ -38,14 +38,14 @@ ], "autoload": { "psr-4": { - "Burzum\\FileStorage\\": "src", - "Burzum\\FileStorage\\Test\\Fixture\\": "tests\\Fixture" + "Burzum\\FileStorage\\": "src/", + "Burzum\\FileStorage\\Test\\Fixture\\": "tests/Fixture/" } }, "autoload-dev": { "psr-4": { - "Cake\\Test\\": "/vendor/cakephp/cakephp/tests", - "Burzum\\FileStorage\\Test\\": "tests" + "TestApp\\": "tests/test_app/src/", + "Burzum\\FileStorage\\Test\\TestCase\\": "tests/TestCase/" } }, "suggest": { diff --git a/config/Migrations/20201110234846_AddCollectionColumn.php b/config/Migrations/20201110234846_AddCollectionColumn.php new file mode 100644 index 00000000..2edca26d --- /dev/null +++ b/config/Migrations/20201110234846_AddCollectionColumn.php @@ -0,0 +1,21 @@ +table('file_storage') + ->addColumn('collection', 'string', ['length' => 128, 'null' => true, 'default' => null]) + ->update(); + } +} diff --git a/docs/Documentation/Getting-a-File-Path-and-URL.md b/docs/Documentation/Getting-a-File-Path-and-URL.md deleted file mode 100644 index 0e59b586..00000000 --- a/docs/Documentation/Getting-a-File-Path-and-URL.md +++ /dev/null @@ -1,102 +0,0 @@ -# Getting a file path and URL - -The path and filename of a stored file in the storage backend that was used to store the file is generated by a [path builder](Path-Builders.md). The event listener that stored your file has used a path builder to generate the path based on the entity data. This means that if you have the entity and instantiate a path builder you can build the path to it in any place. - -The plugin already provides you with several convenience short cuts to do that. - -Be aware that whenever you use a path builder somewhere, you **must** use the same path builder and options as when the entity was created. They're usually the same as configured in your event listener. - -## Getting it from an entity - -If you're using an entity from this plugin, or extending it they'll implement the PathBuilderTrait. This enables you to set and get the path builder on the entities. - -Due to some [limitations of the CakePHP core](http://api.cakephp.org/3.1/source-class-Cake.ORM.Table.html#1965) you can't pass options to the entity when calling `Table::newEntity()`. - -There are two workarounds for that issue. Either you'll have to set it manually on the entity instance: - -```php -$entity->pathBuilder('PathBuilderName', ['options-array' => 'goes-here']); -$entity->path(); // Gets you the path in the used storage backend to the file -$entity->url(); // Gets you the URL to the file if possible -``` - -Or do it in the constructor of the entity. Pay attention to the two properties `_pathBuilderClass` and `_pathBuilderOptions`. Set whatever you need here. If you're inheriting `Burzum\FileStorage\Model\Entity\FileStorage` these options and the code below will be already present. - -```php -namespace App\Model\Entity; - -use Burzum\FileStorage\Storage\PathBuilder\PathBuilderTrait; - -class SomeEntityInYourApp extends Entity { - - use PathBuilderTrait; - - /** - * Path Builder Class. - * - * This is named $_pathBuilderClass because $_pathBuilder is already used by - * the trait to store the path builder instance. - * - * @param array - */ - protected $_pathBuilderClass = null; - - /** - * Path Builder options - * - * @param array - */ - protected $_pathBuilderOptions = []; - - /** - * Constructor - * - * @param array $properties hash of properties to set in this entity - * @param array $options list of options to use when creating this entity - */ - public function __construct(array $properties = [], array $options = []) { - $options += [ - 'pathBuilder' => $this->_pathBuilderClass, - 'pathBuilderOptions' => $this->_pathBuilderOptions - ]; - - parent::__construct($properties, $options); - - if (!empty($options['pathBuilder'])) { - $this->pathBuilder( - $options['pathBuilder'], - $options['pathBuilderOptions'] - ); - } - } -} -``` - -If you want to use path builders depending on the kind of file or the identifier which is stored in the `model` field of the `file_storage` table, you can implement that logic as well there and use the entities data to determine the path builder class or options. - -## Getting it using the storage helper - -The storage helper is basically just a proxy to a path builder. The helper takes two configuration options: - - * **pathBuilder**: Name of the path builder to use. - * **pathBuilderOptions**: The options passed to the path builders constructor. - -Make sure that the options you pass and the path builder are the same you've used when you uploaded the file! Otherwise you end up with a different path! - -```php -// Load the helper -$this->loadHelper('Burzum/FileStorage.Storage', [ - 'pathBuilder' => 'Base', - // The builder options must match the options and builder class that were used to store the file! - 'pathBuilderOptions' => [ - 'modelFolder' => true, - ] -]); - -// Use it in your views -$url = $this->Storage->url($yourEntity); - -// Change the path builder at run time -// Be carefully, this will change the path builder instance in the helper! -$this->Storage->pathBuilder('SomePathBuilder', ['options' => 'here']); -``` diff --git a/docs/Documentation/How-To-Use.md b/docs/Documentation/How-To-Use.md index 721b0732..7b267645 100644 --- a/docs/Documentation/How-To-Use.md +++ b/docs/Documentation/How-To-Use.md @@ -1,136 +1,4 @@ How to Use It ============= -Before you continue to read this page it is recommended that you have read about [the Storage Manager](The-Storage-Manager.md) before. - -The following text is going to describe two ways to store a file. Which of both you choose depends at the end on your use case but it is recommended to use the events because they automate the whole process much more. - -The basic idea of this plugin is that files are always handled as separate entities and are associated to other models. The reason for that is simple. A file has multiple properties like size, mime type and other entities in the system can have more than one file for example. It is considered as *bad* practice to store lots of file paths as reference in a table together with other data. - -This plugin resolves that issue by handling each file as a completely separate entity in the application. There is just one table `file_storage` that will keep the reference to all your files, no matter where they're stored. - -Preparing the File Upload -------------------------- - -This section is going to show how to store a file using the Storage Manager directly. - -For example you have a `reports` table and want to save a PDF to it, you would then create an association like: - -```php -public function initialize(array $config) -{ - parent::initialize($config); - $this->table('reports'); - - $this->hasOne('PdfFiles', [ - 'className' => 'Burzum/FileStorage.PdfFiles', - 'foreignKey' => 'foreign_key', - 'conditions' => [ - 'PdfFiles.model' => 'Reports' - ] - ]); -} -``` - -In your `add.ctp` or `edit.ctp` views you would add something like this. - -```php -echo $this->Form->create($report, ['type' => 'file']); -echo $this->Form->input('title'); -echo $this->Form->file('pdf_files.file'); // Pay attention here! -echo $this->Form->input('description'); -echo $this->Form->submit(__('Submit')); -echo $this->Form->end(); -``` - -[Make sure your form is using the right HTTP method](http://book.cakephp.org/3.0/en/views/helpers/form.html#changing-the-http-method-for-a-form)! - -Store an uploaded file using Events ------------------------------------ - -The **FileStorage** plugin comes with a class that acts just as a listener to some of the events in this plugin. Take a look at [ImageProcessingListener.php](../../src/Event/ImageProcessingListener.php). - -This class will listen to all the ImageStorage model events and save the uploaded image and then create the versions for that image and storage adapter. - -It is important to understand that nearly each storage adapter requires a little different handling: Most of the time you can't treat a local file the same as a file you store in a cloud service. -The interface that this plugin and Gaufrette provide is the same but not the internals. So a path that works for your local file system might not work for your remote storage system because it has other requirements or limitations. - -So if you want to store a file using Amazon S3 you would have to store it, -create all the versions of that image locally and then upload each of them -and then delete the local temp files. The good news is the plugin can already take care of that. - -When you create a new listener it is important that you check the `model` field and -the event subject object (usually a table object inheriting `\Cake\ORM\Table`) if it -matches what you expect. -Using the event system you could create any kind of storage and upload behavior without -inheriting or touching the model code. Just write a listener class and attach it to the global EventManager. - -List of events --------------- - -Events triggered in the `ImageStorage` model: - - * ImageVersion.createVersion - * ImageVersion.removeVersion - * ImageStorage.beforeSave - * ImageStorage.afterSave - * ImageStorage.beforeDelete - * ImageStorage.afterDelete - -Events triggered in the `FileStorage` model: - - * FileStorage.beforeSave - * FileStorage.afterSave - * FileStorage.afterDelete - -Event Listeners ---------------- - -See [this page](Included-Event-Listeners.md) for the event listeners that are included in the plugin. - - -Handling the File Upload Manually ---------------------------------- - -You'll have to customize it a little but its just a matter for a few lines. - -Note the Listener expects a request data key `file` present in the form, so use `echo $this->Form->input('file');` to allow the Marshaller pick the right data from the uploaded file. - -Lets go by this scenario inside the report table, assuming there is an add() method: - -```php -public function add() { - $entity = $this->newEntity($postData); - $saved = $this->save($entity); - if ($saved) { - $key = 'your-file-name'; - if (StorageManager::get('Local')->write($key, file_get_contents($this->data['pdf_files']['file']['tmp_name']))) { - $postData['pdf_files']['foreign_key'] = $saved->id; - $postData['pdf_files']['model'] = 'Reports'; - $postData['pdf_files']['path'] = $key; - $postData['pdf_files']['adapter'] = 'Local'; - $this->PdfDocuments->save($this->PdfDocuments->newEntity($postData)); - } - } - return $entity; -} -``` - -Later, when you want to delete the file, for example in the beforeDelete() or afterDelete() callback of your Report model, you'll know the adapter you have used to store the attached PdfFile and can get an instance of this adapter configuration using the StorageManager. By having the path or key available you can then simply call: - -```php -StorageManager::get($data['PdfFile']['adapter'])->delete($data['PdfFile']['path']); -``` - -Insted of doing all of this in the table object that has the files associated to it you can also simply extend the FileStorage table from the plugin and add your storage logic there and use that table for your association. - -Why is it done like this? -------------------------- - -Every developer might want to store the file at a different point or apply other operations on the file before or after it is stored. Based on different circumstances you might want to save an associated file even before you created the record its going to get attached to, in other scenarios like in this documentation you might want to do it after. - -The ``$key`` is also a key aspect of it: Different adapters might expect a different key. A key for the Local adapter of Gaufrette is usually a path and a file name under which the data gets stored. That's also the reason why you use `file_get_contents()` instead of simply passing the tmp path as it is. - -It is up to you how you want to generate the key and build your path. You can customize the way paths and file names are build by writing a custom event listener for that. - -It is highly recommended to read the Gaufrette documentation for the read() and write() methods of the adapters. +TO BE DONE diff --git a/docs/Documentation/How-it-works.md b/docs/Documentation/How-it-works.md index 93d0a6c7..8b7d9d98 100644 --- a/docs/Documentation/How-it-works.md +++ b/docs/Documentation/How-it-works.md @@ -3,7 +3,7 @@ How it works The whole plugin is build with clear [Separation of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. -The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. +The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. You associate the `file_storage` table with your model using the FileStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events. [The listeners](../../src/Storage/Listener) listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a [path builder](Path-Builders.md) class. diff --git a/docs/Documentation/List-of-included-Adapters.md b/docs/Documentation/List-of-included-Adapters.md deleted file mode 100644 index 6c4feef7..00000000 --- a/docs/Documentation/List-of-included-Adapters.md +++ /dev/null @@ -1,24 +0,0 @@ -Included storage adapters -------------------------- - -The following adapters are coming along with the Gaufrette library that is used by this plugin as a base. - - * Apc - * Amazon S3 - * ACL Aware Amazon S3 - * Azure - * Doctrine DBAL - * Dropbox - * Ftp - * Grid FS - * In Memory - * Local File System - * MogileFS - * Open Cloud - * Rackspace Cloudfiles - * Sftp - * Zip File - -Check [the adapter folder](https://github.com/KnpLabs/Gaufrette/tree/master/src/Gaufrette/Adapter) of the Gaufrette lib for a complete list. - -If you need another adapter that is not included you can implement it yourself or try searching the web first. diff --git a/docs/README.md b/docs/README.md index 466c4c40..4c438a19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,22 +1,12 @@ # Home The **File Storage** plugin is giving you the possibility to store files in virtually any kind of storage backend. -This plugin is wrapping the [Gaufrette](https://github.com/KnpLabs/Gaufrette) library in a CakePHP fashion -and provides a simple way to use the storage adapters through the [StorageManager](../Lib/StorageManager.php) class. - -[See this list of included storage adapters.](Docs/Documentation/List-of-included-Adapters.md) - -Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored were. You can always write your own adapter or extend and overload existing ones. ## Documentation * [Installation](Documentation/Installation.md) * [How it works](Documentation/How-it-works.md) * [How to Use it](Documentation/How-To-Use.md) -* [Migrating from File Storage v1 to v2](Migrating-from-File-Storage-v1-to-v2.md) -* [The Storage Manager](Documentation/The-Storage-Manager.md) -* [Included Event Listeners](Documentation/Included-Event-Listeners.md) -* [Path Builders](Documentation/Path-Builders.md) * [Getting a file path and URL](Documentation/Getting-a-File-Path-and-URL.md) * Image processing * [Image Storage and Versioning](Documentation/Image-Storage-And-Versioning.md) diff --git a/docs/Tutorials/Replacing-Files.md b/docs/Tutorials/Replacing-Files.md deleted file mode 100644 index 666b4976..00000000 --- a/docs/Tutorials/Replacing-Files.md +++ /dev/null @@ -1,4 +0,0 @@ -Replacing Files -=============== - -**Don't** use Table::deleteAll() if you don't want to end up with orphaned files! The reason for that is that deleteAll() doesn't fire the callbacks. So the events that will remove the files won't get fired. diff --git a/docs/Tutorials/Validating-File-Uploads.md b/docs/Tutorials/Validating-File-Uploads.md deleted file mode 100644 index f0c86a8c..00000000 --- a/docs/Tutorials/Validating-File-Uploads.md +++ /dev/null @@ -1,36 +0,0 @@ -Validating File Uploads -======================= - -You can validate your uploads by extending `Burzum\FileStorage\Storage\Listener\ValidationListener` and implement your validation methods just like you do in table objects: - -```php -use Burzum\FileStorage\Storage\Listener\ValidationListener; -use Cake\Validation\Validator; - -class TestValidationListener extends ValidationListener { - - public function validationAvatar(Validator $validator) { - $validator->add('file', 'mimeType', [ - 'rule' => ['mimeType', ['image/jpg', 'image/jpeg', 'image/png']] - ]); - - $validator->add('file', 'imageSize', [ - 'rule' => ['imageSize', [ - 'height' => ['>=', 200], - 'width' => ['>=', 200] - ]] - ]); - - return $validator; - } -} - -EventManager::instance()->on(new FileValidationListener()); -``` - -This will attach the listener to the `Model.initialize()` event and add your configured validators to the FileStorage table. - -References: - -* (Using A Different Validation Set)[http://book.cakephp.org/3.0/en/orm/validation.html#using-a-different-validation-set] -* (Table::validator())[http://api.cakephp.org/3.3/class-Cake.Validation.ValidatorAwareTrait.html#_validator] diff --git a/phpcs.xml b/phpcs.xml index 260755e3..61f0a9a2 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -10,12 +10,12 @@ - + - + diff --git a/phpstan.neon b/phpstan.neon index bd7887ab..02e6eb73 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false bootstrapFiles: @@ -7,3 +7,5 @@ parameters: earlyTerminatingMethodCalls: Cake\Console\Shell: - abort + ignoreErrors: + - '#Cannot cast array\\|string to string#' diff --git a/src/FileStorage/DataTransformer.php b/src/FileStorage/DataTransformer.php index 943de9c4..d19c0185 100644 --- a/src/FileStorage/DataTransformer.php +++ b/src/FileStorage/DataTransformer.php @@ -73,7 +73,7 @@ public function entityToFileObject(EntityInterface $entity): FileInterface public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity): EntityInterface { $data = [ - 'id' => $file->uuid(), + 'id' => $file->uuid(), //FIXME 'model' => $file->model(), 'foreign_key' => $file->modelId(), 'filesize' => $file->filesize(), diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php new file mode 100644 index 00000000..59debdee --- /dev/null +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -0,0 +1,151 @@ + [], + ]; + + /** + * @inheritDoc + */ + public function initialize(array $config): void + { + parent::initialize($config); + + $model = $this->getTable()->getAlias(); + foreach ($config['associations'] as $association => $assocConfig) { + $associationObject = $this->getTable()->getAssociation($association); + + $defaults = [ + 'replace' => $associationObject instanceof HasOne, + 'model' => $model, + 'collection' => $association, + 'property' => $this->getTable()->getAssociation($association)->getProperty(), + ]; + + $config['associations'][$association] = $assocConfig + $defaults; + } + + $this->setConfig('associations', $config['associations']); + } + + /** + * @param \Cake\Event\EventInterface $event + * @param \Cake\Datasource\EntityInterface $entity + * @param \ArrayObject $options + * + * @return void + */ + public function beforeSave( + EventInterface $event, + EntityInterface $entity, + ArrayObject $options + ): void + { + $associations = $this->getConfig('associations'); + foreach ($associations as $association => $assocConfig) { + $property = $assocConfig['property']; + if ($entity->{$property} === null) { + continue; + } + + $entity->{$property}->set('collection', $assocConfig['collection']); + } + } + + /** + * @param \Cake\Event\EventInterface $event + * @param \Cake\Datasource\EntityInterface $entity + * @param \ArrayObject $options + * + * @return void + */ + public function afterSave( + EventInterface $event, + EntityInterface $entity, + ArrayObject $options + ): void { + $associations = $this->getConfig('associations'); + + foreach ($associations as $association => $assocConfig) { + $property = $assocConfig['property']; + if ($entity->{$property} === null) { + continue; + } + + if ($entity->id && $entity->{$property} && $entity->{$property}->file) { + $file = $entity->{$property}->file; + + $ok = false; + if (is_array($file) && $file['error'] === UPLOAD_ERR_OK) { + $ok = true; + } elseif ($file instanceof UploadedFile && $file->getError() === UPLOAD_ERR_OK) { + $ok = true; + } + + if (!$ok) { + continue; + } + + if ($assocConfig['replace'] === true) { + $this->findAndRemovePreviousFile($entity, $association, $assocConfig); + } + + $entity->{$property}->set('collection', $assocConfig['collection']); + $entity->{$property}->set('model', $assocConfig['model']); + $entity->{$property}->set('foreign_key', $entity->id); + + $this->getTable()->{$association}->saveOrFail($entity->{$property}); + } + } + } + + /** + * @param \Cake\Datasource\EntityInterface $entity + * @param string $association + * @param array $assocConfig + * + * @return void + */ + protected function findAndRemovePreviousFile( + EntityInterface $entity, + string $association, + array $assocConfig + ): void { + $result = $this->getTable()->{$association}->find() + ->where([ + 'model' => $assocConfig['model'], + 'collection' => $assocConfig['collection'] ?? null, + 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()), + 'id !=' => $entity->get($assocConfig['property'])->get((string)$this->getTable()->{$association}->getPrimaryKey()), + ]) + ->first(); + + if ($result) { + $this->getTable()->{$association}->delete($result); + } + } +} diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 4f95446a..9b6053ed 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -33,17 +33,17 @@ class FileStorageBehavior extends Behavior /** * @var \Phauthentic\Infrastructure\Storage\FileStorage */ - protected FileStorage $fileStorage; + protected $fileStorage; /** - * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface + * @var \Burzum\FileStorage\FileStorage\DataTransformerInterface */ - protected ?ProcessorInterface $imageProcessor; + protected $transformer; /** - * @var \Burzum\FileStorage\FileStorage\DataTransformerInterface + * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface */ - protected DataTransformerInterface $transformer; + protected $processor; /** * Default config @@ -55,14 +55,9 @@ class FileStorageBehavior extends Behavior 'ignoreEmptyFile' => true, 'fileField' => 'file', 'fileStorage' => null, - 'imageProcessor' => null, + 'fileProcessor' => null, ]; - /** - * @var array - */ - protected array $processors = []; - /** * @inheritDoc * @@ -85,6 +80,8 @@ public function initialize(array $config): void $this->getTable() ); } + + //$this->processors = (array)$this->getConfig('processors'); } /** @@ -183,15 +180,37 @@ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayO if ($entity->isNew()) { try { $file = $this->entityToFileObject($entity); + + $this->dispatchEvent('FileStorage.beforeStoringFile', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + $file = $this->fileStorage->store($file); + + $this->dispatchEvent('FileStorage.afterStoringFile', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + $file = $this->processImages($file, $entity); - foreach ($this->processors as $processor) { - $file = $processor->process($file); - } + $processor = $this->getFileProcessor(); + + $this->dispatchEvent('FileStorage.beforeFileProcessing', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + + $file = $processor->process($file); + + $this->dispatchEvent('FileStorage.afterFileProcessing', [ + 'entity' => $entity, + 'file' => $file + ], $this->getTable()); $entity = $this->fileObjectToEntity($file, $entity); - $this->getTable()->save( + $this->getTable()->saveOrFail( $entity, ['callbacks' => false] ); @@ -219,7 +238,7 @@ protected function checkEntityBeforeSave(EntityInterface $entity) { if ($entity->isNew()) { if (!$entity->has('model')) { - $entity->set('model', $this->getTable()->getTable()); + $entity->set('model', $this->getTable()->getAlias()); } if (!$entity->has('adapter')) { @@ -266,7 +285,7 @@ protected function getFileInfoFromUpload(&$upload, $field = 'file') if (!is_array($uploadedFile)) { $upload['filesize'] = $uploadedFile->getSize(); $upload['mime_type'] = $uploadedFile->getClientMediaType(); - $upload['extension'] = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); + $upload['extension'] = pathinfo((string)$uploadedFile->getClientFilename(), PATHINFO_EXTENSION); $upload['filename'] = $uploadedFile->getClientFilename(); } else { $upload['filesize'] = $uploadedFile['size']; @@ -285,7 +304,7 @@ protected function getFileInfoFromUpload(&$upload, $field = 'file') * * @return int Number of deleted records / files */ - public function deleteAllFiles($conditions) + public function deleteAllFiles(array $conditions) { $table = $this->getTable(); @@ -334,16 +353,15 @@ public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity */ public function processImages(FileInterface $file, EntityInterface $entity): FileInterface { - $imageSizes = Configure::read('FileStorage.imageVariants'); + $imageSizes = (array)Configure::read('FileStorage.imageVariants'); $model = $file->model(); - $identifier = $entity->get('identifier'); + $collection = $entity->get('collection'); - if (!isset($imageSizes[$model][$identifier])) { + if (!isset($imageSizes[$model][$collection])) { return $file; } - $file = $file->withVariants($imageSizes[$model][$identifier]); - $file = $this->imageProcessor->process($file); + $file = $file->withVariants($imageSizes[$model][$collection]); return $file; } @@ -353,20 +371,20 @@ public function processImages(FileInterface $file, EntityInterface $entity): Fil * * @return \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface */ - protected function getImageProcessor(): ProcessorInterface + protected function getFileProcessor(): ProcessorInterface { - if ($this->imageProcessor !== null) { - return $this->imageProcessor; + if ($this->processor !== null) { + return $this->processor; } - if ($this->getConfig('imageProcessor') instanceof ProcessorInterface) { - $this->imageProcessor = $this->getConfig('imageProcessor'); + if ($this->getConfig('fileProcessor') instanceof ProcessorInterface) { + $this->processor = $this->getConfig('fileProcessor'); } - if ($this->imageProcessor === null) { - throw new RuntimeException('No image processor found'); + if ($this->processor === null) { + throw new RuntimeException('No processor found'); } - return $this->imageProcessor; + return $this->processor; } } diff --git a/src/Model/Entity/FileStorage.php b/src/Model/Entity/FileStorage.php index 4913d8ac..c4da85d0 100644 --- a/src/Model/Entity/FileStorage.php +++ b/src/Model/Entity/FileStorage.php @@ -12,6 +12,24 @@ * @author Florian Krämer * @copyright 2012 - 2020 Florian Krämer * @license MIT + * + * @property array $variants + * @property array $metadata + * @property int $id + * @property int|null $user_id + * @property int|null $foreign_key + * @property string|null $model + * @property string|null $filename + * @property int|null $filesize + * @property string|null $mime_type + * @property string|null $extension + * @property string|null $hash + * @property string|null $path + * @property string|null $adapter + * @property \Cake\I18n\FrozenTime $created + * @property \Cake\I18n\FrozenTime $modified + * @property string|null $collection + * @property array $variant_urls */ class FileStorage extends Entity implements FileStorageEntityInterface { @@ -22,6 +40,7 @@ class FileStorage extends Entity implements FileStorageEntityInterface */ protected $_accessible = [ '*' => true, + 'id' => false, ]; /** diff --git a/src/Model/Table/FileStorageTable.php b/src/Model/Table/FileStorageTable.php index 9aa7ceda..95e357b8 100644 --- a/src/Model/Table/FileStorageTable.php +++ b/src/Model/Table/FileStorageTable.php @@ -25,6 +25,22 @@ * @author Florian Krämer * @copyright 2012 - 2020 Florian Krämer * @license MIT + * + * @method \Burzum\FileStorage\Model\Entity\FileStorage newEmptyEntity() + * @method \Burzum\FileStorage\Model\Entity\FileStorage newEntity(array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[] newEntities(array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage get($primaryKey, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage findOrCreate($search, ?callable $callback = null, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[] patchEntities(iterable $entities, array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage|false save(\Cake\Datasource\EntityInterface $entity, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = []) + * @mixin \Cake\ORM\Behavior\TimestampBehavior + * @mixin \Burzum\FileStorage\Model\Behavior\FileStorageBehavior */ class FileStorageTable extends Table { diff --git a/src/Shell/ImageVersionShell.php b/src/Shell/ImageVersionShell.php index 53fbd6f1..2f6a0eb5 100644 --- a/src/Shell/ImageVersionShell.php +++ b/src/Shell/ImageVersionShell.php @@ -25,7 +25,7 @@ class ImageVersionShell extends Shell /** * Storage Table Object * - * @var \Cake\ORM\Table|null + * @var \Cake\ORM\Table */ public $Table; @@ -200,6 +200,9 @@ public function regenerate(): void foreach ($operations as $version => $operation) { try { + if ($this->command === null) { + $this->abort('No command given'); + } $this->_loop($this->command, $this->args[0], [$version => $operation], $options); } catch (Exception $e) { $this->abort($e->getMessage()); diff --git a/src/View/Helper/ImageHelper.php b/src/View/Helper/ImageHelper.php index e1f36f53..0ac05d6f 100644 --- a/src/View/Helper/ImageHelper.php +++ b/src/View/Helper/ImageHelper.php @@ -88,7 +88,7 @@ public function imageUrl(FileStorageEntityInterface $image, ?string $variant = n } if (!$path) { - throw VariantDoesNotExistException::withName($variant); + throw VariantDoesNotExistException::withName((string)$variant); } $options = array_merge($this->getConfig(), $options); diff --git a/tests/Fixture/File/demo.png b/tests/Fixture/File/demo.png new file mode 100644 index 00000000..ded48c3a Binary files /dev/null and b/tests/Fixture/File/demo.png differ diff --git a/tests/Fixture/FileStorageFixture.php b/tests/Fixture/FileStorageFixture.php index b72d5a6a..a04c68cc 100644 --- a/tests/Fixture/FileStorageFixture.php +++ b/tests/Fixture/FileStorageFixture.php @@ -28,17 +28,18 @@ class FileStorageFixture extends TestFixture * @var array */ public $fields = [ - 'id' => ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], - 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], - 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'id' => ['type' => 'integer', 'null' => true, 'default' => null, 'autoIncrement' => true], + 'user_id' => ['type' => 'integer', 'null' => true, 'default' => null], + 'foreign_key' => ['type' => 'integer', 'null' => true, 'default' => null], 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'collection' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 128], 'filename' => ['type' => 'string', 'null' => false, 'default' => null], 'filesize' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 16], 'mime_type' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'extension' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'hash' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], 'path' => ['type' => 'string', 'null' => true, 'default' => null], - 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32, 'comment' => 'Gaufrette Storage Adapter Class'], + 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'variants' => ['type' => 'json', 'null' => true, 'default' => null], 'metadata' => ['type' => 'json', 'null' => true, 'default' => null], 'created' => ['type' => 'datetime', 'null' => true, 'default' => null], @@ -55,9 +56,8 @@ class FileStorageFixture extends TestFixture */ public $records = [ [ - 'id' => 'file-storage-1', - 'user_id' => 'user-1', - 'foreign_key' => 'item-1', + 'user_id' => 1, + 'foreign_key' => 1, 'model' => 'Item', 'filename' => 'cake.icon.png', 'filesize' => '', @@ -72,9 +72,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-2', - 'user_id' => 'user-1', - 'foreign_key' => 'item-1', + 'user_id' => 1, + 'foreign_key' => 1, 'model' => 'Item', 'filename' => 'titus-bienebek-bridle.jpg', 'filesize' => '', @@ -89,9 +88,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-3', - 'user_id' => 'user-1', - 'foreign_key' => 'item-2', + 'user_id' => 1, + 'foreign_key' => 2, 'model' => 'Item', 'filename' => 'titus.jpg', 'filesize' => '335872', @@ -106,9 +104,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-4', - 'user_id' => 'user-1', - 'foreign_key' => 'item-4', + 'user_id' => 1, + 'foreign_key' => 4, 'model' => 'Item', 'filename' => 'titus.jpg', 'filesize' => '335872', diff --git a/tests/Fixture/ItemsFixture.php b/tests/Fixture/ItemsFixture.php new file mode 100644 index 00000000..51a73e25 --- /dev/null +++ b/tests/Fixture/ItemsFixture.php @@ -0,0 +1,56 @@ + ['type' => 'integer', 'null' => true, 'default' => null, 'autoIncrement' => true], + 'name' => ['type' => 'string', 'null' => true, 'default' => null], + 'path' => ['type' => 'string', 'null' => true, 'default' => null], + 'filename' => ['type' => 'string', 'null' => true, 'default' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'name' => 'Cake', + ], + [ + 'name' => 'More Cake', + ], + [ + 'name' => 'A lot Cake', + ], + ]; +} diff --git a/tests/Fixture/UuidFileStorageFixture.php b/tests/Fixture/UuidFileStorageFixture.php new file mode 100644 index 00000000..1e2de0ac --- /dev/null +++ b/tests/Fixture/UuidFileStorageFixture.php @@ -0,0 +1,127 @@ + ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], + 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'collection' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 128], + 'filename' => ['type' => 'string', 'null' => false, 'default' => null], + 'filesize' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 16], + 'mime_type' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'extension' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'hash' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'path' => ['type' => 'string', 'null' => true, 'default' => null], + 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'variants' => ['type' => 'json', 'null' => true, 'default' => null], + 'metadata' => ['type' => 'json', 'null' => true, 'default' => null], + 'created' => ['type' => 'datetime', 'null' => true, 'default' => null], + 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => 'file-storage-1', + 'user_id' => 'user-1', + 'foreign_key' => 'item-1', + 'model' => 'Item', + 'filename' => 'cake.icon.png', + 'filesize' => '', + 'mime_type' => 'image/png', + 'extension' => 'png', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-2', + 'user_id' => 'user-1', + 'foreign_key' => 'item-1', + 'model' => 'Item', + 'filename' => 'titus-bienebek-bridle.jpg', + 'filesize' => '', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-3', + 'user_id' => 'user-1', + 'foreign_key' => 'item-2', + 'model' => 'Item', + 'filename' => 'titus.jpg', + 'filesize' => '335872', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-4', + 'user_id' => 'user-1', + 'foreign_key' => 'item-4', + 'model' => 'Item', + 'filename' => 'titus.jpg', + 'filesize' => '335872', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '09d82a31', + 'path' => null, + 'adapter' => 'S3', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + ]; +} diff --git a/tests/Fixture/ItemFixture.php b/tests/Fixture/UuidItemsFixture.php similarity index 94% rename from tests/Fixture/ItemFixture.php rename to tests/Fixture/UuidItemsFixture.php index af3e09c1..d6265f04 100644 --- a/tests/Fixture/ItemFixture.php +++ b/tests/Fixture/UuidItemsFixture.php @@ -6,14 +6,14 @@ use Cake\TestSuite\Fixture\TestFixture; -class ItemFixture extends TestFixture +class UuidItemsFixture extends TestFixture { /** * Name * * @var string */ - public $name = 'Item'; + public $name = 'Items'; /** * Table diff --git a/tests/TestCase/FileStorageTestCase.php b/tests/TestCase/FileStorageTestCase.php index bfad795b..77d0b391 100644 --- a/tests/TestCase/FileStorageTestCase.php +++ b/tests/TestCase/FileStorageTestCase.php @@ -85,29 +85,26 @@ public function setUp(): void /** * @return void */ - private function configureImageVariants(): void + protected function configureImageVariants(): void { Configure::write('FileStorage.imageVariants', [ - 'Test' => [ - 't50' => [ + 'Photos' => [ + 'Photos' => [ 'thumbnail' => [ - 'mode' => 'outbound', 'width' => 50, 'height' => 50, ], ], 't150' => [ 'thumbnail' => [ - 'mode' => 'outbound', 'width' => 150, 'height' => 150, ], ], ], - 'UserAvatar' => [ - 'small' => [ + 'Avatars' => [ + 'Avatars' => [ 'thumbnail' => [ - 'mode' => 'inbound', 'width' => 80, 'height' => 80, ], @@ -119,7 +116,7 @@ private function configureImageVariants(): void /** * @return void */ - private function prepareDependencies(): void + protected function prepareDependencies(): void { $pathBuilder = new PathBuilder([ 'pathTemplate' => '{model}{ds}{collection}{ds}{randomPath}{ds}{strippedId}{ds}{strippedId}.{extension}', @@ -153,10 +150,17 @@ private function prepareDependencies(): void $pathBuilder, $imageManager ); + $imageDimensionsProcessor = new \TestApp\Storage\Processor\ImageDimensionsProcessor( + $this->testPath + ); + $stackProcessor = new \Phauthentic\Infrastructure\Storage\Processor\StackProcessor([ + $imageProcessor, + $imageDimensionsProcessor, + ]); Configure::write('FileStorage.behaviorConfig', [ 'fileStorage' => $fileStorage, - 'imageProcessor' => $imageProcessor, + 'fileProcessor' => $stackProcessor, ]); } diff --git a/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php b/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php index 5069823e..71e0b4f1 100644 --- a/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php @@ -24,15 +24,6 @@ class FileStorageBehaviorTest extends FileStorageTestCase */ protected $FileStorage; - /** - * Fixtures - * - * @var array - */ - protected $fixtures = [ - 'plugin.Burzum/FileStorage.FileStorage', - ]; - /** * startTest * @@ -75,7 +66,7 @@ public function testAfterDelete() $file = $this->_createMockFile('/Item/00/14/90/filestorage1/filestorage1.png'); $this->assertFileExists($file); - $entity = $this->FileStorage->get('file-storage-1'); + $entity = $this->FileStorage->get(1); $entity->adapter = 'Local'; $entity->path = '/Item/00/14/90/filestorage1/filestorage1.png'; @@ -123,7 +114,7 @@ public function testBeforeSave() $this->assertSame($entity->adapter, 'Local'); $this->assertSame($entity->filesize, 332643); $this->assertSame($entity->mime_type, 'image/jpeg'); - $this->assertSame($entity->model, 'file_storage'); + $this->assertSame($entity->model, 'FileStorage'); } /** @@ -152,6 +143,6 @@ public function testBeforeSaveArray() $this->assertSame($entity->adapter, 'Local'); $this->assertSame($entity->filesize, 332643); $this->assertSame($entity->mime_type, 'image/jpeg'); - $this->assertSame($entity->model, 'file_storage'); + $this->assertSame($entity->model, 'FileStorage'); } } diff --git a/tests/TestCase/Model/Entity/FileStorageTest.php b/tests/TestCase/Model/Entity/FileStorageTest.php index 52c81cec..99a66313 100644 --- a/tests/TestCase/Model/Entity/FileStorageTest.php +++ b/tests/TestCase/Model/Entity/FileStorageTest.php @@ -7,13 +7,6 @@ use Burzum\FileStorage\Model\Entity\FileStorage; use Burzum\FileStorage\Test\TestCase\FileStorageTestCase; -/** - * File Storage Entity Test - * - * @author Florian Kr�mer - * @copyright 2012 - 2017 Florian Kr�mer - * @license MIT - */ class FileStorageTest extends FileStorageTestCase { /** diff --git a/tests/TestCase/Model/Table/FileStorageTableTest.php b/tests/TestCase/Model/Table/FileStorageTableTest.php index 0e55be7e..98aee083 100644 --- a/tests/TestCase/Model/Table/FileStorageTableTest.php +++ b/tests/TestCase/Model/Table/FileStorageTableTest.php @@ -35,8 +35,8 @@ public function tearDown(): void */ public function testInitialize() { - $this->assertEquals($this->FileStorage->getTable(), 'file_storage'); - $this->assertEquals($this->FileStorage->getDisplayField(), 'filename'); + $this->assertSame('file_storage', $this->FileStorage->getTable()); + $this->assertSame('filename', $this->FileStorage->getDisplayField()); } /** @@ -58,7 +58,7 @@ public function testFileSaving() 'tituts.jpg', 'image/jpeg' ), - ], ['accessibleFields' => ['*' => true]]); + ]); $this->assertSame([], $entity->getErrors()); $this->FileStorage->saveOrFail($entity); @@ -83,7 +83,7 @@ public function testFileSavingArray() 'name' => 'tituts.jpg', 'tmp_name' => $this->fileFixtures . 'titus.jpg', ], - ], ['accessibleFields' => ['*' => true]]); + ]); $this->assertSame([], $entity->getErrors()); $this->FileStorage->saveOrFail($entity); diff --git a/tests/TestCase/Model/Table/ItemsTableTest.php b/tests/TestCase/Model/Table/ItemsTableTest.php new file mode 100644 index 00000000..08223b7a --- /dev/null +++ b/tests/TestCase/Model/Table/ItemsTableTest.php @@ -0,0 +1,181 @@ +table = $this->getTableLocator()->get('Items'); + + $this->table->hasOne('Avatars', [ + 'className' => 'Burzum/FileStorage.FileStorage', + 'foreignKey' => 'foreign_key', + 'conditions' => [ + 'Avatars.model' => 'Items', + ], + 'joinType' => 'LEFT', + ]); + + $this->table->hasMany('Photos', [ + 'className' => 'Burzum/FileStorage.FileStorage', + 'foreignKey' => 'foreign_key', + 'conditions' => [ + 'Photos.model' => 'Items', + ], + 'joinType' => 'LEFT', + ]); + + $this->table->addBehavior( + 'Burzum/FileStorage.FileAssociation', [ + 'associations' => [ + 'Avatars' => [ + 'collection' => 'Avatars', + 'replace' => true + ], + 'Photos' => [ + 'collection' => 'Photos', + ] + ] + ] + ); + } + + /** + * endTest + * + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + + unset($this->FileStorage, $this->table); + $this->getTableLocator()->clear(); + } + + /** + * @return void + */ + public function testUploadNew() + { + $entity = $this->table->newEntity([ + 'name' => 'Test', + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'titus.jpg', + filesize($this->fileFixtures . 'titus.jpg'), + UPLOAD_ERR_OK, + 'tituts.jpg', + 'image/jpeg', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); + + $this->assertNotEmpty($entity->avatar); + + $this->assertSame('Items', $entity->avatar->model); + $this->assertNotEmpty($entity->avatar->foreign_key); + $this->assertSame('Avatars', $entity->avatar->collection); + $this->assertStringStartsWith('Avatars', $entity->avatar->path); + $this->assertNotEmpty($entity->avatar->metadata); + $this->assertNotEmpty($entity->avatar->variants); + } + + /** + * @return void + */ + public function testUploadOverwriteExisting() + { + // Upload first pic + $entity = $this->table->newEntity([ + 'name' => 'Test', + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'titus.jpg', + filesize($this->fileFixtures . 'titus.jpg'), + UPLOAD_ERR_OK, + 'tituts.jpg', + 'image/jpeg', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); + + $this->assertNotEmpty($entity->avatar); + + $expected = [ + 'width' => 512, + 'height' => 768, + ]; + $this->assertSame($expected, $entity->avatar->metadata); + + // Upload second pic + $entity = $this->table->patchEntity($entity, [ + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'demo.png', + filesize($this->fileFixtures . 'demo.png'), + UPLOAD_ERR_OK, + 'demo.png', + 'image/png', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); + + $this->assertNotEmpty($entity->avatar); + + $this->assertSame('Items', $entity->avatar->model); + $this->assertNotEmpty($entity->avatar->foreign_key); + $this->assertSame('Avatars', $entity->avatar->collection); + $this->assertStringStartsWith('Avatars', $entity->avatar->path); + $this->assertNotEmpty($entity->avatar->metadata); + $this->assertNotEmpty($entity->avatar->variants); + + $expected = [ + 'width' => 512, + 'height' => 512, // !!! + ]; + $this->assertSame($expected, $entity->avatar->metadata); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dc92d2d7..b658a928 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,10 +2,6 @@ declare(strict_types = 1); -/** - * Bootstrap - */ - use Cake\Core\Plugin; $findRoot = function ($root) { diff --git a/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php b/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php new file mode 100644 index 00000000..4c5c972e --- /dev/null +++ b/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php @@ -0,0 +1,43 @@ +root = $root ?: TMP; + } + + /** + * @inheritDoc + */ + public function process(FileInterface $file): FileInterface + { + $path = $this->root . $file->path(); + if (!file_exists($path)) { + throw new \RuntimeException('Cannot find file: ' . $path); + } + + $dimensions = @getimagesize($path); + if (!$dimensions) { + return $file; + } + + $file = $file->withMetadataKey('width', $dimensions[0]) + ->withMetadataKey('height', $dimensions[1]); + + return $file; + } +}