CakePHP:文件上传时 class Laminas\Diactoros\UploadedFile 的对象无法转换为字符串

CakePHP : Object of class Laminas\Diactoros\UploadedFile could not be converted to string while file upload

我正在尝试在 cakephp 版本 4 中上传文件。

我正在关注这个 docs

我在控制器里试过了

if ($this->request->is('post')) {

            $image = $this->request->getData('image');
            $fileName = $image->getClientFilename();

            $targetPath = WWW_ROOT.'img'.DS.$fileName;

            $image->moveTo($targetPath);

            $user = $this->Users->patchEntity($user, $this->request->getData());   //line 58

            $user->image = $fileName;

            $this->Users->save($user);
}

图片上传正常,名称也保存在数据库中。但是当发生验证错误时,我得到

Warning (4096): Object of class Laminas\Diactoros\UploadedFile could not be converted to string [CORE\src\Database\Type\StringType.php, line 97]

日志

Cake\Database\Type\StringType::marshal() - CORE\src\Database\Type\StringType.php, line 97
Cake\ORM\Marshaller::Cake\ORM\{closure}() - CORE\src\ORM\Marshaller.php, line 78
Cake\ORM\Marshaller::merge() - CORE\src\ORM\Marshaller.php, line 558
Cake\ORM\Table::patchEntity() - CORE\src\ORM\Table.php, line 2761
App\Controller\UsersController::add() - APP/Controller\UsersController.php, line 58
Cake\Controller\Controller::invokeAction() - CORE\src\Controller\Controller.php, line 524
Cake\Controller\ControllerFactory::invoke() - CORE\src\Controller\ControllerFactory.php, line 79
Cake\Http\BaseApplication::handle() - CORE\src\Http\BaseApplication.php, line 229
Cake\Http\Runner::handle() - CORE\src\Http\Runner.php, line 77
Cake\Http\Runner::handle() - CORE\src\Http\Runner.php, line 77
Cake\Http\Middleware\CsrfProtectionMiddleware::process() - CORE\src\Http\Middleware\CsrfProtectionMiddleware.php, line 132
Cake\Http\Runner::handle() - CORE\src\Http\Runner.php, line 73
Cake\Http\Runner::run() - CORE\src\Http\Runner.php, line 58
Cake\Routing\Middleware\RoutingMiddleware::process() - CORE\src\Routing\Middleware\RoutingMiddleware.php, line 162
Cake\Http\Runner::handle() - CORE\src\Http\Runner.php, line 73
Cake\Routing\Middleware\AssetMiddleware::process() - CORE\src\Routing\Middleware\AssetMiddleware.php, line 68

首先,您的代码示例盲目地信任客户端,它接受任何文件,并允许通过将路径作为文件名传递来选择任意目标位置!切勿在没有 validating/sanitizing 的情况下使用客户端数据,相信客户端提供的文件日期会导致各种漏洞!!!

话虽这么说,通常您要么使用单独的字段来上传文件,要么使用在数据编组时不转换数据的自定义数据库类型,以便您可以在之后手动转换它。目前,您似乎使用了应该在数据库中保存字符串的上传字段,因此编组器将尝试将输入转换为字符串,这当然会失败。

因此,例如,将表单控件从 image 重命名为 image_file(数据库中不应存在此类列),为 image_file 添加适当的验证规则,这几行,以确保上传有效,即检查类型、大小、名称等:

$validator
    ->notEmptyFile('image_file')
    ->uploadedFile('image_file', [
        'types' => ['image/png'], // only PNG image files
        'minSize' => 1024, // Min 1 KB
        'maxSize' => 1024 * 1024 // Max 1 MB
    ])
    ->add('image_file', 'minSize', [
        'rule' => ['imageSize', [
            // Min 10x10 pixel
            'width' => [Validation::COMPARE_GREATER_OR_EQUAL, 10],
            'height' => [Validation::COMPARE_GREATER_OR_EQUAL, 10],
        ]]
    ])
    ->add('image_file', 'maxSize', [
        'rule' => ['imageSize', [
            // Max 100x100 pixel
            'width' => [Validation::COMPARE_LESS_OR_EQUAL, 100],
            'height' => [Validation::COMPARE_LESS_OR_EQUAL, 100],
        ]]
    ])
    ->add('image_file', 'filename', [
        'rule' => function (UploadedFileInterface $file) {
            // filename must not be a path
            $filename = $file->getClientFilename();
            if (strcmp(basename($filename), $filename) === 0) {
                return true;
            }

            return false;
        }
    ])
    ->add('image_file', 'extension', [
        'rule' => ['extension', ['png']] // .png file extension only
    ]);

然后在补丁之后处理上传,即在验证发生之后,只有在验证成功时才移动文件!

$user = $this->Users->patchEntity($user, $this->request->getData());
if (!$user->getErrors()) {
    // never trust anything in `$image` if you haven't properly validated it!!!
    $image = $this->request->getData('image_file');
    $fileName = $image->getClientFilename();
    // if you want to take it a step further, drop the user supplied filename
    // $fileName = \Cake\Utility\Security::hash($image->getClientFilename(), 'sha1');
    // and re-encode the image to remove possible dangerous payloads, meta data, etc
    // reEncodeImage($image->getStream());

    $image->moveTo(WWW_ROOT . 'img' . DS . $fileName);

    $user->image = $fileName;
}

if ($this->Users->save($user)) {
    // success
} else {
    // failure
}

一旦掌握了工作原理,您可能应该考虑在 table class 或行为(beforeSave() event/callback 在执行上传逻辑时非常流行)或某种服务。

您可能还想看看现有插件如何处理上传,例如 https://github.com/FriendsOfCake/awesome-cakephp#user-content-files

// add
public function add()
{
    $info = $this->Infos->newEmptyEntity();           //debug($info);
    $this->Authorization->authorize($info);           

    if ($this->request->is('post')) {

        $data       = $this->request->getData();                 //debug($data);
        $dimage     = $this->request->getData('image');                 

        $name       = $dimage->getClientFilename();              //debug($name);
        $type       = $dimage->getClientMediaType();             //debug($type);           
        $size       = $dimage->getSize();                        //debug($size);
        $tmpName    = $dimage->getStream()->getMetadata('uri');  //debug($tmpName);
        $error      = $dimage->getError();                       //debug($error); 
        
        $targetPath = WWW_ROOT . 'img' . DS . 'infos' . DS . $name;  //debug($targetPath); 

        if ($type == 'image/jpeg' || $type == 'image/jpg'  || $type == 'image/png') {
            if (!empty($name)) {
                if ( ($size > 0) && ($size < 1048576) && ($error == 0)) {    // less 1MB    
                    $dimage->moveTo($targetPath);     
                }
            }
        } 

        $data['image'] = $name;  //debug($info); //debug($data);
        $info          = $this->Infos->patchEntity($info, $data);        
        $info->user_id = $this->request->getAttribute('identity')->getIdentifier();        

        if ($this->Infos->save($info)) {
            $this->Flash->success(__('The info has been saved.'));
            return $this->redirect(['controller' => 'Infos', 'action' => 'view', $info->id]);
        } else {
            $this->Flash->error(__('The info could not be saved. Please, try again.'));
            return $this->redirect(['controller'=>'Infos', 'action' => 'add']);
        }    
    }
    
    $infoCategories = $this->Infos->InfoCategories->find('list', ['limit' => 200])->all();
    $this->set(compact('info','infoCategories'));
}