iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 1
0
Modern Web

Laravel 8: For Beginners系列 第 12

留言板(Part2)

前言

昨天完成了基礎環境的構建,今天就可以完成其它的 API 與視圖。

設計 API:新增留言

建立基礎功能

$ php artisan make:controller PostController
<?php

// route/web.php

use App\Http\Controllers\PostController;

Route::post('/', PostController::class)->name('post');
<?php

// app/Http/Controllers/PostController.php

use App\Models\Message;
use Illuminate\Support\Str;

class PostController extends Controller
{
	public function __invoke(Request $request)
	{
		// 驗證資料
		// title 為必需字串;content 可以為空白字串;attachment 為可選圖片,但圖片小於 5MB
		$request->validate([
			'title' => 'required|string',
			'content' => 'string|nullable',
			'attachment' => 'image|max:5120', 
		]);

		// 暫不處理
		if ($request->has('attachment')) {
			// ...
		}

		Message::create([
			'name' => Str::random(8),
			'title' => $request->title,
			// 當 content 為空時,使用「無內文」的預設值
			'content' => $request->content ?? '無內文',
			// 目前 $attachment 永遠未設定,所以此處必為 null
			'attachment' => $attachment ?? null,
		]);

		// 回傳字串表示成功建立,之後再修改
		return 'Success';
	}
}

撰寫測試

養成良好習慣,無論是在開發功能前先測試,或是在功能開發完成後再寫測試,請務必要寫測試。

$ php artisan make:test PostTest
<?php

// tests/Feature/PostTest

class PostTest extends TestCase
{
	use RefreshDatabase;

	public function test_post()
	{
        // 根據 Message Factory 建立一組假的測資
		$post = Message::factory()->make();

        // 利用 $this->post 去對目前的應用程式模擬請求
		$response = $this->post(route('post'), [
			'title' => $post->title,
			'content' => $post->content,
		]);

        // 確定 $response 符合預期
		$response->assertSuccessful();
        // 確定資料庫中含有期望的資料
		$this->assertDatabaseHas('messages', [
			'title' => $post->title,
			'content' => $post->content,
		]);
	}

	public function test_post_without_content()
	{
		$post = Message:factory()->make();

		$response = $this->post(route('post'), [
			'title' => $post->title,
			'content' => '',
		]);

		$response->assertSuccessful();
		$this->assertDatabaseHas('messages', [
			'title' => $post->title,
			'content' => '無內文',
		]);
	}
}

附圖功能

<?php

// app/Http/Controllers/PostController.php

// ...

if ($request->has('attachment')) {
	// 利用 store 將圖片儲存於 attachments/ 資料夾下
	$attachment = $request->attachment->store('attachments');
}

// ...
<?php

// tests/Feature/PostTest

// ...

use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;

public function test_post_with_attachment()
{
	// 建立一個假的儲存空間,每次測試結束後都會自動清空
	Storage::fake();

	$post = Message::factory()->make();

	$response = $this->post(route('post'), [
		'title' => $post->title,
		'content' => $post->content,
		// 由 Laravel 自動生成一張假的圖片供測試使用
		'attachment' => UploadedFile::fake()->image('attachment.png'),
	]);

	$response->assertSuccessful();
	$this->assertDatabaseHas('messages', [
		'title' => $post->title,
		'content' => $post->content,
	]);
	// 尋找剛剛生成的 Message
	// store 時 Laravel 會重新生成一個檔名用於儲存,所以需要先取得資料庫內的檔名才能做後續搜索
	$message = Message::where('title', $post->title)->first();
	// 確定 attachment 有被加入
	$this->assertNotNull($message->attachment);
	// 確定 attachment 的檔案存在
	Storage::assertExists($message->attachment);
}

設計用戶介面(視圖)

建立基礎功能

$ php artisan make:controller ViewController
<?php

// routes/web.php

use Illuminate\Http\Controllers\PostController;
use Illuminate\Http\Controllers\ViewController;

Route::post('/', PostController::class)->name('post');
Route::get('/', ViewController::class)->name('view');
<?php

// app/Http/Controllers/ViewController

class ViewController extends Controller
{
	public function __invoke()
	{
		return view('main')
			->with('messages', Message::all());
	}
}
{{-- resources/views/main.blade.php --}}
<html lang="en">
	<head>
		<title>Message Board</title>
	</head>
	<body>
		<form method="post" action="{{ route('post') }}" enctype="multipart/form-data">
			@csrf
			<label for="title">標題:</label>
			<input id="title" name="title">

			<label for="content">內文:</label>
			<textarea id="content" name="content"></textarea>

			<label for="attachment">附加圖檔:</label>
			<input id="attachment" name="attachment">
		</form>

		@foreach($messages as $message)
		<div>
			<h1>{{ $message->title }}</h1>
			<p>{{ $message->content }}</p>
			{{-- 先以路徑的方式將 attachment 印出 --}}
			<code>{{ $message->attachment }}</code>
		</div>
		@endforeach
	</body>
</html>

分頁功能

如果留言數增加,每次都需要直接取出上千甚至上萬則留言,勢必會對效能有所影響。

此時可以通過分頁(Pagination)功能,每次僅取出一部份的內容(例如 10 筆)以供瀏覽。

<?php

// app/Http/Controllers/ViewController.php

public function __invoke()
{
	return view('main')
		->with('messages', Message::paginate(10));
}

利用 Message::paginate(10) 可以每 10 筆資料為一頁進行分頁

{{-- resources/views/main.blade.php --}}
{{-- ... --}}

@foreach ($messages as $message)
<div>
	<h1>{{ $message->title }}</h1>
	<p>{{ $message->content }}</p>
	<code>{{ $message->attachment }}</code>
</div>
@endforeach

{{-- 使用 links() 就可以顯示頁碼工具 --}}
{{ $messages->links() }}

{{-- ... --}}

圖片顯示功能

目前,attachment 是以「路徑」的型式存放於資料庫中。

Laravel 為了能夠使檔案可以適配各式服務(AWS S3、FTP、Local File System 等),使用了 thephpleague/flysystem ,它為多種儲存服務提供了磁碟的概念。

舉例來說,我們可以將需要本地處理的資料(如要轉檔的影片)放在本機的硬碟上,讓大家可以存取的資料(如公開的圖片)放在 AWS S3 上等等。

我們可以從 config/filesystem.php 中看到目前使用的 Disk

<?php

// config/filesystem.php

return [
	'default' => env('FILESYSTEM_DRIVER', 'loacl'),

	'disks' => [
		'local' => [
			'driver' => 'local',
			'root' => storage_path('app'),
		],
	],
];

由此可知,我們的檔案預設是存放在 storage_path('app') 之下

我們將其改為 public 這個 Disk

<?php

// config/filesystem.php

return [
	'default' => env('FILESYSTEM_DRIVER', 'public'),
];

然後使用 php artisan storage:link 即可在 public/ 下建立一個捷徑,使用戶能夠存取。

之後,在視圖中利用 Storage::url($message->attachment) 就可以順利取得圖片 URLs

{{-- resources/views/main.blade.php --}}
{{-- ... --}}
@foreach ($messages as $message)
<div>
	<h1>{{ $message->title }}</h1>
	<p>{{ $message->content }}</p>
	<img src="{{ Storage::url($message->attachment) }}"/>
</div>
@endforeach
{{-- ... --}}
  • 值得注意的是,修改 config 之前上傳的圖片仍然留在原本的路徑,記得要搬移過去才能正常使用

上一篇
留言板(Part1)
下一篇
留言板(Part3)
系列文
Laravel 8: For Beginners14

尚未有邦友留言

立即登入留言