Bài 2 - Laravel quản lý tập tin và hình ảnh

Học lập trình Laravel căn bản - Bài 2

Chào các bạn, hôm nay mình sẽ tiếp tục chia sẻ với các bạn một số chức năng thường xuyên sử dụng trong lập trình php với Laravel:

- Kiểm tra dữ liệu trước khi lưu xuống cơ sở dữ liệu (sử dụng hàm validate()).

- Upload hình ảnh hoặc file lên website và xóa file cũ khi sửa.

- Chọn giá trị từ một bảng dữ liệu khác.

Yêu cầu bài tập: 

1. Tạo một bảng dữ liệu học sinh gồm 2 thông tin (tên học sinh, số điện thoại) và viết chức năng thêm, sửa, xóa, hiển thị danh sách (Lập trình Laravel căn bản - Bài 1).

2. Thêm trường dữ liệu (hình thẻ, lý lịch, khối) cho bảng học sinh. Trong đó, trường hình thẻ là đường dẫn hình thẻ của học sinh được upload lên và lý lịch là đường dẫn file word lý lịch của của học sinh được upload lên, khối là trường dữ liệu được kết từ bảng khối (id, tenkhoi).

Sau khi chúng ta thực hiện xong yêu cầu đầu tiên từ bài tập trước, chúng ta sẽ tiếp tục với yêu cầu thứ 2.

Chúng ta thêm 3 trường dữ liệu trên như sau:

+ hinhthe: varchar(255)

+ lylich: varchar(255)

+ khoi: int (10)

Và tạo bảng dữ liệu tên "tbl_khoi" với 2 trường dữ liệu sau:

+ id: int(10)

+ tenkhoi: varchar(255)

Sau khi tạo bảng dữ liệu "tbl_khoi" xong các bạn có thể viết quản lý thêm, sửa, xóa như đã được hướng dẫn từ bài trước và thêm vào các khối như KHỐI 6, KHỐI 7, KHỐI 8, KHỐI 9.

Tiếp theo chúng ta sẽ chỉnh sửa lại một số function và form.

- Function create(): hàm hiển thị trang thêm học sinh của file "HocSinhController.php"

Chúng ta sẽ chỉnh sửa lại hàm này lại như sau:

public function create()
{
	//Lấy danh sách bảng khối
	$dskhoi = DB::table('tbl_khoi')->select('id','tenkhoi')->get();
	//Hiển thị trang thêm học sinh
	return view('hocsinh.create')->with('dskhoi',$dskhoi);
}

- Trong file "create.blade.php" các bạn thêm dòng code sau để hiển thị kết quả kiểm tra dữ liệu không thỏa điều kiện validate() được viết trong function store(Request $request) của file "HocSinhController.php".

@if ($errors->any())
<div class="alert alert-danger alert-dismissible" role="alert">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
@endif

Sửa lại form thêm mới học sinh:

+ Thêm đoạn code sau để có thể upload file trong thẻ form thêm học sinh: enctype="multipart/form-data"

+ Thêm 3 trường dữ liệu nhập vào trong form thêm học sinh:

<div class="form-group">
<label for="hinhthe">Chọn hình thẻ</label>
<input type="file" class="form-control" id="hinhthe" name="hinhthe"/>
</div>
<div class="form-group">
<label for="lylich">Chọn file lý lịch</label>
<input type="file" class="form-control" id="lylich" name="lylich"/>
</div>
<div class="form-group">
<label for="khoi">Chọn khối</label>
<select class="form-control" id="khoi" name="khoi" required>
<option value="">-- Chọn khối --</option>
@foreach($dskhoi as $khoi)
<option value="{!! $khoi->id !!}">{!! $khoi->tenkhoi !!}</option>
@endforeach
</select>
</div>

- Trong file "HocSinhController.php" chúng ta chỉnh sửa nội dung function store() như sau:

