pool13433

สร้าง API ด้วย App Script ดึงข้อมูลบน Google Sheets

บทความแนะนำการสร้าง RESTFul API โดยที่จะใช้การเก็บข้อมูลบน Google Sheets ที่ทุกคนคุณเคย แค่มี gmail account ก็สามารถสร้าง API ได้แล้ว บวกกับความรู้เรื่องการเขียน javascript พื้นฐาน เข้าใจพื้นฐานเรื่องการพัฒนาเว็บ (API) น่าจะเพียงพอในการทำ API ในบทความนี้ได้แล้ว

สร้าง Google Sheets เพื่อเป็น ฐานข้อมูล

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

  1. ให้สร้างไฟล์ google sheets ขอตั้งชื่อว่า “poolsawat.com” ชื่อไม่สำคัญ โปรแกรมจะ refer กับ sheetId ฉะนั้นชื่อจะตั้งเป็นอะไรก็ได้
  2. สร้างคอลัมน์ที่มีชื่อตามนี้ (ภาษาอังกฤษ เท่านั้น) id ,topic ,content ,author ,date ,view
  3. สำหรับข้อมูลให้ใส่ตามใจเลย

4. เริ่มเขียนโปรแกรมเลือกที่ เครื่องมือ (tools) -> โปรแกรมแก้ไขสคริป (Script editor)

5. ได้หน้า AppScript Editor เพื่อใช้เขียนโปรแกรมให้แก้ไข ตามนี้

function doGet(request) {
  // sheet id 
  var ss = SpreadsheetApp.openById("ใส่ sheet id ของคุณ");
  var sheet = ss.getActiveSheet()

  const values = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).getValues();
  // Converts data rows in json format
  const result = values.map(([a, b, c , d , e , f]) => {
    return ({ id: a, topic: b, content: c, author : d , date : e , view : f})
  })
  return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);
}

openById : ให้ระบุ sheet id ที่ได้จาก https://docs.google.com/spreadsheets/d/187hL4DXXXXXXXXXXXXXXXXXXXXXXX-p_JOGjYxU/edit#gid=0 ซึ่งของแต่ละ sheet จะไม่ซ้ำกัน

6.กด “บันทึกโครงการ” script editor จากนั้นกด “เรียกใช้” ระบบขออนุญาตการบัญชีเจ้าของข้อมูล ก็ให้เลือกบัญชีเรา

7. ทำการเริ่มทดสอบ API

8. คัดลอกเว็บแอป URL ไปวางที่ browser ได้เลย

หลัก ๆ ขั้นตอนก็ประมาณนี้ ศึกษาเพิ่มเติมได้ที่นี้ Google App Script

Javascript Zero to Hero บทนำ EP1

javascript มีอายุของภาษาก็ 25 ปีแล้ว ถือว่าเป็นภาษาที่ได้รับความนิยมมายาวนาน ด้วยความสามารถที่เขียนไครเอนซ์ไซน์ (client side) และเซอร์เวอร์ไซต์ (server side) ทำให้ตัวภาษารับความนิยม จึงทำให้บริษัทเจ้าใหญ่ๆ ตั้งทีมงานเพื่อมาพัฒนา framework เพื่อเป็นกรอบการทำงานของการพัฒนาแอพพลิเคชั่น (application) ที่เป็นมาตรฐานที่เป็น standard แบบเดียวกัน เช่น React (facebook) , Angular (google) ,Vue (Evan U) เป็นต้น สิ่งที่มีเหมือนกันของแต่ละ framework คือ fundamental ของตัวภาษา javascript เอง ชุดบทความนี้จะพาทำความรู้จักภาษา javascript ตั้งแต่เริ่ม zero จนกระทั่งถึง hero คือสามารถนำไปใช้งานและพัฒนาแอพพลิเคชั่นได้อย่างดีแน่นอน

ทำความรู้จักเครื่องมือ สิ่งที่ช่วยให้การเริ่มเขียน Javascript น่าสนุกมากยิ่งขึ้น

เครื่องมือที่เป็นเหมือนสิ่งที่แปล หรือแปลงโค๊ดของภาษา javascript มากกว่าให้สามารถทำงานได้มีหลัก ๆ อยู้ด้วยกัน 2 การคอมไพล์ (compile) ผ่านเครื่องมือเหล่านี้

  1. NodeJS Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine.
  2. Browser ต่าง ๆ ที่จะทำหน้าที่มี javascript runtime engine มาให้อยู่แล้ว เช่น Firefox , Safari ,Chrome แนะนำใช้งาน Google Chrome เพราะจะอัพเกรด version Javascript Engine ก่อน browser อื่น ๆ

console.log คืออะไร

เริ่มต้นก่อนที่จะพาไปรู้จักส่วนอื่น ๆ ในขั้นถัดๆ ไป ที่เรียกว่าขาดไม่ได้เลยคือการที่จะ debug หรือการที่จะแสดงผลลัพธ์ของตัวแปรที่เก็บอยู่ในโค๊ด การเรียกใช้งานคำสั่ง console.log(‘message or variable’)

console.log('Hello', 'World', '!')
console.log('HAPPY', 'NEW', 'YEAR', 2020)
console.log('Welcome', 'to', 'Zero', 'to', 'Hero')

การคอมเม้นท์ (comment)

การคอมเม้นท์ (comment code) คือการแทรกข้อความ หรืออื่น ๆ ที่ต้องการใส่ลงไปในไฟล์ของ โค๊ด javascript ที่ไม่ต้องการที่จะให้ javascript compile โค๊ดในส่งนี้เข้าไปในการทำงานของโปรแกรม จะไม่มีผลต่อการทำงานของโปรแกรม เพียงแต่จะมี text ข้อความแทรกอยู่ในไฟล์เท่านั้นเอง

ตัวอย่างการคอมเม้นท์ 1 บรรทัด
// This is the first comment
// This is the second comment
// I am a single line comment

ตัวอย่างการคอมเม้นท์ หลายบรรทัด
/_ This is a multiline comment
Multiline comments can take multiple lines
JavaScript is the language of the web
_/

การใช้เครื่องหมายดำเนินการทางคณิตศาสตร์

console.log(2 + 3) // Addition
console.log(3 - 2) // Subtraction
console.log(2 * 3) // Multiplication
console.log(3 / 2) // Division
console.log(3 % 2) // Modulus - finding remainder
console.log(3 ** 2) // Exponentiation 3 ** 2 == 3 * 3

แนวทางการเขียน Javascript Code ต่าง ๆ บนหน้าเว็บ

แนวทางการเขียนโค๊ด javascript กับหน้าเว็บไซต์ในรูปแบบต่าง ๆ หลัก ๆ มีด้วยกัน 4 แนวทางคือ

  • inline script การแทรก javascript ใน tag html เป็นการเขียนที่ง่ายที่สุด ตัวอย่าง
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript Zero to Hero:Inline Script</title>
  </head>
  <body>
    <button onclick="alert('Welcome to Javascript Zero to Hero!')">Click Me</button>
  </body>
</html>
  • internal script การเขียนแทรก javascript code ในไฟล์ html โดยคำสั่งต่าง ๆ จะถูกเขียนอยู่ใน tag <script> </script>
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript Zero to Hero:Internal Script</title>
    <script>
      console.log('Welcome to Javascript Zero to Hero')
    </script>
  </head>
  <body></body>
</html>
  • external script การเขียนโค๊ด javascript แยกเป็นอีก 1 ไฟล์ (นามสกุล .js) จากนั้นเรียกใช้งานเข้ามาในไฟล์ html ด้วยคำสั่ง <script src=”?”/>
    index.js
console.log('Welcome to Javascript Zero to Hero')

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Javascript Zero to Hero:External script</title>
    <script src="index.js"></script>
  </head>
  <body></body>
</html>

Javascript Data Type ต่าง ๆ

ในภาษา javascript จะเรียกเป็นรูปแบบ Dynamic Type ความหมายคือเป็นการกำหนด type ที่สามารถเปลี่ยนได้ตลอดเวลา โดยมีการกำนด type อยู่ด้วยกันหลัก ๆ ตามนี้ String, Number, Boolean, undefined, Null, and Symbol.

String

'Poolsawat'
'Apin'
'Javascript Zero to Hero'

Number

Integers: Integer (negative, zero and positive) numbers Example: ... -3, -2, -1, 0, 1, 2, 3 ...
Float-point numbers: Decimal number Example ... -3.5, -2.25, -1.0, 0.0, 1.1, 2.2, 3.5 ...

Boolean

True/true , False/false

Undefined

let name;
console.log(name) // undefined

Null

let emptyValue = null

การตรวจสอบประเภท หรือชนิดของตัวแปร

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

console.log(typeof 'Poolsawat') // string
console.log(typeof 5) // number
console.log(typeof true) // boolean
console.log(typeof null) // object type
console.log(typeof undefined) // undefined

การประกาศตัวแปร และการกำหนดขอบเขตของการเข้าถึง

ภาษา javascript จะมีการกำหนดประเภทตัวแปร โดยมีอยู่ด้วยกัน 3 รูปแบบ คือ

Var การกำหนดประเภทตัวแปรที่เป็นระดับ global ที่จาก function หรือ condition ไหน ๆ ก็สามารถเข้าถึงได้สามารถ assign value ได้

var name = 'Poolsawat' , lname = 'Apin'
var age = 99
var isMale = true

Const การกำหนดประเภทตัวแปรที่จะคล้ายกับ Var แต่จะไม่สามารถ assign value ใหม่ให้ได้ แต่จะมี scope ที่แคบกว่า var

const name = 'Poolsawat' , lname = 'Apin'
const age = 99
const isMale = true

Let การกำหนดประเภทข้อมูลในระดับที่แคบมาก ๆ สามารถ assign value ใหม่ได้ สามารถกำหนดชื่อซ้ำกันได้ แต่อยู่คนละ statement code ได้

let name = 'Poolsawat' , lname = 'Apin'
let age = 99
let isMale = true
if (age == 99 ){
    isMale = false
}

สรุปท้ายบทความ

จากบทความนี้จะเป็นการปูพื้นฐานก่อนเริ่มการเขียน javascript ในบทถัด ๆ ไปสิ่งเหล่านี้จำเป็นต้องจดจำและฝึกฝนเขียนให้จำได้ เพราะในบทความต่อ ๆ ไปจะเจอคำสั่งหรือเรื่องจากบทความนี้เยอะมาก สำหรับการทดสอบเรียกคำสั่งแบบง่ายที่สุด เพียงแต่เปิด browser chrome กด F12 เลือกไปที่ tab console จากนั้นพิมพ์ console.log(‘message ‘); กด enter ผลลัพธ์ก็จะแสดงทันที บทความถัดไปจะมาอธิบายเกี่ยวกับเรื่องอะไรคอยติดตามกันด้วยนะครับ

Laravel Upload File to local storage and saved to database with Postman upload client UI EP8

File Storage เป็นเรื่องนึงที่ Laravel จัดการมาให้เรียบร้อยพร้อมใช้งาน ประหยัดเวลานักพัฒนาไปได้มากเลยทีเดียว ไม่ว่าจะเป็นการดึงไฟล์ (Retrieving Files) ,อัพโหลดไฟล์ (Storing Files) ,ดาวห์โหลดไฟล์ (Downloading Files) ,ลบไฟล์ (Delete Files) เหล่านี้เป็นเรื่องง่ายมาก ๆ ถ้าแอพของเราพัฒนามาด้วย Laravel

บทความนี้จะพาทุกคนมาลองเขียนโค๊ดง่าย ๆ ทดสอบการอัพโหลดไฟล์ด้วยเครื่องมือ Postman client ที่รองรับการอัพโหลดไฟล์ (แทนการเขียนหน้าฟอร์มอัพโหลดเอง) ทำการอัพโหลดไฟล์รูปภาพไปที่ Laravel Project ที่เขียนโค๊ดการอัพโหลดไฟล์ไว้ ทำหน้าที่รับ request ที่ส่ง binary data ระบบจะทำการ Save File ลง local drive และ Save Data ลง Database

เริ่มโค๊ดเลยดีกว่า

สร้าง Table photos เพื่อเอาไว้เก็บข้อมูล photos ที่อัพโหลดเข้าระบบ

CREATE TABLE `photos` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `photo_origin_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `photo_new_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `photo_temp_path` varchar(255) NOT NULL,
  `photo_extension` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `photo_status` enum('active','inactive') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `photo_date` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

สร้าง Class Model Photo (Photo.php)

php artisan make:Model Photo

