導入
PHPの cURL関数群 はC言語で書かれた libcurl の極薄ラッパーとして実装されていますが、ポストフィールドのファイル取り扱いに関する実装が クソ仕様 としか思えないようになっています。
以下の2つのファイルを用意します。
<?php
$params = /* something */ ;
$ch = curl_init();
curl_setopt_array($ch, array(
    CURLOPT_URL        => 'http://localhost/respond.php',
    CURLOPT_POST       => true,
    CURLOPT_POSTFIELDS => $params,
));
curl_exec($ch);
<?php
$headers = apache_request_headers();
var_dump(compact('headers', '_POST', '_FILES'));
$params が 文字列 のとき
$params = 'a=b.jpg&c=@d.jpg';
array(3) {
  ["headers"]=>
  array(4) {
    ["Host"]=>
    string(9) "localhost"
    ["Accept"]=>
    string(3) "*/*"
    ["Content-Length"]=>
    string(2) "16"
    ["Content-Type"]=>
    string(33) "application/x-www-form-urlencoded"
  }
  ["_POST"]=>
  array(2) {
    ["a"]=>
    string(5) "b.jpg"
    ["c"]=>
    string(6) "@d.jpg"
  }
  ["_FILES"]=>
  array(0) {
  }
}
全てが文字列のパラメータとして application/x-www-form-urlencoded で送信されます。こっちは特に問題なし。
$params が (1次元) 連想配列 のとき
$params = array('a' => 'b.jpg', 'c' => '@d.jpg');
array(3) {
  ["headers"]=>
  array(5) {
    ["Host"]=>
    string(9) "localhost"
    ["Accept"]=>
    string(3) "*/*"
    ["Content-Length"]=>
    string(6) "219587"
    ["Expect"]=>
    string(12) "100-continue"
    ["Content-Type"]=>
    string(70) "multipart/form-data; boundary=----------------------------300acbbc7ca5"
  }
  ["_POST"]=>
  array(1) {
    ["a"]=>
    string(5) "b.jpg"
  }
  ["_FILES"]=>
  array(1) {
    ["c"]=>
    array(5) {
      ["name"]=>
      string(5) "d.jpg"
      ["type"]=>
      string(24) "application/octet-stream"
      ["tmp_name"]=>
      string(24) "/var/tmp/phpC9D4.tmp"
      ["error"]=>
      int(0)
      ["size"]=>
      int(219264)
    }
  }
}
(PHPバージョン5.5以降は非推奨とされていますが)@ を先頭につけたものはファイル名と見なされ、ファイルのアップロードが行われます。しかし何と… PHP5.4以前 のcURL関数群には@ をエスケープする手段が用意されていません!
問題点
ファイルをアップロードしなくてもいいものに関しては、単に http_build_query 関数でクエリ文字列化するなどして送れば解決します。しかし、ファイルのアップロードをしたい、且つ @ 付きの文字列を送りたいときに困りますね。
具体的には…TwitterのAPI POST statuses/update_with_media を利用して @ 付きのツイートを送りたいときに…
$params = array(
    'status'  => '@mpyw テストツイート!',
    'media[]' => '@/home/mpyw/test.jpg', 
);
真っ先に思いつくのはこういう方法です。
statusパラメータに関しては、送信するツイートの頭に半角スペースを付加し、Twitter側にトリミングさせることで解決する。
$params = array(
    'status'  => ' @mpyw テストツイート!',
    'media[]' => '@/home/mpyw/test.jpg', 
);
しかし、ライブラリを使用する側でそんな面倒なことしたくないし、それで解決したとしてもアプリケーション設計という観点からみればやはり大問題です。また、cURLを利用している多くのTwitterAPI用のライブラリは、このことを一切考慮しない実装となっていることが多いです。
解決策
array(3) {
  ["headers"]=>
  array(5) {
    ["Host"]=>
    string(9) "localhost"
    ["Accept"]=>
    string(3) "*/*"
    ["Expect"]=>
    string(12) "100-continue"
    ["Content-Type"]=>
    string(83) "multipart/form-data; boundary=---------------------e00ed8ddd5d4d50c1dc97be740f0bb27"
    ["Content-Length"]=>
    string(6) "219628"
  }
  ["_POST"]=>
  array(1) {
    ["status"]=>
    string(30) "@mpyw テストツイート!"
  }
  ["_FILES"]=>
  array(1) {
    ["media"]=>
    array(5) {
      ["name"]=>
      array(1) {
        [0]=>
        string(5) "test.jpg"
      }
      ["type"]=>
      array(1) {
        [0]=>
        string(24) "application/octet-stream"
      }
      ["tmp_name"]=>
      array(1) {
        [0]=>
        string(24) "/var/tmp/php1EC9.tmp"
      }
      ["error"]=>
      array(1) {
        [0]=>
        int(0)
      }
      ["size"]=>
      array(1) {
        [0]=>
        int(219264)
      }
    }
  }
}
PHP5.6以降
- CURLFile クラスを利用する。
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL         => 'http://localhost/respond.php',
    CURLOPT_POST        => true,
    CURLOPT_POSTFIELDS  => [
        'status'  => '@mpyw テストツイート!',
        'media[]' => new CURLFile('/home/mpyw/test.jpg'),
    ],
]);
curl_exec($ch);
PHP5.6以降ではデフォルトで @ が無効になっています。
PHP5.5以降
- CURLOPT_SAFE_UPLOADを有効にする。
- CURLFile クラスを利用する。
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL         => 'http://localhost/respond.php',
    CURLOPT_POST        => true,
    CURLOPT_SAFE_UPLOAD => true,
    CURLOPT_POSTFIELDS  => [
        'status'  => '@mpyw テストツイート!',
        'media[]' => new CURLFile('/home/mpyw/test.jpg'),
    ],
]);
curl_exec($ch);
CURLOPT_SAFE_UPLOAD を有効にすることで、PHP5.6と同じ状態に出来ます。
PHP5.3以降
- 自前でマルチパートボディを組み立てて文字列にする。
- 自前でContent-Typeを変更する。
$ch = curl_init('http://localhost/respond.php');
curl_custom_postfields(
    $ch,
    array('status'  => '@mpyw テストツイート!'),
    array('media[]' => '/home/mpyw/test.jpg')
);
curl_exec($ch);
/**
 * マルチパートボディを組み立てて文字列としてセットする。
 * 
 * @param resource $ch cURLリソース
 * @param array $assoc 「送信する名前 => 送信する値」の形の連想配列
 * @param array $files 「送信する名前 => ファイルパス」の形の連想配列
 * @return bool 成功 or 失敗
 */
