Routing

เริ่มต้นสร้างเว็บ Laravel ใหม่อีกครั้ง โดยการสร้างในชื่อ routingApp

CLI 1.


>composer create-project laravel/laravel routingApp
        

ปรับปรุงเส้นทางใหม่ จากเดิมที่คืนค่า view ให้แก้เป็นคืนข้อมูลอักษรอย่างเดียว

Code 1. routes/web.php

<?php
use Illuminate\Support\Facades\Route;

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

    return 'Hello World';
});
        

เมื่อดูผลการทำงาน เหมือนกับการเรียกใช้ คำสั่ง echo "Hello World";

เส้นทางมี URL กับ View

ตามปกติ เส้นทางจะมี จะระบุชื่อ เส้นทาง และ view ดังตัวอย่างที่ผ่านมา ใช้ “/” แทนเส้นทางเริ่มต้น แต่เนื่องจากไม่ได้ระบุชื่อ view จึงส่งคืนค่า อักษร ดังตัวอย่างที่ผ่านมา และนี้ถือเป็นเส้นทางแรกที่พบ จึงต้องเขียนเส้นทางไว้เป็นเส้นทางแรก ถ้าจะให้เขียนเส้นทางต่อไป ก็ต้องเขียนเส้นทางตามลำดับ

เส้นทางในลำดับที่สอง ต่อไปนี้ ต้องการให้ ไปยัง "http://localhost:8000/users" ซึ่งเป็น URI และจะต้องมีตัวแปรที่สอง เป็นชื่อ view เช่น ใช้ชื่อเหมือน URL นอกจากนี้ยังอาจมีตัวแปรที่สามได้เพื่อแทนข้อมูลที่ส่ง

Code 2. routes/web.php

<?php
use Illuminate\Support\Facades\Route;

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

Route::view('/users', 'users'); 
        

ดังนั้นเราจะต้องมีหน้า view ในชื่อไฟล์ users.blade.php ในหน้านี้ต้องการแสดงรายชื่อ ต่าง ๆ ในรูปตาราง ใช้รูปแบบตาม Bootstrap ตาม URL ใน <head>

Code 3. resources/views/users.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Laravel</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
    <div class="content">
        <h2>Users</h2>
        <table class="table">
            <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">First</th>
                    <th scope="col">Last</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <th scope="row">1</th>
                    <td>Mark</td>
                    <td>Otto</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html> 
        

เราจะทดสอบการทำงานตาม URL นี้ได้: http://localhost:8000/users

กรณีต้องการใช้ ไฟล์ bootstrap.min.css โดยที่เก็บไว้ในเว็บแอปฯ ของเราเอง ให้สร้าง โฟล์เดอร์เก็บไฟล์ไว้ที่ public โดยอาจสร้างเป็นโฟลเดอร์ css เช่นเดียวกับใช้งานไฟล์ jquery.js ก็ทำโฟลเดอร์ js

public/css/bootstrap.min.css
public/js/jquery.min.js

ให้คอนโทรลเลอร์ส่ง View

นอกจากจะส่ง view ตามเส้นทาง URI แล้วยังสามารถส่ง view ที่มาจากคอนโทรลเลอร์ได้ด้วย จากตัวอย่างต่อไปนี้ ใช้คอนโทรลเลอร์ ในชื่อ UserController จึงต้องประกาศใช้ (use) คอนโทรลเลอร์นี้ก่อน

Code 4. routes/web.php

<?php
use Illuminate\Support\Facades\Route;

use App\Http\Controllers\UserController;

Route::get('/', function () {
    return 'Hello World';
});

Route::get('/users', [UserController::class, 'index']);
        

จากตัวอย่างนี้เป็นใช้คอนโทรลเลอร์เป็นตัวเลือก view โดยที่ UserController เรียกใช้ฟังก์ชัน index ซึ่งจะคืนค่า view ดังนั้นเราจะต้องมีคลาส UserController และมีฟังก์ชัน index( ) ที่จะคืนค่า view

Code 5. app/Http/Controllers/UserController.php

<?php
namespace App\Http\Controllers;

use App\Models\User;

class UserController extends Controller{
    public function index()  {
        return view('users', [
            'users' => User::all()
        ]);
    }
}
        