แก้ไขไฟล์ app\Models\Photo.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Photo extends Model
{
  use HasFactory;
  protected $table = 'photos';
  protected $fillable = [
    'id', 'photo_origin_name', 'photo_new_name',
    'photo_extension', 'photo_status', 'photo_date', 'photo_temp_path'
  ];
  public $timestamps = false;
}

สร้าง Class Controller UploadFileController (UploadFileController.php)

<?php

namespace App\Http\Controllers;

use App\Models\Photo;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class UploadFileController extends Controller
{

  public function simpleUplaod(Request $request)
  {
    try {
      if ($request->file('photo')->isValid()) {
        $path = $request->photo->path();
        $extension = $request->photo->extension();
        $clientOriginalName = $request->photo->getClientOriginalName();
        $newFileName = time() . $clientOriginalName;
        $uploadedFile = $request->file('photo');

        // Save File to local drive
        Storage::putFileAs('photos', $uploadedFile, $newFileName);

        //Save File to Photo table
        $photo = new Photo();
        $photo->photo_temp_path = $path;
        $photo->photo_origin_name = $clientOriginalName;
        $photo->photo_new_name = $newFileName;
        $photo->photo_extension = $extension;
        $photo->photo_status = 'ACTIVE';
        $photo->photo_date = Carbon::now();
        $photo->save();

        return [
          'path' => $path,
          'extension' => $extension,
          'clientOriginalName' => $clientOriginalName,
          'newFileName' => $newFileName
        ];
      }
    } catch (\Throwable $th) {
      return $th->getMessage();
    }
  }
}

แก้ไขไฟล์ routes.php เพิ่ม /upload

Route::post('upload', 'App\Http\Controllers\[email protected]');

เนื่องด้วยการ request ส่ง data ไปที่ laravel โดยต้องมีการส่งจำเป็นต้องส่ง @csrf ไม่เช่นนั้น ระบบจะ response (419 PAGE EXPIRED) ให้เพิ่ม ignore path “/upload” App\Http\Middleware\VerifyCsrfToken

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
  /**
   * The URIs that should be excluded from CSRF verification.
   *
   * @var array
   */
  protected $addHttpCookie = true;

  protected $except = [
    'upload'
  ];
}

start server

php artisan serve

เปิดโปรแกรม Postman ขึ้นมา กำหนดตามนี้ จากนั้นกด Send

POST : http://127.0.0.1:8000/upload 
Body : form-data
        KEY : photo (File)
        VALUE : Chose File

ตรวจสอบข้อมูลที่ Table photos

ตรวจสอบไฟล์ที่ storage/app/photos/1608200088upload-1.PNG

สรุปท้ายบทความ

จากที่ได้ทดสอบการสร้างระบบ upload ไม่ได้ยุ่งยากอย่างที่คิด ตัวอย่างโค๊ดเป็นเพียงการ Push File ลง local Storage ความสามารถของ Laravel กับเรื่อง File System สามารถศึกษาเพิ่มเติมได้ที่ลิ้งนี้ https://laravel.com/docs/7.x/filesystem หากผู้อ่านท่านใดลองทำตามแล้วไม่ได้ หรือ ติดปัญหาสามารถ ทักมาสอบถามกันได้ที่ https://www.facebook.com/poolsawat.apin/ สำหรับบทความนี้ขอจบเนื้อหาเพียงเท่านี้ครับ

Laravel ทำ JWT Authentication มีโค๊ดตัวอย่าง (Laravel 7.x) EP7

JWT คืออะไร บทความนี้ไม่ขอกล่าว แต่แนะนำบทความนี้ อธิบายได้ละเอียดเลย “JSON Web Token มาตรฐานใหม่ ในการทำ Authentication” บทความที่ผมจะเขียนนี้จะพาทำ workshop ง่ายๆ แทบเหมือนว่า copy code จาก web “JSON Web Token Authentication for Laravel & Lumen” นี้มาเลยก็ว่าได้

ติดตั้ง Dependencies และ Config Project

  1. ต้องทำการติดตั้ง dependency ที่ชื่อว่า tymon/jwt-auth ด้วยคำสั่ง
$composer require tymon/jwt-auth

Using version ^1.0 for tymon/jwt-auth
./composer.json has been updated
Running composer update tymon/jwt-auth
Loading composer repositories with package information
Updating dependencies
...
[32mPackage manifest generated successfully.[39m
2 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

2. รันคำสั่ง Publish the config เพื่อสร้างไฟล์ jwt.php config ไฟล์ ที่ config/jwt.php ให้อัตโนมัติเองหลังจากรันคำสั่ง

$php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Copied File [\vendor\tymon\jwt-auth\config\config.php] To [\config\jwt.php]
Publishing complete.

3. รันคำสั่ง Generate secret key เพื่อ generate jwt key ในไฟล์ .env JWT_SECRET=? เพื่อเป็น key ตั้งต้นให้การ Generate Token ทำ authenticate token หลังรันคำสั่งตรวจสอบ secret key ที่ .env

$php artisan jwt:secret

jwt-auth secret [Wwt4F********************************************UNvQ14f] set successfully.

4. แก้ไข class User.php เพิ่ม implement JWTSubject

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

5. แก้ไข gaurds driver ของ api จากเดิม passport ไปเป็น jwt
– driver => jwt (jwt auth)
– provider => users (model auth user)

'defaults' => [
    'guard' => 'api',
    'passwords' => 'users',
],

...

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

...
 'providers' => [
    'users' => [
      'driver' => 'eloquent',
      'model' => App\Models\User::class,
    ],

6. create table users

CREATE TABLE `users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `email_verified_at` timestamp NULL DEFAULT NULL,
  `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

7. เพิ่ม basic authenticate route ที่ไฟล์ routes/api.php
– ‘middleware’ => ‘api’ (ประตูดัก request , response ของ route ที่มี pattern: /api/*)
– ‘prefix’ => ‘auth’ (prefix url ex: /api/auth/login )

Route::group([

    'middleware' => 'api',
    'prefix' => 'auth'

], function ($router) {


    Route::post('register', 'App\Http\Controllers\[email protected]');
    Route::post('login', 'App\Http\Controllers\[email protected]');
    Route::post('logout', 'App\Http\Controllers\[email protected]');
    Route::post('refresh', 'App\Http\Controllers\[email protected]');
    Route::post('me', 'App\Http\Controllers\[email protected]');

});

8. สร้าง AuthController สำหรับใส่ Auth Logic (login , logout , register , info)

$php artisan make:controller AuthController
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class AuthController extends Controller
{
  /**
   * Create a new AuthController instance.
   *
   * @return void
   */
  public function __construct()
  {
    $this->middleware('auth:api', ['except' => ['login', 'register']]);
  }

  public function register(Request $request)
  {
    $user = User::create([
      'name' => $request->name,
      'email' => $request->email,
      'password' => bcrypt($request->password),
    ]);

    $token = auth()->login($user);

    return $this->respondWithToken($token);
  }

  /**
   * Get a JWT via given credentials.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function login()
  {
    $credentials = request(['email', 'password']);
    //return response()->json($credentials);
    if (!$token = auth()->attempt($credentials)) {
      return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
  }

  /**
   * Get the authenticated User.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function me()
  {
    return response()->json(auth()->user());
  }

  /**
   * Log the user out (Invalidate the token).
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function logout()
  {
    auth()->logout();

    return response()->json(['message' => 'Successfully logged out']);
  }

  /**
   * Refresh a token.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function refresh()
  {
    return $this->respondWithToken(auth()->refresh());
  }

  /**
   * Get the token array structure.
   *
   * @param  string $token
   *
   * @return \Illuminate\Http\JsonResponse
   */
  protected function respondWithToken($token)
  {
    return response()->json([
      'access_token' => $token,
      'token_type' => 'bearer',
      'expires_in' => auth()->factory()->getTTL() * 60
    ]);
  }
}

9. ทดสอบระบบ
– [POST] http://127.0.0.1:8000/api/auth/register (ระบบจะสร้าง user ใน table users )
– [POST] http://127.0.0.1:8000/api/auth/login (ระบบเช็ค user ใน table users)
– [POST] http://127.0.0.1:8000/api/auth/me (ระบบแสดง user จาก bearer token )
– [POST] http://127.0.0.1:8000/api/auth/logout (ระบบ expired token ออกจากระบบ)

http://127.0.0.1:8000/api/auth/register
http://127.0.0.1:8000/api/auth/login
http://127.0.0.1:8000/api/auth/me (Bearer Token ด้วย)
http://127.0.0.1:8000/api/auth/logout (Bearer Token ด้วย)

สรุปท้ายบทความ

JWT เป็นเพียงการ Encode Data ที่จะกอบไปด้วย 3 ส่วนหลัก ๆ คือ Headers (เข้ารหัสแบบไหนอยู่ (เช่น SHA256, RSA)) ,Payload เป็นข้อมูลจริง ,Signature เอาไว้เช็คกับ Payload เป็นเพียงมาตรฐานเปิด (RFC 7519) ที่ช่วยเข้ามาแก้ไขโดยมีข้อดีหลัก ๆ 2 ข้อ คือขนาดไม่ใหญ่มาก ,ลักษณะการเก็บข้อมูลในตัวเอง

หากผู้อ่านท่านใดสงสัยหรืออยากที่จะแนะนำบทความ สามารถคอมเม้นทิ้งท้ายไว้ได้เลยครับ ^^

Spring Boot สร้าง RESTful API พร้อม Unit Tests แบบเข้าใจง่าย และรวดเร็ว EP2

การพัฒนาแอพพลิเคชั่น ข้อแนะนำคือควรทำ Unit Tests มองการพัฒนาในระยะยาว หากต้องมีการแก้ไข issue เล็กน้อย แต่ไม่แน่ใจว่าจะเกิด defect กับระบบเดิมหรือไม่ การมี unit tests ช่วยประหยัดเวลาการ regression tests ได้เยอะมาก และคุณจะเห็นประโยชน์ของการทำ Unit tests อย่างแน่นอน

รู้จัก @RestController annotation สร้าง routes RESTful API

annotation @RestController implement มาจาก @Controller ของ spring web เพื่อใช้งานสำหรับการสร้าง RESTful API (ไม่ใช่ static view) เพิ่มความสะดวกในการพัฒนา RESTful API ให้ง่ายยิ่งขึ้น

ตัวอย่างการสร้าง Route (text/plain)

package com.poolsawat.starter.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
	
	@RequestMapping("/")
	public String index() {
		return "Greetings from Spring Boot!";
	}
	
}

ทดสอบ http://localhost:8080/

Greetings from Spring Boot!

เพิ่ม Route (application/json)

... 
	@RequestMapping("/domain")
	public String[] json() {		
		return new String[] {"www",".","poolsawat",".","com"};
	}
...

restart server จากนั้น ทดสอบ http://localhost:8080/domain

[
  "www",
  ".",
  "poolsawat",
  ".",
  "com"
]

เพิ่ม Test Dependencies ใน POM

ก่อนที่จะเริ่มเขียน Unit Tests จำเป็นต้องเพิ่ม “spring-boot-starter-test” เข้าไปที่ไฟล์ POM (ไฟล์ pom.xml) มาเพิ่มกันเลย

...
<dependencies>
	...
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	...
</dependencies>
...

สร้าง src/test/java/com/poolsawat/starter/controller/HelloControllerTest.java

package com.poolsawat.starter.controller;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
	@Autowired
	private MockMvc mvc;

	@Test
	public void getHello() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().string(equalTo("Greetings from Spring Boot!")));
	}
	@Test
	public void getDomain() throws Exception {
		mvc.perform(MockMvcRequestBuilders.get("/domain")
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andExpect(content().json("[\"www\",\".\",\"poolsawat\",\".\",\"com\"]"));
	}
}

อธิบายการทำงาน

  • @SpringBootTest inject annotation Test เพื่อเริมการ Test
  • @AutoConfigureMockMvc inject annotation AutoConfiMockMVC (MockMVC มีหน้าที่สำหรับทำ Unit Tests มี feature mockup data)
  • @Autowired เป็นการ auto inject MockMVC bean ก่อนการจะเรียกใช้งานใน Testcase
  • @Test ระบุที method บอกให้รู้ว่า รันเคส method นี้
  • mvn.perform mockup route ที่ต้องการทดสอบ ระบุ MockMvcRequestBuilders.get(“/”) หมายถึง (“localhost:8080/”) ตรวจสอบ content-type คือต้องเป็น “MediaType.APPLICATION_JSON” กำหนดให้เป็น application/json เท่านั้น
  • andExpect
    • .andExpect(status().isOk()) expect htto status code OK (status code 200) หากไม่ใช่จะ fail ทันที
    • .andExpect(content().string(equalTo(“Greetings from Spring Boot!”))) expect content body ต้องเป็นคำว่า “Greetings from Spring Boot!” หากไม่ใช่จะ fail ทันที

