处理上传文件

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>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->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/

您应该会看到一个上传表单。尝试上传一个图像文件(**jpg**、**gif**、**png** 或 **webp**)。如果控制器中的路径正确,它应该可以工作。

访问文件

所有文件

当您上传文件时,可以通过 PHP 中的 $_FILES 超级全局变量以原生方式访问它们。当处理一次上传的多个文件时,此数组有一些重大缺点,并且存在许多开发人员没有意识到的潜在安全漏洞。CodeIgniter 通过在通用接口后面标准化您的文件使用来帮助解决这两个问题。

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

<?php

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

当然,有多种方法可以命名文件输入,除了最简单的之外,任何方法都可能产生奇怪的结果。数组以您期望的方式返回。对于最简单的用法,单个文件可能会像这样提交

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

这将返回一个简单的数组,例如

[
    'avatar' => // UploadedFile instance,
];

注意

UploadedFile 实例对应于 $_FILES。即使用户只单击提交按钮而不上传任何文件,该实例仍然存在。您可以通过 UploadedFile 中的 isValid() 方法检查文件是否已实际上传。请参阅 验证文件

如果您对名称使用了数组表示法,则输入将类似于

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

getFiles() 返回的数组将更像这样

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

在某些情况下,您可能需要指定要上传的文件数组

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 instance,
                1 => // UploadedFile instance,
            ],
        ],
    ],
]

单个文件

如果您只需要访问单个文件,可以使用 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');

另一个例子

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 类型(MIME 类型)。这不是一个可信的值。对于可信版本,请改用 getMimeType()

<?php

$type = $file->getClientMimeType();

echo $type; // image/png

getClientPath()

版本 4.4.0 中新增。

当客户端通过目录上传上传文件时,返回上传文件的 webkit 相对路径。在低于 8.1 的 PHP 版本中,这将返回 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,在某些情况下。

  • 文件已移动

  • 文件未成功上传

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