PageRenderTime 30ms CodeModel.GetById 4ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Model/Behavior/UploadBehavior.php

http://github.com/josegonzalez/upload
PHP | 276 lines | 169 code | 28 blank | 79 comment | 24 complexity | 7e0ce06afe4047a1a618c47a3ba97d00 MD5 | raw file
  1. <?php
  2. declare(strict_types=1);
  3. namespace Josegonzalez\Upload\Model\Behavior;
  4. use ArrayObject;
  5. use Cake\Collection\Collection;
  6. use Cake\Datasource\EntityInterface;
  7. use Cake\Event\EventInterface;
  8. use Cake\ORM\Behavior;
  9. use Cake\Utility\Hash;
  10. use Josegonzalez\Upload\File\Path\DefaultProcessor;
  11. use Josegonzalez\Upload\File\Path\ProcessorInterface;
  12. use Josegonzalez\Upload\File\Transformer\DefaultTransformer;
  13. use Josegonzalez\Upload\File\Transformer\TransformerInterface;
  14. use Josegonzalez\Upload\File\Writer\DefaultWriter;
  15. use Josegonzalez\Upload\File\Writer\WriterInterface;
  16. use Psr\Http\Message\UploadedFileInterface;
  17. use UnexpectedValueException;
  18. class UploadBehavior extends Behavior
  19. {
  20. /**
  21. * Protected file names
  22. *
  23. * @var array
  24. */
  25. private $protectedFieldNames = [
  26. 'priority',
  27. ];
  28. /**
  29. * Initialize hook
  30. *
  31. * @param array $config The config for this behavior.
  32. * @return void
  33. */
  34. public function initialize(array $config): void
  35. {
  36. $configs = [];
  37. foreach ($config as $field => $settings) {
  38. if (is_int($field)) {
  39. $configs[$settings] = [];
  40. } else {
  41. $configs[$field] = $settings;
  42. }
  43. }
  44. $this->setConfig($configs);
  45. $this->setConfig('className', null);
  46. $schema = $this->_table->getSchema();
  47. /** @var string $field */
  48. foreach (array_keys($this->getConfig()) as $field) {
  49. $schema->setColumnType($field, 'upload.file');
  50. }
  51. $this->_table->setSchema($schema);
  52. }
  53. /**
  54. * Modifies the data being marshalled to ensure invalid upload data is not inserted
  55. *
  56. * @param \Cake\Event\EventInterface $event an event instance
  57. * @param \ArrayObject $data data being marshalled
  58. * @param \ArrayObject $options options for the current event
  59. * @return void
  60. */
  61. public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
  62. {
  63. $validator = $this->_table->getValidator();
  64. $dataArray = $data->getArrayCopy();
  65. /** @var string $field */
  66. foreach (array_keys($this->getConfig(null, [])) as $field) {
  67. if (!$validator->isEmptyAllowed($field, false)) {
  68. continue;
  69. }
  70. if (!empty($dataArray[$field]) && $dataArray[$field]->getError() !== UPLOAD_ERR_NO_FILE) {
  71. continue;
  72. }
  73. if (isset($data[$field])) {
  74. unset($data[$field]);
  75. }
  76. }
  77. }
  78. /**
  79. * Modifies the entity before it is saved so that uploaded file data is persisted
  80. * in the database too.
  81. *
  82. * @param \Cake\Event\EventInterface $event The beforeSave event that was fired
  83. * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
  84. * @param \ArrayObject $options the options passed to the save method
  85. * @return void|false
  86. */
  87. public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
  88. {
  89. foreach ($this->getConfig(null, []) as $field => $settings) {
  90. if (
  91. in_array($field, $this->protectedFieldNames, true)
  92. || !$entity->isDirty($field)
  93. ) {
  94. continue;
  95. }
  96. $data = $entity->get($field);
  97. if (!$data instanceof UploadedFileInterface) {
  98. continue;
  99. }
  100. if ($entity->get($field)->getError() !== UPLOAD_ERR_OK) {
  101. if (Hash::get($settings, 'restoreValueOnFailure', true)) {
  102. $entity->set($field, $entity->getOriginal($field));
  103. $entity->setDirty($field, false);
  104. }
  105. continue;
  106. }
  107. $path = $this->getPathProcessor($entity, $data, $field, $settings);
  108. $basepath = $path->basepath();
  109. $filename = $path->filename();
  110. $pathinfo = [
  111. 'basepath' => $basepath,
  112. 'filename' => $filename,
  113. ];
  114. $files = $this->constructFiles($entity, $data, $field, $settings, $pathinfo);
  115. $writer = $this->getWriter($entity, $data, $field, $settings);
  116. $success = $writer->write($files);
  117. if ((new Collection($success))->contains(false)) {
  118. return false;
  119. }
  120. $entity->set($field, $filename);
  121. $entity->set(Hash::get($settings, 'fields.dir', 'dir'), $basepath);
  122. $entity->set(Hash::get($settings, 'fields.size', 'size'), $data->getSize());
  123. $entity->set(Hash::get($settings, 'fields.type', 'type'), $data->getClientMediaType());
  124. $entity->set(Hash::get($settings, 'fields.ext', 'ext'), pathinfo($filename, PATHINFO_EXTENSION));
  125. }
  126. }
  127. /**
  128. * Deletes the files after the entity is deleted
  129. *
  130. * @param \Cake\Event\EventInterface $event The afterDelete event that was fired
  131. * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
  132. * @param \ArrayObject $options the options passed to the delete method
  133. * @return bool
  134. */
  135. public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
  136. {
  137. $result = true;
  138. foreach ($this->getConfig(null, []) as $field => $settings) {
  139. if (in_array($field, $this->protectedFieldNames) || Hash::get($settings, 'keepFilesOnDelete', true)) {
  140. continue;
  141. }
  142. $dirField = Hash::get($settings, 'fields.dir', 'dir');
  143. if ($entity->has($dirField)) {
  144. $path = $entity->get($dirField);
  145. } else {
  146. $path = $this->getPathProcessor($entity, $entity->get($field), $field, $settings)->basepath();
  147. }
  148. $callback = Hash::get($settings, 'deleteCallback', null);
  149. if ($callback && is_callable($callback)) {
  150. $files = $callback($path, $entity, $field, $settings);
  151. } else {
  152. $files = [$path . $entity->get($field)];
  153. }
  154. $writer = $this->getWriter($entity, null, $field, $settings);
  155. $success = $writer->delete($files);
  156. if ($result && (new Collection($success))->contains(false)) {
  157. $result = false;
  158. }
  159. }
  160. return $result;
  161. }
  162. /**
  163. * Retrieves an instance of a path processor which knows how to build paths
  164. * for a given file upload
  165. *
  166. * @param \Cake\Datasource\EntityInterface $entity an entity
  167. * @param \Psr\Http\Message\UploadedFileInterface|string $data the data being submitted for a save or the filename
  168. * @param string $field the field for which data will be saved
  169. * @param array $settings the settings for the current field
  170. * @return \Josegonzalez\Upload\File\Path\ProcessorInterface
  171. */
  172. public function getPathProcessor(EntityInterface $entity, $data, string $field, array $settings): ProcessorInterface
  173. {
  174. $processorClass = Hash::get($settings, 'pathProcessor', DefaultProcessor::class);
  175. return new $processorClass($this->_table, $entity, $data, $field, $settings);
  176. }
  177. /**
  178. * Retrieves an instance of a file writer which knows how to write files to disk
  179. *
  180. * @param \Cake\Datasource\EntityInterface $entity an entity
  181. * @param \Psr\Http\Message\UploadedFileInterface|null $data the data being submitted for a save
  182. * @param string $field the field for which data will be saved
  183. * @param array $settings the settings for the current field
  184. * @return \Josegonzalez\Upload\File\Writer\WriterInterface
  185. */
  186. public function getWriter(
  187. EntityInterface $entity,
  188. ?UploadedFileInterface $data = null,
  189. string $field,
  190. array $settings
  191. ): WriterInterface {
  192. $writerClass = Hash::get($settings, 'writer', DefaultWriter::class);
  193. return new $writerClass($this->_table, $entity, $data, $field, $settings);
  194. }
  195. /**
  196. * Creates a set of files from the initial data and returns them as key/value
  197. * pairs, where the path on disk maps to name which each file should have.
  198. * This is done through an intermediate transformer, which should return
  199. * said array. Example:
  200. *
  201. * [
  202. * '/tmp/path/to/file/on/disk' => 'file.pdf',
  203. * '/tmp/path/to/file/on/disk-2' => 'file-preview.png',
  204. * ]
  205. *
  206. * A user can specify a callable in the `transformer` setting, which can be
  207. * used to construct this key/value array. This processor can be used to
  208. * create the source files.
  209. *
  210. * @param \Cake\Datasource\EntityInterface $entity an entity
  211. * @param \Psr\Http\Message\UploadedFileInterface $data the data being submitted for a save
  212. * @param string $field the field for which data will be saved
  213. * @param array $settings the settings for the current field
  214. * @param array $pathinfo Path info.
  215. * @return array key/value pairs of temp files mapping to their names
  216. */
  217. public function constructFiles(
  218. EntityInterface $entity,
  219. UploadedFileInterface $data,
  220. string $field,
  221. array $settings,
  222. array $pathinfo
  223. ): array {
  224. $basepath = $pathinfo['basepath'];
  225. $filename = $pathinfo['filename'];
  226. $basepath = substr($basepath, -1) == DS ? $basepath : $basepath . DS;
  227. $transformerClass = Hash::get($settings, 'transformer', DefaultTransformer::class);
  228. $results = [];
  229. if (is_subclass_of($transformerClass, TransformerInterface::class)) {
  230. $transformer = new $transformerClass($this->_table, $entity, $data, $field, $settings);
  231. $results = $transformer->transform($filename);
  232. foreach ($results as $key => $value) {
  233. $results[$key] = $basepath . $value;
  234. }
  235. } elseif (is_callable($transformerClass)) {
  236. $results = $transformerClass($this->_table, $entity, $data, $field, $settings, $filename);
  237. foreach ($results as $key => $value) {
  238. $results[$key] = $basepath . $value;
  239. }
  240. } else {
  241. throw new UnexpectedValueException(sprintf(
  242. "'transformer' not set to instance of TransformerInterface: %s",
  243. $transformerClass
  244. ));
  245. }
  246. return $results;
  247. }
  248. }