จากนั้นสร้าง maven test build

apply -> run

ตรวจสอบที่ console panel

[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.poolsawat.starter.controller.HelloControllerTest
...
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.606 s - in com.poolsawat.starter.controller.HelloControllerTest
2020-10-15 10:57:54.262  INFO 5864 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.231 s
[INFO] Finished at: 2020-10-15T10:57:54+07:00
[INFO] Final Memory: 18M/220M
[INFO] ------------------------------------------------------------------------

ในทุกครั้งที่มีการแก้ไข java code จำเป็นต้องมีการ restart web server spring boot มี hot reload (การ auto restart web server เมื่อมี code change)

เพิ่ม “spring-boot-devtools”

...
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
	<optional>true</optional>
</dependency>
...

จากนั้นทำการ restart อีกครั้ง ต่อไปเวลาแก้ไข code ก็จะไม่ต้อง restart เองแล้ว

สรุปท้ายบทความ

การพัฒนาแอพพลิเคชั่นที่มี Test ถือว่าเป็นแอพพลิเคชั่นที่ดี ข้อดีของการทำ Test มีเยอะมากช่วยลดเวลาการ Regression Test ได้เยอะ ถ้าแอพพลิเคชั่นของคุณยังไม่มี test เริ่มทำได้แล้ว เพราะถ้าไม่ทำคุณจะคุยกับเขา (ทีมพัฒนา แอพพลิเคชั่นที่มี test) ไม่รู้เรื่อง บทความต่อไปจะเป็นเรื่องเกี่ยวกับอะไร คอยติดตามกันนะครับ

Spring Boot ทำความรู้จัก เริ่มติดตั้งและเริ่มใช้งานเบื้องต้น EP1

Spring boot คืออะไร

spring boot เป็น framework package ที่ต่อยอดมาจาก spring framework ช่วยให้เราสามารสร้างแอพพลิเคชั่น (application) แบบ stand-alone แบบรวดเร็ว และง่ายดาย

บ่อยครั้งที่ต้องมานั่งปวดหัวกับเรื่องการ configuration project ในขั้นตอนการ setup เริ่มต้นในการสร้างแอพพลิเคชั่น ปัญหาเหล่านี้จะถูกทำให้เสร็จเรียบร้อยแล้ว เมื่อมาใช้งาน spring boot

Features ต่าง ๆ ที่มีมาใน Spring boot

  • การสร้างแอพพลิเคชั่นแบบ stand-alone
  • embed Tomcat ,Jetty web server ยอดนิยมมาให้เรียบร้อยแล้ว หรือแม้แต่ Underflow ก็มีมาให้แล้ว
  • มีการกำหนด dependencies starter (POM) สำหรับพัฒนาไม่ต้องเพิ่มหลาย dependencies เหมือน spring framework แบบแต่ก่อน
  • มี auto configuration มาให้พร้อม ไม่ต้องยุ่งวุ้นวายกับการ config project เหมือนแต่ก่อน (ดีม๊วก ๆ )
  • ให้ feature monitoring ,metrics, health checks มาพร้อมแล้วเรียกใช้งานได้เลย
  • ไม่มีการสร้าง XML configuration file อีกต่อไปเพราะทุกอย่างจะ code บน java (kotlin) ทั้งหมดเลย

จะเริ่มเขียน Spring boot ต้องเตรียมตัวอย่างไรบ้าง

สำหรับมือใหม่ หลาย ๆ คนที่กำลังจะเรียนรู้ spring framework สั่งที่ต้องทราบก่อนที่จะเริ่มพัฒนาแอพพลิเคชั่นด้วย Java Framework อย่าง Spring Boot ต้องรู้เกี่ยวกับอะไร จะอธิบายย่อย ๆ ตามนี้

  • IDE (Integrated Development Environment) เครื่องมือสำหรับพัฒนาแอพพลิเคชั่น ที่ดัง ๆ สำหรับพัฒนา Java ก็พวก Eclipse ,IntelliJ IDEA เป็นต้น
  • Maven (Project Management Tools) สำหรับ Java Developer ทำงานได้ง่ายยิ่งขึ้น การที่จะต้องคอยหา JAR หรือบางครั้งเราจะเรียกว่า lib (dependencies) ที่มีมากมายซะเหลือเกิน อีกทั้งยังต้องมานั่งตรวจสอบเรื่องเวอชั่นของ JAR อีกละก็ ไม่สนุกแน่ ๆ สำหรับ Developer Java ปัญหาเหล่านี้จะถูกแก้ไขด้วย Maven อีกทั้งยังมีความสามารถในการทำกระบวนการ Build ,Compile , Test ,Deploy ,Documentation ได้อีกมากมาย อีกทั้งยังมี plugins ให้เรียกใช้งานอีกเพียบเลย ถือว่าดีม๊วก ๆ
  • Spring IO หน่วยงานพัฒนา Java Framework ที่โด่งดัง และยังคงได้รับความนิยมจนถึงยุคปัจจุบันด้วยความที่ไม่หยุดพัฒนา จึงทำให้ framework ได้รับความนิยมอยู่จนถึงปัจจุบัน

เริ่มติดตั้ง และสร้าง Hello World กัน

การ initializr Spring Boot Project มีด้วยกันหลายวิธี ผมขอยกตัวอย่างวิธีนี้

  1. เข้าไป generate spring boot project ที่ https://start.spring.io/
    1. Project ให้เลือก Maven Project (Gradle Project) เป็นเครื่องมืออีกแบบ ฝั่ง mobile dev ชอบใช้กัน เพราะมีมาใน android project)
    2. Language ให้เลือก Java (แล้วแต่ความชำนาญ ของผู้พัฒนาจะเลือกเป็น Kotlin หรือ Groovy ก็ได้)
    3. Spring Boot ให้เลือก version 2.3.4 เป็นเวอร์ชั่น ที่ stable แล้ว
    4. Project Metadata
      1. Group ใส่เป็น com.poolsawat (กำหนดเป็น domain ขององค์กร)
      2. Artifact ใส่เป็น Poolsawat (ชื่อสิ่งประดิษฐ์กำหนดเป็นชื่อของแอพพลิเคชั่น )
      3. Name ใส่เป็น Poolsawat (ชื่อแอพพลิเคชั่น)
      4. Description ใส่ Demo project for Spring Boot หรือจะใส่อธิบาย destination ของแอพพลิเคชั่น สั้น ๆ
      5. Package name ใส่ com.poolsawat.starter ใส่เป็น java package สำหรับเก็บ source code จะกำหนดภายใต้ src/com/poolsawat/starter/**
      6. Packaging เลือก Jar เพราะจะเลือกใช้ stand-lone application ที่รันด้วย embed Tomcat web server
      7. Java เลือก 8 (จะเลือก 10 , 15 แล้วแต่ JDK ในเครื่องที่ติดตั้งไว้)
      8. Dependencies เลือก “web”
      9. กด “GENERATE” downloadลงเครื่อง จะได้ไฟล์ .zip มา รอทำในขั้นถัดไป

2. เปิด IDE ตัวอย่างจะใช้ Eclipse IDE
        1. Unzip Poolsawat.zip ย้ายไฟล์ทั้งหมดไปยัง workspace ของ Eclipse ที่สร้างไว้
        2. หน้า Eclipse เลือก File -> Import… -> Maven -> Poolsawat

        3. จะมี Poolsawat เข้ามาที่ Project Explorer ทางซ้ายมือของ Eclipse IDE

3. แก้ไข Code ไฟล์ PoolsawatApplication.java ตามนี้

package com.poolsawat.starter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class PoolsawatApplication {

	public static void main(String[] args) {
		SpringApplication.run(PoolsawatApplication.class, args);
	}

	@GetMapping("/hello")
	public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
		return String.format("Hello %s!", name);
	}

}

4. สร้าง Maven build เลือกเมนู Run -> Run Configurations… -> Maven Build -> New Launch configuration -> ใส่ค่าตามภาพนี้

Apply -> Run

5. รอ… maven download dependencies สักครู่…..

...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.4.RELEASE)

2020-10-15 00:06:06.708  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : Starting PoolsawatApplication on PoolPC-PC with PID 4832 (D:\workspace-blog\Poolsawat\target\classes started by PoolPC in D:\workspace-blog\Poolsawat)
2020-10-15 00:06:06.711  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : No active profile set, falling back to default profiles: default
2020-10-15 00:06:07.773  INFO 4832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-10-15 00:06:07.781  INFO 4832 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-10-15 00:06:07.782  INFO 4832 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.38]
2020-10-15 00:06:07.848  INFO 4832 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-10-15 00:06:07.848  INFO 4832 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1078 ms
2020-10-15 00:06:08.018  INFO 4832 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-15 00:06:08.174  INFO 4832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-10-15 00:06:08.191  INFO 4832 --- [           main] c.p.starter.PoolsawatApplication         : Started PoolsawatApplication in 1.823 seconds (JVM running for 2.231)

6. ทดสอบเข้าลิ้ง http://localhost:8080/hello

สรุปท้ายบทความ

การพัฒนาเว็บด้วย Spring boot นั้นทำได้ไม่ยากเลย ค่อนข้างง่ายกว่าวิธีการเดิมของ Spring Framework ที่ผ่าน ๆ มา ไม่ต้อง configuration ให้ยุ่งยาก ไม่ต้องลำคาญกับการต้องมา control version ของ dependencies ที่มากมายซะเหลือเกิน เลยทำให้ผมชอบที่จะใช้งาน spring boot กับทุก ๆ project ของผมเลยครับ บทความหน้าจะเป็นเรื่องอะไร คอยติดตามกันนะครับ

Laravel connect MySQL Database ทำ RESTful API CRUD [เพิ่ม(create) ,อ่าน(read) , แก้ไข(update), ลบ(delete)] EP6

Laravel Series เดินทางมาถึง EP6 ซึ่งถ้าใครยังไม่ได้อ่านบทความ EP 1 – 5 สามารถเข้าได้ที่ลิ้งใต้ล่างนี้

  1. Laravel เริ่มติดตั้งและสร้าง route ง่าย ๆ EP1
  2. Laravel แนะนำโครงสร้างภายในโปรเจค อธิบายแต่ละส่วนการทำงาน EP2
  3. Laravel ใช้งาน views blade template engine EP3
  4. Laravel รู้จัก Routes และ การรับค่าจาก HttpRequest EP4
  5. Laravel สร้าง RESTful API ง่าย ๆ ด้วย Resource Routes Controller EP5

บทความนี้จะมาทำให้ application ของเราต่อกับฐานข้อมูล (database) ของ MySQL และเมื่อต่อได้แล้วจะมาใช้งาน migrate และมาทำระบบ CRUD ของข้อมูล Photos กัน

สร้าง Schema และ Migrate Table ด้วย artisan migrate

  1. สร้าง schema ด้วยคำสั่ง
CREATE SCHEMA `db_poolsawat` DEFAULT CHARACTER SET utf8 ;

2. แก้ไขไฟล์ .env

...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=db_poolsawat
DB_USERNAME=root
[email protected]
...

3. ทดสอบการต่อ connection ทำตามขั้นตอนข้างล่าง ถ้าขึ้นแบบนี้แสดงว่าต่อสำเร็จ

$ php artisan tink
// Psy Shell v0.10.4 (PHP 7.3.7 — cli) by Justin Hileman

$ DB::connection()->getPdo();
/* 
PDO {#4182
     inTransaction: false,
     attributes: {
       CASE: NATURAL,
       ERRMODE: EXCEPTION,
       AUTOCOMMIT: 1,
       PERSISTENT: false,
       DRIVER_NAME: "mysql",
       SERVER_INFO: "Uptime: 433  Threads: 4  Questions: 152  Slow queries: 0  Opens: 209  Flush tables: 3  Open tables: 130  Queries per second avg: 0.351",
       ORACLE_NULLS: NATURAL,
       CLIENT_VERSION: "mysqlnd 5.0.12-dev - 20150407 - $Id: 7cc7cc96e675f6d72e5cf0f267f48e167c2abb23 $",
       SERVER_VERSION: "8.0.21",
       STATEMENT_CLASS: [
         "PDOStatement",
       ],
       EMULATE_PREPARES: 0,
       CONNECTION_STATUS: "127.0.0.1 via TCP/IP",
       DEFAULT_FETCH_MODE: BOTH,
     },
   }
*/

