CURL: POST запрос, составное содержимое (multipart/form-data)

CURL upload    Очень часто встает вопрос отправки POST запроса на удаленный сервер. Давайте подробно рассмотрим как это сделать с помощью библиотеки CURL.    Самый простой вариант эмуляция отправки INPUT полей формы. Допустим есть некая форма:

<html>
<head>
<title>Загрузка формы на сервер</title>
</head>
<body>
<form method="POST" action="http://server/upload/">
<input type="text" name="field1">
<input type="text" name="field2">
<input type="submit">
</form>
</body>
</html>

Тогда наш код, эмулирующий отправку данных, будет следующим:

// Инициализируем библиотеку CURL
$ch = curl_init();

// Укажем куда будем отправлять нашу форму POST запросом
curl_setopt($ch, CURLOPT_URL, 'http://server/upload/');

// Указываем CURL, что будем отправлять POST запрос
curl_setopt($ch, CURLOPT_POST, 1);

// Передаем массив с полями формы, где field1, field2 - имена тегов, а value1, value2 - значения тегов
curl_setopt($ch, CURLOPT_POSTFIELDS, array('field1'=>'value1', 'field2'=>'value2'));

А что делать если нам нужно отправить не только текст с формы, но и отправить файл? Для этого в ячейку массива заносим путь к файлу, который нужно отправить и перед ним ставим знак «@». Данный знак является директивой для CURL, что это путь к файлу и соответственно передать его на заданный ресурс. Наш код преобразится в такой вид:

// Инициализируем библиотеку CURL
$ch = curl_init();

// Укажем куда будем отправлять нашу форму POST запросом
curl_setopt($ch, CURLOPT_URL, 'http://server/upload/');

// Указываем CURL, что будем отправлять POST запрос
curl_setopt($ch, CURLOPT_POST, 1);

// Передаем массив с полями формы, где field1, field2 - имена тегов, а value1, value2 - значения тегов
curl_setopt($ch, CURLOPT_POSTFIELDS, array('field1'=>'value1', 'field2'=>'value2', 'file'=>'@/path/to/file'));

Все вроде хорошо, но вот беда, мы формируем сами этот файл в памяти, или же получаем его откуда-то. В этом случае не всегда удобно сохранять файл на диск, что бы CURL отправил его POST запросом на удаленный ресурс. Как быть тогда? Для решения этой задачи будем формировать запрос «вручную» формируя тело и заголовок сами.

Для удобства напишем вначале класс-контейнер, в котором будет находится содержимое файла и его параметры:

class oFile
{
 private $name;
 private $mime;
 private $content;

 public function __construct($name, $mime=null, $content=null)
 {
// Проверяем, если $content=null, значит в переменной $name - путь к файлу
  if(is_null($content))
  {
   $info = pathinfo($name);
   if(!empty($info['basename']) && is_readable($name))
    {
     $this->name = $info['basename'];
// Определяем MIME тип файла
     $this->mime = mime_content_type($name);
// Загружаем файл
     $content = file_get_contents($name);
     if($content!==false) $this->content = $content;
       else throw new Exception('Don`t get content - "'.$name.'"');
    } else throw new Exception('Error param');
  } else
     {
      $this->name = $name;
      if(is_null($mime)) $mime = mime_content_type($name);
      $this->mime = $mime;
      $this->content = $content;
     };
 }

 public function Name() { return $this->name; }

 public function Mime() { return $this->mime; }

 public function Content() { return $this->content; }

};

Этот класс нужен, что бы в массиве для POST запроса, можно было идентифицировать элементы содержащие файл. Затем напишем универсальный класс, формирующий «multipart/form-data» следуя  RFC7478:

class BodyPost
 {
  public static function PartPost($name, $val)
  {
   $body = 'Content-Disposition: form-data; name="' . $name . '"';
// Проверяем передан ли класс oFile
   if($val instanceof oFile)
    {
// Извлекаем имя файла
     $file = $val->Name();
// Извлекаем MIME тип файла
     $mime = $val->Mime();
// Извлекаем содержимое файла
     $cont = $val->Content();

     $body .= '; filename="' . $file . '"' . "\r\n";
     $body .= 'Content-Type: ' . $mime ."\r\n\r\n";
     $body .= $cont."\r\n";
    } else $body .= "\r\n\r\n".urlencode($val)."\r\n";
   return $body;
  }

  public static function Get(array $post, $delimiter='-------------0123456789')
  {
   if(is_array($post) && !empty($post))
    {
     $bool = false;
// Проверяем есть ли среди элементов массива файл
     foreach($post as $val) if($val instanceof oFile) {$bool = true; break; };
     if($bool)
      {
       $ret = '';
// Формируем из каждого элемента массива, составное тело POST запроса
       foreach($post as $name=>$val)
        $ret .= '--' . $delimiter. "\r\n". self::PartPost($name, $val);
        $ret .= "--" . $delimiter . "--\r\n";
      } else $ret = http_build_query($post);
    } else throw new \Exception('Error input param!');
   return $ret;
  }
};

Этот класс обработает массив с элементами формы, и если встретится среди элементов массива класс — oFile, то он сформирует тело POST запроса с учетом требований «multipart/form-data» для составного типа содержимого.

Теперь наша программа будет выглядеть так:

include "ofile.class.php";
include "bodypost.class.php";

$delimiter = '-------------'.uniqid();

// Формируем объект oFile содержащий файл
$file = new oFile('sample.txt', 'text/plain', 'Content file');

// Формируем тело POST запроса
$post = BodyPost::Get(array('field'=>'text', 'file'=>$file), $delimiter);

// Инициализируем  CURL
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'http://server/upload/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);

/* Указываем дополнительные данные для заголовка:
     Content-Type - тип содержимого, 
     boundary - разделитель и 
     Content-Length - длина тела сообщения */
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: multipart/form-data; boundary=' . $delimiter,
'Content-Length: ' . strlen($post)));

// Отправляем POST запрос на удаленный ресурс
curl_exec($ch);

Вот так можно выполнить нашу задачу, миновав лишнее звено сохранения на диск. Этим мы сэкономим память, время и ресурсы.

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *