$_FILES 键用于构建 PSR-7 上传文件列表

$_FILES key used for building a PSR-7 uploaded files list

短版:

当用户使用表单上传文件时,全局变量$_FILES中保存了一个数组。例如,当使用:

<input type="file" name="myfiles0" />

全局变量如下所示:

$_FILES = [
    'myfiles0' => [
        'name' => 'image-1.jpg',
        'type' => 'image/jpeg',
        'tmp_name' => '[path-to]/tmp/php/phptiV897',
        'error' => 0,
        'size' => 92738,
    ],
]

原则上,我需要知道数组 $_FILES['myfiles0'] 中的哪些键始终存在并且(可能)始终设置,无论其他键看起来如何,或使用哪种浏览器。你能告诉我吗?

请注意,$_FILES 变量还可以包含使用数组表示法上传的文件的多维数组,如下所示:

<input type="file" name="myfiles1[demo][images][]" multiple />

长版:

为了实现 PSR-7 Uploaded files,我需要对上传的文件列表进行 规范化 。初始列表可以由用户提供,也可以是使用表单上传标准文件的结果,例如$_FILES 全局变量。对于规范化过程,我需要检查以下标准文件上传密钥之一的存在和 "correctness"(可能这个词的选择不当):

原则上,如果在提供的上传文件列表(也可以是多维数组)中找到选择的key(我暂时选择tmp_name),则为假设key所属的数组项是一个标准的文件上传数组项,包含上面的key列表。否则,例如如果未找到所选键,则假定相应的数组项是 UploadedFileInterface.

的实例

不幸的是,在标准文件上传的情况下,我无法在任何地方找到关于哪个键(来自上面的列表)总是存在并且(可能)总是在 $_FILES 变量中设置的信息,无论其他列表键看起来如何,或者使用哪个浏览器。

如果你能在这件事上帮助我,我将不胜感激。

谢谢。

我决定使用 tmp_name 密钥进行文件上传验证。

很遗憾,我很久以前就做出了这个决定。所以我不记得所有支持它的论点了,这是我阅读的文档和我执行的测试的结果。虽然,其中一个论点是,与其他键相比,tmp_name 键的值在客户端不能是 set/changed。应用程序所在的环境 运行 决定应为其设置哪个值。

我将在此处 post 我当时写的 PSR-7 & PSR-17 实现的最终版本(关于 上传的文件 )。也许对某人有帮助。


执行ServerRequestFactoryInterface:

它读取上传文件列表(在 $_FILES 中找到,或作为参数手动传递),如果尚未完成,则将其转换为 "a normalized tree of upload metadata, with each leaf an instance of Psr\Http\Message\UploadedFileInterface"(参见 PSR-7 中的 “1.6 上传文件”)。

然后它创建一个 ServerRequestInterface 实例,将规范化的上传文件列表传递给它。

<?php

namespace MyLib\Http\Message\Factory\SapiServerRequestFactory;

use MyLib\Http\Message\Uri;
use MyLib\Http\Message\Stream;
use MyLib\Http\Message\UploadedFile;
use MyLib\Http\Message\ServerRequest;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\ServerRequestInterface;
use MyLib\Http\Message\Factory\ServerRequestFactory;
use Fig\Http\Message\RequestMethodInterface as RequestMethod;

/**
 * Server request factory for the "apache2handler" SAPI.
 */
class Apache2HandlerFactory extends ServerRequestFactory {

    /**
     * Create a new server request by seeding the generated request
     * instance with the elements of the given array of SAPI parameters.
     *
     * @param array $serverParams (optional) Array of SAPI parameters with which to seed
     *     the generated request instance.
     * @return ServerRequestInterface The new server request.
     */
    public function createServerRequestFromArray(array $serverParams = []): ServerRequestInterface {
        if (!$serverParams) {
            $serverParams = $_SERVER;
        }

        $this->headers = $this->buildHeaders($serverParams);
        $method = $this->buildMethod($serverParams);
        $uri = $this->buildUri($serverParams, $this->headers);
        $this->parsedBody = $this->buildParsedBody($this->parsedBody, $method, $this->headers);
        $this->queryParams = $this->queryParams ?: $_GET;
        $this->uploadedFiles = $this->buildUploadedFiles($this->uploadedFiles ?: $_FILES);
        $this->cookieParams = $this->buildCookieParams($this->headers, $this->cookieParams);
        $this->protocolVersion = $this->buildProtocolVersion($serverParams, $this->protocolVersion);

        return parent::createServerRequest($method, $uri, $serverParams);
    }

