class FormFileUploader

Helper class for multiple form file uploads.

This class provides API-level methods for uploading multiple files from a form.

For Form API elements or user-facing Messenger messages, use \Drupal\file\Upload\ManagedFileElementHelper instead.

Hierarchy

  • class \Drupal\file\Upload\FormFileUploader uses \Drupal\Core\StringTranslation\StringTranslationTrait

Expanded class hierarchy of FormFileUploader

See also

\Drupal\file\Upload\ManagedFileElementHelper

3 files declare their use of FormFileUploader
file.module in core/modules/file/file.module
FileSaveUploadTest.php in core/modules/file/tests/src/Kernel/FileSaveUploadTest.php
FileTestForm.php in core/modules/file/tests/file_test/src/Form/FileTestForm.php

File

core/modules/file/src/Upload/FormFileUploader.php, line 32

Namespace

Drupal\file\Upload
View source
class FormFileUploader {
  use StringTranslationTrait;
  public function __construct(protected readonly UploadedFilesExtractor $uploadedFilesExtractor, protected readonly FileUploadHandlerInterface $fileUploadedHandler, #[Autowire(service: 'cache.memory')] protected readonly MemoryCacheInterface $memoryCache, #[AutowireServiceClosure('logger.channel.file')] protected readonly \Closure $logger) {
  }
  
  /**
   * Saves file uploads to a new location.
   *
   * The files will be added to the {file_managed} table as temporary files.
   * Temporary files are periodically cleaned. Use the 'file.usage' service to
   * register the usage of the file which will automatically mark it as
   * permanent.
   *
   * Note that this function does not support correct form error handling. The
   * file upload widgets in core do support this. It is advised to use these in
   * any custom form, instead of calling this function.
   *
   * @param string $formFieldName
   *   A string that is the associative array key of the upload form element in
   *   the form array.
   * @param array $validators
   *   (optional) An associative array of Validation Constraint plugins used to
   *   validate the file.
   *   If the array is empty, 'FileExtension' will be used by default with a
   *   safe list of extensions, as follows: "jpg jpeg gif png txt doc xls pdf
   *   ppt pps odt ods odp". To allow all extensions, you must explicitly set
   *   this array to ['FileExtension' => []]. (Beware: this is not safe and
   *   should only be allowed for trusted users, if at all.)
   * @param string $destination
   *   (optional) A string containing the URI that the file should be copied
   *   to. This must be a stream wrapper URI. temporary:// is the default.
   * @param null|int $delta
   *   (optional) The delta of the file to return the file entity.
   *   Defaults to NULL.
   * @param \Drupal\Core\File\FileExists $fileExists
   *   (optional) The replace behavior when the destination file already
   *   exists.
   *
   * @return array|\Drupal\file\FileInterface|null|false
   *   An array of file entities or a single file entity if $delta != NULL.
   *   Each array element contains the file entity if the upload succeeded or
   *   FALSE if there was an error. Function returns NULL if no file was
   *   uploaded.
   *
   * @see ManagedFileElementHelper::saveFileUploads()
   */
  public function saveFormUploadedFiles(string $formFieldName, array $validators = [], string $destination = 'temporary://', ?int $delta = NULL, FileExists $fileExists = FileExists::Rename) : array|FileInterface|null|false {
    $cid = 'file:uploads:' . $formFieldName;
    $uploadedFiles = $this->uploadedFilesExtractor
      ->extractUploadedFiles($formFieldName);
    if (empty($uploadedFiles)) {
      return NULL;
    }
    // Return cached objects without processing since the file will have
    // already been processed and the paths in $_FILES will be invalid.
    if ($cached = $this->memoryCache
      ->get($cid)) {
      $files = $cached->data;
      return isset($delta) ? $files[$delta] : $files;
    }
    $files = [];
    foreach ($uploadedFiles as $i => $uploadedFile) {
      try {
        $formUploadedFile = new FormUploadedFile($uploadedFile);
        $result = $this->fileUploadedHandler
          ->handleFileUpload($formUploadedFile, $validators, $destination, $fileExists);
        if ($result->hasViolations()) {
          $errors = [];
          foreach ($result->getViolations() as $violation) {
            $errors[] = $violation->getMessage();
          }
          $message = [
            'error' => [
              '#markup' => $this->t('The specified file %name could not be uploaded.', [
                '%name' => $uploadedFile->getClientOriginalName(),
              ]),
            ],
            'item_list' => [
              '#theme' => 'item_list',
              '#items' => $errors,
            ],
          ];
          // @todo Add support for render arrays in
          // \Drupal\Core\Messenger\MessengerInterface::addMessage()?
          // @see https://www.drupal.org/node/2505497.
          \Drupal::messenger()->addError(\Drupal::service('renderer')->renderInIsolation($message));
          $files[$i] = FALSE;
          continue;
        }
        $file = $result->getFile();
        // Log security renames to help detect potential malicious uploads.
        if ($result->isSecurityRename()) {
          ($this->logger)()
            ->notice('For security reasons, the uploaded file %original_filename has been renamed to %filename.', [
            '%original_filename' => $result->getOriginalFilename(),
            '%filename' => $file->getFilename(),
          ]);
        }
        $files[$i] = $file;
      } catch (FileExistsException) {
        \Drupal::messenger()->addError($this->t('Destination file "%file" exists', [
          '%file' => $destination . $uploadedFile->getFilename(),
        ]));
        $files[$i] = FALSE;
      } catch (InvalidStreamWrapperException) {
        \Drupal::messenger()->addError($this->t('The file could not be uploaded because the destination "%destination" is invalid.', [
          '%destination' => $destination,
        ]));
        $files[$i] = FALSE;
      } catch (FileWriteException) {
        \Drupal::messenger()->addError($this->t('File upload error. Could not move uploaded file.'));
        ($this->logger)()
          ->notice('Upload error. Could not move uploaded file %file to destination %destination.', [
          '%file' => $uploadedFile->getClientOriginalName(),
          '%destination' => $destination . '/' . $uploadedFile->getClientOriginalName(),
        ]);
        $files[$i] = FALSE;
      } catch (FileException) {
        \Drupal::messenger()->addError($this->t('The file %filename could not be uploaded because the name is invalid.', [
          '%filename' => $uploadedFile->getClientOriginalName(),
        ]));
        $files[$i] = FALSE;
      } catch (LockAcquiringException) {
        \Drupal::messenger()->addError($this->t('File already locked for writing.'));
        $files[$i] = FALSE;
      }
    }
    // Add files to the cache.
    $this->memoryCache
      ->set($cid, $files, Cache::PERMANENT);
    return isset($delta) ? $files[$delta] : $files;
  }

}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.