处理上传文件

相比直接使用 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/

应该会看到一个上传表单。尝试上传一个图片文件(jpggifpngwebp 均可)。如果控制器中的路径正确,应该就能成功上传。

访问文件

所有文件

上传的文件可以通过 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

  • 文件已被移动

  • 文件未成功上传

  • 文件移动操作失败(如权限不足)