Странный $_FILES или «проблема использования синтаксиса массива в полях формы типа файл»

Меня всегда мучал вопрос по поводу того, почему так устроен массив $_FILES в PHP, точнее то, почему он очень странным образом формирует его. В случае, если имена полей формы оформлены с использованием синтаксиса массива, $_REQUEST, $_GET или $_POST будут содержать правильное представление, но… такое использование не подходит для $_FILES!

Проблема

Имеем форму:
<form action="" method="post" enctype="multipart/form-data">
  <input type="file" name="oneLevel[]">
  <input type="file" name="oneLevel[]">
  <input type="submit">
</form>

При загрузке файлов через эту форму получаем массив $_FILES следующего вида:
array(
 'files' => array (
  'name' => array (
   0 => 'Lighthouse.jpg',
   1 => 'Hydrangeas.jpg',
  ),
  'type' => array (
   0 => 'image/jpeg',
   1 => 'image/jpeg',
  ),
  'tmp_name' => array (
   0 => '/tmp/phpQR67Qp',
   1 => '/tmp/phpJjnAHA',
  ),
  'error' => array (
   0 => 0,
   1 => 0,
  ),
  'size' => array (
   0 => 561276,
   1 => 595284,
  ),
 ),
)

Мне эта «фича» в большинстве случаев не подходит и кажется нелогичной. Простейшее решение — использовать простые (строковые) имена для полей типа «файл», например file_0, file_1, ..., file_N, но это не так удобно. Если, все-таки, вас интересует решение этой проблемы — читаем далее…

Решение

В autoprepend-файле или где-нибудь в момент инициализации вашего приложения стоит обработать массив $_FILE следующим образом:
/**
* Рекурсивная функция для реструктуризации массива
*
* @param array   $arrayForFill          Массив для заполнения.
*                                       Этот массив будет содержать "правильное"
*                                       представление $_FILES
* @param string  $currentKey            Ключ текущей позиции
* @param mixed   $currentMixedValue     Значение текущей позиции
* @param string  $fileDescriptionParam  Текущий параметр описания файла
*                                       (name, type, tmp_name, error или size)
* @return void
*/
function rRestructuringFilesArray(&$arrayForFill, $currentKey,
  $currentMixedValue, $fileDescriptionParam)
{
  if (is_array($currentMixedValue)) {
    foreach ($currentMixedValue as $nameKey => $mixedValue) {
      rRestructuringFilesArray($arrayForFill[$currentKey],
                               $nameKey,
                               $mixedValue,
                               $fileDescriptionParam);
    }
  } else {
    $arrayForFill[$currentKey][$fileDescriptionParam] = $currentMixedValue;
  }
}

// массив, в котором будем формировать "правильный" $_FILES
$arrayForFill = array();

// первый уровень проходим без изменения
foreach ($_FILES as $firstNameKey => $arFileDescriptions) {

  // а вот со второго уровня интерпритатор делает то,
  // что мне в большинстве случаев не подходит
  foreach ($arFileDescriptions as $fileDescriptionParam => $mixedValue) {
    rRestructuringFilesArray($arrayForFill,
                             $firstNameKey,
                             $_FILES[$firstNameKey][$fileDescriptionParam],
                             $fileDescriptionParam);
  }
}
// перегружаем $_FILES сформированным массивом
$_FILES = $arrayForFill;

В итоге имеем более «логичный» миссив:
array(
 'files' => array (
  0 => array (
   'name' => 'Lighthouse.jpg',
   'type' => 'image/jpeg',
   'tmp_name' => '/tmp/phpKNqlsc',
   'error' => 0,
   'size' => 561276,
  ),
  1 => array (
   'name' => 'Hydrangeas.jpg',
   'type' => 'image/jpeg',
   'tmp_name' => '/tmp/phpB8X3E8',
   'error' => 0,
   'size' => 595284,
  ),
 ),
)

Разместил это решение комментарием в официальной документации, т.к. вопросов много, а конкретного примера нет. Но, на момент публикации здесь его там пока ещё не появилось (не знаю, появится ли?).

Спасибо за внимание, надеюсь кому-нибудь пригодится.