function curl_custom_postfields($ch, array $assoc = array(), array $files = array())
{
    $body = array();
    foreach ($assoc as $k => $v) {
        $k = str_replace(array("\0", "\"", "\r", "\n"), "_", $k);
        $body[] = implode("\r\n", array(
            "Content-Disposition: form-data; name=\"{$k}\"",
            "",
            filter_var($v), 
        ));
    }
    foreach ($files as $k => $v) {
        switch (true) {
            case false === $v = realpath(filter_var($v)):
            case !is_file($v):
            case !is_readable($v):
                continue; // return false や throw new InvalidArgumentException もアリ
        }
        $data = file_get_contents($v);
        $v = call_user_func("end", explode(DIRECTORY_SEPARATOR, $v));
        list($k, $v) = str_replace(array("\0", "\"", "\r", "\n"), "_", array($k, $v));
        $body[] = implode("\r\n", array(
            "Content-Disposition: form-data; name=\"{$k}\"; filename=\"{$v}\"",
            "Content-Type: application/octet-stream",
            "",
            $data,
        ));
    }
    do {
        $boundary = "---------------------" . md5(mt_rand() . microtime());
    } while (preg_grep("/{$boundary}/", $body));
    array_walk($body, function (&$part) use ($boundary) {
        $part = "--{$boundary}\r\n{$part}";
    });
    $body[] = "--{$boundary}--";
    $body[] = "";
    return curl_setopt_array($ch, array(
        CURLOPT_POST       => true,
        CURLOPT_POSTFIELDS => implode("\r\n", $body),
        CURLOPT_HTTPHEADER => array(
            "Expect: 100-continue",
            "Content-Type: multipart/form-data; boundary={$boundary}",
        ),
    ));
}
内部でクロージャを使用しているためPHP5.3以降とさせていただきました。
(書き方次第でPHP5.2以前にも対応出来ますが、今更サポートの切れたそんな古いバージョン使わないでください)