    /*
     * Custom methods.
     */

    // [... All other methods ...]

    /**
     * Build the list of uploaded files as a normalized tree of upload metadata,
     * with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
     *
     * Not part of PSR-17.
     *
     * @param array $uploadedFiles The list of uploaded files (normalized or not).
     *  Data MAY come from $_FILES or the message body.
     * @return array A tree of upload files in a normalized structure, with each leaf
     *  an instance of UploadedFileInterface.
     */
    private function buildUploadedFiles(array $uploadedFiles) {
        return $this->normalizeUploadedFiles($uploadedFiles);
    }

    /**
     * Normalize - if not already - the list of uploaded files as a tree of upload
     * metadata, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
     *
     * Not part of PSR-17.
     *
     * IMPORTANT: For a correct normalization of the uploaded files list, the FIRST OCCURRENCE
     *            of the key "tmp_name" is checked against. See "POST method uploads" link.
     *            As soon as the key will be found in an item of the uploaded files list, it
     *            will be supposed that the array item to which it belongs is an array with
     *            a structure similar to the one saved in the global variable $_FILES when a
     *            standard file upload is executed.
     *
     * @link https://secure.php.net/manual/en/features.file-upload.post-method.php POST method uploads.
     * @link https://secure.php.net/manual/en/reserved.variables.files.php $_FILES.
     * @link https://tools.ietf.org/html/rfc1867 Form-based File Upload in HTML.
     * @link https://tools.ietf.org/html/rfc2854 The 'text/html' Media Type.
     *
     * @param array $uploadedFiles The list of uploaded files (normalized or not). Data MAY come
     *  from $_FILES or the message body.
     * @return array A tree of upload files in a normalized structure, with each leaf
     *  an instance of UploadedFileInterface.
     * @throws \InvalidArgumentException An invalid structure of uploaded files list is provided.
     */
    private function normalizeUploadedFiles(array $uploadedFiles) {
        $normalizedUploadedFiles = [];

        foreach ($uploadedFiles as $key => $item) {
            if (is_array($item)) {
                $normalizedUploadedFiles[$key] = array_key_exists('tmp_name', $item) ?
                        $this->normalizeFileUploadItem($item) :
                        $this->normalizeUploadedFiles($item);
            } elseif ($item instanceof UploadedFileInterface) {
                $normalizedUploadedFiles[$key] = $item;
            } else {
                throw new \InvalidArgumentException(
                        'The structure of the uploaded files list is not valid.'
                );
            }
        }

        return $normalizedUploadedFiles;
    }

    /**
     * Normalize the file upload item which contains the FIRST OCCURRENCE of the key "tmp_name".
     *
     * This method returns a tree structure, with each leaf
     * an instance of Psr\Http\Message\UploadedFileInterface.
     *
     * Not part of PSR-17.
     *
     * @param array $item The file upload item.
     * @return array The file upload item as a tree structure, with each leaf
     *  an instance of UploadedFileInterface.
     * @throws \InvalidArgumentException The value at the key "tmp_name" is empty.
     */
    private function normalizeFileUploadItem(array $item) {
        // Validate the value at the key "tmp_name".
        if (empty($item['tmp_name'])) {
            throw new \InvalidArgumentException(
                    'The value of the key "tmp_name" in the uploaded files list '
                    . 'must be a non-empty value or a non-empty array.'
            );
        }

        // Get the value at the key "tmp_name".
        $filename = $item['tmp_name'];

        // Return the normalized value at the key "tmp_name".
        if (is_array($filename)) {
            return $this->normalizeFileUploadTmpNameItem($filename, $item);
        }

        // Get the leaf values.
        $size = $item['size'] ?? null;
        $error = $item['error'] ?? \UPLOAD_ERR_OK;
        $clientFilename = $item['name'] ?? null;
        $clientMediaType = $item['type'] ?? null;

        // Return an instance of UploadedFileInterface.
        return $this->createUploadedFile(
                        $filename
                        , $size
                        , $error
                        , $clientFilename
                        , $clientMediaType
        );
    }

