# 处理上传文件

相比直接使用 PHP 的 `$_FILES` 数组，CodeIgniter 处理表单上传文件更加简单、安全。该类扩展了 File 类，因此具备 File 类的全部功能。

备注

这与 CodeIgniter 3 中的文件上传类不同。此类提供对上传文件的原始接口，附带少量便捷功能。

## 文件上传教程

上传文件的一般流程如下：

- 显示上传表单，用户选择文件并上传。

- 表单提交后，文件上传到指定的目标位置。

- 上传过程中，文件会根据设定的偏好进行验证，确保允许上传。

- 上传成功后，向用户显示成功消息。

以下是一个简短的教程来演示此过程，后面附有参考信息。

### 创建上传表单

使用文本编辑器创建名为 upload_form.php 的表单，将以下代码放入其中并保存到 app/Views 目录：

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Upload Form</title>
    </head>
    <body>

    <?php foreach ($errors as $error): ?>
        <li><?= esc($error) ?></li>
    <?php endforeach ?>

    <?= form_open_multipart('upload/upload') ?>
        <input type="file" name="userfile" size="20">
        <br><br>
        <input type="submit" value="upload">
    </form>

    </body>
    </html>

此处使用了表单辅助函数来创建表单起始标签。文件上传需要 multipart 表单，辅助函数会生成正确的语法。

同时可以看到 `$errors` 变量，用于在用户操作出错时显示错误消息。

### 成功页面

使用文本编辑器创建名为 upload_success.php 的表单，将以下代码放入其中并保存到 app/Views 目录:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Upload Form</title>
    </head>
    <body>

    <h3>Your file was successfully uploaded!</h3>

    <ul>
        <li>name: <?= esc($uploaded_fileinfo->getBasename()) ?></li>
        <li>size: <?= esc($uploaded_fileinfo->getSizeByUnit('kb')) ?> KB</li>
        <li>extension: <?= esc($uploaded_fileinfo->guessExtension()) ?></li>
    </ul>

    <p><?= anchor('upload', 'Upload Another File!') ?></p>

    </body>
    </html>

### 控制器

使用文本编辑器创建名为 Upload.php 的控制器，将以下代码放入其中并保存到 app/Controllers 目录：

    <?php

    namespace App\Controllers;

    use CodeIgniter\Files\File;

    class Upload extends BaseController
    {
        protected $helpers = ['form'];

        public function index()
        {
            return view('upload_form', ['errors' => []]);
        }

        public function upload()
        {
            $validationRule = [
                'userfile' => [
                    'label' => 'Image File',
                    'rules' => [
                        'uploaded[userfile]',
                        'is_image[userfile]',
                        'mime_in[userfile,image/jpg,image/jpeg,image/gif,image/png,image/webp]',
                        'max_size[userfile,100]',
                        'max_dims[userfile,1024,768]',
                    ],
                ],
            ];
            if (! $this->validateData([], $validationRule)) {
                $data = ['errors' => $this->validator->getErrors()];

                return view('upload_form', $data);
            }

            $img = $this->request->getFile('userfile');

            if (! $img->hasMoved()) {
                $filepath = WRITEPATH . 'uploads/' . $img->store();

                $data = ['uploaded_fileinfo' => new File($filepath)];

                return view('upload_success', $data);
            }

            $data = ['errors' => 'The file has already been moved.'];

            return view('upload_form', $data);
        }
    }

只有 文件上传规则 可用于验证上传的文件。

因此也无法使用 `required` 规则，如果文件是必需的，应改用 `uploaded` 规则。

注意，传递给 `$this->validateData()` 的第一个参数是空数组（`[]`）。这是因为文件验证规则会直接从 Request 对象获取上传文件的数据。

如果表单中除文件上传外还有其他字段，将字段数据作为第一个参数传递即可。

### 路由

使用文本编辑器打开 app/Config/Routes.php，添加以下两条路由：

    <?php

    // ...

    /*
     * --------------------------------------------------------------------
     * Route Definitions
     * --------------------------------------------------------------------
     */

    // We get a performance increase by specifying the default
    // route since we don't have to scan directories.
    $routes->get('/', 'Home::index');

    $routes->get('upload', 'Upload::index');          // Add this line.
    $routes->post('upload/upload', 'Upload::upload'); // Add this line.

    // ...

