教程源自:Laravel学院
这一节 咱来说说上传文件的功能实现,我们会把上传的文件保存到项目本地,不仅上传 还有删除和预览功能。
1 配置
我们先从配置开始做起,先修改我们自己创建的 blog.php
<?php return [ 'title' => "Larger K's Blog", 'posts_per_page' => 10, 'uploads' => [ 'storage' => 'local', // 储存处 'webpath' => '/uploads' // 储存路径 ], ];
然后编辑 config/filesystems.php
'disks' => [ 'local' => [ 'driver' => 'local', // 'root' => storage_path('app'), 'root' => public_path('uploads'), ],
2 创建文件服务类
我们需要创建一个Manager来封装需要用到的功能。
2.1 引入dflydev
difydev包是主要用于分别MIME类型的,我们需要根据不同的文件类型进行不同的操作,所以需要使用到它,如果你记不住它的名字可以到Packagist 中搜索 MIME
然后我们使用 composer 引入它:
composer require "dflydev/apache-mime-types"
2.2 创建UploadsManager
我们在 app/Service 目录下创建:
<?php namespace AppServices; use CarbonCarbon; use DflydevApacheMimeTypesPhpRepository; use IlluminateSupportFacadesStorage; use phpDocumentorReflectionDocBlockTagsReturn_; class UploadsManager { protected $disk; protected $mimeDelect; /** * UploadsServices constructor. * @param $mimeDelect */ public function __construct(PhpRepository $mimeDelect) { $this->mimeDelect = $mimeDelect; $this->disk = Storage::disk(config('blag.uploads.storage')); } /** * 返回目录详情 * * @param $folder * @return array [ * 'folder', 文件夹的路径 * 'folderName', 文件夹名称 * 'breadcrumbs', 文件夹被分割后的数组 * 'subfolders', 此文件夹下的所有子文件夹 * 'files' 此文件夹下的所有文件详情 * ] */ public function folderInfo($folder) { // 处理$folder $folder = $this->cleanFolder($folder); // 获得路径数组 $breadcrumbs = $this->breadcrumbs($folder); // 取出最后一段 $slice = array_slice($breadcrumbs, -1); // 获得当前数组指针下的value $folderName = current($slice); // 获取0到倒数第二个的片段 $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' ); } /** * 去除路径中首尾的..和/ * * @param string $folder * @return string * 例子:传入 ../public/images/home/01/ 返回 "/public/images/home/01" */ protected function cleanFolder($folder) { return '/' . trim(str_replace('..', '', $folder), '/'); } /** * 返回路径数组 * * @param $folder * @return array when $folder is empty[ * '/' => 'root' * ] when folder is not empty[ * "/" => "root" * "/public" => "public" * "/images" => "images" * "/home" => "home" * ] */ protected function breadcrumbs($folder) { // 去除首尾的 / $folder = trim($folder, '/'); $crumbs = ['/' => 'root']; if (empty($folder)){ return $crumbs; } // 分割路径 $folders = explode('/', $folder); // build $build = ''; foreach ($folders as $folder) { $build = '/'.$folder; $crumbs[$build] = $folder; } return $crumbs; } /** * 返回文件的详情信息 * * @param string $path * @return array */ protected function fileDetails($path) { $path = '/' . trim($path, '/'); return [ 'name' => basename($path), 'fullpath' => $path, 'webPath' => $this->fileWebpath($path), 'mimeType' => $this->fileMimeType($path), 'size' => $this->fileSize($path), 'modified' => $this->fileModified($path), ]; } /** * 返回文件的Web路径 * * @param $path * @return string */ protected function fileWebpath($path) { $path = rtrim(config('blog.uploads.webpath'), '/') . '/' . ltrim($path, '/'); return url($path); } /** * 返回文件的MIME类型 * * @param $path * @return mixed|null|string */ protected function fileMimeType($path) { return $this->mimeDelect->findType( pathinfo( $path, PATHINFO_EXTENSION ) ); } /** * 返回文件的大小 * * @param $path * @return mixed */ protected function fileSize($path) { return $this->disk->size($path); } /** * 返回最后一次被修改的时间 * * @param $path * @return static */ protected function fileModified($path) { return Carbon::createFromTimestamp( $this->disk->lastModified($path) ); } }
注释写的很尽力了,敲一遍你就懂了,上面的代码主要是folderInfo这个方法 它返回我们需要用到的一些数据
2.3 创建帮助函数
其实把Manager创建好后就可以展示视图了,但是方便我们使用先来创建两个帮助函数,在app目录下创建一个helper.php
<?php /** * 返回可读性更好的文件大小 * * @param $bytes * @param int $decimals * @return string */ 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 type是否是图片类型 * * @param $mime_type * @return bool */ function is_image($mime_type){ return starts_with($mime_type, 'image/'); }
现在有个问题,我们如何在不import的情况下使用到帮助函数呢 它并不是一个类 没有命名空间,解决这个方法就要修改下composer.json文件让它自动加载文件:
"autoload": { "classmap": [ "database" ], "psr-4": { "App\": "app/" }, "files": [ "app/helper.php" ]
最后执行以下命令:
composer dumpauto
3 展示文件视图
3.1 首先编辑UploadController的index方法:
class UploadController extends Controller { protected $manager; /** * UploadController constructor. * @param UploadsManager $manager */ public function __construct(UploadsManager $manager) { $this->manager = $manager; } public function index(Request $request) { // 取到目录的详情 $data = $this->manager->folderInfo($request->get('folder')); return view('admin.upload.index', $data); } }
创建一个 /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.error') @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._modal') @endsection @section('scripts') <script> $(function(){ $("#uploads-table").DataTable(); }) </script> @endsection
经过上面的代码,我们可以看到展示效果,你可以自己在upload目录下创建几个文件夹和文件试试。
下面我们继续写我们 include的 _modal
{{--创建目录--}} <div class="modal fade" id="modal-folder-create"> <div class="modal-dialog"> <div class="modal-content"> <form action="/admin/upload/folder" method="post" 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"> x </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" class="form-control" name="new_folder"> </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-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-delete"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <div class="modal-title"> <button type="button" class="close" data-dismiss="modal"> x </button> <h4 class="modal-title">Please Confirm</h4> </div> </div> <div class="modal-body"> <p class="lead"> <i class="fa fa-question-circle fa-lg"></i> Are you sure your want to delete the <kbd><span id="delete-file-name1"></span></kbd> file? </p> </div> <div class="modal-footer"> <form action="/admin/upload/file" method="post"> <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-file-upload"> <div class="modal-dialog"> <div class="modal-content"> <form action="/admin/upload/file" method="post" 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"> x </button> <h4 class="modal-title">Upload New Folder</h4> </div> <div class="modal-body"> <div class="form-group"> <label for="file" class="control-label col-sm-3">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="control-label col-sm-3">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 class="btn btn-default" data-dismiss="modal" type="button"> Cancel </button> <button class="btn btn-primary" type="submit"> 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"> x </button> <h4 class="modal-title">Image Preview</h4> </div> <div class="modal-body"> <img src="" id="preview-image" class="img-responsive"> </div> <div class="modal-footer"> <button class="btn btn-default" type="button" data-dismiss="modal">Cancel</button> </div> </div> </div> </div>
紧接着在 admin/upload/index.blade.php 尾部写js:
@section('scripts') <script> // 删除文件 function delete_file(name){ $("#delete-file-name1").html(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> @endsection
至此,咱就可以浏览到效果了。
4 实现功能
4.1 准备
首先添加路由:
Route::group(['namespace' => 'Admin', 'middleware' => 'auth', 'prefix' => 'admin'], function(){ Route::resource('post', 'PostController'); Route::resource('tag', 'TagController', ['except' => 'show']); Route::get('upload', 'UploadController@index'); // 上传文件 Route::post('upload/file', 'UploadController@uploadFile'); // 删除文件 Route::delete('upload/file', 'UploadController@deleteFile'); // 创建文件夹 Route::post('upload/folder', 'UploadController@createFolder'); // 删除文件夹 Route::delete('upload/folder', 'UploadController@deleteFolder'); });
创建需要的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' ]; } }
<?php namespace AppHttpRequests; use AppHttpRequestsRequest; 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' ]; } }
4.2 创建新的目录
UploadController的createFolder方法:
public function createFolder(RequestsUploadNewFolderRequest $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]); }
在uploadManager下完善刚刚用到的方法createDirectory
/** * 创建一个目录 成功时返回true错误时返回错误信息或false。 * * @param $folder * @return string|bool */ public function createDirectory($folder) { $folder = $this->cleanFolder($folder); // 判断是否存在 if ($this->disk->exists($folder)){ return "Folder $folder already exists"; } return $this->disk->makeDirectory($folder); }
4.3 删除目录
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."); } return redirect()->back()->withErrors($result); }
在uploadManager下完善刚刚用到的方法deleteDirectory
/** * 删除一个目录 成功时返回true错误时返回错误信息或false。 * * @param $folder * @return string|bool */ 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); }
4.4 删除文件
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]); }
在uploadManager下完善刚刚用到的方法deleteDirectory
/** * 删除一个文件 成功时返回true错误时返回错误信息或false。 * * @param $path * @return string|bool */ public function deleteFile($path) { $path = $this->cleanFolder($path); // 判断文件是否存在 if (! $this->disk->exists($path)) { return "File does not exist."; } return $this->disk->delete($path); }
4.5 上传文件
public function uploadFile(RequestsUploadFileRequest $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]); }
在uploadManager下完善刚刚用到的方法deleteDirectory
/** * 保存文件 成功时返回true错误时返回错误信息或false。 * * @param $path * @param $content * @return string */ public function saveFile($path, $content) { $path = $this->cleanFolder($path); if ($this->disk->exists($path)) { return "File already exists."; } return $this->disk->put($path, $content); }
注意:如果重命名文件名的话要自行加上后缀哦