    /**
     * Normalize the array assigned as value to the FIRST OCCURRENCE of the key "tmp_name" in a
     * file upload item of the uploaded files list. It is recursively iterated, in order to build
     * a tree structure, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
     *
     * Not part of PSR-17.
     *
     * @param array $item The array assigned as value to the FIRST OCCURRENCE of the key "tmp_name".
     * @param array $currentElements An array holding the file upload key/value pairs
     *  of the current item.
     * @return array A tree structure, with each leaf an instance of UploadedFileInterface.
     * @throws \InvalidArgumentException
     */
    private function normalizeFileUploadTmpNameItem(array $item, array $currentElements) {
        $normalizedItem = [];

        foreach ($item as $key => $value) {
            if (is_array($value)) {
                // Validate the values at the keys "size" and "error".
                if (
                        !isset($currentElements['size'][$key]) ||
                        !is_array($currentElements['size'][$key]) ||
                        !isset($currentElements['error'][$key]) ||
                        !is_array($currentElements['error'][$key])
                ) {
                    throw new \InvalidArgumentException(
                            'The structure of the items assigned to the keys "size" and "error" '
                            . 'in the uploaded files list must be identical with the one of the '
                            . 'item assigned to the key "tmp_name". This restriction does not '
                            . 'apply to the leaf elements.'
                    );
                }

                // Get the array values.
                $filename = $currentElements['tmp_name'][$key];
                $size = $currentElements['size'][$key];
                $error = $currentElements['error'][$key];
                $clientFilename = isset($currentElements['name'][$key]) &&
                        is_array($currentElements['name'][$key]) ?
                        $currentElements['name'][$key] :
                        null;
                $clientMediaType = isset($currentElements['type'][$key]) &&
                        is_array($currentElements['type'][$key]) ?
                        $currentElements['type'][$key] :
                        null;

                // Normalize recursively.
                $normalizedItem[$key] = $this->normalizeFileUploadTmpNameItem($value, [
                    'tmp_name' => $filename,
                    'size' => $size,
                    'error' => $error,
                    'name' => $clientFilename,
                    'type' => $clientMediaType,
                ]);
            } else {
                // Get the leaf values.
                $filename = $currentElements['tmp_name'][$key];
                $size = $currentElements['size'][$key] ?? null;
                $error = $currentElements['error'][$key] ?? \UPLOAD_ERR_OK;
                $clientFilename = $currentElements['name'][$key] ?? null;
                $clientMediaType = $currentElements['type'][$key] ?? null;

                // Create an instance of UploadedFileInterface.
                $normalizedItem[$key] = $this->createUploadedFile(
                        $filename
                        , $size
                        , $error
                        , $clientFilename
                        , $clientMediaType
                );
            }
        }

        return $normalizedItem;
    }

    /**
     * Create an instance of UploadedFileInterface.
     *
     * Not part of PSR-17.
     *
     * @param string $filename The filename of the uploaded file.
     * @param int|null $size (optional) The file size in bytes or null if unknown.
     * @param int $error (optional) The error associated with the uploaded file. The value MUST be
     *  one of PHP's UPLOAD_ERR_XXX constants.
     * @param string|null $clientFilename (optional) The filename sent by the client, if any.
     * @param string|null $clientMediaType (optional) The media type sent by the client, if any.
     * @return UploadedFileInterface
     */
    private function createUploadedFile(
            string $filename
            , int $size = null
            , int $error = \UPLOAD_ERR_OK
            , string $clientFilename = null
            , string $clientMediaType = null
    ): UploadedFileInterface {
        // Create a stream with read-only access.
        $stream = new Stream($filename, 'rb');

        return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
    }

}

基础classServerRequestFactory:

<?php

namespace MyLib\Http\Message\Factory;

use MyLib\Http\Message\Uri;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\StreamInterface;
use MyLib\Http\Message\ServerRequest;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;

/**
 * Server request factory.
 */
class ServerRequestFactory implements ServerRequestFactoryInterface {

    /**
     * Message body.
     *
     * @var StreamInterface
     */
    protected $body;

    /**
     * Attributes list.
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * Headers list with case-insensitive header names.
     * A header value can be a string, or an array of strings.
     *
     *  [
     *      'header-name 1' => 'header-value',
     *      'header-name 2' => [
     *          'header-value 1',
     *          'header-value 2',
     *      ],
     *  ]
     *
     * @link https://tools.ietf.org/html/rfc7230#section-3.2 Header Fields.
     * @link https://tools.ietf.org/html/rfc7231#section-5 Request Header Fields.
     *
     * @var array
     */
    protected $headers = [];