### 上传目录

上传的文件存储在 writable/uploads/ 目录中。

### 试一试！

要尝试此表单，访问类似以下 URL 的地址:

    example.com/index.php/upload/

应该会看到一个上传表单。尝试上传一个图片文件（jpg、gif、png 或 webp 均可）。如果控制器中的路径正确，应该就能成功上传。

## 访问文件

### 所有文件

上传的文件可以通过 PHP 的 `$_FILES` 超全局变量直接访问。这个数组在处理多个文件同时上传时存在重大缺陷，并且存在许多开发者不了解的潜在安全漏洞。CodeIgniter 通过统一接口来标准化文件的使用，解决了这两个问题。

通过当前的 `IncomingRequest` 实例访问文件。要检索此请求中上传的所有文件，使用 `getFiles()`。将返回一个由 `CodeIgniter\HTTP\Files\UploadedFile` 实例表示的文件数组：

    <?php

    $files = $this->request->getFiles();

当然，文件 input 标签的命名方式有多种，除最简单的情况外，其他都可能产生奇怪的结果。数组的返回方式和你期望的一样。对于最简单的用法，可能只提交一个文件:

    <input type="file" name="avatar">

将返回一个简单的数组:

    [
        'avatar' => // UploadedFile 实例,
    ];

备注

UploadedFile 实例对应 `$_FILES`。即使用户仅点击提交按钮而未上传任何文件，该实例仍然存在。可通过 UploadedFile 的 `isValid()` 方法检查文件是否真正上传。详见 验证文件。

如果使用了数组表示法命名，输入字段类似:

    <input type="file" name="my-form[details][avatar]">

`getFiles()` 返回的数组类似:

    [
         'my-form' => [
            'details' => [
                'avatar' => // UploadedFile 实例
            ],
        ],
    ]

在某些情况下，可以指定一个文件数组进行上传:

    Upload an avatar: <input type="file" name="my-form[details][avatars][]">
    Upload an avatar: <input type="file" name="my-form[details][avatars][]">

此时返回的文件数组类似:

    [
        'my-form' => [
            'details' => [
                'avatar' => [
                    0 => // UploadedFile 实例,
                    1 => // UploadedFile 实例,
                ],
            ],
        ],
    ]

### 单个文件

如果只需访问单个文件，可以使用 `getFile()` 直接获取文件实例。将返回一个 `CodeIgniter\HTTP\Files\UploadedFile` 实例：

#### 最简单的用法

最简单的用法，可能只提交一个文件:

    <input type="file" name="userfile">

将返回一个简单的文件实例：

    <?php

    $file = $this->request->getFile('userfile');

#### 数组表示法

如果使用了数组表示法来命名，input 标签类似:

    <input type="file" name="my-form[details][avatar]">

获取文件实例：

    <?php

    $file = $this->request->getFile('my-form.details.avatar');

### 多个文件

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

在控制器中：

    <?php

    if ($imagefile = $this->request->getFiles()) {
        foreach ($imagefile['images'] as $img) {
            if ($img->isValid() && ! $img->hasMoved()) {
                $newName = $img->getRandomName();
                $img->move(WRITEPATH . 'uploads', $newName);
            }
        }
    }

其中 `images` 是表单字段名称。

如果有多个文件使用相同的名称，可以使用 `getFile()` 逐个检索每个文件。

在控制器中：

    <?php

    $file1 = $this->request->getFile('images.0');
    $file2 = $this->request->getFile('images.1');

也可以使用 `getFileMultiple()`，获取同名的上传文件数组：

    <?php

    $files = $this->request->getFileMultiple('images');

另一个示例:

    Upload an avatar: <input type="file" name="my-form[details][avatars][]">
    Upload an avatar: <input type="file" name="my-form[details][avatars][]">

在控制器中：

    <?php

    $file1 = $this->request->getFile('my-form.details.avatars.0');
    $file2 = $this->request->getFile('my-form.details.avatars.1');

备注

