文件上传在HTTP服务器中是最常见的一种的业务需求,Swoft本身为文件上传提供了支持。
业务需求
现在需要附件上传的功能,前端使用AJAX对图片进行上传,为此需要提供一个接口,接口的路径为admin/attachment/upload
,接收HTTP POST提交的文件上传信息。
实现流程
- 使用HTTP服务器中Request类提供的
file()
方法接口POST
发送过来的文件上传数据 - 从
file()
方法中获取上传文件相关数据 - 使用
moveTo()
方法将上传过来的临时文件转移到目标位置
关键点
- 获取表单数据
- 获取上传数据
- 移动临时文件
最终效果

数据结构
CREATE TABLE `web_attachment` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '名称',
`type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '附件类型',
`mimetype` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '媒体类型',
`hash` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '文件哈希',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '文件地址',
`size` int(10) unsigned DEFAULT '0' COMMENT '文件大小',
`storage` enum('local','upyun','qiniu') CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT 'local' COMMENT '存储位置',
`create_time` int(10) unsigned DEFAULT '0' COMMENT '创建日期',
`create_date` datetime DEFAULT NULL COMMENT '创建日期',
`update_time` int(10) unsigned DEFAULT '0' COMMENT '更新时间',
`update_date` datetime DEFAULT NULL COMMENT '更新日期',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='附件表';
实现代码
<?php
namespace App\Controllers\Admin;
use App\Models\Entity\WebAttachment;
use Swoft\Http\Message\Server\Request;
use Swoft\Http\Message\Server\Response;
use Swoft\Http\Server\Bean\Annotation\Controller;
use Swoft\Http\Server\Bean\Annotation\RequestMapping;
use Swoft\Http\Server\Bean\Annotation\RequestMethod;
use Swoft\View\Bean\Annotation\View;
/**
* 控制器 附件
* Class GroupController
* @Controller(prefix="/admin/attachment")
* @package App\Controllers\Admin
*/
class AttachmentController
{
/**
* @RequestMapping(route="upload", method={RequestMethod::POST})
* @param Request $request
*/
public function upload(Request $request)
{
if(!$request->isAjax()){
return response()->json(["error"=>1]);
}
$file = $request->file("file");
if(empty($file)){
return response()->json(["error"=>1]);
}
$arr = $file->toArray();
if(isset($arr["error"]) && $arr["error"] != 0){
return response()->json(["error"=>1]);
}
$error = $arr["error"];
$name = $arr["name"];
$type = $arr["type"];
$size = $arr["size"];
$hash = sha1(time().uniqid());
$ext = end(explode("/",$type));
$filename = $hash.".".$ext;
$datedir = date("Ymd");
$target_dir = BASE_PATH."/public/upload/{$datedir}";
if(!is_dir($target_dir)){
mkdir($target_dir);
}
$target_path = $target_dir."/".$filename;
$result = $file->moveTo($target_path);
$url = "/upload/{$datedir}/{$filename}";
$time = time();
$date = date("Y-m-d H:i:s", $time);
$model = new WebAttachment();
$model["name"] = $name;
$model["type"] = "image";
$model["url"] = $url;
$model["hash"] = $hash;
$model["mimetype"] = $type;
$model["size"] = $size;
$model["storage"] = 'local';
$model["create_time"] = $time;
$model["create_date"] = $date;
$pk = $model->save()->getResult();
if($pk > 0){
return response()->json(["error"=>0, "hash"=>$hash]);
}else{
return response()->json(["error"=>1]);
}
}
}
接收表单数据
使用HTTP
的Request
对象中的file()
方法接收表单POST
的数据
$file = $request->file("file");
首先看下file()
方法,位于swoft\wendor\http-message\src\Server\Concerns\InteractsWithInput.php
文件中。
/**
* Retrieve a upload item from the request
* 从HTTP的Request请求中检索一个文件上传项
* @param string|null $key
* @param null $default
* @return array|\Swoft\Web\UploadedFile|null
*/
public function file(string $key = null, $default = null)
{
if (is_null($key)) {
return $this->getUploadedFiles();
}
return $this->getUploadedFiles()[$key] ?? $default;
}
file()
方法会从HTTP
的Request
请求中检索一个文件上传项,方法接收一个字符串的key
的键名和一个默认值default
。
string $key = null // null值是NULL类型的唯一值,可表示尚未被赋值,同unset()后的效果一样。
$default = null
这里的键名key
实际上指的是前端表单file
上传文本域中name
属性的值,这里可以将变量$key
视为upload
。
<input type="file" name="upload" value="" />
接着通过getUploadedFiles()
方法获取表单上传的文件数据
$this->getUploadedFiles()
getUploadedFiles()
来源于HTTP
的Request
类,位于swoft\wendor\http-message\src\Server\Request.php
,其原型为:
/**
* Retrieve normalized file upload data.
* This method returns upload metadata in a normalized tree, with each leaf
* an instance of Psr\Http\Message\UploadedFileInterface.
* These values MAY be prepared from $_FILES or the message body during
* instantiation, or MAY be injected via withUploadedFiles().
*
* @return array An array tree of UploadedFileInterface instances; an empty
* array MUST be returned if no data is present.
*/
public function getUploadedFiles()
{
return $this->uploadedFiles;
}
简单来说,getUploadedFiles()
方法就是获取表单上传的所有文件信息,在PHP中提供了全局变量$_FILES
用于保存上传文件的数据。注意uploadedFiles
属性是复数,说明是一个数组,而且是私有的。
private $uploadedFiles = [];
实际上,通过getUploadedFiles()
最终会获取的是Swoft\Http\Message\Upload\UploadedFile
类的对象。
获取上传文件数据
UploadedFile
类是用来处理文件上传工作的核心,我们通过它来获取上传成功后的临时文件的信息,并将临时文件转移到目标文件。UploadedFile
类位于swoft\wendor\http-message\src\Upload\UploadedFile.php
文件中。
$file = $request->file("upload");
$arr = $file->toArray();
通过UploadedFile
对象的toArray()
方法,可以获取上传成功后临时文件相关信息。
public function toArray()
{
return [
'name' => $this->getClientFilename(),
'type' => $this->getClientMediaType(),
'tmp_file' => $this->tmpFile,
'error' => $this->getError(),
'size' => $this->getSize(),
];
}
其中主要包括五项数据
-
name
客户端上传文件的原始文件名,注意只是文件名称,包含文件后缀。 -
type
上传文件的媒体类型,也就是MIME (Multipurpose Internet Mail Extensions) 类型。 -
tmp_file
上传成功后保存在服务器中的临时文件路径 -
error
上传错误信息 -
size
原始文件大小,单位字节(byte)。
可以发现$_FILES
全局变量中保存的数据与它一致。
$_FILES["upload"]["name"] //被上传文件的名称
$_FILES["upload"]["type"] //被上传文件的类型
$_FILES["upload"]["size"] //被上传文件的大小,以字节计
$_FILES["upload"]["tmp_name"] //存储在服务器的文件的临时副本的名称
$_FILES["upload"]["error"] //由文件上传导致的错误代码
有了这些数据,基本上剩下的工作就是文件转移操作。
移动临时文件
对于原生PHP而言,提供了move_upload_file($tmp_file, $target_path)
方法用于移动文件的目标文件夹下。
UploadedFile
类提供了moveTo()
方法用于移动临时文件
/**
* Move the uploaded file to a new location.
* Use this method as an alternative to move_uploaded_file(). This method is
* guaranteed to work in both SAPI and non-SAPI environments.
* Implementations must determine which environment they are in, and use the
* appropriate method (move_uploaded_file(), rename(), or a stream
* operation) to perform the operation.
* $targetPath may be an absolute path, or a relative path. If it is a
* relative path, resolution should be the same as used by PHP's rename()
* function.
* The original file or stream MUST be removed on completion.
* If this method is called more than once, any subsequent calls MUST raise
* an exception.
* When used in an SAPI environment where $_FILES is populated, when writing
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
* used to ensure permissions and upload status are verified correctly.
* If you wish to move to a stream, use getStream(), as SAPI operations
* cannot guarantee writing to stream destinations.
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @throws \InvalidArgumentException if the $targetPath specified is invalid.
* @throws \RuntimeException on any error during the move operation, or on
* the second or subsequent call to the method.
*/
public function moveTo($targetPath)
{
$targetPath = App::getAlias($targetPath);
$this->validateActive();
if (! $this->isStringNotEmpty($targetPath)) {
throw new \InvalidArgumentException('Invalid path provided for move operation');
}
if ($this->tmpFile) {
$this->moved = php_sapi_name() == 'cli' ? rename($this->tmpFile, $targetPath) : move_uploaded_file($this->tmpFile, $targetPath);
}
if (! $this->moved) {
throw new \RuntimeException(sprintf('Uploaded file could not be move to %s', $targetPath));
}
}
未完待续...