public function store(Request $request)
{		
	//Kiểm tra giá trị tenhocsinh, sodienthoai, khoi
	$this->validate($request, 
		[
			//Kiểm tra giá trị rỗng
			'tenhocsinh' => 'required',
			'sodienthoai' => 'required',
			'khoi' => 'required',
		],			
		[
			//Tùy chỉnh hiển thị thông báo
			'tenhocsinh.required' => 'Bạn chưa nhập tên học sinh!',
			'sodienthoai.required' => 'Bạn chưa nhập số điện thoại!',
			'khoi.required' => 'Bạn chưa chọn khối!',
		]
	);
	
	//Lưu hình thẻ khi có file hình
	$gethinhthe = '';
	if($request->hasFile('hinhthe')){
		//Hàm kiểm tra dữ liệu
		$this->validate($request, 
			[
				//Kiểm tra đúng file đuôi .jpg,.jpeg,.png.gif và dung lượng không quá 2M
				'hinhthe' => 'mimes:jpg,jpeg,png,gif|max:2048',
			],			
			[
				//Tùy chỉnh hiển thị thông báo không thõa điều kiện
				'hinhthe.mimes' => 'Chỉ chấp nhận hình thẻ với đuôi .jpg .jpeg .png .gif',
				'hinhthe.max' => 'Hình thẻ giới hạn dung lượng không quá 2M',
			]
		);
		
		//Lưu hình ảnh vào thư mục public/upload/hinhthe
		$hinhthe = $request->file('hinhthe');
		$gethinhthe = time().'_'.$hinhthe->getClientOriginalName();
		$destinationPath = public_path('upload/hinhthe');
		$hinhthe->move($destinationPath, $gethinhthe);
	}
	
	//Lưu file lý lịch khi có file
	$getlylich = '';
	if($request->hasFile('lylich')){
		$this->validate($request, 
			[
				//Kiểm tra đúng file đuôi .doc hay .docx và dung lượng không quá 5M
				'lylich' => 'mimes:doc,docx|max:5120',
			],			
			[
				//Tùy chỉnh hiển thị thông báo không thõa điều kiện
				'lylich.mimes' => 'Chỉ chấp nhận lý lịch với đuôi .doc .docx',
				'lylich.max' => 'Lý lịch giới hạn dung lượng không quá 5M',
			]
		);
		
		//Lưu file vào thư mục public/upload/lylich
		$lylich = $request->file('lylich');
		$getlylich = time().'_'.$lylich->getClientOriginalName();
		$destinationPath = public_path('/upload/lylich');
		$lylich->move($destinationPath, $getlylich); 
	}
	
	//Lấy giá trị học sinh đã nhập
	date_default_timezone_set("Asia/Ho_Chi_Minh");
	$allRequest  = $request->all();
	$tenhocsinh  = $allRequest['tenhocsinh'];
	$sodienthoai = $allRequest['sodienthoai'];
	$khoi = $allRequest['khoi'];
	
	//Gán giá trị vào array
	$dataInsertToDatabase = array(
		'tenhocsinh'  => $tenhocsinh,
		'sodienthoai' => $sodienthoai,
		'hinhthe' => $gethinhthe,
		'lylich' => $getlylich,
		'khoi' => $khoi,
		'created_at' => date('Y-m-d H:i:s'),
		'updated_at' => date('Y-m-d H:i:s'),
	);
	
	//Insert vào bảng tbl_hocsinh
	$insertData = DB::table('tbl_hocsinh')->insert($dataInsertToDatabase);
	if ($insertData) {
		Session::flash('success', 'Thêm mới học sinh thành công!');
	}else {                        
		Session::flash('error', 'Thêm thất bại!');
	}
	
	//Thực hiện chuyển trang
	return redirect('hocsinh/create');
}

- Sửa chức năng hiển thị danh sách học sinh:

+ Sửa function index() trong file "HocSinhController.php"

public function index()
{
	//Lấy danh sách học sinh từ database
	$getData = DB::table('tbl_hocsinh as hs')
				->leftJoin('tbl_khoi as khoi', 'hs.khoi', '=', 'khoi.id')
				->select('hs.id','hs.tenhocsinh','hs.sodienthoai','hs.hinhthe','hs.lylich','khoi.tenkhoi')->get();
				
	//Gọi đến file list.blade.php trong thư mục "resources/views/hocsinh" với giá trị gửi đi tên listhocsinh = $getData
	return view('hocsinh.list')->with('listhocsinh',$getData);
}

+ Chỉnh sửa nội dung file "list.blade.php" như đoạn code sau:

@extends('templates.master')
@section('title','Quản lý học sinh')
@section('content')
<?php //Hiển thị thông báo thành công?>
<div class="page-header"><h4>Quản lý học sinh</h4></div>
@if ( Session::has('success') )
<div class="alert alert-success alert-dismissible" role="alert">
<strong>{{ Session::get('success') }}</strong>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
@endif
<?php //Hiển thị thông báo lỗi?>
@if ( Session::has('error') )
<div class="alert alert-danger alert-dismissible" role="alert">
<strong>{{ Session::get('error') }}</strong>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
@endif
<?php //Hiển thị danh sách học sinh?>
<style>
#myImg { border-radius: 5px; cursor: pointer; transition: 0.3s; }
#myImg:hover {opacity: 0.7;}
/* The Modal (background) */
.modal { display: none; position: fixed; z-index: 9999; padding-top: 50px; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.9);}
/* Modal Content (image) */
.modal-content { margin: auto; display: block; width: 50%; max-width: 400px; }
/* Caption of Modal Image */
#caption { margin: auto; display: block; width: 80%; max-width: 700px; text-align: center; color: #ccc; padding: 10px 0; height: 50px; }
/* Add Animation */
.modal-content, #caption { -webkit-animation-name: zoom; -webkit-animation-duration: 0.6s; animation-name: zoom; animation-duration: 0.6s; }
@-webkit-keyframes zoom { from {-webkit-transform:scale(0)} to {-webkit-transform:scale(1)} }
@keyframes zoom { from {transform:scale(0)} to {transform:scale(1)} }
/* The Close Button */
.close { position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; transition: 0.3s; }
.close:hover, .close:focus { color: #bbb; text-decoration: none; cursor: pointer; }
/* 100% Image Width on Smaller Screens */
@media only screen and (max-width: 700px){
.modal-content {
width: 100%;
}
}
</style>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="table-responsive">
<p><a class="btn btn-primary" href="/{{ url('/hocsinh/create') }}">Thêm mới</a></p>
<table id="DataList" class="table table-bordered table-hover">
<thead>
<tr>
<th>STT</th>
<th>Tên học sinh</th>
<th>Số điện thoại</th>
<th>Hình thẻ</th>
<th>File lý lịch</th>
<th>Khối</th>
<th>Sửa</th>
<th>Xóa</th>
</tr>
</thead>
<tbody>
<?php //Vòng lập foreach lấy giá vào bảng?>
@foreach($listhocsinh as $key => $hocsinh)
<tr>
<td style="text-align: center; vertical-align: middle;">{{ $key+1 }}</td>
<td style="vertical-align: middle;">{{ $hocsinh->tenhocsinh }}</td>
<td style="vertical-align: middle;">{{ $hocsinh->sodienthoai }}</td>
<td style="text-align: center; vertical-align: middle; width: 10%;">
@if($hocsinh->hinhthe != '')
<img onclick="MymodalImage(this);" alt="{{ $hocsinh->tenhocsinh }}" src="/public/upload/hinhthe/{{ $hocsinh->hinhthe }}" style="cursor: zoom-in;" width="60"/>
@else
<img onclick="MymodalImage(this);" alt="{{ $hocsinh->tenhocsinh }}" src="/public/upload/hinhthe/noimage.png" style="cursor: zoom-in;" width="60"/>
@endif
<td style="text-align: center; vertical-align: middle; width: 10%;">
@if($hocsinh->lylich != '')
<a class="btn btn-primary" href="/public/upload/lylich/{{ $hocsinh->lylich }}">Download về máy</a>
@else
<img onclick="MymodalImage(this);" src="/public/upload/lylich/nofile.png" alt="{{ $hocsinh->tenhocsinh }}" style="cursor: zoom-in;" width="60"/>
@endif
</td>
<td style="text-align: center; vertical-align: middle;">
{{ $hocsinh->tenkhoi }}
</td>
<td style="text-align: center; vertical-align: middle;"><a href="/hocsinh/{{ $hocsinh->id }}/edit">Sửa</a></td>
<td style="text-align: center; vertical-align: middle;"><a href="/hocsinh/{{ $hocsinh->id }}/delete">Xóa</a></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<div id="myModal" class="modal">
<span class="close">&times;</span>
<img class="modal-content" id="img01">
<div id="caption"></div>
</div>
<script>
function MymodalImage(e)
{
// Get the modal
var modal = document.getElementById('myModal');
// Get the image and insert it inside the modal - use its "alt" text as a caption
var modalImg = document.getElementById("img01");
var captionText = document.getElementById("caption");
modal.style.display = "block";
modalImg.src = e.src;
captionText.innerHTML = e.alt;
// Get the <span> element that closes the modal
var span = document.getElementsByClassName("close")[0];
// When the user clicks on <span> (x), close the modal
span.onclick = function() {
modal.style.display = "none";
}
}
</script>
@endsection

+ Trong đoạn code trên. trường hợp không có file hình thẻ sẽ hiển thị hình ảnh mặc định có tên là "noimage.png" và nếu như không có file lý lịch sẽ hiển thị hình ảnh mặc định là "nofile.png". Các bạn có thể lấy hình ảnh noimage trên mạng mà các bạn thích vào thư mục "public/hocsinh/hinhthe" và thư mục "public/hocsinh/lylich".

- Sửa chức năng sửa học sinh:

+ Sửa nội dung function edit($id) trong file "HocSinhController.php" với nội dung code sau:

public function edit($id)
{
	//Lấy dữ liệu bảng tbl_khoi từ Database
	$dskhoi = DB::table('tbl_khoi')->select('id','tenkhoi')->get();
	
	//Lấy dữ liệu từ Database với các trường được lấy và với điều kiện id = $id
	$getData = DB::table('tbl_hocsinh')->select('id','tenhocsinh','sodienthoai','hinhthe','lylich','khoi')->where('id',$id)->get();
	
	//Gọi đến file edit.blade.php trong thư mục "resources/views/hocsinh" với giá trị gửi đi tên getHocSinhById = $getData và dskhoi = $dskhoi
	return view('hocsinh.edit', ['getHocSinhById' => $getData, 'dskhoi' => $dskhoi]);
}

+ Sửa nội dung form sửa học sinh: 

Thêm đoạn code sau để hiển thị kết quả kiểm tra của hàm validate() không thỏa điều kiện.

@if ($errors->any())
<div class="alert alert-danger alert-dismissible" role="alert">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
</div>
@endif

Thêm đoạn code sau để có thể upload file trong thẻ form: enctype="multipart/form-data"

Thêm 3 trường dữ liệu nhập vào form sửa với đoạn code sau:

<div class="form-group">
<label for="hinhthe">Chọn hình thẻ mới</label>
<input type="file" class="form-control" id="hinhthe" name="hinhthe"/>
</div>
<div class="form-group">
<label for="lylich">Chọn file lý lịch mới</label>
<input type="file" class="form-control" id="lylich" name="lylich"/>
</div>
<div class="form-group">
<label for="khoi">Chọn khối</label>
<select class="form-control" id="khoi" name="khoi" required>
<option value="">-- Chọn khối --</option>
@foreach($dskhoi as $khoi)
<option value="{!! $khoi->id !!}" {!! ($getHocSinhById[0]->khoi == $khoi->id) ? 'selected="selected"' : null !!}>{!! $khoi->tenkhoi !!}</option>
@endforeach
</select>
</div>

Sửa function update(Request $request) trong file "HocSinhController.php" với nội dung code như sau:

public function update(Request $request)
{
	//Cap nhat sua hoc sinh
	date_default_timezone_set("Asia/Ho_Chi_Minh");	

	//Kiểm tra giá trị tenhocsinh, sodienthoai, khoi
	$this->validate($request, 
		[
			'tenhocsinh' => 'required',
			'sodienthoai' => 'required',
			'khoi' => 'required',
		],			
		[
			'tenhocsinh.required' => 'Bạn chưa nhập tên học sinh!',
			'sodienthoai.required' => 'Bạn chưa nhập số điện thoại!',
			'khoi.required' => 'Bạn chưa chọn khối!',
		]
	);
	
	//Thực hiện lưu thay đổi hình thẻ khi có file
	if($request->hasFile('hinhthe')){
		$this->validate($request, 
			[
				'hinhthe' => 'mimes:jpg,jpeg,png,gif|max:2048',
			],			
			[
				'hinhthe.mimes' => 'Chỉ chấp nhận hình thẻ với đuôi .jpg .jpeg .png .gif',
				'hinhthe.max' => 'Hình thẻ giới hạn dung lượng không quá 2M',
			]
		);
		
		//Xóa file hình thẻ cũ
		$getHT = DB::table('tbl_hocsinh')->select('hinhthe')->where('id',$request->id)->get();
		if($getHT[0]->hinhthe != '' && file_exists(public_path('upload/hinhthe/'.$getHT[0]->hinhthe)))
		{
			unlink(public_path('upload/hinhthe/'.$getHT[0]->hinhthe));
		}
		
		//Lưu file hình thẻ mới
		$hinhthe = $request->file('hinhthe');
		$gethinhthe = time().'_'.$hinhthe->getClientOriginalName();
		$destinationPath = public_path('upload/hinhthe');
		$hinhthe->move($destinationPath, $gethinhthe);
		$updateHinhThe = DB::table('tbl_hocsinh')->where('id', $request->id)->update([
			'hinhthe' => $gethinhthe
		]);
	}
	
	//Thực hiện lưu thay đổi lý lịch khi có file
	if($request->hasFile('lylich')){
		$this->validate($request, 
			[
				'lylich' => 'mimes:doc,docx|max:5120',
			],			
			[
				'lylich.mimes' => 'Chỉ chấp nhận lý lịch với đuôi .doc .docx',
				'lylich.max' => 'Lý lịch giới hạn dung lượng không quá 5M',
			]
		);
		
		//Xóa file lý lịch cũ
		$getLL = DB::table('tbl_hocsinh')->select('lylich')->where('id',$request->id)->get();
		if($getLL[0]->lylich != '' &&file_exists(public_path('upload/lylich/'.$getLL[0]->lylich)))
		{
			unlink(public_path('upload/lylich/'.$getLL[0]->lylich));
		}
		
		//Lưu file lý lịch mới
		$lylich = $request->file('lylich');
		$getlylich = time().'_'.$lylich->getClientOriginalName();
		$destinationPath = public_path('upload/lylich');
		$lylich->move($destinationPath, $getlylich);
		$updateLylich = DB::table('tbl_hocsinh')->where('id', $request->id)->update([
			'lylich' => $getlylich
		]);
	}
	
	//Thực hiện câu lệnh update với các giá trị $request trả về
	$updateData = DB::table('tbl_hocsinh')->where('id', $request->id)->update([
		'tenhocsinh' => $request->tenhocsinh,
		'sodienthoai' => $request->sodienthoai,
		'khoi' => $request->khoi,
		'updated_at' => date('Y-m-d H:i:s')
	]);
	
	//Kiểm tra lệnh update để trả về một thông báo
	if ($updateData) {
		Session::flash('success', 'Sửa học sinh thành công!');
	}else {                        
		Session::flash('error', 'Sửa thất bại!');
	}
	
	//Thực hiện chuyển trang
	return redirect('hocsinh/'.$request->id.'/edit');
}

Chức năng xóa không ảnh thưởng nên chúng ta không cần sửa. Bạn có thể thêm xóa file hình thẻ và file lý lịch khi xóa học sinh như đã ghi chú trong đoạn code trên.

Vậy là chúng ta đã hoàn thành yêu cầu bài số 2. Qua bài này chúng ta có thể đúc kết được những kinh nghiệm sau:

- Sử dùng hàm validate() để kiểm tra dữ liệu nhập vào trước khi lưu xuống Database, giúp tránh bị lỗi câu lệnh sql và kiểm tra file upload không đúng định dạng.

- Muốn upload file lên website từ form chúng ta cần thêm thuộc tính sau cho thẻ <form>: enctype="multipart/form-data"

- Cách xóa file cũ để tối ưu dung lượng sử dụng trên website: unlink('đường dẫn file hình')  - Lưu ý kiểm tra tồn tại file với hàm file_exists() trước khi xóa để tránh bị lỗi.

Tôi sẽ hướng dẫn các bạn thực hiện form đăng nhập và phân quyền trong bài tập tiếp theo, mong các bạn đọc giả ủng hộ.

Chúc các bạn thực hiện thành công dự án của mình!

Ngụy Kim Hưng