จากตัวอย่างนี้ คลาสนี้ กำหนดให้มี namespace เป็น App\Http\Controllers ดังเราจะต้องสร้างโฟลเดอร์ Controllers เพิ่มขึ้นอีกภายโฟลเดอร์ App/Http และมีการใช้ User เป็นข้อมูลที่จะนำมาแสดงหน้าเว็บ ซึ่งจะสร้างเพิ่มอีกภายหลัง

สำหรับฟังก์ชัน index() ฟังก์ชันนี้ ทำหน้าที่คืนค่า view และข้อมูลเพิ่มเติม จากการเรียกคลาส User และฟังก์ชัน all() โดยฟังก์ชัน all() ต้องการให้คืนข้อมูลผู้ใช้ในรูปแบบอาร์เรย์

ให้ Model ส่งค่าไปกับคอนโทรเลอร์

เราจะใช้โฟลเดอร์ Models เพื่อเก็บกลุ่มข้อมูลที่เราสนใจ เราอาจสร้างโฟดเดอร์เพิ่มอีกก็ได้เพื่อรวมกลุ่มข้อมูลที่ใกล้เคียงกัน ซึ่งแล้วแต่ว่าเราจะออกแบบที่เหมาะสม

ในที่นี้เราใช้คลาส User.php เพื่อเป็นตัวแทนข้อมูลผู้ใช้ ข้อมูลเป็นการสมมุติ ในได้มาซึ่งข้อมูล อาจได้มาจากฐานข้อมูล แล้วค่อยเลือกมาใช้งานในการแสดงผลตามความต้องการใช้ในแต่ละงาน

Code 6. app/Models/User.php

<?php
namespace App\Models;

class User{
    public static $users =array(
        array("id" => 1, "fname" => "Tee", "lname" => "L."),
        array("id" => 2, "fname" => "Mon", "lname" => "K."),
        array("id" => 3, "fname" => "Pol", "lname" => "V."),        
    ); 

    public static function all(){
       return self::$users;
    }
}
        

จากคลาส User นี้ สังเกตว่าสมาชิกแต่ละตัวเป็นค่า static เพื่อให้เรียกใช้งานผ่านคลาสโดยตรง ข้อมูลที่สมมุติขึ้น มีเพียงสามรายการ แต่ละรายการมี id, fname, lname

เมื่อแบบจำลองข้อมูลพร้อมรองรับการทำงานตามการเรียกของคอนโทรลเลอร์ ก็พร้อมส่งไปแสดงผลในรูปตารางที่เขียนไว้ที่ users.blade.php โดยเขียนส่งไปในรูปตัวแปร $users ตามที่ระบุตามคอนโทรลเลอร์ ในการแสดงผลใช้การวนซ้ำ @foreach … @endforeach ดังแก้ไขเฉพาะข้อมูลในส่วน <tbody> ดังนี้

Code 7. resources/views/users.blade.php

<tbody>
            @foreach($users as $user)
                <tr>
                    <th scope="row">{{$user['id']}}</th>
                    <td>{{$user['fname']}}</td>
                    <td>{{$user['lname']}}</td>
                </tr>
            @endforeach
</tbody>
        

เปลี่ยนเส้นทาง ด้วย redirect( )

ในบางครั้ง เราต้องการเปลี่ยนเส้นทาง ไปยังอีกทางหนึ่ง เช่น บน URI: “/” ซึ่งเป็นหน้าเริ่มต้น แต่ต้องการให้ไปยังหน้า users เราใช้คำสั่ง redirect( )

Code 8. routes/web.php

Route::get('/', function () {
    return 'Hello World';
});
Route::redirect('/', '/users');
Route::get('/users', [UserController::class, 'index']);
        

จากตัวอย่างนี้ เมื่อผู้ใช้ไปยัง URL:http://localhost:8000/ จะถูกเปลี่ยนเส้นทางไปยัง URL:/users และพบต่อไปว่าเส้นทาง URL:/users จะเป็น view ที่คอนโทรลเลอร์ UserController เป็นผู้เลือกให้

ส่งข้อมูลไปกับเส้นทาง

ตามที่เคยได้กล่าวมาก่อนแล้วว่า การส่งข้อมูลไป จะเป็นตัวแปรที่สาม เช่น ส่งไปยัง URI: /user, ตัวแปรที่สอง เป็น view: user และข้อมูลเลือกเฉพาะรายแรก จะเขียนดังนี้

