实现文件上传管理功能
本节我们将在后台为博客应用实现文件上传管理(包括文件上传、预览及删除、目录创建及删除)功能,并且使用本地文件系统保存上传的文件。
1、配置本地文件系统
让我们从配置开始吧,我们在public
目录下创建一个 uploads
目录用来存放上传的文件,这样所有上传文件都可以通过浏览器直接访问。
首先我们在博客项目目录下使用如下命令在 public
目录下创建 uploads
子目录:
很简单。接下来我们来编辑 config/blog.php
:
<?php return [ 'title' => 'My Blog', 'posts_per_page' => 5, 'uploads' => [ 'storage' => 'local', 'webpath' => '/uploads', ], ];我们在
uploads
配置项中使用 storage
定义使用的文件系统,使用 webpath
定义 web 访问根目录。
最后,编辑 config/filesystems.php
如下:
//将如下区块代码 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], // 修改成这样 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => public_path('uploads'), ],我们将本地存储的根目录修改为前面创建的
public/uploads
目录。
2、创建帮助函数文件
在 Laravel 5.1 项目中有时我们会需要一些不依赖于类的辅助函数,通常我们会将这些辅助函数定义在一个单独文件如helpers.php
中。我们在 app
目录下创建这个名为 helpers.php
的文件,并编辑其内容如下:
<?php /** * 返回可读性更好的文件尺寸 */ function human_filesize($bytes, $decimals = 2) { $size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; $factor = floor((strlen($bytes) - 1) / 3); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) .@$size[$factor]; } /** * 判断文件的MIME类型是否为图片 */ function is_image($mimeType) { return starts_with($mimeType, 'image/'); }其中
human_filesize()
函数返回一个易读的文件尺寸,is_image()
函数在文件类型为图片的时候返回 true
。
要让应用能够正确找到 helpers.php
文件,还要修改项目根目录下的 composer.json
:
{ ... "autoload": { "classmap": [ "database" ], "psr-4": { "App\\": "app/" }, "files": [ "app/helpers.php" ] }, ... }在
autoload
配置项的 files
数组中指定要被加载的文件/文件夹。修改完成后记得运行 composer dumpauto
确保修改生效:
现在 helpers.php
中的所有函数都会载入到自动加载器中,你可以在博客应用的代码中任意使用其中的函数。
3、创建文件上传管理服务
现在基本配置已经完成了,让我们创建一个服务类来管理上传文件。检测文件 MIME 类型
我们想要基于不同类型的上传文件进行不同的操作,这可以通过检测上传文件 MIME 类型轻松实现。 PHP 有一个内置函数mime_content_type()
用于检测文件的MIME类型,但是该函数已经废弃了,我们使用另一个解决方案。
在 Packagist 中搜索 “mime” 会查询到一个名为 dflydev
的包,我们在博客项目中使用 Composer 安装该依赖包:
composer require "dflydev/apache-mime-types"我们将使用该依赖包提供的方法来检测文件的 MIME 类型。
创建UploadsManager类
在app/Services
目录下创建 UploadsManager.php
,编辑其内容如下:
<?php namespace App\Services; use Carbon\Carbon; use Dflydev\ApacheMimeTypes\PhpRepository; use Illuminate\Support\Facades\Storage; class UploadsManager { protected $disk; protected $mimeDetect; public function __construct(PhpRepository $mimeDetect) { $this->disk = Storage::disk(config('blog.uploads.storage')); $this->mimeDetect = $mimeDetect; } /** * Return files and directories within a folder * * @param string $folder * @return array of [ * 'folder' => 'path to current folder', * 'folderName' => 'name of just current folder', * 'breadCrumbs' => breadcrumb array of [ $path => $foldername ] * 'folders' => array of [ $path => $foldername] of each subfolder * 'files' => array of file details on each file in folder * ] */ public function folderInfo($folder) { $folder = $this->cleanFolder($folder); $breadcrumbs = $this->breadcrumbs($folder); $slice = array_slice($breadcrumbs, -1); $folderName = current($slice); $breadcrumbs = array_slice($breadcrumbs, 0, -1); $subfolders = []; foreach (array_unique($this->disk->directories($folder)) as $subfolder) { $subfolders["/$subfolder"] = basename($subfolder); } $files = []; foreach ($this->disk->files($folder) as $path) { $files[] = $this->fileDetails($path); } return compact( 'folder', 'folderName', 'breadcrumbs', 'subfolders', 'files' ); } /** * Sanitize the folder name */ protected function cleanFolder($folder) { return '/' . trim(str_replace('..', '', $folder), '/'); } /** * 返回当前目录路径 */ protected function breadcrumbs($folder) { $folder = trim($folder, '/'); $crumbs = ['/' => 'root']; if (empty($folder)) { return $crumbs; } $folders = explode('/', $folder); $build = ''; foreach ($folders as $folder) { $build .= '/'.$folder; $crumbs[$build] = $folder; } return $crumbs; } /** * 返回文件详细信息数组 */ protected function fileDetails($path) { $path = '/' . ltrim($path, '/'); return [ 'name' => basename($path), 'fullPath' => $path, 'webPath' => $this->fileWebpath($path), 'mimeType' => $this->fileMimeType($path), 'size' => $this->fileSize($path), 'modified' => $this->fileModified($path), ]; } /** * 返回文件完整的web路径 */ public function fileWebpath($path) { $path = rtrim(config('blog.uploads.webpath'), '/') . '/' .ltrim($path, '/'); return url($path); } /** * 返回文件MIME类型 */ public function fileMimeType($path) { return $this->mimeDetect->findType( pathinfo($path, PATHINFO_EXTENSION) ); } /** * 返回文件大小 */ public function fileSize($path) { return $this->disk->size($path); } /** * 返回最后修改时间 */ public function fileModified($path) { return Carbon::createFromTimestamp( $this->disk->lastModified($path) ); } }
4、实现文件上传管理列表
现在UploadsManager
服务类已经创建,接下来我们来实现控制器的 index
方法。
创建 index 方法
编辑app/Http/Controllers/Admin
目录下的 UploadController.php
文件内容如下:
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Services\UploadsManager; use Illuminate\Http\Request; class UploadController extends Controller { protected $manager; public function __construct(UploadsManager $manager) { $this->manager = $manager; } /** * Show page of files / subfolders */ public function index(Request $request) { $folder = $request->get('folder'); $data = $this->manager->folderInfo($folder); return view('admin.upload.index', $data); } }构造方法中注入了
UploadsManager
依赖,在 index()
方法中只需传入 folderInfo()
返回的数据到要渲染的视图并返回即可。
你可能已经注意到 $folder
从请求中获取,是的,我们只需要通过请求参数即可实现文件夹修改。
创建 index 视图
在resources/views/admin
目录下新建 upload
目录,并在该目录下创建 index.blade.php
文件,编辑该文件内容如下:
@extends('admin.layout') @section('content') <div class="container-fluid"> {{-- 顶部工具栏 --}} <div class="row page-title-row"> <div class="col-md-6"> <h3 class="pull-left">Uploads </h3> <div class="pull-left"> <ul class="breadcrumb"> @foreach ($breadcrumbs as $path => $disp) <li><a href="/admin/upload?folder={{ $path }}">{{ $disp }}</a></li> @endforeach <li class="active">{{ $folderName }}</li> </ul> </div> </div> <div class="col-md-6 text-right"> <button type="button" class="btn btn-success btn-md" data-toggle="modal" data-target="#modal-folder-create"> <i class="fa fa-plus-circle"></i> New Folder </button> <button type="button" class="btn btn-primary btn-md" data-toggle="modal" data-target="#modal-file-upload"> <i class="fa fa-upload"></i> Upload </button> </div> </div> <div class="row"> <div class="col-sm-12"> @include('admin.partials.errors') @include('admin.partials.success') <table id="uploads-table" class="table table-striped table-bordered"> <thead> <tr> <th>Name</th> <th>Type</th> <th>Date</th> <th>Size</th> <th data-sortable="false">Actions</th> </tr> </thead> <tbody> {{-- 子目录 --}} @foreach ($subfolders as $path => $name) <tr> <td> <a href="/admin/upload?folder={{ $path }}"> <i class="fa fa-folder fa-lg fa-fw"></i> {{ $name }} </a> </td> <td>Folder</td> <td>-</td> <td>-</td> <td> <button type="button" class="btn btn-xs btn-danger" onclick="delete_folder('{{ $name }}')"> <i class="fa fa-times-circle fa-lg"></i> Delete </button> </td> </tr> @endforeach {{-- 所有文件 --}} @foreach ($files as $file) <tr> <td> <a href="{{ $file['webPath'] }}"> @if (is_image($file['mimeType'])) <i class="fa fa-file-image-o fa-lg fa-fw"></i> @else <i class="fa fa-file-o fa-lg fa-fw"></i> @endif {{ $file['name'] }} </a> </td> <td>{{ $file['mimeType'] or 'Unknown' }}</td> <td>{{ $file['modified']->format('j-M-y g:ia') }}</td> <td>{{ human_filesize($file['size']) }}</td> <td> <button type="button" class="btn btn-xs btn-danger" onclick="delete_file('{{ $file['name'] }}')"> <i class="fa fa-times-circle fa-lg"></i> Delete </button> @if (is_image($file['mimeType'])) <button type="button" class="btn btn-xs btn-success" onclick="preview_image('{{ $file['webPath'] }}')"> <i class="fa fa-eye fa-lg"></i> Preview </button> @endif </td> </tr> @endforeach </tbody> </table> </div> </div> </div> @include('admin.upload._modals') @stop @section('scripts') <script> // 确认文件删除 function delete_file(name) { $("#delete-file-name1").html(name); $("#delete-file-name2").val(name); $("#modal-file-delete").modal("show"); } // 确认目录删除 function delete_folder(name) { $("#delete-folder-name1").html(name); $("#delete-folder-name2").val(name); $("#modal-folder-delete").modal("show"); } // 预览图片 function preview_image(path) { $("#preview-image").attr("src", path); $("#modal-image-view").modal("show"); } // 初始化数据 $(function() { $("#uploads-table").DataTable(); }); </script> @stop尽管这个模板文件很长,但是理解起来并没有什么困难,所有文件上传和下载管理都将在这里进行。 有没有注意到我们在最后包含了
admin.upload._modals
?是的,我们将模态对话框放到了一个单独的视图模板中。现在,我们在 resources/views/admin/upload
目录下创建一个空的 _modals.blade.php
文件。
上传管理界面
打开浏览器,进入博客应用后台管理页面,点击顶部导航条的“上传”(Uploads)链接,将会跳转到如下页面: 既漂亮又清爽,有木有?接下来让我们来实现所有的模态对话框及其背后的业务逻辑。5、完成文件上传管理功能
对于完整的文件上传管理器而言剩下的工作已经不多了,现在是时候完成所有功能了。添加路由
我们需要为上传管理器定义所有需要的路由,编辑app/Http/routes.php
添加如下路由:
// 在这一行下面 get('admin/upload', 'UploadController@index'); // 添加如下路由 post('admin/upload/file', 'UploadController@uploadFile'); delete('admin/upload/file', 'UploadController@deleteFile'); post('admin/upload/folder', 'UploadController@createFolder'); delete('admin/upload/folder', 'UploadController@deleteFolder');
定义所有模态对话框
编辑我们之前创建的_modals.blade.php
文件内容如下:
{{-- 创建目录 --}} <div class="modal fade" id="modal-folder-create"> <div class="modal-dialog"> <div class="modal-content"> <form method="POST" action="/admin/upload/folder" class="form-horizontal"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="folder" value="{{ $folder }}"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> × </button> <h4 class="modal-title">Create New Folder</h4> </div> <div class="modal-body"> <div class="form-group"> <label for="new_folder_name" class="col-sm-3 control-label"> Folder Name </label> <div class="col-sm-8"> <input type="text" id="new_folder_name" name="new_folder" class="form-control"> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"> Cancel </button> <button type="submit" class="btn btn-primary"> Create Folder </button> </div> </form> </div> </div> </div> {{-- 删除文件 --}} <div class="modal fade" id="modal-file-delete"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> × </button> <h4 class="modal-title">Please Confirm</h4> </div> <div class="modal-body"> <p class="lead"> <i class="fa fa-question-circle fa-lg"></i> Are you sure you want to delete the <kbd><span id="delete-file-name1">file</span></kbd> file? </p> </div> <div class="modal-footer"> <form method="POST" action="/admin/upload/file"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="folder" value="{{ $folder }}"> <input type="hidden" name="del_file" id="delete-file-name2"> <button type="button" class="btn btn-default" data-dismiss="modal"> Cancel </button> <button type="submit" class="btn btn-danger"> Delete File </button> </form> </div> </div> </div> </div> {{-- 删除目录 --}} <div class="modal fade" id="modal-folder-delete"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> × </button> <h4 class="modal-title">Please Confirm</h4> </div> <div class="modal-body"> <p class="lead"> <i class="fa fa-question-circle fa-lg"></i> Are you sure you want to delete the <kbd><span id="delete-folder-name1">folder</span></kbd> folder? </p> </div> <div class="modal-footer"> <form method="POST" action="/admin/upload/folder"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="folder" value="{{ $folder }}"> <input type="hidden" name="del_folder" id="delete-folder-name2"> <button type="button" class="btn btn-default" data-dismiss="modal"> Cancel </button> <button type="submit" class="btn btn-danger"> Delete Folder </button> </form> </div> </div> </div> </div> {{-- 上传文件 --}} <div class="modal fade" id="modal-file-upload"> <div class="modal-dialog"> <div class="modal-content"> <form method="POST" action="/admin/upload/file" class="form-horizontal" enctype="multipart/form-data"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> <input type="hidden" name="folder" value="{{ $folder }}"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> × </button> <h4 class="modal-title">Upload New File</h4> </div> <div class="modal-body"> <div class="form-group"> <label for="file" class="col-sm-3 control-label"> File </label> <div class="col-sm-8"> <input type="file" id="file" name="file"> </div> </div> <div class="form-group"> <label for="file_name" class="col-sm-3 control-label"> Optional Filename </label> <div class="col-sm-4"> <input type="text" id="file_name" name="file_name" class="form-control"> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"> Cancel </button> <button type="submit" class="btn btn-primary"> Upload File </button> </div> </form> </div> </div> </div> {{-- 浏览图片 --}} <div class="modal fade" id="modal-image-view"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> × </button> <h4 class="modal-title">Image Preview</h4> </div> <div class="modal-body"> <img id="preview-image" src="x" class="img-responsive"> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"> Cancel </button> </div> </div> </div> </div>在该文件中总共有5个不同的模态弹出框,分别对应上面定义的5个路由。
添加表单请求验证类
使用 Artisan 命令创建UploadFileRequest
,并编辑其内容如下:
<?php namespace App\Http\Requests; use App\Http\Requests\Request; class UploadFileRequest extends Request { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'file' => 'required', 'folder' => 'required', ]; } }使用 Artisan 命令创建
UploadNewFolderRequest
,并编辑其内容如下:
<?php namespace App\Http\Requests; use App\Http\Requests\Request; class UploadNewFolderRequest extends Request { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'folder' => 'required', 'new_folder' => 'required', ]; } }同样,这些请求类用于验证表单输入。
完成 UploadController 所有方法
编辑UploadController.php
文件内容如下:
<?php // 添加如下三个use语句到UploadController控制器顶部 use App\Http\Requests\UploadFileRequest; use App\Http\Requests\UploadNewFolderRequest; use Illuminate\Support\Facades\File; // 添加如下四个方法到UploadController控制器类 /** * 创建新目录 */ public function createFolder(UploadNewFolderRequest $request) { $new_folder = $request->get('new_folder'); $folder = $request->get('folder').'/'.$new_folder; $result = $this->manager->createDirectory($folder); if ($result === true) { return redirect() ->back() ->withSuccess("Folder '$new_folder' created."); } $error = $result ? : "An error occurred creating directory."; return redirect() ->back() ->withErrors([$error]); } /** * 删除文件 */ public function deleteFile(Request $request) { $del_file = $request->get('del_file'); $path = $request->get('folder').'/'.$del_file; $result = $this->manager->deleteFile($path); if ($result === true) { return redirect() ->back() ->withSuccess("File '$del_file' deleted."); } $error = $result ? : "An error occurred deleting file."; return redirect() ->back() ->withErrors([$error]); } /** * 删除目录 */ public function deleteFolder(Request $request) { $del_folder = $request->get('del_folder'); $folder = $request->get('folder').'/'.$del_folder; $result = $this->manager->deleteDirectory($folder); if ($result === true) { return redirect() ->back() ->withSuccess("Folder '$del_folder' deleted."); } $error = $result ? : "An error occurred deleting directory."; return redirect() ->back() ->withErrors([$error]); } /** * 上传文件 */ public function uploadFile(UploadFileRequest $request) { $file = $_FILES['file']; $fileName = $request->get('file_name'); $fileName = $fileName ?: $file['name']; $path = str_finish($request->get('folder'), '/') . $fileName; $content = File::get($file['tmp_name']); $result = $this->manager->saveFile($path, $content); if ($result === true) { return redirect() ->back() ->withSuccess("File '$fileName' uploaded."); } $error = $result ? : "An error occurred uploading file."; return redirect() ->back() ->withErrors([$error]); }
完成 UploadsManager 服务类
最后编辑app/Services/UploadsManager.php
内容如下:
<?php // 在该类中新增以下4个方法 /** * 创建新目录 */ public function createDirectory($folder) { $folder = $this->cleanFolder($folder); if ($this->disk->exists($folder)) { return "Folder '$folder' already exists."; } return $this->disk->makeDirectory($folder); } /** * 删除目录 */ public function deleteDirectory($folder) { $folder = $this->cleanFolder($folder); $filesFolders = array_merge( $this->disk->directories($folder), $this->disk->files($folder) ); if (! empty($filesFolders)) { return "Directory must be empty to delete it."; } return $this->disk->deleteDirectory($folder); } /** * 删除文件 */ public function deleteFile($path) { $path = $this->cleanFolder($path); if (! $this->disk->exists($path)) { return "File does not exist."; } return $this->disk->delete($path); } /** * 保存文件 */ public function saveFile($path, $content) { $path = $this->cleanFolder($path); if ($this->disk->exists($path)) { return "File already exists."; } return $this->disk->put($path, $content); }至此,已经完成了文件上传管理的所有工作,可以去浏览器完成上传文件、创建目录、删除文件等操作了。
6、测试文件上传和删除功能
在浏览器中访问http://blog.app/admin/upload
,点击“New Folder”创建新目录,在弹出的模态对话框中填写表单:
点击“Create Folder”提交表单,创建目录成功:
点击进入新创建的子目录 laravelacademy
,在该目录下点击“Upload”按钮上传文件:
点击“Upload File”上传文件,上传成功后跳转到文件列表:
但是去 public/uploads/laravelacademy
目录下查看,上传的文件 Laravel学院
并没有扩展名,而且上面列表里 Type
类型值为 Unkown
,预览按钮没显示出来也说明了有问题,正确的文件名应该包含扩展名,看来是上传图片时填写的文件名称有问题,应该这样填写为 LaravelAcademy.jpg
,这样重新上传后文件列表显示如下:
这样数据都对了,预览按钮也显示出来了,点击“Preview”按钮,页面显示如下:
最后我们将无效的Laravel学院
文件删除,点击其对应的“Delete”按钮,页面弹出确认删除对话框:
点击“Delete File”,确认删除。
76 Comments