前言
Laravel 10: Small Reservation Project Step-By-Step 是教學網站 Laravel Daily 於 2023 年推出的專欄課程,課程中基於自由職業接案平台 Upwork 上的一個真實需求,逐步地構建一個預約系統。
需求描述
接案平台上的需求,由於客戶本身可能並不具備開發經驗或相關專業知識,所以經常會以他們自己的方式去描述需求,因此並不會將具體的內容描述地十分仔細。
比如這個來自於 Upwork 上的真實案例:
Hi,
I’m looking for help to build a straightforward reservation system in Laravel. I’ve built a similar system using Node.js, Express.js and MongoDB, but want to scale the project and add some functionalities.
Essentially, the system allows for adding clients, activities, and send out a link to fill in their sizes. The system also generates a PDF for each trip on demand for the guide.
I’m short on time so I’d appreciate help 🙂
Code should be clean, commented, DRY, … etc. Well built logic, readable, and project structure. We’ll get a basic version up and running, then add functionalities along the way, such as online payment integration etc… layout should be responsive and optimised for mobile.
Thanks for reading!
釐清需求
為此,作為一名開發者需要將這樣的需求描述轉換為適當的行動計畫(Plan of Action),反覆地蒐集相關資訊並向業主提出問題,逐漸地去完善整個計畫;畢竟釐清整個思路與需求的最佳方式 —— 就是直接詢問客戶。
比如對於上述的需求,或許可以詢問以下問題:
- 需求中沒有提到前端視覺設計,有什麼想法嗎?或者是希望可以使用既有的模板?(可以附上連結)
- 每一個公司是否只有一位使用者能夠管理活動呢?還是需要有多個使用者具備管理權限?(表示需要額外付費)
- 所謂的活動,需要具備哪些資訊?標題、敘述、照片、價格等就足夠了嗎?還是需要別的東西?
- 生成的 PDF 文件會長什麼樣子?有實際的範例可以供參考嗎?
行動計畫
在釐清需求的過程中,我們就能夠逐步地去建立行動計畫(Plan of Action),或者更具體地來說 —— 規劃資料庫的表結構與模型、條列待實作的功能清單。
初始化專案
創建專案並進行資料庫遷移
我們可以透過以下命令初始化一個 Laravel 專案:
# 創建專案
$ laravel new reservations
初始完專案之後,透過 php artisan
命令創建資料庫模型與對應的遷移文件:
$ cd reservations
# 創建模型與遷移文件
$ php artisan make:model Role --migration
$ php artisan make:migration add_role_id_to_users_table
$ php artisan make:model Company --migration
$ php artisan make:migration add_company_id_to_users_table
$ php artisan make:model Activity --migration
$ php artisan make:migration add_activity_user_table
在創建完對應的 Model 文件和 Migration 文件後,我們需要根據資料表的欄位與對應關係修改文件中的內容。
Role 資料表
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
class Role extends Model
{
use HasFactory;
protected $fillable = ['name'];
}
schema::table('users', function (Blueprint $table) {
$table->foreignId('role_id')->constrained();
});
class User Extends Authenticatable
{
protected $fillable = ['name', 'email', 'password', 'role_id'];
// ...
public function role()
{
return $this->belongsTo(Role::class);
}
}
Company 資料表
public function up(): void
{
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
class Company extends Model
{
use HasFactory;
protected $fillable = ['name'];
}
schema::table('users', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->constrained();
});
class User Extends Authenticatable
{
protected $fillable = ['name', 'email', 'password', 'role_id', `company_id`];
// ...
public function role()
{
return $this->belongsTo(Role::class);
}
public function company()
{
return $this->belongsTo(Company::class);
}
}
Activity 資料表
public function up(): void
{
Schema::create('activities', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained();
$table->foreignId('guide_id')->nullable()->constrained('users');
$table->string('name');
$table->text('description');
$table->dateTime('start_time');
$table->integer('price');
$table->string('photo')->nullable();
$table->timestamps();
});
}
class Company extends Model
{
use HasFactory;
protected $fillable = [ 'company_id', 'guide_id', 'name', 'description', 'start_time', 'price', 'photo' ];
public function company()
{
return $this->belongsTo(Company::class);
}
public function participants()
{
return $this->belongsToMany(User::class)->withTimestamps();
}
}
schema::create('activity_user', function (Blueprint $table) {
$table->foreignId('activity_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->timestamps();
});
修改完上述文件後,我們可以執行以下命令進行資料庫遷移:
$ php artisan migrate
添加依賴實現身分驗證
Laravel 提供了一系列的入門套件(Starter Kits)幫助開發者快速建構應用程式,其中 Laravel Breeze 包含基礎的身分驗證功能,如登入、註冊、重設密碼、電子郵件驗證等。
首先,我們需要透過 composer
來安裝依賴,並執行 php artisan breeze:install
打包前端資源:
$ composer require laravel/breeze --dev
$ php artisan breeze:install blade
建立使用者種子資料
我們在 User
模型中添加了 role_id
欄位,但資料庫中尚未有任何資料,如果此時進行註冊,系統會拋出以下錯誤:
SQLSTATE[HY000]: General error: 1364 Field 'role_id' doesn't have a default value
為此,我們需要創建預設的 role
資料,並在 user
註冊時賦予角色代號;我們經常在開發中需要事先在資料表中填入資料,比如有些功能需要在有資料的狀況下才能正常運作,或者是基於開發需求需要預先使用測試資料。
除了直接操作資料庫添加資料之外,在 Laravel 中可以使用 Seeder 來實現這樣的操作,執行以下命令創建 RoleSeeder
文件:
$ php artisan make:seeder RoleSeeder
我們需要在 RoleSeeder.php
文件中撰寫插入資料的操作,在將這個 Seeder 添加到 DatabaseSeeder.php
文件中:
use App\Models\Role;
class RoleSeeder extends Seeder
{
public function run(): void
{
Role::create(['name' => 'administrator']);
Role::create(['name' => 'company owner']);
Role::create(['name' => 'customer']);
Role::create(['name' => 'guide']);
}
}
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
RoleSeeder::class,
]);
}
}
在使用者註冊時,需要分派一個 role
給他,我們當然可以在 Laravel Breeze 的 RegisteredUserController
控制器中直接指派對應的角色編號,但這樣的實現並不夠優雅;在此可以善用 PHP 中的 Enum:
enum Role: int
{
case ADMINISTRATOR = 1;
case COMPANY_OWNER = 2;
case CUSTOMER = 3;
case GUIDE = 4;
}
use App\Enums\Role;
class RegisteredUserController extends Controller
{
public function store(Request $request): RedirectResponse
{
// ...
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role_id' => Role::CUSTOMER->value,
]);
// ...
}
}
增刪改查操作:以 Company 資源為例
創建控制器與添加路由
我們可以使用 php artisan make:controller
命令創建控制器(Controller),添加對應的增刪改查邏輯,並將他添加到路由(Route)中:
$ php artisan make:controller CompanyController
use App\Models\Company;
use Illuminate\View\View;
use App\Http\Requests\StoreCompanyRequest;
use App\Http\Requests\UpdateCompanyRequest;
class CompanyController extends Controller
{
public function index(): View
{
$companies = Company::all();
return view('companies.index', compact('companies'));
}
public function create(): View
{
return view('companies.create');
}
public function store(StoreCompanyRequest $request): RedirectResponse
{
Company::create($request->validated());
return to_route('companies.index');
}
public function edit(Company $company)
{
return view('companies.edit', compact('company'));
}
public function update(UpdateCompanyRequest $request, Company $company): RedirectResponse
{
$company->update($request->validated());
return to_route('companies.index');
}
public function destroy(Company $company)
{
$company->delete();
return to_route('companies.index');
}
}
use App\Http\Controllers\CompanyController;
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
// ...
Route::resource('companies', CompanyController::class);
});
驗證表單內容:使用 Form Requests
創建與編輯操作需要驗證表單內容,可以使用 Laravel 的 Form Requests 來實現,我們可以使用 php artisan make:request
命令創建相關文件:
$ php artisan make:request StoreCompanyRequest
$ php artisan make:request UpdateCompanyRequest
class StoreCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string'],
];
}
}
class UpdateCompanyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string'],
];
}
}
至於 View 層的模板實現就不在此處贅述了,可以在課程的 GitHub 專案倉庫中檢視。
操作權限控制:使用 Middleware
系統中資源的增刪改查操作,通常不會對所有使用者開放,因此需要增加權限控制機制,比如確認使用者為特定的角色;為了實現這樣的功能,我們可以透過 Laravel 中的 Middleware 來處理。
使用 php artisan make:middleware
命令創建相關文件,並在 Kernel.php
文件中添加中間件的別名;中間件的作用時機是在訪問 Company 資源時,還需要在路由中添加中間件:
$ php artisan make:middleware IsAdminMiddleware
class Kernel extends HttpKernel
{
// ...
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
// ...
'isAdmin' => \App\Http\Middleware\IsAdminMiddleware::class,
];
}
use App\Enums\Role;
use Symfony\Component\HttpFoundation\Response;
class IsAdminMiddleware
{
public function handle(Request $request, Closure $next): Response
{
abort_if($request->user()->role_id !== Role::ADMINISTRATOR->value, Response::HTTP_FORBIDDEN);
return $next($request);
}
}
Route::middleware('auth')->group(function () {
// ...
- Route::resource('companies', CompanyController::class);
+ Route::resource('companies', CompanyController::class)->middleware('isAdmin');
});