昨天完成了基礎環境的構建,今天就可以完成其它的 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
{{-- ... --}}