Code 9. routes/web.php

//เขียนเพิ่มไปกับก่อนหน้านี้ในบรรทัดถัดไป
Route::view("/user", "user", 
           ['user'=>array('id'=>1, 'fname'=>'Pol', 'lname'=>'L.')]);
        

แต่การส่งแบบนี้ต้องการจะแจ้งอะไรบางอย่างไปยัง view

Code 10. resources/views/user.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Laravel</title>
</head>
<body>
    <div class="content">
        <h2>Users</h2>
        <p class="lead">
            {{$user['id']}}:{{$user['fname']}} {{$user['lname']}}
        </p>
    </div>
</body>
</html>
        

เมื่อทดลองใส่ URL: http://localhost:8000/user ก็จะแสดงผลข้อมูล

แต่ดูจะมีประโยชน์ไม่มาก เพราะข้อมูลกำหนดแน่นอน ทางที่ดีกว่า คือควรส่งไปในรูปตัวแปร ซึ่งเปลี่ยนแปลงได้การใช้งาน URL

ส่งตัวแปรไปกับเส้นทาง

การส่งข้อมูลผ่านในรูปตัวแปร แล้วให้คอนโทรลเลอร์จัดการดูจะมีประโยชน์กว่า เพราะนำไปทำอะไรได้ เช่น ต้องการข้อมูลเฉพาะรายที่มี id=1 ตัวเลข 2 หรือ 3 เป็นตัวเลขที่เปลี่ยนแปลงได้ ดังนั้นแล้ว จึงควรเขียนส่งผ่านคอนโทรลเลอร์ดังนี้

Code 11. routes/web.php

//เขียนเพิ่มไปกับก่อนหน้านี้ในบรรทัดถัดไป
Route::get('/user/{id}', function($id){
    return UserController::getById($id);
});
        

จากตัวอย่างนี้มีฟังก์ชันใหม่ คือ getById($id) ที่มีตัวแปรเข้าหนึ่งตัว เราจำเป็นต้องสร้างฟังก์ชันนี้ด้วย ใช้เป็นค่าสเตติก เพราะเรียกใช้แบบสเตติก (::)

Code 12. app/Http/Controllers/UserController.php

public static function getById($id){
        return view('user', [
           'user'=> User::getById($id)
        ]);
}
        

การเรียก User::getById($id) ทำให้เห็นว่า คลาส User ต้องมีฟังก์ชันสเตติกชื่อ getById($id) ด้วย ดังนั้นแล้วเราจะต้องสร้างฟังก์ชันนี้เพื่อคืนค่าตามเลข id ที่ร้องขอมา

Code 13. app/Models/User.php

public static function getById($id){
    foreach(self::$users as $user){
        if($id == $user['id']){
            return $user;
        }       
    }
    //เพิ่มค่าปริยาย กรณีหาไม่พบ เพื่อป้องกันความผิดพลาด
    return array("id" => 0, "fname" => "NONE", "lname" => "NONE");
}

เมื่อทุกอย่างพร้อมแล้วก็สามารถทดสอบผลการทำงานโดยใส่เลขid ตามที่มีในระบบ เช่น ใส่ URI: user/1

URL:http://localhost:8000/user/1

เป็นตัวแทนของ การเลือกเฉพาะรายที่มี id=1 โดยเมื่อพิมพ์ URL นี้จะได้ตัวแปร ซึ่งเขียนภายใน {id} แล้วตัวแปรนี้นำไปใช้กับฟังก์ชัน function($id) ในชื่อตัวแปรว่า $id ชื่อตัวแปรนี้จะแทน ค่า {id} ซึ่งจะเขียนในชื่อตัวเป็นอย่างอื่นก็ได้ เช่น function($userId) เพราะใช้การนับตำแหน่ง กรณีนี้มีตัวแปรหนึ่งตัว จึงเป็นตำแหน่งที่หนึ่ง ถ้ามีหลายตัวแปรเช่น /user/{group}/{id} เขียนลักษณะนี้มีสองตัวแปร จึงเป็นเป็นตัวแปรในฟังก์ชัน เป็น function($g, $i) แทนตัวแปร {group} และ {id} ตามลำดับ

