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],