Laravel PHP

Laravel (3): Intermediate Task

https://laravel.com/docs/5.2/quickstart-intermediate 

# Prepare the database

上一篇 Basic Tasks 教程中產生的 /database/migrations/year_month_date_randomNumber_create_tasks_table.php,裡面加上 user_id 當作 index key (user_id 存的是 users table 的 primary key)

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->index(); // new in this tutorial
            $table->string('name');
            $table->timestamps();
        });
    }
}

Migrate 後產生的 tasks table:

Field Type Null Key Default Extra
id int(10) unsigned NO PRI NULL auto_increment
user_id int(11) NO MUL NULL
name varchar(255) NO NULL
created_at timestamp YES NULL
updated_at timestamp YES NULL

# Mass Assignment


Laravel 提供 $fillable / $guarded 兩個屬性,有設定到 $fillable 的 ORM 欄位是 “mass-assignable”

class Task extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

https://laravel.tw/docs/5.2/eloquent#mass-assignment
 “The $guarded property should contain an array of attributes that you do not want to be mass assignable. All other attributes not in the array will be mass assignable. So, $guarded functions like a “black list”. Of course, you should use either $fillable or $guarded – not both
你的 ORM class 只能用 $fillable / $guarded 擇一,當使用 $guarded 時,不在 $guarded array 中的變數是 mass assignable。

# Eloquent Relationships


https://laravel.com/docs/5.2/eloquent-relationships

class Task extends Model
{
    ....

    /**
     * Get the user that owns the task.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
class User extends Authenticatable
{
    ....

    /**
     * Get all of the tasks for the user.
     */
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
}

如果你的 DB 有 relation table 的話,記住在有 relation 的兩個 ORM class 寫上類似上方的 function。不是必須,如果你有辦法 work around 自行處理 relation index 的話。但建議養成好習慣,有 relation table 就在 ORM class 寫上兩者的 relation.

Summary 一下,Laravel ORM class 中不會出現 table 中全部欄位,甚至只會出現少數欄位。目前 ORM class 中只做兩件事:
1. 註冊那些欄位是 massive assignable.
2. 註冊不同 table (ORM) 間的關係。 

# 產生 controller


在繼續談 data CRUD 之前我們先產生 controller, 因為 controller 是 data model 的 caller side:
php artisan make:controller TaskController

會產生 /app/Http/Controllers/TaskController.php,在其中加上:

public function __construct()
{
    $this->middleware('auth');
}

public function index(Request $request)
{
    return view('tasks.index');
}

注意在 constructor 註冊了 ‘auth’ middleware 保護 tasks 頁面,如果是沒有登入的狀態,則導到 login 頁面 (auth middleware 中定義的,當然可以更改)。auth 保護也可以在 route 做 (->middleware(‘auth’)),差別在哪?如果你的 tasks 頁面是”一定”要保護的,auth 就放在 controller 裡;如果你的 tasks 頁面可保護可不保護,case by case 的話,auth 就可以放在 route 裡。

加上 view: resources/views/tasks/index.blade.php。routes.php 加上:

Route::get('/tasks', 'TaskController@index');
# Displaying Existing Tasks

TaskController::index 改成

public function index(Request $request)
{
    $tasks = $request->user()->tasks()->get();

    return view('tasks.index', [
        'tasks' => $tasks,
    ]);
}

# Dependency Injection


Laravel 跟其他 framework 一樣,有 service 和 dependency injection。新增 app/Repositories/TaskRepository.php,裡面 class TaskRepository 加上 function forUser:

public function forUser(User $user)
{
    return $user->tasks()
                ->orderBy('created_at', 'asc')
                ->get();
}

TaskController 的 constructor 加上:

public function __construct(TaskRepository $tasks)
{
    $this->middleware('auth');
    // assign to the controller's member
    $this->tasks = $tasks;
}  

注意”直接”傳入 TaskRepository 就可以了。

為什麼要用 service? 官方教程有一段: “we want to define a TaskRepository that holds all of our data access logic for the Task model. This will be especially useful if the application grows and you need to share some Eloquent queries across the application.

其實就是把讀取邏輯放在 service, 例如 order, filter, etc. 如果不用 service, 當專案稍大時,就會發現多處有重複的程式碼,所以將它們包進 service 裡。

# 新增 task


TaskController.php 加上:

public function store(Request $request)
{
    $this->validate($request, [
        'name' => 'required|max:255',
    ]);

    $request->user()->tasks()->create([
        'name' => $request->name,
    ]);

    return redirect('/tasks');
}

routes.php 要加上 Route::post(‘/task’, ‘TaskController@store’)。注意這邊 validate 的寫法跟 Basic Task 教程不同,事實上簡單多了。因為這邊 validate 是做在 controller 裡,而 validate 是 class Controller 的一個 method。另外注意我們不需要明文寫 fails 時 redirect,Controller::validate 在 fail 時會自動導回 post 的頁面。