4. generate migrations file ด้วยคำสั่งเหล่านี้

php artisan make:migration create_photos_table

4.1 ตรวจสอบไฟล์ที่ถูกสร้างขึ้นมาที่ database/migrations/*_create_photos_table.php (* คือวันที่ generate file) จากนั้นเปิดไฟล์ขึ้นมาแก้ไข ตามนี้

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePhotosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('photos', function (Blueprint $table) {
            $table->id();
            $table->string('photo_name');
            $table->integer('photo_size');
            $table->string('photo_url');
            $table->enum('photo_status', ['active', 'inactive']);
            $table->dateTime('photo_date', 0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('photos');
    }
}

ในการกำหนด datatype ของ column ตารางมีรูปแบบที่หลากหลาย มี function รองรับอยู่แล้ว สำหรับการกำหนดสร้าง สามารถดูเพิ่มเติมได้จากตารางนี้

Available Column Types

The schema builder contains a variety of column types that you may specify when building your tables:

CommandDescription
$table->id();Alias of $table->bigIncrements('id').
$table->foreignId('user_id');Alias of $table->unsignedBigInteger('user_id').
$table->bigIncrements('id');Auto-incrementing UNSIGNED BIGINT (primary key) equivalent column.
$table->bigInteger('votes');BIGINT equivalent column.
$table->binary('data');BLOB equivalent column.
$table->boolean('confirmed');BOOLEAN equivalent column.
$table->char('name', 100);CHAR equivalent column with a length.
$table->date('created_at');DATE equivalent column.
$table->dateTime('created_at', 0);DATETIME equivalent column with precision (total digits).
$table->dateTimeTz('created_at', 0);DATETIME (with timezone) equivalent column with precision (total digits).
$table->decimal('amount', 8, 2);DECIMAL equivalent column with precision (total digits) and scale (decimal digits).
$table->double('amount', 8, 2);DOUBLE equivalent column with precision (total digits) and scale (decimal digits).
$table->enum('level', ['easy', 'hard']);ENUM equivalent column.
$table->float('amount', 8, 2);FLOAT equivalent column with a precision (total digits) and scale (decimal digits).
$table->geometry('positions');GEOMETRY equivalent column.
$table->geometryCollection('positions');GEOMETRYCOLLECTION equivalent column.
$table->increments('id');Auto-incrementing UNSIGNED INTEGER (primary key) equivalent column.
$table->integer('votes');INTEGER equivalent column.
$table->ipAddress('visitor');IP address equivalent column.
$table->json('options');JSON equivalent column.
$table->jsonb('options');JSONB equivalent column.
$table->lineString('positions');LINESTRING equivalent column.
$table->longText('description');LONGTEXT equivalent column.
$table->macAddress('device');MAC address equivalent column.
$table->mediumIncrements('id');Auto-incrementing UNSIGNED MEDIUMINT (primary key) equivalent column.
$table->mediumInteger('votes');MEDIUMINT equivalent column.
$table->mediumText('description');MEDIUMTEXT equivalent column.
$table->morphs('taggable');Adds taggable_id UNSIGNED BIGINT and taggable_type VARCHAR equivalent columns.
$table->uuidMorphs('taggable');Adds taggable_id CHAR(36) and taggable_type VARCHAR(255) UUID equivalent columns.
$table->multiLineString('positions');MULTILINESTRING equivalent column.
$table->multiPoint('positions');MULTIPOINT equivalent column.
$table->multiPolygon('positions');MULTIPOLYGON equivalent column.
$table->nullableMorphs('taggable');Adds nullable versions of morphs() columns.
$table->nullableUuidMorphs('taggable');Adds nullable versions of uuidMorphs() columns.
$table->nullableTimestamps(0);Alias of timestamps() method.
$table->point('position');POINT equivalent column.
$table->polygon('positions');POLYGON equivalent column.
$table->rememberToken();Adds a nullable remember_token VARCHAR(100) equivalent column.
$table->set('flavors', ['strawberry', 'vanilla']);SET equivalent column.
$table->smallIncrements('id');Auto-incrementing UNSIGNED SMALLINT (primary key) equivalent column.
$table->smallInteger('votes');SMALLINT equivalent column.
$table->softDeletes('deleted_at', 0);Adds a nullable deleted_at TIMESTAMP equivalent column for soft deletes with precision (total digits).
$table->softDeletesTz('deleted_at', 0);Adds a nullable deleted_at TIMESTAMP (with timezone) equivalent column for soft deletes with precision (total digits).
$table->string('name', 100);VARCHAR equivalent column with a length.
$table->text('description');TEXT equivalent column.
$table->time('sunrise', 0);TIME equivalent column with precision (total digits).
$table->timeTz('sunrise', 0);TIME (with timezone) equivalent column with precision (total digits).
$table->timestamp('added_on', 0);TIMESTAMP equivalent column with precision (total digits).
$table->timestampTz('added_on', 0);TIMESTAMP (with timezone) equivalent column with precision (total digits).
$table->timestamps(0);Adds nullable created_at and updated_at TIMESTAMP equivalent columns with precision (total digits).
$table->timestampsTz(0);Adds nullable created_at and updated_at TIMESTAMP (with timezone) equivalent columns with precision (total digits).
$table->tinyIncrements('id');Auto-incrementing UNSIGNED TINYINT (primary key) equivalent column.
$table->tinyInteger('votes');TINYINT equivalent column.
$table->unsignedBigInteger('votes');UNSIGNED BIGINT equivalent column.
$table->unsignedDecimal('amount', 8, 2);UNSIGNED DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).
$table->unsignedInteger('votes');UNSIGNED INTEGER equivalent column.
$table->unsignedMediumInteger('votes');UNSIGNED MEDIUMINT equivalent column.
$table->unsignedSmallInteger('votes');UNSIGNED SMALLINT equivalent column.
$table->unsignedTinyInteger('votes');UNSIGNED TINYINT equivalent column.
$table->uuid('id');UUID equivalent column.
$table->year('birth_year');YEAR equivalent column.
https://laravel.com/docs/8.x/migrations

4.2 หลังจากที่แก้ไขไฟล์ *_create_photos_table.php เป็นที่เรียบร้อยแล้ว ให้ รันคำสั่งต่อไปนี้เพิ่มเพิ่ม migrate table (artisan จะทำการไป create table ให้ที่ database )

php artisan migrate

เปรียบเหมือนว่าเรากำลังใช้คำสั่งชุดนี้เพื่อสร้างตาราง

CREATE TABLE `photos` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `photo_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `photo_size` int NOT NULL,
  `photo_url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `photo_status` enum('active','inactive') COLLATE utf8mb4_unicode_ci NOT NULL,
  `photo_date` datetime NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

5. สร้าง Model class ด้วย artisan cli

php artisan make:model Photo

ตรวจสอบไฟล์ที่ app/Models/Photo.php

5.1 แก้ไขไฟล์ app/Models/Photo.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Photo extends Model
{
  use HasFactory;
  protected $fillable = [
    'created_at', 'id', 'photo_date', 'photo_name', 'photo_size',
    'photo_status', 'photo_url', 'updated_at'
  ];
}

6. สร้าง Controller class ด้วย artisan cli

php artisan make:controller PhotoController --resource

ตรวจสอบไฟล์ที่ app/Http/Controller/PhotoController.php

6.1 แก้ไขไฟล์ app/Http/Controller/PhotoController.php ตามข้างล่างนี้ ส่วนนี้จะได้มี function RESTful API template ที่ได้จากการ artisan cli แล้ว บทความ “Laravel สร้าง RESTful API ง่าย ๆ ด้วย Resource Routes Controller EP5” ได้อธิบายไว้บ้างแล้ว บทความนี้จะขอข้ามส่วนนี้ไป

<?php

namespace App\Http\Controllers;

use App\Models\Models\Photo as ModelsPhoto;
use App\Models\Photo;
use Carbon\Carbon;
use Illuminate\Http\Request;

class PhotoController extends Controller
{
  /**
   * Display a listing of the resource.
   *
   * @return \Illuminate\Http\Response
   */
  public function index()
  {
    return response()->json(['name' => 'index', 'photos' => Photo::all()]);
  }

  /**
   * Show the form for creating a new resource.
   *
   * @return \Illuminate\Http\Response
   */
  public function create()
  {
    $photo = new Photo();
    $photo->photo_name = 'laravel';
    $photo->photo_size = 1024;
    $photo->photo_url = 'https://laravel.com/img/logotype.min.svg';
    $photo->photo_status = 'active';
    $photo->photo_date = Carbon::now();
    return response()->json(['name' => 'create', 'status' => $photo->save()]);
  }

  /**
   * Store a newly created resource in storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @return \Illuminate\Http\Response
   */
  public function store(Request $request)
  {
    $photo = new Photo($request->all());
    $photo->photo_date = Carbon::now();
    return response()->json(['name' => 'store', 'payload' => $request->all(), 'status' => $photo->save()]);
  }

  /**
   * Display the specified resource.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function show($id)
  {
    $photo = Photo::find($id);
    return response()->json(['name' => 'show', 'id' => $id, 'photo' => $photo]);
  }

  /**
   * Show the form for editing the specified resource.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function edit($id)
  {
    $photo = Photo::find($id);
    return response()->json(['name' => 'edit', 'id' => $id, 'photo' => $photo]);
  }

  /**
   * Update the specified resource in storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function update(Request $request, $id)
  {
    $photo = Photo::find($id);
    $photo->photo_name = $request->photo_name;
    $photo->photo_size = $request->photo_size;
    $photo->photo_url = $request->photo_url;
    $photo->photo_status = $request->photo_status;
    $photo->photo_date = Carbon::now();
    return response()->json(['name' => 'update', 'status' => $photo->save(),  'payload' => $request->all(), 'id' => $id]);
  }

  /**
   * Remove the specified resource from storage.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function destroy($id)
  {
    $photo = Photo::find($id);
    return response()->json(['name' => 'destroy', 'status' => $photo->delete(), 'id' => $id]);
  }
}

7. แก้ไขไฟล์ routes/api.php เพิ่มโค๊ด 1 บรรทัด

...
Route::resource('photos', 'App\Http\Controllers\PhotoController');
...

8. ทดสอบ RESTful API

ก่อนที่จะทดสอบติดตั้ง npm i -g json เพื่อ pritty json response เพื่อความสวยงาม

$ curl localhost:8000/api/photos | json

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    28    0    28    0     0     65      0 --:--:-- --:--:-- --:--:--    65
{
  "name": "index",
  "photos": []
}
/* fetch photos all */
$ curl localhost:8000/api/photos/create | json

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    31    0    31    0     0     73      0 --:--:-- --:--:-- --:--:--    73
{
  "name": "create",
  "status": true
}
/* create photo */
$ curl -X POST -H "Content-Type: application/json" -d "{\"photo_name\":\"laravel\",\"photo_size\":1024,\"photo_url\":\"https://laravel.com/img/logotype.min.svg\",\"photo_status\":\"active\",\"photo_date\":\"2020-10-06 10:24:01\"}" http://127.0.0.1:8000/api/photos | json

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   357    0   201  100   156   1116    866 --:--:-- --:--:-- --:--:--  1994
{
  "name": "store",
  "payload": {
    "photo_name": "laravel",
    "photo_size": 1024,
    "photo_url": "https://laravel.com/img/logotype.min.svg",
    "photo_status": "active",
    "photo_date": "2020-10-06 10:24:01"
  },
  "status": true
}
/* create photo with payload */
$ curl localhost:8000/api/photos/1 | json

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   286    0   286    0     0    722      0 --:--:-- --:--:-- --:--:--   722
{
  "name": "show",
  "id": "1",
  "photo": {
    "id": 1,
    "photo_name": "laravel",
    "photo_size": 1024,
    "photo_url": "https://laravel.com/img/logotype.min.svg",
    "photo_status": "active",
    "photo_date": "2020-10-06 10:32:24",
    "created_at": "2020-10-06T10:32:24.000000Z",
    "updated_at": "2020-10-06T10:32:24.000000Z"
  }
}
/* fetch photo by id */
$ curl localhost:8000/api/photos/1/edit | json

 % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   286    0   286    0     0    709      0 --:--:-- --:--:-- --:--:--   709
{
  "name": "edit",
  "id": "1",
  "photo": {
    "id": 1,
    "photo_name": "laravel",
    "photo_size": 1024,
    "photo_url": "https://laravel.com/img/logotype.min.svg",
    "photo_status": "active",
    "photo_date": "2020-10-06 10:32:24",
    "created_at": "2020-10-06T10:32:24.000000Z",
    "updated_at": "2020-10-06T10:32:24.000000Z"
  }
}
/* fetch photo by id for edit */
$ curl -X PATCH -H "Content-Type: application/json" -d "{\"id\":1,\"photo_name\":\"laravel patch\",\"photo_size\":2048,\"photo_url\":\"https://laravel.com/img/logotype.min.svg\",\"photo_status\":\"active\",\"photo_date\":\"2020-10-06 10:24:01\"}" http://127.0.0.1:8000/api/photos/1 | json

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   393    0   224  100   169   1108    836 --:--:-- --:--:-- --:--:--  1945
{
  "name": "update",
  "status": true,
  "payload": {
    "id": 1,
    "photo_name": "laravel patch",
    "photo_size": 2048,
    "photo_url": "https://laravel.com/img/logotype.min.svg",
    "photo_status": "active",
    "photo_date": "2020-10-06 10:24:01"
  },
  "id": "1"
}
/* patch photo with payload */