使用 `getFiles()` 更为合适。

## 文件操作

获取 UploadedFile 实例后，即可安全地获取文件信息，并将文件移动到新位置。

### 验证文件

调用 `isValid()` 方法检查文件是否通过 HTTP 上传且无错误：

    <?php

    if (! $file->isValid()) {
        throw new \RuntimeException($file->getErrorString() . '(' . $file->getError() . ')');
    }

如上例所示，如果文件上传出错，可通过 `getError()` 和 `getErrorString()` 方法获取错误代码（整数）和错误消息。通过此方法可以发现以下错误：

- 文件超出 `upload_max_filesize` ini 指令限制。

- 文件超出表单定义的上传限制。

- 文件仅部分上传。

- 未上传任何文件。

- 文件无法写入磁盘。

- 文件无法上传：缺少临时目录。

- 文件上传被 PHP 扩展停止。

### 文件名

#### getName()

使用 `getName()` 方法获取客户端提供的原始文件名。通常是客户端发送的文件名，不应完全信任。如果文件已被移动，将返回移动后的最终文件名：

    <?php

    $name = $file->getName();

#### getClientName()

始终返回客户端上传文件的原始名称，即使文件已被移动：

    <?php

    $originalName = $file->getClientName();

#### getTempName()

要获取上传时创建的临时文件的完整路径，可以使用 `getTempName()` 方法：

    <?php

    $tempfile = $file->getTempName();

### 其他文件信息

#### getClientExtension()

根据上传的文件名返回原始文件扩展名：

    <?php

    $ext = $file->getClientExtension();

警告

这不是可信来源。如需可信版本，请改用 `guessExtension()`。

#### getClientMimeType()

返回客户端提供的文件 MIME 类型。这不是可信值。如需可信版本，请改用 `getMimeType()`：

    <?php

    $type = $file->getClientMimeType();

    echo $type; // image/png

#### getClientPath()

Added in version 4.4.0.

当客户端通过目录上传文件时，返回 webkitRelativePath。在 PHP 8.1 以下版本中，返回 `null`。

    <?php

    $clientPath = $file->getClientPath();
    echo $clientPath; // dir/file.txt, or dir/sub_dir/file.txt

### 移动文件

#### 使用原始文件名

每个文件都可以通过 `move()` 方法移动到新位置。第一个参数指定目标目录：

    <?php

    $file->move(WRITEPATH . 'uploads');

默认情况下使用原始文件名。

#### 使用新文件名

可以将新文件名作为第二个参数传入：

    <?php

    $newName = $file->getRandomName();
    $file->move(WRITEPATH . 'uploads', $newName);

#### 覆盖现有文件

默认情况下，如果目标文件已存在，将使用新文件名。例如，如果目录中已存在 image_name.jpg，则文件名会自动变为 image_name_1.jpg。

要将第三个参数设为 `true` 即可覆盖现有文件：

    <?php

    $file->move(WRITEPATH . 'uploads', null, true);

#### 检查文件是否已被移动

文件移动后会删除临时文件。通过 `hasMoved()` 方法检查文件是否已被移动，返回布尔值：

    <?php

    if ($file->isValid() && ! $file->hasMoved()) {
        $file->move($path);
    }

#### 移动失败时

在以下情况下，移动上传文件可能会失败并抛出 `HTTPException`：

- 文件已被移动

- 文件未成功上传

- 文件移动操作失败（如权限不足）

### 存储文件

每个文件都可通过 `store()` 方法移动到新位置。

最简单的用法，可能只提交一个文件:

    <input type="file" name="userfile">

默认情况下，上传文件保存在 writable/uploads 目录中。会自动创建 YYYYMMDD 文件夹和随机文件名。返回文件路径：

    <?php

    $path = $this->request->getFile('userfile')->store();

可以将目标目录作为第一个参数传入。将新文件名作为第二个参数传入：

    <?php

    $path = $this->request->getFile('userfile')->store('head_img/', 'user_name.jpg');

在以下情况下，移动上传文件可能会失败并抛出 `HTTPException`：

- 文件已被移动

- 文件未成功上传

- 文件移动操作失败（如权限不足）