關於 create method,還記得 protected $fillable = [‘name’] (Mass Assignment),所以我們可以在 create task 時 assign name。另外 Laravel 會”自動”設定新生成 task 的 foreign key (in this case ‘user_id’)。用 new Task() 一樣可以在 DB 可以新增 task,但其 user_id 為 0。

# 刪除 task

Method Spoofing 和 Implicit Model Binding 與上一篇 basic tasks 一樣。Laravel 對 ORM 存取有 policy 的設計:

php artisan make:policy TaskPolicy

新增 app/Policies/TaskPolicy.php,裡面加上 function destroy(User $user, Task $task)。

app/Providers/AuthServiceProvider.php $policies 加上:

protected $policies = [
    'AppTask' => 'AppPoliciesTaskPolicy',
];

以上關於 Task ORM 的存取 policy 就設定完成了,但要如何調用呢?TaskController 加上:

public function destroy(Request $request, Task $task)
{
    $this->authorize('destroy', $task);

    // Delete The Task...
}

以上
* ‘destroy’ argument 指出我們是要用 TaskPolicy 的 destroy method。
* $task 傳到 TaskPolicy::destroy 當 argument,current user $user “自動”傳入。
* 如果 TaskPolicy::destroy return false (違反 policy),403 error;return true,繼續執行。

注意 policy 與 service (TaskRepository) 的區別。Policy 有點像是 utility functions,將要確認的事項 delegate 出來,policy return 的是 true/false (合乎或不合乎該 policy)。程式碼方面,controller php 完全沒有任何 policy 的類別 (in this case TaskPolicy),要改的只有 AuthServiceProvider::policies。

# Create auth views

php artisan make:auth --views

Created View: resources/views/auth/login.blade.php
Created View: resources/views/auth/register.blade.php
Created View: resources/views/auth/passwords/email.blade.php
Created View: resources/views/auth/passwords/reset.blade.php
Created View: resources/views/auth/emails/password.blade.php
Created View: resources/views/layouts/app.blade.php => navbar, font awesome, google font, bootstrap, jquery
Created View: resources/views/home.blade.php =>  You are logged in!
Created View: resources/views/welcome.blade.php =>  Your Application’s Landing Page.

新增完上述 view 後, route “至少”要有:

Route::get('/', function () {
    return view('welcome');
});

Route::auth();

Route::auth() 負責將 login / register 等頁面 routing 做完 (不用明文做 routing)。Route::get(‘/’ 負責首頁,Route::auth() 不包含首頁。

更改 app/Http/Controllers/Auth/AuthController.php 中的 redirect:

protected $redirectTo = '/tasks';

這變數上面 comment 指出這是登入/註冊成功後導入的位置。接著更改 app/Http/Middleware/RedirectIfAuthenticated.php 的 redirect:

return redirect('/tasks');

去查 app/Http/Kernel.php,RedirectIfAuthenticated 註冊為名稱 “guest” 的 middleware,所以你的 route 要改成:

Route::get('/', function () {
    return view('welcome');
})->middleware('guest');

這樣產生的效果就是如果你已經是登入狀態,要進入首頁時,會被導到 /tasks 頁面。有一個細節可以記錄一下,更改 AuthController 和 RedirectIfAuthenticated 在中文版網頁都”沒有”。


# Layouts & Views


上面利用 artisan command “php artisan make:auth” 產生了 “resources/views/layouts/app.blade.php”,也加上了 “resources/views/tasks/index.blade.php”。


layouts/app.blade.php 有 “@yield(‘content’)”,tasks/index.blade.php 有 “@extends(‘layouts.app’)”,是 parent / child 的關係。

tasks/index.blade.php 的 @section(‘content’)/@endsection 會取代掉 layouts/app.blade.php 的 @yield(‘content’)。

tasks/index.blade.php 有一行 @include(‘common.errors’),指向 common/errors.blade.php,其中有一個參數 $errors。他是 Laravel global variable,在這教程中,如果 input validate fail 的話,$errors 會有值。

關於這種後端的 template engine,larry 還是沒想透。主要是的確,header, menu 這種不太更動,較為簡單的元件,由後端直接 template 是比較合理而且自然的做法,甚至 error handle 的頁面/元件也應該由後端做掉。但是目前 web 介面有很多類似 app 的操作 (single page),其中包含了客戶的商業邏輯,很多 fancy 的效果、動畫,第三方的 plugin、api 等等。有經驗的工程師可以想像以上這些 front end 的東西跟 php mess 在一起會是甚麼情況 (即使使用後端 template engine)

其實還是要回歸軟體工程:模組化、分工。前後端分離也是一種模組化的動作,api 是介面。模組化的好處,相信做軟體的朋友都知道,larry 就不再贅言,還是老話一句: “如果沒有模組化開發與驗證,產品出了問題就從 main function 開始追吧。”

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。