เราอาจสงสัยว่าทำไมต้องใช้แบบสเตติก ตลอดหรือไม่ ความจริงคือไม่จำเป็น เราสามารถสร้างออบเจ็กต์แล้วเรียกใช้งานฟังก์ชันจากคลาส โดยไม่จำเป็นต้องสร้างเป็นสเตติก แต่วิธีการส่งข้อมูลไปยังคอนโทรลเลอร์จะใช้อีกวิธีหนึ่ง และวิธีนี้ถือว่าละเว้นการส่งตัวแปรไปยังคอนโทรลเลอร์ได้ด้วย

Code 14. routes/web.php

//Controller base: object method
Route::get('/user/{id}', [UserController:: class, 'getById']);
        

จากตัวอย่างนี้ ให้สังเกตว่า มีตัวแปรใน URI แต่ไม่จำเป็นต้องใส่ตัวแปรในฟังก์ชัน getById เพราะวิธีการนี้จะถือว่าใช้ตัวแปรใน URI ได้เลย และที่สำคัญฟังก์ชันนี้ในคอนโทรลเลอร์ ไม่จำเป็นต้องใส่ค่าสเตติกอีกต่อไป (ทดสอบโดยให้คำว่า static ออกในฟังก์ชัน getById ของ UserController)

ส่งตัวแปรทางเลือกไปกับเส้นทาง

ในบางครั้งเราอาจตั้งค่าปริยายไว้ เช่น กำหนดค่า id=1 ไว้ หมายความว่าถ้าผู้ใช้งาน ไม่ใส่ค่า id ตัวแปรเส้นทางจะกำหนดให้เองคือมีค่า id=1 แต่อย่างไรก็ตาม การสร้างตัวแปรในเส้นทาง URL ต้องกำหนดเป็นเครื่องหมาย “?” หลังตัวแปร ดังตัวอย่างต่อไปไปนี้

Code 15. routes/web.php

Route::get('/user/{id?}', function($id=1){
    return UserController::getById($id);
});
        

ถึงตอนนี้เราทดสอบค่า URL:http://localhost:8000/user ก็จะได้ผลเหมือนใส่ user/1 (หากว่า เส้นทาง /user ที่เคยสร้างไว้ดังใน Code 9 ต้องลบที่ทิ้งก่อน เพราะเมื่อไม่ใส่ ค่า id จะกลับไปยังเส้นทางเดิมตามเขียนที่ Code 9)

ถ้าจะส่งตัวแปรแบบ Code 14 ด้วยก็ทำได้

Code 16. routes/web.php

Route::get('/user/{id?}', [UserController::class, 'show'],['id'=>1]);
        

สร้างข้อกำหนดให้กับตัวแปร

เมื่อตัวแปรให้ผู้ใช้เลือกใส่แล้ว เรายังสามารถสร้างข้อกำหนดให้ผู้ใช้ใส่ตัวแปรตามข้อกำหนดที่เราสร้างขึ้นได้ วิธีการสร้างใช้รูปแบบ ประพจน์รีกูล่าร์ (Regular Expression)

จากตัวอย่างข้อมูลในโมเดล User ที่ได้สร้างจำลองไว้มีเพียงแค่สามรายการ คือ รายการที่มี id เท่ากับ 1, 2, และ 3 เท่านั้น ดังนั้นจึงกำหนดให้ใส่ข้อมูลได้เพียง 1, 2, หรือ 3 นอกเหนือจากนี้จะไม่ยอมรับ หมายความว่าจะไม่มี URL ที่ไม่ยอมรับนี้

Code 17. routes/web.php

Route::get('/user/{id?}', function($id=1){
    return UserController::getById($id);
})->where('id','[1-3]');
        

จากตัวอย่างนี้ใช้ฟังก์ชัน where( ) โดยใส่สองตัวแปร ตัวแปรแรกแทน ตัวแปรของ URL และตัวแปรที่สองแทน ประพจน์รีกูล่าร์

สมมุติว่าเราใส่ URL:http://localhost:8000/user/4 ซึ่ง URL ที่ไม่ยอมรับ หน้าเว็บขี้นแสดงว่า "404 | Not found"

สร้าง Page Not Found