UPD: Глядя на то, как мои слова «В autoprepend-файле или где-нибудь в момент инициализации вашего приложения стоит обработать массив $_FILE» и перезапись в коде массива $_FILES вызвали некоторое негодование некоторой части сообщества, заявляю: это пример того, как можно реализовать необходимый функционал, и я не призываю использовать данный код «как есть» в боевых проектах.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 40

    +1
    Если поле назвать «name[files][]»?
      +2
      Будет
      array (
        'name' => array (
          'files' => array (
            0 => array (
              'name' => 'Новый текстовый документ.txt',
              'type' => 'text/plain',
              'tmp_name' => '/tmp/phpFYV1jb',
              'error' => 0,
              'size' => 13950,
            ),
            1 => array (
              'name' => 'Безымянный.png',
              'type' => 'image/png',
              'tmp_name' => '/tmp/phpOcCqkl',
              'error' => 0,
              'size' => 59329,
            ),
          ),
        ),
      )
        +2
        На самом деле архитектура $_FILES логична. Нужно только вспомнить о представлении таблиц в базах данных — таблица-столбец-строка.
        Именно в этом ключе и нужно рассуждать:
        files — это таблица с параметрами файлов
        name — столбец имен файлов
        type — столбец типов
        и т.п.
        Все крайне логично. И не получится, что будут потеряны какие-нибудь из параметров файла, например, размер, как в случае Вашей иерархической системы.
          +2
          Не совсем понятно как в моём случае был (будет) потерян размер.

          Плюс ко всему — в случае files[pictures][animals][dog], files[pictures][animals][cat] идеология двумерной «таблицы БД», на мой взгляд, не совсем уместна. Что в данном случае считать за координаты (БД/таблица/столбец/строка)?
            0
            Я думаю, что ситуацию возможной многомерности массива файлов разработчики PHP как-то упустили… Ну сложно им было представить себе форму в виде двух или трехмерной таблицы для Upload'а файлов :)
            Так что вся реализация основана на линейных списках значений одного типа.
            +1
            Наверное, под самую странную реализацию можно подобрать свою логику. Мне лично странно встретить в реализации $_FILES логику не стандартных хеш-массивов PHP, а баз данных.
              0
              Все очень просто — в зависимости от конкретной задачи я могу выбрать хэш (массив) того признака, который меня интересует. Например, только имена.
              Если следовать логике «иерархического дерева» (как в статье), то мы будем даже для задачи «поиск файла по имени» вынуждены извлекать все дерево.
              В логике же разработчиков PHP — только массив строк.
            0
            Уже вполне приемлемо, по-моему.
          +3
          Обычно делают как: есть какой-то код, который отвечает за сабмит формы. Чтобы отвязать этот код от суперглобальных переменных (например, для юниттестов), обычно в него передают массивы $_POST и $_FILES. Вот на этом этапе и можно преобразовать $_FILES в более удобную и логичную структуру, но только локально в коде, отвечающем за сабмит формы. В частности, именно так было сделано в Symfiny 1.x. А если вы преобразуете прямо суперглобальный массив, потом могут возникнуть проблемы. Например: вы подключите к своему приложению в одном месте сторонний код, который будет надеяться увидеть в $_FILES именно такую структуру, какая там по умолчанию — и на вашем приложении он работать уже не будет.
            0
            При желании массив $arrayForFill можно присвоить чему угодно (ну или передать куда угодно). Простите за воспринятую «пропаганду», но это решение скорее для примера.
            +18
            А еще программист, который будет разбирать код после вас, будет очень удивлен, почему $_FILES не такого формата, как должен быть. И будет очень долго материться, когда наконец-то найдет rRestructuringFilesArray() в auto_prepend-файле )
              –3
              Для таких нюансов, как мне кажется, придумали документирование. Притом, что такие глобальные вещи комментировать надо в самом заметном месте.
                +1
                Например, в каком? :)
                  –5
                  В том, которое не пройдет ни один человек (разработчик), получивший доступ к проекту…

                  Битрикс: bitrx/php_interface/init.php (здесь, собственно, я и применил описанное в посте);
                  ZF: Bootstrap/index.php/Основной_конфиг (это — для примера)
                  Мне кажется, что в любом проекте/фреймворке/CMS есть такие места (те, которые приходят в голову в первую очередь).
                    +10
                    А, так вы это для битрикса пишете, тогда ОК!
                  0
                  Документирование и комментирование как-то разные вещи. Но, у настоящих джедаев, все должно быть понятно и без комментариев.

                  «Комментарии — это дезодорант к плохо пахнущему коду».
                  Кент Бек.

                  Если другому разработчику, прежде чем понять, как работает ваш велосипед придется сначала прочитать килобайты документации и комментариев, он будет вас материть не меньше.

                  А массив $_FILES легко разбирается и без велосипедов:

                  $files = $_FILES['files'];
                  foreach( $files['error'] as $i => $file_error ) {
                  switch ( $file_error ) {
                  case UPLOAD_ERR_OK:
                  $this->fooModel()->doFooUpload( $files['name'][$i], $files['tmp_name'][$i], $files['type'][$i], $files['size'][$i] );
                  break;
                  default:
                  $this->fooResponse()->addFeedback('Error uploading file '.$files['name'][$i]);
                  break;
                  }
                  }
                  +2
                  Как я вас понимаю. Хорошо, что там где я работаю русский мат не понимают.
                  –1
                  А если ООП-вариант? Типа:
                  $files = myFwRequest::Files();
                  foreach($files as $item) {
                      echo $item->name;
                      ...
                      $item->saveAs($path);
                  }
                  
                    +1
                    А мне вот не ясно — что не так в обычном варианте?
                      +3
                      В обычном варианте при помощи foreach красиво по массиву не пробежишь
                        0
                        А Вы бегите фором а не форичем =) Очень даже красиво получается.
                          +1
                          Не вижу проблем
                          foreach($files['name'] as $k => $item) {
                              echo $files['type'][$k];
                              echo $files['tmp_name'][$k];
                              echo $files['error'][$k];
                              echo $files['size'][$k];
                          }
                          
                        0
                        Мой вариант
                        function multiple(array $_files, $top = TRUE)
                        {
                        	$files = array();
                        	foreach($_files as $name=>$file){
                        		if($top) $sub_name = $file['name'];
                        		else	$sub_name = $name;
                        		
                        		if(is_array($sub_name)){
                        			foreach(array_keys($sub_name) as $key){
                        				$files[$name][$key] = array(
                        					'name'     => $file['name'][$key],
                        					'type'     => $file['type'][$key],
                        					'tmp_name' => $file['tmp_name'][$key],
                        					'error'    => $file['error'][$key],
                        					'size'     => $file['size'][$key],
                        				);
                        				$files[$name] = multiple($files[$name], FALSE);
                        			}
                        		}else{
                        			$files[$name] = $file;
                        		}
                        	}
                        	return $files;
                        }
                        

                        Ссылка на код на github github.com/kohana-mdma/mdma-modules/blob/3.2/develop/common/classes/upload.php#L46

                        Переделывать приходится для того чтобы работали функции валидации фреймверка.
                          +1
                          Протестировал. Все работает для случая levelOne[], но не удалось получить результат для levelOne[levelTwo][]. Или я что-то не так понял?
                            +3
                            Да был косяк, вот этот вроде бы работает.
                            function multiple(array $_files)
                            	{
                            		$files = array();
                            		foreach($_files as $name=>$file){
                            			if(is_array($file['name'])){
                            				foreach(array_keys($file['name']) as $key){
                            					$files[$name][$key] = array(
                            						'name'     => $file['name'][$key],
                            						'type'     => $file['type'][$key],
                            						'tmp_name' => $file['tmp_name'][$key],
                            						'error'    => $file['error'][$key],
                            						'size'     => $file['size'][$key],
                            					);
                            					$files[$name] = multiple($files[$name]);
                            				}
                            			}else{
                            				$files[$name] = $file;
                            			}
                            		}
                            		return $files;
                            	}
                              0
                              Чем же Вам так for не угодил что нужно аж массив перестраивать и проходиться по нему в итоге (!!!)3 раза?
                                0
                                Всех очень устраивает for… очень удобный и быстрый цикл. Но что-то я не вижу «навскидку» решения поставленной задачи с помощью этого цикла, да так, чтобы «красиво» было.

                                Не дадите нам всем посмотреть на Ваш вариант?
                                Но учтите, что в данной задаче есть не только массивы вида files[], но и любого измерения (многомерные, типа files[level1][level2][level3] и т.д). Спасибо.
                                  0
                                  Вы же знаете изначально вложенность, вот и ходите по вложенности
                                  $file = $_FILES[вложенность1][вложенность2];
                                  for( $q = 0; $q < count(['errors']); $q++)
                                  {
                                      // тут делаем что надо с элементом $q
                                      // $file['name'][$q]
                                      // $file['error'][$q]
                                      // $file['etc'][$q]
                                  }
                                  
                                    +1
                                    Ответил бы с пояснением (массивами), но не хочу писать некрасивый коммент (без разметки), кармы не хватает.
                                    Вы сделайте var_dump($_FILES) имея форму с полями файлов, имена которых — files[pictures][animals][], files[pictures][animals][]. Как посмотрите их структуру, ещё раз подумайте над использованием for.

                                    Кстати, имена могут быть именноваными (files[pictures][animals][dog] например)… тогда for точно не подойдет.
                                      0
                                      Да =) Так уже фор не катит, в свое оправдание лишь скажу что так не делал никогда =) всегда как массив числовой отдаю, если больше одного файла
                          +2
                          Вы меня простите, но это велосипед, практической пользы от которого, лично я, не вижу.
                          Уже давно фреймворки научились предоставлять разработчику данные в том виде, в котором удобно.

                          Зачем городить то, для чего потом придётся ещё дописывать/переписывать/писать поддерживающие классы/методы/функции?

                          На мой взгляд, Вы из пальца высосали проблему.
                            +3
                            Не всегда и не везде используют фреймворки. Далеко не везде предоставлен инструмент. И ещё не во всех проектах код идет в ногу со временем.
                            Проблема была «высосана из пальца» при работе с одной известной CMS, в которой подобного нет (я предполагаю, что случай не единичный). Почему «проблема»? — потому что гуглы и еже с ним ответа не дали.
                            Выковыривать из фреймворков — дело не всегда удобное… данное решение — 10 строчек кода которые можно вставить куда угодно и получить то, что необходимо и достаточно.
                            Это скорее «костыль», нежели могучий функционал. В данном случае — главное, что работает, думать много не надо, совместимо с PHP4.
                              +2
                              Я понял Вашу позицию.

                              Однако, как тут уже говорилось ранее, имеет место быть такой фактор, как «ожидаемость».

                              Вы ведь не только для себя код пишете, правильно?

                              Документация, как правило, одно из самых слабых мест — поэтому, не стоит на неё надеяться, а если всё-таки надеетесь, то помните, что эту документацию придётся читать, вникать.
                              Т.е. тратить время, которое нет надобности убивать, если использовать «стандартные средства».

                              Но, я не хочу разводить холивар на эту тему, поэтому лишь пожелаю Вам удачи в программировании и успехов на этом поприще.
                                +3
                                Спасибо.

                                Напомню: не обязательно перегружать сам глобальный массив, можно в необходимом месте получать его представление и именно там добавить /* записку_охотника */
                            +1
                            Откуда столько извращенцев, зачем переопределять $_FILES, если можно было сделать простенькую обёртку с итератором и не бояться, что ваш код «сопровождать будет склонный к насилию психопат, который знает, где вы живете».
                            $oFiles = new Http_Files();
                            foreach($oFiles AS $aFile) {
                            echo $aFile['name'], PHP_EOL;
                            }
                              0
                              Вот так с помощью нехитрых манипуляций можно переопределить $_FILES и заставить материться даже очень спокойного и уравновешенного человека. Но зачем?
                              P.S. Кто нибудь добавьте картинку с троллейбусом, с телефона неудобно.
                                –1
                                PHP-шники всегда радуют )
                                  0
                                  Стандартная структура логична и понятна. С ней работать даже проще, чем с «правильной» структурой автора.
                                  Меня всегда удивляло, нет, даже бесило, когда начинают заниматься программированием ради программирования, а не достижения результата. Автору лучше придержать свои собственные хотелки при себе.
                                    0
                                    Может быть, было бы правильнее написать служебный класс и использовать его при разборе массива _FILES?
                                    Такой вот своеобразный интерфейс к массиву. Любой программист поинтересуется, что делает метод, и всё поймёт
                                      0
                                      /**
                                       * Корректирует неадекватную структуру суперглобального массива $_FILES.
                                       * Возвращает рекурсивно упорядоченный массив пришедших файлов.
                                       *
                                       * @param array $input ожидается массив $_FILES
                                       * @return array массив со скорректированной структурой
                                       */
                                      public function ConvertFilesStructure($input) {
                                          $output = [];
                                          foreach ($input as $key => $file) {
                                              $output[$key] = $this->ConvertFilesStructureRecursive(
                                                  $file['name'],
                                                  $file['type'],
                                                  $file['tmp_name'],
                                                  $file['error'],
                                                  $file['size']
                                              );
                                          }
                                          return $output;
                                      }
                                      
                                      public function ConvertFilesStructureRecursive($name, $type, $tmp_name, $error, $size) {
                                          if (!is_array($name)) {
                                              return [
                                                  'name'     => $name,
                                                  'type'     => $type,
                                                  'tmp_name' => $tmp_name,
                                                  'error'    => $error,
                                                  'size'     => $size,
                                              ];
                                          }
                                          $output = [];
                                          foreach ($name as $key => $_crap) {
                                              $output[$key] = $this->ConvertFilesStructureRecursive(
                                                  $name[$key],
                                                  $type[$key],
                                                  $tmp_name[$key],
                                                  $error[$key],
                                                  $size[$key]
                                              );
                                          }
                                          return $output;
                                      }

                                      Only users with full accounts can post comments. Log in, please.