处理上传的文件

在 CodeIgniter 中通过表单使用文件上传功能将会比直接使用 PHP 的 $_FILES 数组更加简单和安全。这是 文件类 的扩展,因此获得了该类所有的特性。

备注

这和 CodeIgniter v3.x 中的文件上传类不太一样。这里提供了一个访问上传文件的原始接口和一些小特性。

过程

上传一个文件涉及以下一般过程:

  • 显示一个上传表单,允许用户选择一个文件并上传。

  • 当表单提交时,文件被上传到你指定的目的地。

  • 在上传过程中,会验证文件是否被允许上传,基于你设置的首选项。

  • 一旦上传完成,用户将看到一个成功的消息。

为了演示这个过程,这里是一个简单的教程。之后你会找到参考信息。

创建上传表单

使用文本编辑器,创建一个名为 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>

你会注意到我们使用了一个表单辅助函数来创建表单开标签。文件上传需要一个多部分表单,所以辅助函数帮我们创建了正确的语法。你也会注意到我们有一个 $errors 变量。这是为了在用户做错事时显示错误信息。

成功页面

使用文本编辑器,创建一个名为 upload_success.php 的页面。在其中放入下面的代码,并保存到你的 app/Views/ 目录:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>上传表单</title>
</head>
<body>

<h3>你的文件上传成功!</h3>

<ul>
    <li>名称:<?= esc($uploaded_fileinfo->getBasename()) ?></li>
    <li>大小:<?= esc($uploaded_fileinfo->getSizeByUnit('kb')) ?> KB</li>
    <li>扩展名:<?= esc($uploaded_fileinfo->guessExtension()) ?></li>
</ul>

<p><?= anchor('upload', '上传另一个文件!') ?></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->validate($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);
    }
}

备注

由于 HTML 文件上传字段的值不存在,它存储在 $_FILES 全局变量中,所以只能使用 文件上传规则 来验证上传的文件,不能使用 验证器required 规则也不能使用,请使用 uploaded 代替。

路由

使用文本编辑器,打开 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 type="file" name="avatar">

它将返回一个简单的像这样的数组:

[
    'avatar' => // 上传的文件实例
];

备注

UploadedFile 实例对应 $_FILES 。即使用户只是点击提交按钮而没有上传任何文件,该实例也仍然存在。你可以通过 UploadedFile 的 isValid() 方法检查文件是否真的被上传。参见 验证文件

如果你为名称使用了数组表示法,输入看起来像这样:

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

getFiles() 返回的数组看起来更像这样:

[
     'my-form' => [
        'details' => [
            'avatar' => // 上传的文件实例
        ],
    ],
]

在某些情况下,你可以指定一个文件数组来上传:

上传头像: <input type="file" name="my-form[details][avatars][]">
上传头像: <input type="file" name="my-form[details][avatars][]">

在这种情况下,返回的文件数组更像是:

[
    'my-form' => [
        'details' => [
            'avatar' => [
                0 => // 上传的文件实例,
                1 => // 上传的文件实例,
            ],
        ],
    ],
]

单个文件

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

最简单的用法

使用最简单的方式,单个文件可能这样提交:

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

它将返回一个简单的文件实例,像这样:

<?php

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

数组表示法

如果你为名称使用数组表示法,输入看起来像这样:

<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');

另一个例子:

上传头像: <input type="file" name="my-form[details][avatars][]">
上传头像: <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()

在 4.4.0 版本加入.

当客户端通过目录上传方式上传文件时,返回上传文件的 webkit 相对路径。 在 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:

  • 文件已经移动过

  • 文件上传不成功

  • 文件移动操作失败(例如权限不正确)