เมื่อเกิดหา URL ไม่พบในระบบเส้นทาง ก็จะเกิดหน้า Page Not Found เกิดขึ้น ซึ่งก็ดีแล้ว แต่บางครั้งเราอาจต้องการสร้างหน้านี้ขึ้นเอง ในรูปแบบของตนเอง

สมมุติให้ต้องการให้หน้า Page Not Fond มีหน้าตาแบบนี้

Code 18. resources/views/pageNotFound.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
        <style>
            html, body { 
            background-color: #fff;color: #636b6f; height: 100vh; margin: 0; }
            .full-height { height: 100vh;  }
            .flex-center {  
            align-items: center; display:  flex;justify-content: center; }
            .position-ref { position: relative; }
            .content {text-align: center; }
            .title { font-size: 84px;  }
            .m-b-md { margin-bottom: 30px;  }
        </style>
    </head>
    <body>
        <div class="flex-center position-ref full-height">
            <div class="content">
                <div class="title m-b-md"> Laravel </div>
                <p>Page Not Found</p>               
            </div>
        </div>
    </body>
</html>
        

แล้วกำหนดกำหนดเส้นทาง ไปยัง view ที่เป็น Page Not Found โดยใช้ฟังก์ชัน fallback( ) โดยต้องให้เส้นทางนี้อยู่เป็นเส้นทางสุดท้ายเสมอ

Code 19. routes/web.php

Route::fallback(function(){
   return view('pageNotFound');
});
        

สร้างชื่อให้เส้นทาง

เส้นทางแต่ละเส้นทางก็มีชื่ออยู่แล้วตาม URI ที่ระบุ แต่บางที เส้นทางอาจจะใช้ชื่อยาวไป ทำให้การอ้างอิงทำได้ไม่สะดวก เช่น URL: user/profile แต่จะอ้างอิงเพียงแค่ URI: profile พอแค่นี้

วิธีการคือการสร้างชื่อเส้นทางด้วยฟังก์ชัน name() ตามหลังฟังก์ชัน get() เช่น

Code 20. routes/web.php

Route::get('/user/{id?}', function($id=1){
    return UserController::getById($id);
})->name('profile');
        

ทันที่ตั้งชื่อเส้นใหม่ ก็สามารถนำไปอ้างอิงในที่อื่น ๆ ได้ เช่น เมื่อนำไปใช้งานในกรณีนี้

$url = route('profile',['id'=> 1]);

ถ้าต้องการใส่ข้อมูลเพิ่มเติมไปกับฟังก์ชัน get() ก็ใส่ดังนี้

$url = route('profile', ['id' => 1, 'photos' => 'yes']);

จะทำให้ URI: /user/1/profile?photos=yes แต่ดูเหมือนว่าตัวแปรต้องระบุอย่างนี้คงไม่ดีแน่ ซึ่งก็มีทางแก้ได้ แต่อย่างไรก็ตามต้องเขียนในรูปแบบนี้ ยกตัวอย่างเช่น ต้องการส่ง URL ตามชื่อที่สร้างใหม่ในชื่อ profile และมีตัวแปรตามที่ผู้ใช้คลิกเลือก ตามตาราง Users ดังที่เคยทำไว้ในไฟล์ users.blade.php

Code 21. resources/views/users.blade.php

@foreach($users as $user)
<tr>
    	<th scope="row">
    	@php
            $url = route('profile', ['id'=>$user['id']]); 
        @endphp
     	<a href='{{$url}}'>  {{$user['id']}} </a>
    	</th>
    	<td>{{$user['fname']}}</td>
    	<td>{{$user['lname']}}</td>
</tr>
@endforeach
        

ฟังก์ชันสร้างเส้นทางตามแบบ RESTful

การทำงานของ RESTful ต้องการคำขอบริการ HTTP ในรูปแบบต่าง ๆ ซึ่ง มีครบให้เลือกใช้แล้ว ดังเขียนเป็นฟังก์ชันของ Route คือ


Route::get($uri, $callback);  // แทนการอ่าน
Route::post($uri, $callback); // แทนการสร้างใหม่
Route::put($uri, $callback);  // แทนการสร้างแทนของเดิม
Route::patch($uri, $callback); // แทนการปรับปรุง
Route::delete($uri, $callback); // แทนการลบ
Route::options($uri, $callback); // แทนทางเลือกอื่น ที่เซิร์บเวอร์ให้ทำอะไร

$callback คือฟังก์ชันที่ทำงานกับ $uri หรือเมื่อมีการเรียก URI นี้ก็จะมีฟังก์ชัน $callback ทำงาน นอกจากนี้ในบางครั้งถ้าต้องการให้เส้นทางตอบสนองหลายคำขอ จะต้องใช้ฟังก์ชัน match( ) หรือแม้กระทั้งให้ตอบสนองทุกคำขอบริการใช้ฟังก์ชัน any( ) แทน ดังตัวอย่างต่อไปนี้


Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('/', function () {
    //
});

ตาราง 1 เมธอดของคอนโทรลเลอร์สำหรับ RESTful : กรณีใช้กับ User
กริยาคำขอ URL เมธอดในคอนโทรลเลอร์ ชื่อ คำอธิบาย
GET users index() users.index แสดงข้อมูล User ทั้งหมด
GET users/create create() users.create แสดงฟอร์มสร้าง User
POST users store() users.store สร้างข้อมูล User ขึ้นมาใหม่
GET users/{id} show() users.show อ่านข้อมูล User ตาม id
GET users/{id}/edit edit() users.edit แทนการแก้ไขข้อมูลตาม id
PUT/PATCH users/{id} update() users.update แทนการปรับปรุงข้อมูลตาม id
DELETE users/{id} destroy() users.destroy แทนการลบ User ตาม id

การใช้งาน REESful ตามปกติจะใช้งานผ่าน API ซึ่งสร้างจาก ResourceController ซึ่งจะสร้างฟังก์ชันมาตรฐานตามตาราง 1 แต่เพื่อเป็นทดสอบอย่างง่าย โดยจะใช้กับไฟล์ web.php โดยลบเส้นทางอื่นทั้งหมดก่อน หรือปิดเส้นทางอื่นไว้ก่อนก็ได้ ดังรายการสร้างทางดังนี้

Code 22. routes/web.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UserController;

//RESTfull
Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id?}', [UserController::class, 'show']);
Route::post('/users', [UserController::class, 'store']);
        

