diff --git a/config/Migrations/20201110234846_AddCollectionColumn.php b/config/Migrations/20201110234846_AddCollectionColumn.php new file mode 100644 index 0000000..2edca26 --- /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/phpcs.xml b/phpcs.xml index 260755e..61f0a9a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -10,12 +10,12 @@ - + - + diff --git a/phpstan.neon b/phpstan.neon index bd7887a..02e6eb7 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 943de9c..d19c018 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 0000000..aaf7531 --- /dev/null +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -0,0 +1,126 @@ + [], + ]; + + /** + * @inheritDoc + */ + public function initialize(array $config): void + { + parent::initialize($config); + + $class = get_class($this->getTable()); + foreach ($config['associations'] as $association => $assocConfig) { + $associationObject = $this->getTable()->getAssociation($association); + + $defaults = [ + 'replace' => $associationObject instanceof HasOne, + 'model' => substr($class, strrpos($class, '\\') + 1, -5), + '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 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([ + 'collection' => $assocConfig['collection'], + 'model' => $assocConfig['model'], + '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 4f95446..190fb93 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'); } /** @@ -184,11 +181,12 @@ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayO try { $file = $this->entityToFileObject($entity); $file = $this->fileStorage->store($file); + + // TODO: move into stack processing $file = $this->processImages($file, $entity); - foreach ($this->processors as $processor) { - $file = $processor->process($file); - } + $processor = $this->getFileProcessor(); + $file = $processor->process($file); $entity = $this->fileObjectToEntity($file, $entity); $this->getTable()->save( @@ -219,7 +217,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 +264,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 +283,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 +332,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 +350,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 4913d8a..1b063c0 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 { diff --git a/src/Model/Table/FileStorageTable.php b/src/Model/Table/FileStorageTable.php index 9aa7ced..95e357b 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 53fbd6f..2f6a0eb 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 e1f36f5..0ac05d6 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/FileStorageFixture.php b/tests/Fixture/FileStorageFixture.php index b72d5a6..60b5d6e 100644 --- a/tests/Fixture/FileStorageFixture.php +++ b/tests/Fixture/FileStorageFixture.php @@ -32,13 +32,14 @@ class FileStorageFixture extends TestFixture '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, '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],