Search K
Appearance
🍵 欢迎来到技术茶馆 🍵
这里是一个分享技术、交流学习的地方
技术札记 | 茶馆周刊 | 工具书签 | 作品展示
让我们一起品茗技术,共同成长
Appearance
作为一名开发者,你是否遇到过这样的困扰:
今天,我要分享一个基于 ThinkPHP 8.0 的企业级文件上传封装库,它用设计模式优雅地解决了上述所有痛点。看完这篇文章,你不仅能理解它的设计思想,还能直接应用到自己的项目中。
这个上传库采用经典的分层架构,每一层都有其独特的职责:
UploadManager (管理层)
↓
StorageRepository (仓库层)
↓
BaseUpload (抽象层)
↓
具体驱动实现 (Local/Aliyun/Qiniu/Tencent)
↓
UploadDriver (接口层)让我用一句话概括:接口定义契约,抽象类提取共性,具体类实现差异,仓库管理配置,管理器统一入口。
interface UploadDriver
{
public function initialize(array $config): void;
public function read(\think\file\UploadedFile|string $fileOrField, bool $isRename = true): static;
public function upload(string $dir): bool;
public function base64(string $data, ?string $key = null): bool;
public function fetch(string $url, ?string $key = null): bool;
public function delete(string $fileName): bool;
public function thumb(string $filePath, string|array $thumbType): array;
}核心思想:接口是所有驱动必须遵守的"宪法",确保了所有存储驱动的行为一致性。这就是 接口隔离原则 的实践。
BaseUpload 抽象类扮演了"模板方法模式"中的模板角色:
abstract class BaseUpload implements UploadDriver
{
protected array $config = [];
protected string $storageType = '';
// ... 其他属性
public function initialize(array $config): void {
$this->config = $config;
$this->storageType = $config['storage_type'] ?? 'local';
}
public function read(\think\file\UploadedFile|string $fileOrField, bool $isRename = true): static
{
// 统一的文件读取逻辑
if ($fileOrField instanceof \think\file\UploadedFile) {
$this->file = $fileOrField;
$this->name = $fileOrField->getOriginalName();
} else {
$this->file = request()->file($fileOrField);
}
// ... 提取文件信息
return $this;
}
}设计亮点:
每个存储驱动只关注自己的核心逻辑。以阿里云OSS为例:
public function upload(string $dir): bool {
$this->validate();
$this->client()->uploadFile(
$this->config['bucket'],
$this->getFullPath($dir),
$this->fileInfo['real_path']
);
return true;
}对比本地存储:
public function upload(string $dir): bool
{
$this->validate();
mkdirs_or_notexist($dir, 0777);
$this->file->move($dir, $this->fileName);
return true;
}看到了吗?相同的接口,不同的实现,这就是策略模式的核心。切换存储方式,只需要换一个驱动实例,业务代码完全不变。
问题场景:你的系统需要支持本地存储、阿里云OSS、七牛云、腾讯云COS等多种存储方式。
传统做法(❌ 不推荐):
if ($storageType == 'local') {
// 本地存储逻辑
} elseif ($storageType == 'aliyun') {
// 阿里云逻辑
} elseif ($storageType == 'qiniu') {
// 七牛逻辑
}
// ... 无尽的 if-else策略模式做法(✅ 推荐):
$driver = $uploadManager->driver('aliyun_oss');
$driver->read('avatar')->setType('image')->upload('uploads');关键代码:UploadManager 作为策略的创建者
public function driver(?string $code = null, ?string $tenantId = null): UploadDriver {
$code = $code ?: config('upload.default', 'local');
$cacheKey = $code.'|'.($tenantId ?? 'default');
if (isset($this->instances[$cacheKey])) return $this->instances[$cacheKey];
// 校验是否受支持
$supported = $this->repo->getSupportedStorages();
if (!isset($supported[$code])) return new NullUpload("STORAGE_NOT_SUPPORTED: {$code}");
// 解析驱动类
$class = $this->repo->getDriverClass($code);
if (!$class || !class_exists($class)) return new NullUpload("DRIVER_CLASS_MISSING: {$code}");
// 创建并初始化
$driver = new $class();
if (!$driver instanceof UploadDriver) return new NullUpload("DRIVER_NOT_IMPLEMENTED: {$class}");
// 合并配置
$merged = array_merge([
'rules' => config('upload.rules', []),
'thumb' => config('upload.thumb', []),
], $this->repo->getConfig($code, $tenantId));
$driver->initialize($merged);
return $this->instances[$cacheKey] = $driver;
}优势:
UploadManager 实际上是一个简单工厂的变体。它隐藏了对象创建的复杂逻辑:
StorageRepository)NullUpload)工厂模式的好处:
// 使用者无需关心如何创建
$driver = $manager->driver('aliyun_oss');
// 而不是
$config = $repo->getConfig('aliyun_oss');
$class = $repo->getDriverClass('aliyun_oss');
$driver = new $class();
$driver->initialize($config);NullUpload 是一个典型的空对象实现:
class NullUpload extends BaseUpload implements UploadDriver
{
public function __construct(string $errorMessage) { $this->setError($errorMessage); }
public function initialize(array $config): void {}
public function read(string $name, bool $isRename = true): static { return $this; }
public function setType(string $type): static { return $this; }
public function setValidate(array $validate = []): static { return $this; }
public function upload(string $dir): bool { return $this->setError($this->getError()); }
public function base64(string $data, ?string $key = null): bool { return $this->setError($this->getError()); }
public function fetch(string $url, ?string $key = null): bool { return $this->setError($this->getError()); }
public function delete(string $fileName): bool { return $this->setError($this->getError()); }
public function thumb(string $filePath, string|array $thumbType): array { return []; }
}为什么需要空对象模式?
传统做法需要大量的 null 检查:
$driver = $manager->driver('invalid');
if ($driver === null) {
// 错误处理
}
$driver->upload(); // 可能报错使用空对象模式:
$driver = $manager->driver('invalid'); // 返回 NullUpload
$driver->upload(); // 安全执行,返回 false,可通过 getError() 获取错误信息好处:避免了大量的空值检查,代码更简洁,更安全。
BaseUpload 定义了上传的"标准流程",具体实现类只需实现差异化的部分:
abstract public function upload(string $dir): bool;
abstract public function base64(string $data, ?string $key = null): bool;
abstract public function fetch(string $url, ?string $key = null): bool;
abstract public function delete(string $fileName): bool;
abstract public function thumb(string $filePath, string|array $thumbType): array;模板方法的体现:
BaseUpload 中实现(如文件读取、验证、路径生成)实际使用流程:
// 1. 读取文件(BaseUpload 统一实现)
$driver->read('file');
// 2. 设置类型和验证规则(BaseUpload 统一实现)
$driver->setType('image')->setValidate(['ext' => ['jpg', 'png']]);
// 3. 执行上传(子类差异化实现)
$driver->upload('uploads');StorageRepository 负责管理存储配置:
public function getConfig(string $code, ?string $tenantId = null): array {
// 获取当前默认存储
$store_default = config('upload.default', 'local');
if($store_default == 'local'){
return [
'storage_type' => $store_default,
];
}
// 组装当前通用的配置信息 存储空间/域名/密钥/KEY
$common_config = [
'access_key' => site($store_default."_access_key"),
'secret_key' => site($store_default."_access_secret"),
'bucket' => site($store_default."_bucket"),
'domain' => site($store_default."_domain"),
'storage_type' => $store_default,
];
// 阿里云 和 腾讯云 有一个不同的参数 需要根据 对应的条件追加进去
switch ($store_default)
{
case "aliyun_oss":
$common_config['endpoint'] = site($store_default."_endpoint");
break;
case "tencent_cos":
$common_config['region'] = site($store_default."_region");
break;
}
return $common_config;
}仓库模式的价值:
tenantId 区分不同租户的配置)这个库采用了流式 API 设计(Fluent Interface),让代码读起来像自然语言:
$uploadManager = app(UploadManager::class);
// 标准上传
$driver = $uploadManager->driver('aliyun_oss');
$driver->read('avatar')
->setType('image')
->setValidate(['ext' => ['jpg', 'png'], 'size' => 5242880])
->upload('uploads/avatar');
if (!$driver->upload('uploads/avatar')) {
$error = $driver->getError();
// 处理错误
}
// base64 上传
$driver->base64($base64Data, 'uploads/image.jpg');
// 远程抓取
$driver->fetch('https://example.com/image.jpg', 'uploads/remote.jpg');
// 生成缩略图
$thumbs = $driver->thumb('uploads/image.jpg', 'all');
// 返回:['small' => 'uploads/...', 'medium' => 'uploads/...', 'large' => 'uploads/...']链式调用的实现:每个方法都返回 $this
public function setType(string $type): static { $this->type = $type; return $this; }
public function setValidate(array $validate = []): static {
$this->validate = $validate ?: ($this->config['rules'][$this->type] ?? []);
return $this;
}传统做法(需要修改大量代码):
使用本库的做法(只需三步):
// config/upload.php
return [
'default' => 'aliyun_oss', // 只需改这一行
'rules' => [
'image' => [
'ext' => ['jpg', 'png', 'gif'],
'mime' => ['image/jpeg', 'image/png'],
'size' => 5242880, // 5MB
],
],
];namespace app\controller;
use core\Integration\upload\UploadManager;
class UploadController
{
public function upload()
{
$manager = app(UploadManager::class);
$driver = $manager->driver(); // 自动使用 default 配置
if (!$driver->read('file')
->setType('image')
->upload('uploads')) {
return json(['code' => 0, 'msg' => $driver->getError()]);
}
return json([
'code' => 1,
'url' => $driver->getUrl(),
'path' => $driver->getFullPath(),
]);
}
}如果将来要切回本地存储? 只需改配置文件的 default 值。
如果需要同时支持多种存储? 只需:
$localDriver = $manager->driver('local');
$aliyunDriver = $manager->driver('aliyun_oss');
// 根据业务场景选择不同的驱动假设你要新增"又拍云"存储支持:
// core/Integration/upload/storage/Upyun.php
namespace core\Integration\upload\storage;
use core\Integration\upload\BaseUpload;
class Upyun extends BaseUpload
{
public function upload(string $dir): bool
{
$this->validate();
// 又拍云的具体上传逻辑
// ...
return true;
}
public function base64(string $data, ?string $key = null): bool
{
// 实现 base64 上传
return true;
}
public function fetch(string $url, ?string $key = null): bool
{
// 实现远程抓取
return true;
}
public function delete(string $fileName): bool
{
// 实现删除
return true;
}
public function thumb(string $filePath, string|array $thumbType): array
{
// 实现缩略图(又拍云可能有自己的图片处理 API)
return [];
}
}// StorageRepository.php
public function getDriverClass(string $code): string {
return match ($code) {
'local' => 'core\\Integration\\upload\\storage\\Local',
'aliyun_oss' => 'core\\Integration\\upload\\storage\\Aliyun',
'tencent_cos' => 'core\\Integration\\upload\\storage\\Tencent',
'qiniu_oss' => 'core\\Integration\\upload\\storage\\Qiniu',
'upyun' => 'core\\Integration\\upload\\storage\\Upyun', // 新增
default => ''
};
}在系统配置中启用 upyun 存储类型。
完成! 无需修改任何业务代码,新的存储驱动就可以使用了。
单一职责原则:每个类只做一件事
UploadManager 只负责创建驱动StorageRepository 只负责管理配置BaseUpload 只负责公共逻辑开闭原则:对扩展开放,对修改关闭
依赖倒置原则:依赖抽象而非具体
UploadDriver 接口,而非具体实现里氏替换原则:任何驱动都可以无缝替换
Local、Aliyun、Qiniu 可以相互替换而不影响业务逻辑多上传方式支持:
upload)base64)fetch)灵活的验证机制:
智能的缩略图处理:
x-oss-process)完善的错误处理:
确保你的 ThinkPHP 8.0 项目已配置好。
// config/upload.php
return [
'default' => 'local',
'domain' => 'https://your-domain.com',
'rules' => [
'image' => [
'ext' => ['jpg', 'png', 'gif', 'webp'],
'mime' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'size' => 5242880, // 5MB
],
'video' => [
'ext' => ['mp4', 'avi'],
'size' => 104857600, // 100MB
],
],
'thumb' => [
'thumb_type' => [
'small' => ['width' => 200, 'height' => 200],
'medium' => ['width' => 500, 'height' => 500],
'large' => ['width' => 1000, 'height' => 1000],
],
],
];// app/provider.php 或相关服务提供者
use core\Integration\upload\UploadManager;
use core\Integration\upload\StorageRepository;
// 注册仓库
bind(StorageRepository::class, function() {
return new StorageRepository();
});
// 注册管理器
bind(UploadManager::class, function($app) {
return new UploadManager($app->make(StorageRepository::class));
});// 在控制器中
$manager = app(UploadManager::class);
// 上传图片
$driver = $manager->driver('local');
if ($driver->read('avatar')
->setType('image')
->upload('uploads/avatar')) {
$url = $driver->getUrl();
// 保存 $url 到数据库
}
// 上传并生成缩略图
$driver->read('cover')->setType('image')->upload('uploads');
$thumbs = $driver->thumb($driver->getFullPath(), 'all');
// $thumbs = ['small' => '...', 'medium' => '...', 'large' => '...']这个上传库的封装思想,其实可以用一句话概括:用设计模式解耦复杂系统,用接口抽象统一差异,用工厂模式简化创建,用模板方法提取共性,用空对象优雅处理异常。
作为开发者,这个案例告诉我们:
作为一名开发者,我深知代码封装的重要性。一个优秀的上传库,应该让使用者专注于业务逻辑,而不是纠结于如何调用不同的存储 API。
这个 ThinkPHP 8.0 上传库的设计,完美诠释了"简单的事情简单做,复杂的事情优雅做"的哲学。希望这篇文章能帮助你在自己的项目中,设计出同样优雅、可扩展的代码。
如果你觉得这篇文章对你有帮助,记得关注我,我会持续分享更多实用的技术干货。
记住:好的代码不是写出来的,是设计出来的。 让我们一起在编程的路上,用设计模式武装自己,写出更优雅、更可维护的代码。