สร้างสามเส้นทางอย่างง่าย โดยทั้งสามเส้นทางนี้ ทำงานกับฟังก์ชัน index(), show(), และ store() ของ UserController โดยเส้นทางแรก อ่านรายชื่อทั้งหมด เส้นทางที่สองอ่านเฉพาะที่ระบุ id และเส้นทางสุดท้ายบันทึกเพิ่มข้อมูล แต่ในที่นี้จะเพิ่มรายการ user ลงในอาร์เรย์ $users

UserController ใหม่ก็มีฟังก์ชันที่ใช้กับ RESTful ดังเขียนเป็นฟังก์ชันที่จะคืนค่าเป็น JSON โดยไม่ต้องเขียนอะไรเพิ่ม เพราะข้อมูล $users เป็นอาร์เรย์แบบมีคีย์ เมื่อส่งออกโปรแกรมของ Laravelจะแปลงเป็น JSON อัตโนมัติ

Code 23. app/Http/Controllers/UserController.php

<?php
namespace App\Http\Controllers;

use App\Models\User;

class UserController extends Controller{
    public function index(){
         return User::all();
    }
    public function show($id){
        return User::getById($id);
    }
    public function store(){
        //ใส่โปรแกรมภายหลัง
    }
}
        

ต่อมาก็ทดสอบอ่านข้อมูลในแบบต่าง ๆ ก็พิมพ์ค่า ผ่าน URL :


        http://127.0.0.1:8000/users/1
        http://127.0.0.1:8000/users
        

สำหรับการทดสอบคำขอแบบ POST จะทดลองกับฟอร์ม ซึ่งจะอธิบายภายหลังอีกครั้ง เพราะคำขอแบบนี้จะทำให้เกิดเปลี่ยนแปลงข้อมูล จึงต้องมีข้อกำหนดด้านความปลอดภัยเพิ่มเติม

ป้องกัน CSRF