    /**
     * Parsed body, e.g. the deserialized body parameters, if any.
     *
     * @var null|array|object
     */
    protected $parsedBody;

    /**
     * Query string arguments.
     *
     * @var array
     */
    protected $queryParams = [];

    /**
     * Uploaded files.
     *
     * @var array
     */
    protected $uploadedFiles = [];

    /**
     * Cookies.
     *
     * @var array
     */
    protected $cookieParams = [];

    /**
     * HTTP protocol version.
     *
     * @var string
     */
    protected $protocolVersion;

    /**
     *
     * @param StreamInterface $body Message body.
     * @param array $attributes (optional) Attributes list.
     * @param array $headers (optional) Headers list with case-insensitive header names.
     *  A header value can be a string, or an array of strings.
     * @param null|array|object $parsedBody (optional) Parsed body, e.g. the deserialized body
     *  parameters, if any. The data IS NOT REQUIRED to come from $_POST, but MUST be the
     *  results of deserializing the request body content.
     * @param array $queryParams (optional) Query string arguments. They MAY be injected from
     *  PHP's $_GET superglobal, or MAY be derived from some other value such as the URI.
     * @param array $uploadedFiles (optional) Uploaded files list as a normalized tree of upload
     *  metadata, with each leaf an instance of Psr\Http\Message\UploadedFileInterface.
     * @param array $cookieParams (optional) Cookies. The data IS NOT REQUIRED to come from
     *  the $_COOKIE superglobal, but MUST be compatible with the structure of $_COOKIE.
     * @param string $protocolVersion (optional) HTTP protocol version.
     */
    public function __construct(
            StreamInterface $body
            , array $attributes = []
            , array $headers = []
            , $parsedBody = null
            , array $queryParams = []
            , array $uploadedFiles = []
            , array $cookieParams = []
            , string $protocolVersion = '1.1'
    ) {
        $this->body = $body;
        $this->attributes = $attributes;
        $this->headers = $headers;
        $this->parsedBody = $parsedBody;
        $this->queryParams = $queryParams;
        $this->uploadedFiles = $uploadedFiles;
        $this->cookieParams = $cookieParams;
        $this->protocolVersion = $protocolVersion;
    }

    /**
     * Create a new server request.
     *
     * Note that server-params are taken precisely as given - no parsing/processing
     * of the given values is performed, and, in particular, no attempt is made to
     * determine the HTTP method or URI, which must be provided explicitly.
     *
     * @param string $method The HTTP method associated with the request.
     * @param UriInterface|string $uri The URI associated with the request. If
     *     the value is a string, the factory MUST create a UriInterface
     *     instance based on it.
     * @param array $serverParams Array of SAPI parameters with which to seed
     *     the generated request instance.
     *
     * @return ServerRequestInterface
     */
    public function createServerRequest(
            string $method
            , $uri
            , array $serverParams = []
    ): ServerRequestInterface {
        // Validate method and URI.
        $this
                ->validateMethod($method)
                ->validateUri($uri)
        ;

        // Create an instance of UriInterface.
        if (is_string($uri)) {
            $uri = new Uri($uri);
        }

        // Create the server request.
        return new ServerRequest(
                $method
                , $uri
                , $this->body
                , $this->attributes
                , $this->headers
                , $serverParams
                , $this->parsedBody
                , $this->queryParams
                , $this->uploadedFiles
                , $this->cookieParams
                , $this->protocolVersion
        );
    }

    // [... Other methods ...]

}

通过 ServerRequestFactoryInterface 实现创建 ServerRequestInterface 实例:

<?php

use MyLib\Http\Message\Factory\SapiServerRequestFactory\Apache2HandlerFactory;

// [...]

// Create stream with read-only access.
$body = $streamFactory->createStreamFromFile('php://temp', 'rb');

$serverRequestFactory = new Apache2HandlerFactory(
    $body
    , [] /* attributes */
    , [] /* headers */
    , $_POST /* parsed body */
    , $_GET /* query params */
    , $_FILES /* uploaded files */
    , $_COOKIE /* cookie params */
    , '1.1' /* http protocol version */
);

$serverRequest = $serverRequestFactory->createServerRequestFromArray($_SERVER);

// [...]