$ curl localhost:8000/api/photos/1 | json
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   292    0   292    0     0    707      0 --:--:-- --:--:-- --:--:--   705
{
  "name": "show",
  "id": "1",
  "photo": {
    "id": 1,
    "photo_name": "laravel patch",
    "photo_size": 2048,
    "photo_url": "https://laravel.com/img/logotype.min.svg",
    "photo_status": "active",
    "photo_date": "2020-10-06 10:36:22",
    "created_at": "2020-10-06T10:32:24.000000Z",
    "updated_at": "2020-10-06T10:36:22.000000Z"
  }
}
curl -X DELETE http://127.0.0.1:8000/api/photos/1 | json

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    41    0    41    0     0    221      0 --:--:-- --:--:-- --:--:--   221
{
  "name": "destroy",
  "status": true,
  "id": "1"
}
/* delete photo by id */

สรุปท้ายบทความ

บทความนี้ต่อยอดความสามารถของบทความ “Laravel สร้าง RESTful API ง่าย ๆ ด้วย Resource Routes Controller EP5” ที่ RESTful template แต่บทความนี้จะสอนเพิ่มเติมการต่อ MySQL เพื่อ connect database ใช้ข้อมูลภายใน เพิ่มเติมด้วยความสามารถของ migration ที่จะ create table schema ให้เองตาม code CreatePhotosTable ที่ได้ทำการสร้างไว้จาก code PHP

บทความหน้าจะมาพูดถึงเรื่องอะไรค่อยติดตามกันนะครับ ขอบคุณที่ติดตามครับ

Laravel สร้าง RESTful API ง่าย ๆ ด้วย Resource Routes Controller EP5

รูปแบบลักษณะงานเว็บไซต์ในปัจจุบัน เกิน 80 – 90 % ต้องมีการทำระบบ RESTful API เพื่อให้บริการข้อมูลกับ Application platform รูปแบบต่าง ๆ ไม่ว่าจะเป็น mobile และ web เพราะเป็นรูปแบบการพัฒนาที่ได้รับความนิยมมากในปัจจุบัน กล่าวคือการเชื่อมโยง application ด้วยข้อมูลจากแหล่งเดียวกัน นั้นเอง

การพัฒนา RESTful API Service สามารถทำได้กับหลาย ๆ ภาษาโปรแกรมมิ่งทั่วไป (ถ้าภาษาโปรแกรมมิ่งมี feature http ต้องสามารถทำ RESTful API ได้อย่างแน่นอน) แต่สิ่งที่จะทำให้การพัฒนา RESTful นั้นง่ายและสะดวกยิ่งขึ้นก็คือเครื่องมือ หรือ cli (command line user interface ) สำหรับช่วยทำให้การเขียนโปรแกรมของเราง่าย และสะดวกมากยิ่งขึ้นไปอีก

laravel มี artisan cli (symfony class console) ช่วยทำให้งาน develop สะดวกและง่ายยิ่งขึ้น บทความนี้จะพามาทำความรู้จัก cli นี้กัน

สร้าง Resource Controller RESTful API แรกกัน

laravel มี feature Resource Controllers ที่จะช่วยสร้าง RESTful function template controller ขึ้นมาให้ เราเพียงแค่ implement function ตาม RESTful action เท่านั้นก็สามารถเรียกใช้งานได้เลย โดยมีวิธีการเรียกใช้งานตามตัวอย่าง ข้างล่างนี้

$ php artisan make:controller PhotoController --resource

laravel สร้าง generate /app/Http/Controllers/PhotoController.php ภายในไฟล์จะมีการสร้าง function RESTful template มาให้

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PhotoController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
    }
}

มาทำความรู้จักการทำงานแต่ละ function มามีการทำงานอย่างไร ตามการอธิบายจากตารางนี้

Actions Handled By Resource Controller

VerbURIActionRoute Name
GET/photosindexphotos.index
GET/photos/createcreatephotos.create
POST/photosstorephotos.store
GET/photos/{photo}showphotos.show
GET/photos/{photo}/editeditphotos.edit
PUT/PATCH/photos/{photo}updatephotos.update
DELETE/photos/{photo}destroyphotos.destroy

เพิ่ม routes mapping ที่ไฟล์ /routes/api.php

Route::resource('photos', 'App\Http\Controllers\PhotoController');

เพิ่ม route:resource เป็น static function สำหรับการทำ RESTful routes เพิ่มเพียงบรรทัดเดียวก็สามารถเรียก http methods ต่าง ๆ เหล่านี้ GET ,POST ,PUT ,DELETE ,PATCH ได้เลย ง่ายมาก ๆ

implement PhotoController ตาม function การทำงานเพื่อใช้ตรวจสอบการ call จาก REST client

ผมจะเพิ่ม function response()->json(); เพื่อทำการ return http response คืนค่ากลับไปในรูปแบบ application/json format

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PhotoController extends Controller
{
  /**
   * Display a listing of the resource.
   *
   * @return \Illuminate\Http\Response
   */
  public function index()
  {
    return response()->json(['name' => 'index']);
  }

  /**
   * Show the form for creating a new resource.
   *
   * @return \Illuminate\Http\Response
   */
  public function create()
  {
    return response()->json(['name' => 'create']);
  }

  /**
   * Store a newly created resource in storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @return \Illuminate\Http\Response
   */
  public function store(Request $request)
  {
    return response()->json(['name' => 'store', 'payload' => $request->all()]);
  }

  /**
   * Display the specified resource.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function show($id)
  {
    return response()->json(['name' => 'show', 'id' => $id]);
  }

  /**
   * Show the form for editing the specified resource.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function edit($id)
  {
    return response()->json(['name' => 'edit', 'id' => $id]);
  }

  /**
   * Update the specified resource in storage.
   *
   * @param  \Illuminate\Http\Request  $request
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function update(Request $request, $id)
  {
    return response()->json(['name' => 'update', 'payload' => $request->all(), 'id' => $id]);
  }

  /**
   * Remove the specified resource from storage.
   *
   * @param  int  $id
   * @return \Illuminate\Http\Response
   */
  public function destroy($id)
  {
    return response()->json(['name' => 'destroy', 'id' => $id]);
  }
}

หลังจากปรับแก้ PhotoController.php เรียบร้อย ทำการ รันคำสั่ง

$ php artisan serve
Starting Laravel development server: http://127.0.0.1:8000

ทดสอบ RESTful API

curl http://127.0.0.1:8000/api/photos

{“name”:”index”}

curl http://127.0.0.1:8000/api/photos/create

{“name”:”create”}

curl -X POST -H "Content-Type: application/json" -d "{\"name\":\"poolsawat.com\"}" http://127.0.0.1:8000/api/photos

{“name”:”store”,”payload”:{“name”:”poolsawat.com”}}

curl http://127.0.0.1:8000/api/photos/9999

{“name”:”show”,”id”:”9999″}

curl http://127.0.0.1:8000/api/photos/9999/edit

{“name”:”edit”,”id”:”9999″}

curl -X PUT -H "Content-Type: application/json" -d "{\"name\":\"poolsawat.com\"}" http://127.0.0.1:8000/api/photos/99999

{“name”:”update”,”payload”:{“name”:”poolsawat.com”},”id”:”99999″}

curl -X DELETE http://127.0.0.1:8000/api/photos/9999

{“name”:”destroy”,”id”:”9999″}

สรุปท้ายบทความ

การทำ RESTful API ด้วย Laravel จากที่ได้ใช้งานมาได้สักระยะ จากประสบการณ์ค่อนข้างชอบ เพราะด้วยตัวภาษาโปรแกรมมิ่ง เวลาจะ compile ไม่จำเป็นต้อง restart server เพราะเป็นภาษาคริป เลยถือเป็นข้อดีอย่างนึงสำหรับทำ prototype api ง่าย ๆ เพื่อนำเสนอลูกค้าหรือใช้งานกันเองภายในบริษัท ต้องการศึกษาเพิ่มเติมเกี่ยวกับการทำ Resource Controllers ของ Laravel ศึกษาได้ที่ลิ้งนี้ Resource Controllers

บทความต่อไปจะเป็นเรื่องอะไร คอยติดตามกันนะครับ

Laravel รู้จัก Routes และ การรับค่าจาก HttpRequest EP4

การพัฒาน web application ข้อที่ควรคำนึงถึงคือเรื่อง url (endpoints) ต่าง ๆ ภายใน application ของเรา อาจจะเป็น url แบบเดียวกันได้เพียงแต่กำหนด http method ไม่เหมือนกัน เช่น

  • [GET] /api/v1/user สร้างเพื่อ provide data ให้กับ client
  • [POST] /api/v1/user สร้างเพื่อกำหนดไว้สำหรับ create data ใหม่

จุดสั่งเกตุที่เห็นได้ชัดคือ ความเหมือนกันของ url แต่ต่างกันในเรื่อง method เท่านั้น ทางฝั่ง application เราเอง (กำหนด route) จะ mapping route ให้ตรงกับการ request เรียกเข้ามา request เรียกมาแบบ [GET] ก็จะเข้าไปทำงานในส่วนของ GET method , เรียกมาแบบ [POST] จะเข้าไปทำงานในส่งของ POST method เรื่องเหล่านี้ต้องมาทำความเข้าใจกัน บทความนี้จะมาอธิบาย

การกำหนด Methods มีกี่แบบ Laravel จะต้องเขียนอย่างไร

  • GET
    • มีการเก็บ cached การ request ได้
    • ไม่ควรอย่างยิ่งสำหรับการส่ง sensitive data มากับ url
    • มีข้อจำกัดเรื่องความยาวของ url + parameters
    • ใช้สำหรับขอข้อมูลเท่านั้น ไม่เหมาะกับการแก้ไขข้อมูล
  • POST
    • ไม่ถูกเก็บ cached เอาไว้ (request body) จะไม่ถูกเก็บไว้
    • ไม่อยู่ในประวัติของ browser
    • request body สามารถกำหนดได้ยาว ขนาดใหญ่
  • PUT
    • เป็นลักษณะการสร้าง (created) หรือ อัพเดท (update) ข้อมูลฝั่ง serve
  • HEAD
    • คล้ายกันกับ GET แต่ไม่มีการคืนค่ากลับ
  • DELETE
    • การลบข้อมูลที่เป็น unique โดยส่วนใหญ่จะเป็น uuid หรือ id ที่ระบุเป็นเฉพาะเจาะจง
  • PATCH
    • เพื่อปรับเปลี่ยนบางอย่างของข้อมูลฝั่ง server
  • OPTIONS
    • เพื่ออธิบายการทำงานของ data ฝั่ง server

Laravel มีการกำหนด http methods Laravel Routing จะถูกสร้างไว้ที่ไฟล์ /routes/web.php และ /routes/api.php

[GET] สำหรับ inline function
Route::get('foo', function () {
    return 'Hello World';
});
[GET] สำหรับ use controller route อื่น ๆ
Route::get('/user', '[email protected]');
[*] ลักษณะการกำหนด methods อื่น ๆ ทำตาามนี้
Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

การกำหนด parameters ให้ทำ route ในรูปแบบต่าง ๆ

[required] การกำหนด parameters บังคับ ในรูแบบ required parameters จะกำหนดภายใต้ {} 
Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});