CSRF ย่อมาจาก Cross Side Request Forgery เป็นการสร้างคำขอบริการของ HTTP ที่อาจมุ่งร้ายต่อระบบเว็บ เพราะสามารถส่งคำขอโดยไม่ได้อนุญาตเข้ามาเว็บเซิร์บเวอร์ได้ ยกตัวอย่างเช่น อาจมีเว็บหนึ่งรู้ว่า URL ที่จะส่งคำขอแบบ POST เพื่อบันทึกข้อมูลได้ ดังนั้นหน้าเว็บฟอร์มที่มุ่งร้ายอาจเขียนได้ดังนี้

นอกจากนี้เมื่อมีการทำงานกับ Session หากไม่ใส่ค่า CSRF อาจมีผลกับฟอร์มที่ส่งข้อมูลซ้ำไม่ได้ เพราะอาจเกิดปัญหา SESSION Expired - 419

Code 24. D:\test_csrf.html

<form action="https://your-application.com/user/email" method="POST">
    <input type="email" value="malicious-email@example.com">
</form>

<script>
    document.forms[0].submit();
</script>
        

การทดสอบใช้ action="http://127.0.0.1:8000/users" และเก็บไฟล์ไว้ที่ไหนก็ได้ เช่น เก็บไว้ที่ D:\test_csrf.htmlแล้วเรียกไฟล์นี้โดยตรง

จากตัวอย่างนี้ เว็บเซิร์บเวอร์จำเป็นต้องป้องกันทุกการมาของคำขอ โดยเฉพาะทำขอที่จะทำให้เกิดการเปลี่ยนแปลงข้อมูลได้ ได้แก่คำขอแบบ POST, PUT, PATCH หรือ DELETE ซึ่งคำขอบริการเหล่านี้จะตรวจสอบในรหัสเข้าใช้บริการในรูปแบบ รหัส Session ที่เซิร์บเวอร์อนุญาตเท่านั้น

Laravel ใช้การสร้าง CSRF token ส่งมากับฟอร์ม โดยการใส่คำสั่งไดเร็กทีป (Directive) หรือคำสั่งฝั่งของ Laravel ใส่ร่วมไปกับฟอร์ม


<form>
	@csrf
	</form>

	หรือจะใช้ <input> แบบซ่อนการแสดงก็ได้
<form>
     <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>
        

ซึ่งถ้าไม่ใส่ รหัสโทเคนนี้ จะไม่สามารถส่งข้อคำขอแบบ POST โดยแจ้งความผิดพลาด ในรหัส 419 ตัวอย่างต่อไปนี้ใช้เป็นฟอร์ม Login โดยที่ส่งคำขอไป เส้นทาง /token

Code 25. resources/views/login.blade.php

<!doctype html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">    
    <link type="text/css" href="css/bootstrap.min.css" rel="stylesheet">
    <script src="js/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div style="width:500px; margin:20px auto">
        <h2>Login</h2>
        <form method="POST" action="/token">
        @csrf
        <div class="mb-3 row">
            <label class="col-sm-2 col-form-label">Email</label>
            <div class="col-sm-10">
                <input type="email" name="email" class="form-control" 
                id="inputEmail">
            </div>
        </div>
        <div class="mb-3 row">
            <label class="col-sm-2 col-form-label">Password</label>
            <div class="col-sm-10">
                <input type="password" name="password" class="form-control" 
                autocomplete="off">
            </div>
        </div>
        <div>            
            <button type="submit" class="btn btn-primary" 
            style="float:right">Submit</button>
        </div>
        </form>
    </div>
</body>
</html>
        

ต่อมาสร้างเส้นทาง ที่ไปยัง login และ token

Code 26. routes/web.php

use Illuminate\Http\Request;
Route::post('/token', function (Request $request) {
    $token = $request->session()->token();
    $token = csrf_token();
    echo "Token:". $token;
    echo "<br>";
    echo "Email:". $request->input('email') .
         "PASSW:". $request->input('password');
});

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

และทดสอบการทำงานของฟอร์ม Login โดยไปยัง URI: /login ก่อน กรองข้อมูลใน <input> ให้ครบแล้วส่งข้อมูลไปเซิร์บเวอร์จะได้พบการแสดงข้อมูลตามที่เขียนเส้นทางของ /token

รูป 1 แสดงผลของ Token

ตามรูปแบบของ RESTful มีการสร้างคำขอในรูปแบบคำกริยาอื่น คือ PUT, PATCH, หรือ DELETE ซึ่งคำขอเหล่านี้ไม่สนับสนุนจาก <form> โดยตรง เพื่อใช้คำเขาเหล่านี้ได้ จึงต้องสร้างตัวแปรในชื่อ _method ดังตัวอย่างต่อไปนี้


<form action="/example" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

หรือจะใช้คำสั่งฝั่งของ Laravel @method('PUT')


<form action="/example" method="POST">
        @method('PUT')
        @csrf
</form>

ก่อนหน้านี้เราได้ค้างการแก้ไข ฟังก์ชัน store สำหรับเก็บข้อมูลใหม่ของ User ตอนนี้เรามาทำให้เสร็จกัน ดูตัวอย่างต่อไปนี้ ฟังก์ชันนี้ ต้องรับข้อมูลจากฟอร์ม จึงต้องใช้ Request ร่วมทำงานด้วย เมื่อเก็บข้อมูลมาครบนำกลับมาสร้างเป็นอาร์เรย์ ในชื่อตัวแปร $user และใส่กลับไปใน Model/User.php

Code 27. app/Http/Controllers/UserController.php

use Illuminate\Http\Request;
public function store(Request $request){
        $id = count(User::$users) + 1;
        $fname = $request->input('fname');
        $lname = $request->input('lname');
        $user = array('id'=>$id, 'fname'=>$fname, 'lname'=>$lname);
        User::append($user);
        return User::$users;
}
        
Code 28. app/Models/User.php

public static function append($user){        
    array_push(self::$users, $user);        
}
        

สิ่งที่ต้องทำเพิ่มเติมสองรายการคือ

ทั้งสองอย่างนี้ ให้ใช้แนวทางการสร้างฟอร์ม และเส้นทาง ก่อนหน้านี้เป็นตัวอย่าง

ใช้งานกับ JavaScript

จากที่ผ่านมาใช้ฟอร์มส่งข้อมูลเป็นยังเซิร์บเวอร์ แต่บางกรณีใช้ภาษา JavaScirpt/jQuery ส่งข้อมูลไปยังเซิร์บเวอร์ ซึ่งก็ทำได้เช่นกัน การส่งตัวแปรของ csrf_token ก็ต้องส่งไปด้วย ซึ่งทำได้หลายวิธี

วิธีแรกคือกำหนดค่า csrf_token ไว้ที่ <meta> ใน <head>


<meta name="csrf-token" content="{{ csrf_token() }}">
        

และกำหนดค่า ajaxSetup() เป็นส่วนหนึ่งของ JavaScript


$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
  });

วิธีที่สอง สร้างตัวแปร _token ไว้กับการข้อมูลที่ส่งไปด้วยกันทีเดียว ซึ่งวิธีนี้จะดูสะดวกกว่า ยกตัวอย่างเช่น ต้องส่งข้อมูล email และ password ไปกับคำสั่ง ajax

Code 29. resources/views/login.blade.php

<script>
function submit() {           
    var email = $("#inputEmail").val();
    var passw = $("#inputPassword").val();            
    
    $.ajax({
        type: "POST",
        url: 'token'',
        data: {
            'email': email,
            'password': passw,
            _token: '{{ csrf_token() }}',
        },
        success: function(data) {
            console.log("Success:" + data);
        },
        error: function(data) {
            console.log("Error:" + data);
        },
    });}
</script>
        

ข้อมูลที่ส่งไปกับ JavaScript อยู่ในรูปแบบ JSON การรับข้อมูลของเซิร์บเวอร์ จึงอ่านข้อมูลในรูปแบบออบเจ็กต์ที่มีสมาชิกตามชื่อคีย์ของ JSON ตามตัวอย่างนี้ข้อมูล จะแสดงที่ Console.log() ไม่ได้แสดงที่หน้าเว็บ

Code 30. routes/web.php

use Illuminate\Http\Request;
Route::post('/token', function (Request $request) {
    echo "Email:". $request->email . "PASSW:". $request->'password';
});
        

การที่จะทำให้เว็บไปยังหน้าต่อไป จะต้องเขียนในภาษา JavaScript เอง เหมือนตัวอย่างที่ผ่านมา เช่น เพิ่มเติมการเปลี่ยน URI หรือใช้ URL


success: function(data) {
    console.log("Success:" + data);
    window.location.href = "/users";
}