[non required] กำหนด parameters แบบไม่บังคับให้ส่งเข้ามา
Route::get('user/{name?}', function ($name = null) {
    return $name;
});
Route::get('user/{name?}', function ($name = 'John') {
    return $name;
});

[regular] กำหนด parameter ด้วย rules ตาม pattern ด้วย regular expression
Route::get('user/{name}', function ($name) {
    //
})->where('name', '[A-Za-z]+');
Route::get('user/{id}', function ($id) {
    //
})->where('id', '[0-9]+');
Route::get('user/{id}/{name}', function ($id, $name) {
    //
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

ศึกษาเพิ่มเติมเรื่อง Routes การทำ Routes Grouping จากลิ้งนี้ Laravel Routes

การรับค่าจาก HttpRequest ในรูปแบบต่าง ๆ

การที่จะได้มาของ parameters data ที่ได้จากการ request client นั้นมีได้หลายรูปแบบ ตามตัวอย่าง

การเข้าถึงค่าของ url information ที่ถูก request เข้ามา

[url path]
$uri = $request->path();

// Without Query String...
$url = $request->url();

// With Query String...
$url = $request->fullUrl();

การรับค่า (request body data) จาก HttpRequest (Payload Request Body)

[all] จะได้ค่าเป็น object ของ request data
$input = $request->all();

[name] การเข้าถึง value ด้วยการกำหนดจาก name ของ parameter
$name = $request->input('name');

[empty then value] กำหนดค่า default หากไม่พบค่าจาก parameters นั้น ๆ 
$name = $request->input('name', 'Sally');

[json array] การเข้าถึง request ที่เป็นลักาณะ raw json array ก็ทำได้
$name = $request->input('products.0.name');
$names = $request->input('products.*.name');

การรับค่า (request body data) จาก HttpRequest (Query Parameters)

[?name=hello] การเข้าถึง queryString จาก parameters
$name = $request->query('name');

[empty then value]
$name = $request->query('name', 'Helen');

ตรวจสอบค่าจาก parameters ด้วย name

[checking] ตรวจสอบ parameter ชื่อ name หากมีจะเข้าเงื่อนไขการทำงาน
if ($request->has('name')) {
    //
}
if ($request->has(['name', 'email'])) {
    //
}
if ($request->hasAny(['name', 'email'])) {
    //
}

[not empty] ตรวจสอบค่า parameter ต้องไม่เป็นค่าว่าง จะใช้ function filled ตรวจสอบ
if ($request->filled('name')) {
    //
}

รูปแบบการใช้งาน Routes และ HttpRequest ใน Laravel มีเนื้อหาเพิ่มเติมอีกมากมาย สามารถศึกษาเพิ่มเติมได้ที่ Laravel Route ,Laravel Request

ขอบคุณที่ติดตามบทความครับ

Laravel ใช้งาน views blade template engine EP3

การพัฒนาเว็บไซต์ด้วยชุด framework ต่าง ๆ จะให้ได้รับความนิยมของ framework นั้น ๆ ต้องมีความพร้อมในเรื่องการจัดการ view ก็คือเครื่องมือการพัฒนาในส่วนของงานการแสดงผลของ data ต่าง ๆ การที่ laravel มาพร้อมกับ blade template engine นี้ถือว่าครบเพียบพร้อม เหมาะแก่การใช้งานเป็นอย่างมาก บทความนี้จะพาไปรู้จักการใช้งาน blade template engine นี้กันว่ามีการทำงานร่วมกับ laravel อย่างไรได้บ้าง

การใช้งาน Blade เพื่อ displaying data จาก controller

การที่จะนำค่าที่ผ่านกระบวนการทำงานจาก controller ไปที่ views (*.blade.php) โดยไฟล์ *.blade.php ทั้งหมดจะถูกจัดเก็บไว้ที่ resources/views/*

  • สร้างไฟล์ {viewname}.blade.php ตัวอย่าง views/example/datatype.blade.php
<html>

<body>
  <h1>DataType</h1>
  <h3>String</h3>
  <p>{{$string}}</p>
  <hr />
  <h3>Boolean</h3>
  <p>{{$boolean}}</p>
  <hr />
  <h3>Integer</h3>
  <p>{{$integer}}</p>
  <hr />
  <h3>Object</h3>
  <p>{{$object['content']}}</p>
  <hr />
  <h3>Array</h3>
  <p>
    <ol>
      @foreach($array as $index=> $item)
      <li>{{$item['content']}}</li>
      @endforeach
    </ol>
  </p>
  <hr />
</body>

</html>
  • แก้ไขไฟล์ routes/web.php กำหนด view(‘datatype’,[?]) กำหนด parameter data ที่จะส่งไปที่ view ใน arg ที่ 2 [?]
Route::get('/datatype', function () {
  return view('/example/datatype', [
    'string' => 'Laravel ใช้งาน view (blade template), json ,pdf',
    'boolean' => true,
    'integer' => 9999,
    'object' => [
      'content' => 'Laravel ใช้งาน view (blade template), json ,pdf EP3',
      'author' => 'poolsawat'
    ],
    'array' => [
      ['content' => 'Laravel เริ่มติดตั้งและสร้าง route ง่าย ๆ EP1'],
      ['content' => 'Laravel แนะนำโครงสร้างภายในโปรเจค อธิบายแต่ละส่วนการทำงาน EP2'],
      ['content' => 'Laravel ใช้งาน view (blade template), json ,pdf EP3'],
    ]
  ]);
});

การใช้งาน blade ในกรณีต้องการใช้งาน control structures (IF ,ELSE ,SWITCH ,LOOP (for ,foreach ,while) ต่าง ๆ เหล่านี้ ทำอย่างไร

ในบางครั้ง บางกรณีที่ต้องการตะเช็คเงื่อนไขในการที่จะแสดงผลของ data หรืออยากที่จะแสดงผลค่าที่เป็นในลักษณะชุดข้อมูล (array) ทาง blade ก็มีคำสั่งมาให้พร้อมแล้ว โดยการที่จะเรียกคำสั่งนั้น จะมี tag @ ตามด้วย name เช่น @if ,@for เป็นต้น

  • สร้างไฟล์ views/example/control.blade.php
<!DOCTYPE html>
<html lang="en">

<body>
  <h1>Control Structures</h1>
  <p>IF ,ELSEIF ,ELSE ,LOOP (FOR,FOREACH,WHILE) ,SWITCH</p>
</body>
<h1>IF</h1>
@if (count($records) === 1)
I have one record!
@elseif (count($records) > 1)
I have multiple records!
@else
I don't have any records!
@endif
<hr />
<h1>SWITCH</h1>
@switch($case)
@case(1)
First case...
@break

@case(2)
Second case...
@break

@default
Default case...
@endswitch
<hr />
<h1>LOOP</h1>
<h3>FOR</h3>
@for ($i = 0; $i
< 4; $i++) <br />The current value is {{ $i }} @endfor
<h3>FOREACH</h3>
@foreach ($records as $rec)
<p>This is record {{ $rec }}</p>
@endforeach
<hr />
<h3>FORELSE</h3>
@forelse ($records as $rec)
<li>{{ $rec }}</li>
@empty
<p>No users</p>
@endforelse
<hr />
<h3>WHILE</h3>
@php
$i = 0
@endphp
@while ($i < 4) <p>I'm looping forever.</p>
  @php
  $i++
  @endphp
  @endwhile

</html>
  • แก้ไขไฟล์ routes/web.php โดยเพิ่ม route “control-structures” เข้าไป
Route::get('/control-structures', function () {
  return view('example/control', [
    'records' => [1, 2, 3, 4],
    'case' => 2
  ]);
});

การใช้งาน blade กับการทำ component แยกส่วน

framework ที่ได้รับความนิยมในปัจจุบัน React , Angular ,Vue มีจุดเด่นเรื่องการทำ Component management โดยทั้งสิ้น Laravel เองก็มี concept นี้ด้วยเช่นกัน (ทีมเดียวกับที่พัฒนา Vue) ถือเป็นข้อดีอีกอย่างที่อยากแนะนำให้ใช้งาน laravel

  • สร้างไฟล์ views/example/component.blade.php , views/components/alert.blade.php ,views/components/message.blade.php ,views/components/message-slot.blade.php ตาลำดับ
<!-- views/example/component.blade.php -->
<!DOCTYPE html>
<html lang="en">

<body>
  <h1>Component</h1>
  <h3>empty data</h3>
  @component('components/message',[])
  @endcomponent
  <hr />
  <h3>has data</h3>
  @component('components/message',['name' => 'poolsawat.com'])
  @endcomponent
  <hr />
  <h3>append slot</h3>
  @component('components/message-slot',[])
  <div>element slot 1</div>
  <div>element slot 2</div>
  @endcomponent
  <hr />
  <x-alert />
</body>

</html>
<!-- views/components/alert.blade.php -->
<h1>x-{component}</h1>
<!-- views/components/message.blade.php -->
<h2>
  @if (empty($name))
  component message ...
  @else
  component message {{$name}}
  @endif
</h2>
<!-- views/components/message-slot.blade.php -->
<h2>
  component message append slot
  {{ $slot }}
</h2>
  • แก้ไขไฟล์ routes/web.php เพิ่ม route “components”
Route::get('/components', function () {
  return view('example/component', []);
});
  • ดูผลลัพธ์ที่
  • components ของ laravel มีรูปแบบการเรียกได้ 2 วิธี
    1. @component(‘components/alert’,[]) @endcomponent สามารถเรียกได้แบบนี้จากหน้า views
    2. <x-alert /> สามารถเรียกได้แต่ต้องมีไฟล์ app/View/Components/Alert.php โดย genarate Component ได้ด้วยคำสั่ง artisan package นี้ php artisan make:component Alert
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Alert extends Component
{
    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\View\View|string
     */
    public function render()
    {
        return view('components.alert');
    }
}

เหล่านี้คือการเรียกใช้งาน blade template engine ง่าย ๆ ยังมีความสามารถอีกเยอะมาก ศึกษาเพิ่มเติ่มที่นี่

Laravel แนะนำโครงสร้างภายในโปรเจค อธิบายแต่ละส่วนการทำงาน EP2

หลังจากที่ได้ทำการติดตั้งตามบทความนี้แล้ว “Laravel เริ่มติดตั้งและสร้าง route ง่าย ๆ EP1” บทความนี้จะมาอธิบายเพิ่มเติมแต่ละส่วนของโครงสร้างโปรเจคว่ามีหน้าที่ทำอะไร และตัวอย่างการใช้งานอย่างไรได้บ้าง

มาทำความรู้จักโครงสร้างของ Laravel กัน

  • app/Console
  • app/Exceptions จัดการ Error Exception จากการทำงานที่ผิดพลาด
  • app/Http
    • app/Http/Controllers จัดเก็บ Controllers file (*Controller.php) สำหรับเก็บ Logic ,Route ,Repository ,ORM ต่าง ๆ จะเขียนในไฟล์เหล่านี้
    • app/Http/Middleware จัดเก็บไฟล์ middleware เข้าออกจอง Routes ต่าง ๆ เช่น การทำ Basic Authenticate ,Session Handlers เป็นต้น
    • app/Kernel
  • app/Models เก็บ Model จาก databases ต่าง ๆ กำหนด
  • app/Providers เก็บไฟล์ Providers กำหนด เช่น BroadcasProvider ,EventServiceProvider ,RouteServiceProvider เป็นต้น
  • bootstrap
    • bootstrap/cache
    • bootstrap/app.php
  • config
    • config/app.php เก็บค่า config ระดับ app เช่น ชื่อ app ,env (production) ,timezone UTC เป็นต้น
    • config/auth.php
    • config/broadcasting.php
    • config/cache.php
    • config/cors.php
    • config/databases.php เก็บ database driver ต่าง ๆ mysql , pgsql, sqlsrv ,sqlite เป็นต้น
    • config/filesystems.php
    • config/hashing.php
    • config/logging.php
    • config/mail.php
    • config/queue.php
    • config/services.php
    • config/session.php
    • config/view.php
  • database
    • database/factories
    • database/migrations เก็บไฟล์ script create table ของ database
    • database/seeders
  • public หลังจาก compile laravel mix จะถูกมาเก็บในนี้ ไฟล์ที่นี่
    • public/.htaccess
    • public/favicon.ico
    • public/index.php
    • public/rebots.txt
  • resources
    • resources/css เก็บไฟล์ css ( Cascading Style Sheets ) ต่าง ๆ
    • resources/js เก็บไฟล์ js (Javascript ) ต่าง ๆ
    • resources/lang
    • resources/views เก็บไฟล์ view (*.blade) ต่าง ๆ
  • routes
    • routes/api.php จัดการ route RESTApis ต่าง ๆ (laravel –resource )
    • routes/channels.php
    • routes/console.php
    • routes/web.php จัดการ route ทั่วไป แบบปกติจะจัดการที่ไฟล์นี้เป็นส่วนใหญ๋
  • storage
    • storage/app/public
    • storage/framework
    • storage/logs
  • tests เขียน unittest ต่าง ๆ
    • tests/Feature
    • tests/Unit
  • vender จัดการ libraries ต่าง ๆ ที่ได้จากการติดตั้ง laravel ตั้งแต่เริ่มต้น
  • .env เก็บค่า config ต่าง ๆ โดยจะแบ่งเป็น dev , prod
  • artisan
  • composer.json
  • package.json
  • webpack.mix.js จัดการไฟล์ต่าง ๆ ที่เป็น css ,js ก่อนการ compile minify จะได้ output เก็บที่ public/

ในบ้าง directory ,file ไม่ได้เข้าไปยุ่งเกี่ยวในระหว่างการใช้งาน จะขอไม่อธิบาย หากคุณต้องการศึกษาเพิ่มเติม https://laravel.com/docs/7.x/structure

Laravel เริ่มติดตั้งและสร้าง route ง่าย ๆ EP1

Laravel ปัจจุบันเดินทางมาถึงเวอร์ชั่น 8.X แล้ว ตั้งแต่เวอร์ชั่น 5 จนมาถึงปัจจุบันมีออะไรเปลี่ยนไปค่อนข้างเยอะ ที่เห็นได้ชัด ๆ เรื่องเวอร์ชั่นหลักของ PHP ที่เปลี่ยนจาก 5.X ไปเป็น 7.X เพิ่มความสามารถ ปรับปรุงเรื่องความปลอดภัยและ อื่น ๆ อีกมากมาย

เตรียมเครื่องมือ ทำความรู้จักเบื้องต้น

  • Composer เป็น PPM (PHP Packages management) จัดการ package ในภาษา PHP ส่วนของ Laravel จะให้ในการติดตั้ง global laravel/installers ศึกษาเพิ่มเติมที่นี่
    • composer global require laravel/installer
      • laravel new blog
    • composer create-project –prefer-dist laravel/laravel blog
  • VSCode เครื่องมือใช้สำหรับเขียนโค๊ด laravel ที่จะมี extensions ที่ช่วยเรื่องการเขียน Laravel ให้ดียิ่งขึ้น

เริ่มติดตั้งใช้งานเบื้องต้น

  1. ติดตั้ง composer ให้เรียบร้อย (ข้ามไป ไม่พูดถึงขั้นตอนนี้) จากนั้นตรวจสอบเวอร์ชั่น
    $ composer --version
    > Composer version 1.9.2 2020-01-14 16:30:31
    1. การติดตั้งแบบที่ 1 ติดตั้งด้วยการใช้ laravel cmd ที่ได้จากการติดตั้งจากขั้นตอนนี้
      $ composer global require laravel/installer
      $ laravel new blog
    2. การติดตั้งด้วย composer โดยตรง
      $ composer create-project --prefer-dist laravel/laravel blog
  2. ตัวอย่างใช้คำสั่ง
    $ laravel new poolsawat.com
    > Crafting application... Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 106 installs, 0 updates, 0 removals - Installing doctrine/inflector (2.0.3): Loading from cache - Installing doctrine/lexer (1.2.1): Loading from cache - Installing dragonmantank/cron-expression (3.0.1): Downloading (100%) - Installing voku/portable-ascii (1.5.3): Loading from cache ... > @php -r "file_exists('.env') || copy('.env.example', '.env');" > @php artisan key:generate --ansi [32mApplication key set successfully.[39m > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: [32mfacade/ignition[39m Discovered Package: [32mfideloper/proxy[39m Discovered Package: [32mfruitcake/laravel-cors[39m Discovered Package: [32mlaravel/tinker[39m Discovered Package: [32mnesbot/carbon[39m Discovered Package: [32mnunomaduro/collision[39m [32mPackage manifest generated successfully.[39m Application ready! Build something amazing.
  3. ตรวจสอบโครงสร้างไฟล์ จะได้หน้าตาแบบนี้

4. ทำการสร้าง route “hello” ที่ไฟล์ route/web.php

5. รันคำสั่ง start server
$ php artisan serve
> Starting Laravel development server: http://127.0.0.1:8000

6. เช็คผลการสร้าง route “hello” ได้ที่ http://localhost:8000/hello

นี่คือแบบที่ง่ายที่สุด สำหรับมือใหม่ บทความต่อ ๆ ไปจะมาเรียนรู้เรื่องอะไรกัน คอยติดตามด้วยนะครับ

ทำความเข้าใจ Date & Time Function MySQL ถ้ารู้แล้วจะทำให้ชีวิตดีขึ้น 10 เท่า

สำหรับข้อมูล Datatype ประเภท Date ,DateTime ,Time ,Timestamp ,Year เหล่านี้ จะทำให้เรามึนงงและเป็นปัญหา และจะจัดการได้ยาก ยกตัวอย่างเคสต่าง ๆ เหล่านี้ เช่น

  • หาวันเวลา ปัจจุบัน
  • ต้องการเปลี่ยน format ของ column Date ให้ส่วนของค่าที่ Query มาได้
  • ต้องการที่จะหาวัน ที่ต้องการเมื่อในอดีตที่ผ่านมา
  • อยากที่จะคำนวนช่วงเวลา นาที ,ชั่วโมง ,วัน ,เดือน หรือ ปีของ Datetime ทั้ง 2 columns ว่า มีช่วงห่างกันเท่าไหร่
  • ต้องการหาวันสุดท้ายของเดือน เช่น 28 ,29 ,30 ,31
  • และอื่น ๆ อีกมากมาย

*** Function ต่าง ๆ ที่จะยกตัวอย่าง บาง function จะมาใน MySQL 8.0 อาจจะไม่ทำงานบน MySQL ที่ต่ำกว่า 8.0 ***

https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date

ลองเล่น Function ต่าง ๆ กัน

Function ที่หา Date จากปัจจุบัน

/* แสดงวันที่ เวลา ปัจจุบัน */
SELECT NOW();  /* '2020-09-22 09:15:09' */
SELECT CURRENT_TIMESTAMP(); /* '2020-09-22 09:22:11' */
/* แสดงวันที่ปัจจุบัน เท่านั้น */
SELECT CURRENT_DATE();  /* '2020-09-22' */
SELECT CURRENT_DATE; /* '2020-09-22' */
SELECT CURDATE(); /* '2020-09-22' */
/* แสดงเวลา ปัจจุบัน เท่านั้น */
SELECT CURRENT_TIME();  /* '09:21:30' */
SELECT CURRENT_TIME; /* '09:21:30' */
SELECT CURTIME(); /* '09:21:30' */
/* แสดงวัน ของแต่ละเดือน */ 
SELECT DAY(NOW()); /* 22 */
SELECT DAYOFMONTH(NOW()); /* 22 */
/ * แสดงลำดับวัน ของสัปดาห์ */
SELECT DAYOFWEEK(NOW());  /* 3 (คือวัน วันอังคาร) */
/* แสดงวันที่ ลำดับ ใน 1 ปี ( 0 - 365 ) */
SELECT DAYOFYEAR(NOW()); /* 266 (2020-09-22)*/

แปลง หรือ เปลี่ยน format ของ Date

/* แสดง format ของ Date ต้นทาง */
SET @SOURCE_DATE = DATE('2020-09-22'); 
SELECT @SOURCE_DATE as origin_date; /* '2020-09-22' */
SELECT DATE_FORMAT(@SOURCE_DATE,'%d/%m/%Y') as dmy_date;  /* '22/09/2020' */
SELECT DATE_FORMAT(@SOURCE_DATE,'%d/%m/%Y %H:%i:%s') as dmyhms_date; /* '22/09/2020 00:00:00' */

หาวันก่อนหน้า (วันที่ในอดีต) หรือวันถัดไป (วันที่ในอนาคต)

SET @SOURCE_DATE = TIMESTAMP('2020-09-22 00:00:00');
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 MONTH) as add_10_month; /* '2021-07-22 00:00:00' (เพิ่ม 10 วินาที ) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 DAY) as add_10_day; /* '2020-10-02 00:00:00' (เพิ่ม 10 วินาที ) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 HOUR) as add_10_hour; /* '2020-09-22 10:00:00' (เพิ่ม 10 วินาที ) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 MINUTE) as add_10_minute; /* '2020-09-22 00:10:00' (เพิ่ม 10 วินาที ) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 SECOND) as add_10_second; /* '2020-09-22 00:00:10' (เพิ่ม 10 วินาที ) */


SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 MONTH) as sub_10_month; /* '2019-11-22 00:00:00' ( ลบ 10  เดือน)  */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 DAY) as sub_10_day; /* '2020-09-12 00:00:00'  (ลบ10  วัน) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 HOUR) as sub_10_hour; /* '2020-09-21 14:00:00' (ลบ 10  ชั่วโมง) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 MINUTE) as sub_10_minute; /* '2020-09-21 23:50:00' (ลบ 10  นาที) */
SELECT DATE_ADD(@SOURCE_DATE , INTERVAL 10 SECOND) as sub_10_second; /* '2020-09-21 23:59:50' (ลบ 10  วินาที )*/

คำนวน ปี ,เดือน ,วัน ,ชั่วโมง ,นาที ,วินาที

SET @BEFORE_DATETIME = TIMESTAMP('2020-08-10 10:10:10');
SET @AFTER_DATETIME = TIMESTAMP('2020-09-20 20:20:20');
SELECT  DATEDIFF(@AFTER_DATETIME,@BEFORE_DATETIME) as diff_year;  /* ห่างกัน 41 วัน */

SELECT TIMESTAMPDIFF(YEAR,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_year; /* ห่างกัน 0 ปี */
SELECT TIMESTAMPDIFF(MONTH,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_month; /* ห่างกัน 1 เดือน */
SELECT TIMESTAMPDIFF(DAY,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_day; /* ห่างกัน 41 วัน */
SELECT TIMESTAMPDIFF(HOUR,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_hour; /* ห่างกัน 994 ชั่วโมง */
SELECT TIMESTAMPDIFF(MINUTE,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_minute; /* ห่างกัน 59650 นาที */
SELECT TIMESTAMPDIFF(SECOND,@BEFORE_DATETIME,@AFTER_DATETIME) as diff_second; /* ห่างกัน 3579010 วินาที */

ตัด ปี ,เดือน ,วัน ,ชั่วโมง ,นาที ,วินาที

SET @TARGET_DATETIME = '2020-09-22 10:15:20';
SELECT DATE(@TARGET_DATETIME) as date_date; /* 2020-09-22 */
SELECT YEAR(@TARGET_DATETIME) as year_date; /* 2020 */
SELECT MONTH(@TARGET_DATETIME) as month_date; /* 9 */
SELECT WEEK(@TARGET_DATETIME) as week_date; /* 38 */
SELECT DAY(@TARGET_DATETIME) as day_date; /* 22 */
SELECT HOUR(@TARGET_DATETIME) as hour_date; /* 10 */
SELECT MINUTE(@TARGET_DATETIME) as minute_date; /* 15 */
SELECT SECOND(@TARGET_DATETIME) as second_date; /* 20 */

สำหรับ Function อื่น ๆ ของ Date & Time ของ MySQL Database สามารถเข้าไปศุกษาเพิ่มเติมได้ที่นี่ “Date and Time Functions”

Cypress Route คืออะไร ใช้งานอย่างไร EP6

การทดสอบ application ในบางครั้งก็จะมี dependencies อื่น ๆ ที่เกี่ยวข้องมากมาย โดยหนึ่งในนี้คือ การเรียก API data ที่ไม่สามารถทราบได้เลยว่าการที่ API ตอบกลับมานั้นจะช้าหรือเร็วแค่ใหน การเทสบางครั้งอาจราบลื่นเป็นปกติ เพราะไม่มีการถูกขัดจังหวะด้วย response time ของ API data ที่เร็ว แต่เมื่อ run test อีกครั้งกับได้ผลรับไม่เหมือนเดิม เกิด failures เพราะมี error message แจ้งกลับมาว่า timeout waiting 5000ms บ่งบอกได้ถึงการรอ network request API ที่นานจนโปรแกรมรอไม่ไหว แบบนี้จะแก้ได้อย่างไร

การ Mock API แก้ pain point นี้ได้

การ Mock API ช่วยแก้ pain point นี้ได้ โดยมีหลักการคือการ จำลอง response data ที่สร้างขึ้นมาเพื่อที่จะไม่ต้องเรียก API request จริง วิธีการนี้จะช่วย ควบคุม response time ได้ทำให้ test ของเราทำงานได้ราบลื่นไม่ติดขัด

Network Requests Route คืออะไร

Cypress ได้สร้าง API Command ที่ชื่อว่า route เพื่อใช้บริหารจัดการ XHR Object Request ของระดับ HTTP โดยทำการสร้างสภาพแวดล้อมเสมือนคอยตรวจสอบดักจับการเรียก Network URL ที่สร้างขึ้นโดยที่เราสามารถปรับปรุงเปลี่ยนแปลง Request ,Response ของ HTTP ในระดับ network layer ได้

ตัวอย่างและวิธีการเรียกใช้งาน

cy.server()

cy.route(url)
cy.route(url, response)
cy.route(method, url)
cy.route(method, url, response)
cy.route(callbackFn)
cy.route(options)

usercase ที่จะนำมายกตัวอย่างคือการ call API ของ coronavirus-19-api data ของประเทศทั้งหมดในโลกที่มีการติดเชื้อ Covid ณ ปัจจุบัน จะยกตัวอย่างการเปลี่ยนแปลงข้อมูล เมื่อเรียก api นี้ route จะทำหน้าที่ mock response ของ API เดิมที่จะ return Response Data ของทุกประเทศ แต่จะ return Response Data ของ Thailand เท่านั้น

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Window Methods</title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
    />
  </head>
  <body>
    <div class="ui container">
      <h2>Course Cypress Automated Testing</h2>
      <div class="ui top attached tabular menu">
        <a class="item" href="index.html" data-tab="zero">Index</a>
        <a class="item" href="register.html" data-tab="first">Register</a>
        <a class="item" href="register-list.html" data-tab="first-list"
          >Register List</a
        >
        <a class="item" href="window.html" data-tab="second">Window</a>
        <a class="item" href="login.html" data-tab="third">Login</a>
        <a class="item" href="change-password.html" data-tab="four"
          >Change Password</a
        >
        <a class="item active" href="table.html" data-tab="five"
          >Table Coronavirus-19</a
        >
        <a class="item" href="elements.html" data-tab="six">Chai-jQuery</a>
      </div>
      <div class="ui bottom attached tab segment active" data-tab="first">
        <table class="ui tablet stackable celled striped table" id="covidState">
          <thead>
            <tr>
              <th>No.</th>
              <th>Country</th>
              <th>Cases</th>
              <th>Today Cases</th>
              <th>Deaths</th>
              <th>Today Deaths</th>
              <th>Recovered</th>
              <th>Active</th>
              <th>Critical</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
    <script>
      Number.prototype.toCurrency = function (n = 2, x = 3) {
        var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')';
        return this.toFixed(Math.max(0, ~~n)).replace(
          new RegExp(re, 'g'),
          '$&,'
        );
      };

      window.onload = () => {
        const data = fetch('https://coronavirus-19-api.herokuapp.com/countries')
          .then((http) => http.json())
          .then((data) => {
            //console.log('data ::==', data);
            const $covidState = document.getElementById('covidState');
            let $tbody = covidState.children[1]; // tbody
            //console.log('$tbody ::==',$tbody)
            data
              .map((item, index) => {
                return { ...item, ...{ no: index + 1 } };
              })
              .forEach((item) => {
                let $tr = document.createElement('TR');
                $tr.innerHTML = `
              <td>${item.no}</td>
              <td>${item.country}</td>
              <td>${(item.cases || 0).toCurrency(0)}</td>
              <td>${(item.todayCases || 0).toCurrency(0)}</td>
              <td>${(item.deaths || 0).toCurrency(0)}</td>
              <td>${(item.todayDeaths || 0).toCurrency(0)}</td>
              <td>${(item.recovered || 0).toCurrency(0)}</td>
              <td>${(item.active || 0).toCurrency(0)}</td>
              <td>${(item.critical || 0).toCurrency(0)}</td>
          `;
                $tbody.appendChild($tr);
              });
          });
      };
    </script>
  </body>
</html>

การทำงานของโปรแกรมมหน้านี้ เมื่อโหลดหน้าขึ้นมาจะไปเรียก API Covid นี้ทันทีด้วย window.fetch

ทดสอบสร้าง testscript ของการใช้คำสั่ง route

describe('ทดสอบการทำงานและเรียกใช้งาน Route', () => {

  let polyfill

  before(() => {
    const polyfillUrl = 'https://unpkg.com/[email protected]/dist/fetch.umd.js'
    cy.request(polyfillUrl).then(response => {
      polyfill = response.body
    })
  })

  Cypress.on('window:before:load', win => {
    delete win.fetch
    win.eval(polyfill)
  })

  it('เรียก API Covid', () => {
    cy.server()
    cy.route({
      url: '**/countries',
      method: 'GET',      // Route all GET requests
      response: [{
        "country": "Thailand",
        "cases": 3202,
        "todayCases": 5,
        "deaths": 58,
        "todayDeaths": 0,
        "recovered": 3085,
        "active": 59,
        "critical": 1,
        "casesPerOneMillion": 46,
        "deathsPerOneMillion": 0,
        "totalTests": 603657,
        "testsPerOneMillion": 8648
      }]
    }).as('routeCovid')
    cy.visit('https://cypress-testing-143fd.web.app/table.html')
      .get('#covidState').contains('Country')
      .wait('@routeCovid', { timeout: 10000 }).its('status').should('have.eq', 200)
  })
})

อธิบายการทำงานในส่วนโค๊ดนี้

บรรทัดที่ 18 เริ่มสร้าง network server (cy.server())
บรรทัดที่ 19 สร้าง route เพื่อ mock api ที่ติดต่อภายนอกโดยกำหนด response คือ array object ของ ประเทศไทย เท่านั้น
บรรทัดที่ 37 visit เข้าไปที่หน้าเว็บตัวอย่าง
บรรทัดที่ 39 ตรวจสอบ status code equal 200 หรือไม่

เมื่อลองทดสอบ run test การทำงานของโปรแกรมจะเปลี่ยนไป ระบบจะแสดงแค่ข้อมูลของประเทศไทย โดยที่ไม่ได้มีการแก้ไขโปรแกรมที่ทดสอบแต่อย่างไร เพียงแต่ทำการ mockup API ที่แสดงข้อมูลด้วย Cypress Route เท่านั้น

เพียงเท่านี้เราก็สามารถควบคุม data ที่มาจากการ call API จากที่ต่าง ๆ ได้ แล้วโดยไม่ต้องรอการ response กลับจาก API นั้น ๆ

หากเพื่อนคนใดอยากที่จะทำความรู้จัก Route Command นี้เพิ่มเติมให้เข้าไปที่ลิ้งนี้ได้เลย Cypress Route

Cypress ทำความรู้จัก และตัวอย่างการใช้งาน Spy,Stub EP5

Spy & Stub เป็น API Command ที่มีความพิเศษกว่า API Command อื่น ๆ เราจะไม่ค่อยได้ใช้งาน command นี้มากนัก แต่ถ้าถึงสถานะการนั้นแล้ว spy ,stub นี้แหละที่จะมาเป็นพระเอกช่วยเราได้ในแต่ละสถานะการนั้น ๆ เลย

Spy ,Stub คืออะไร

ก่อนที่จะไปรู้ถึงว่าจะนำ Spy ,Stub ไปใช้กับลักษณะงานแบบไหน ควรต้องเข้าใจก่อนว่า 2 commands นี้ คืออะไร มีความสามารถอย่างไรบ้าง

Stub ทำหน้าที่จำลอง Behavior ของโปรแกรมที่เรากำลังจะทดสอบ โดยหลักการทำงานของ Stub ก็ดัก function ของ Object ของโปรแกรมในที่นี้คือ Object ที่กำลังสนใจมีส่วนการทำงานเกี่ยวข้องกับระบบ หรือ feature ที่กำลังทำงานในขณะนั้น โดยจะคอยดักจับ และยังสามารถเปลี่ยนแปลงการทำงานของ Behavior function นั้น ไปให้เป็นไปตามผลลัพธ์ที่เราต้องการจะให้เป็น โดยข้อดีของการใช้ Stub เพื่อแก้ไข function (Behavior) นี้เพื่อลด dependencies ต่าง ๆ ของโปรแกรมทำไม่ให้เกิด Flaky Test เช่น window.confirm ของ window object ที่ต้องการ action ของปุ่ม Ok (ตกลง) หรือ Cancel (ยกเลิก) เพื่อทำให้โปรแกรมทำงานใน Step ถัด ๆ ไป โดยปกติ API Command ที่มีให้นั้นไม่สามารถจะแก้ไขหรือเลือก choice นี้ได้ จำเป็นต้องใช้ Stub มาเปลี่ยนแปลง Behavior นี้

Spy เกือบที่จะเหมือนกับ Stub ถ้าในทางการเรียกใช้งานแทบจะแยกไม่ออกเลยว่าแตกต่างกันอย่างไร แต่มีสิ่งที่ Spy เองไม่สามารถทำได้เหมือน Stub คือการเปลี่ยนแปลง Behavior ของการทำงานได้เหมือน Stub แต่ทำหน้าที่ได้เพียงแฝงตัว สอดแนม อาจจะเพิ่มการ capture ,record หรือ track log ของการทำงานได้เท่านั้น

Spy /Stub การทำงานเหล่านี้ได้นำเข้ามาจาก Sinon.js จึงสามารถเปิด document ได้เลย สามารถอ่านเนื้อหาการใช้งานได้จากที่นี่
Stub https://sinonjs.org/releases/latest/stubs/
Spy https://sinonjs.org/releases/latest/spies/

เมื่อไหร่ และสถานการณ์แบบไหนถึงจะนำมาใช้ละ

มอง scenarios case จะเกี่ยวกับ behavior ของ javascript function ต่าง ๆ ที่ทีในโปรแกรมที่กำลังทดสอบ เช่น window.alert ,window.confirm ,window.promt หรือแม้แต่ function ต่าง ๆ ที่ custom define ที่ window object ก็สามารถดัก track ติดตามได้หมด

ตัวอย่างโปรแกรม

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button type="button" onclick="clickPromt()">Click Promt</button>
    <script>
      function clickPromt(){
        const data = window.prompt('enter your name.')
        if(data){
          window.alert(data)
        }
      }
    </script>
  </body>
</html>
describe('ทดสอบการทำงานของ Stub', () => {
  it('ทดสอบดักจับ behavior ของ window.promt', () => {
    cy.visit('http://127.0.0.1:5500/cypress/integration/spy-stub/index.html')
    .window().then(win =>{
      cy.stub(win,'prompt',()=>{
        return 'Hardcode "Hello World"'
      }).as('stubPromt')
    })
    .get('button').click()
    .get('@stubPromt').should('be.calledOnce')
  });
});

ตัวอย่างการทำงานของ Stub

เมื่อมีการเขียนคำสั่ง stub เพื่อเริ่มใช้งาน cypress จะสร้างแถบ SPIES/STUBS มาพร้อมแสดงชื่อการทำงานของ behavior นั้น เมื่อโปรแกรมมีการเรียกใช้งาน behavior (function) stub จะแสดงเลข 1 หมายถึงการถูกเรียกทำงาน 1 ครั้ง และโปรแกรมจะ alert ค่าที่รับจาก promt ว่า “Hardcode “Hello World”” ที่ถูก command stub return ค่ามาให้ จึงทำให้การ call function ภายในโปรแกรมทุกครั้งจะได้ promt value ว่า “Hardcode Hello World” เสมอ เพราะถูก stub ดักเปลี่ยนค่าในระหว่างการทำงาน
เพิ่มการ assert เพื่อตรวจสอบการ call function นั้นด้วย “be.calledOnce” อีกครั้งเพื่อให้แน่ใจว่าโปรแกรมทำงาน function อย่างน้อย 1 ครั้ง

นี่คือตัวอย่างการใช้งาน stub กับ function promt ที่ทาง window object ของ browser มีให้ใช้งานเป็นปกติอยู่แล้ว เพื่อน ๆ ที่ติดตามบทความนี้มีปัญหา หรืออยากปรึกษา ขอความช่วยเหลือ สามารถฝากคำถามไว้ใต้โพสนี้ได้เลย