Professional Code Reference

XMAN Code Academy

ศูนย์เรียนรู้โค้ดมืออาชีพ

10+
หมวดหมู่
50+
ตัวอย่างโค้ด
8+
ภาษา
Free
ฟรีทั้งหมด
Home Code Academy

Laravel

PHP Framework ยอดนิยมอันดับ 1 / #1 PHP Framework

PHP — app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name', 'slug', 'description',
        'price', 'category_id', 'is_active',
    ];

    protected $casts = [
        'price'     => 'decimal:2',
        'is_active' => 'boolean',
    ];

    // ความสัมพันธ์: สินค้า → หมวดหมู่
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // ความสัมพันธ์: สินค้า → รีวิว (หลายรายการ)
    public function reviews(): HasMany
    {
        return $this->hasMany(Review::class);
    }

    // Scope: เฉพาะที่เปิดใช้งาน
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    // Accessor: ราคาพร้อมฟอร์แมต
    public function getFormattedPriceAttribute(): string
    {
        return number_format($this->price, 2) . ' ฿';
    }
}
PHP — ProductController.php
class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::active()
            ->with('category')
            ->when($request->search, fn($q, $s) =>
                $q->where('name', 'like', "%{$s}%")
            )
            ->latest()
            ->paginate(12);

        return view('products.index', compact('products'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'        => 'required|string|max:255',
            'slug'        => 'required|unique:products',
            'price'       => 'required|numeric|min:0',
            'category_id' => 'required|exists:categories,id',
            'image'       => 'nullable|image|max:2048',
        ]);

        if ($request->hasFile('image')) {
            $validated['image'] = $request
                ->file('image')
                ->store('products', 'public');
        }

        $product = Product::create($validated);

        return redirect()
            ->route('products.show', $product)
            ->with('success', 'สร้างสินค้าเรียบร้อย!');
    }
}
PHP — migration
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->decimal('price', 10, 2);
            $table->foreignId('category_id')
                  ->constrained()
                  ->cascadeOnDelete();
            $table->string('image')->nullable();
            $table->boolean('is_active')->default(true);
            $table->softDeletes();
            $table->timestamps();

            // Index สำหรับเพิ่มความเร็ว
            $table->index(['is_active', 'created_at']);
        });
    }
};
PHP — Middleware
class EnsureIsAdmin
{
    public function handle(Request $request, Closure $next)
    {
        if (! $request->user()?->is_admin) {
            abort(403, 'ไม่มีสิทธิ์เข้าถึง');
        }

        return $next($request);
    }
}

// การใช้งานใน routes/web.php
Route::middleware(['auth', EnsureIsAdmin::class])
    ->prefix('admin')
    ->group(function () {
        Route::resource('products', AdminProductController::class);
    });
Blade — products/index.blade.php
@extends('layouts.app')
@section('title', 'สินค้าทั้งหมด')

@section('content')
<div class="max-w-7xl mx-auto px-4 py-8">
    <h1 class="text-3xl font-bold mb-6">
        สินค้าทั้งหมด
    </h1>

    <div class="grid md:grid-cols-3 gap-6">
        @forelse($products as $product)
            <x-product-card
                :product="$product"
                :show-price="true"
            />
        @empty
            <p class="text-gray-500 col-span-3">
                ยังไม่มีสินค้า
            </p>
        @endforelse
    </div>

    <!-- Pagination -->
    <div class="mt-8">
        {{ $products->links() }}
    </div>
</div>
@endsection
PHP — ProductResource.php
class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'          => $this->id,
            'name'        => $this->name,
            'slug'        => $this->slug,
            'price'       => $this->price,
            'formatted'   => $this->formatted_price,
            'category'    => new CategoryResource(
                $this->whenLoaded('category')
            ),
            'reviews_count' => $this->whenCounted('reviews'),
            'created_at' => $this->created_at->toISOString(),
        ];
    }
}
P

PHP

ภาษาพื้นฐานสำหรับ Web Development / Server-side scripting language

PHP
interface PaymentGateway
{
    public function charge(float $amount): PaymentResult;
    public function refund(string $transactionId): bool;
}

readonly class PaymentResult
{
    public function __construct(
        public bool   $success,
        public string $transactionId,
        public float  $amount,
        public string $message = '',
    ) {}
}

class StripeGateway implements PaymentGateway
{
    public function __construct(
        private readonly string $apiKey,
    ) {}

    public function charge(float $amount): PaymentResult
    {
        // เรียก Stripe API
        return new PaymentResult(
            success: true,
            transactionId: uniqid('txn_'),
            amount: $amount,
        );
    }
}
PHP
$users = [
    ['name' => 'สมชาย', 'age' => 28, 'role' => 'dev'],
    ['name' => 'สมหญิง', 'age' => 34, 'role' => 'lead'],
    ['name' => 'สมศักดิ์', 'age' => 22, 'role' => 'dev'],
];

// กรองเฉพาะ developer อายุ > 25
$seniorDevs = array_filter($users, fn($u) =>
    $u['role'] === 'dev' && $u['age'] > 25
);

// ดึงเฉพาะชื่อ
$names = array_map(fn($u) => $u['name'], $users);
// ['สมชาย', 'สมหญิง', 'สมศักดิ์']

// รวมอายุทั้งหมด
$totalAge = array_reduce($users,
    fn($sum, $u) => $sum + $u['age'], 0
); // 84

// จัดเรียงตามอายุ
usort($users, fn($a, $b) =>
    $a['age'] <=> $b['age']
);

// Spread operator (PHP 8+)
$merged = [...$names, 'สมปอง', 'สมใจ'];
PHP
enum OrderStatus: string
{
    case Pending    = 'pending';
    case Processing = 'processing';
    case Shipped    = 'shipped';
    case Completed  = 'completed';
    case Cancelled  = 'cancelled';

    // ชื่อแสดงเป็นภาษาไทย
    public function label(): string
    {
        return match($this) {
            self::Pending    => 'รอดำเนินการ',
            self::Processing => 'กำลังดำเนินการ',
            self::Shipped    => 'จัดส่งแล้ว',
            self::Completed  => 'สำเร็จ',
            self::Cancelled  => 'ยกเลิก',
        };
    }

    // สีสำหรับแสดงใน Badge
    public function color(): string
    {
        return match($this) {
            self::Pending    => 'yellow',
            self::Processing => 'blue',
            self::Shipped    => 'purple',
            self::Completed  => 'green',
            self::Cancelled  => 'red',
        };
    }
}
PHP
class InsufficientBalanceException extends \RuntimeException
{
    public function __construct(
        public readonly float $required,
        public readonly float $available,
    ) {
        parent::__construct(
            "ยอดเงินไม่เพียงพอ: ต้องการ {$required} มี {$available}"
        );
    }
}

function processPayment(float $amount): void
{
    try {
        $balance = getWalletBalance();

        if ($balance < $amount) {
            throw new InsufficientBalanceException(
                $amount, $balance
            );
        }

        deductBalance($amount);
    } catch (InsufficientBalanceException $e) {
        logger()->warning('ยอดไม่พอ', [
            'required'  => $e->required,
            'available' => $e->available,
        ]);
        throw $e;
    } finally {
        // ทำงานเสมอ ไม่ว่าจะสำเร็จหรือล้มเหลว
        logTransaction($amount);
    }
}
JS

JavaScript

ภาษาสำหรับ Web ทั้ง Frontend & Backend / The language of the web

JavaScript
// Destructuring — แกะค่าจาก Object / Array
const { name, age, role = 'user' } = userData;
const [first, ...rest] = items;

// Template Literals — สตริงแบบมีตัวแปร
const greeting = `สวัสดี ${name}, คุณอายุ ${age} ปี`;

// Optional Chaining & Nullish Coalescing
const city = user?.address?.city ?? 'ไม่ระบุ';

// Arrow Functions
const double = (n) => n * 2;
const greet = (name) => {
    const msg = `สวัสดี ${name}!`;
    return msg;
};

// Spread & Rest
const merged = { ...defaults, ...overrides };
const allItems = [...oldItems, newItem];

// Object shorthand
const product = { name, price, getTotal() {
    return this.price * 1.07; // รวม VAT 7%
}};
JavaScript
// Fetch API with async/await
async function fetchProducts(category) {
    try {
        const response = await fetch(
            `/api/products?category=${category}`
        );

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        const { data } = await response.json();
        return data;

    } catch (error) {
        console.error('โหลดข้อมูลไม่สำเร็จ:', error);
        return [];
    }
}

// Promise.all — ทำงานหลายอย่างพร้อมกัน
const [products, categories, reviews] =
    await Promise.all([
        fetchProducts('software'),
        fetchCategories(),
        fetchReviews(),
    ]);

// Promise.allSettled — รอทุกตัวจบ (ไม่สนใจ error)
const results = await Promise.allSettled([
    apiCall1(), apiCall2(), apiCall3()
]);

results.forEach((r) => {
    if (r.status === 'fulfilled') console.log(r.value);
    else console.warn(r.reason);
});
JavaScript
// เลือก Element
const btn = document.querySelector('#submit-btn');
const cards = document.querySelectorAll('.product-card');

// สร้าง Element ใหม่
const card = document.createElement('div');
card.className = 'p-4 bg-white rounded-lg shadow';
card.innerHTML = `
    <h3>${product.name}</h3>
    <p>${product.price} ฿</p>
`;
document.querySelector('#grid').appendChild(card);

// Event Listener พร้อม Debounce
function debounce(fn, ms = 300) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(
            () => fn(...args), ms
        );
    };
}

searchInput.addEventListener('input',
    debounce((e) => searchProducts(e.target.value))
);
HTML — Alpine.js
<!-- Dropdown Menu -->
<div x-data="{ open: false }" class="relative">
    <button @click="open = !open">เมนู</button>

    <div
        x-show="open"
        x-transition.opacity
        @click.outside="open = false"
        class="absolute mt-2 bg-white rounded-lg shadow-xl"
    >
        <a href="/profile">โปรไฟล์</a>
        <a href="/settings">ตั้งค่า</a>
    </div>
</div>

<!-- Counter with Animation -->
<div x-data="{ count: 0 }">
    <span x-text="count" class="text-4xl font-bold"></span>
    <button @click="count++">+1</button>
</div>

<!-- Fetch + Loading State -->
<div x-data="{ items: [], loading: true }"
     x-init="
        items = await (await fetch('/api/items')).json();
        loading = false;
     ">
    <div x-show="loading">กำลังโหลด...</div>
    <template x-for="item in items">
        <div x-text="item.name"></div>
    </template>
</div>

Tailwind CSS

Utility-First CSS Framework / เขียน CSS ด้วย Class

HTML — Tailwind
<div class="group relative bg-white rounded-2xl
     overflow-hidden shadow-lg hover:shadow-2xl
     transition-all duration-500 hover:-translate-y-2">

    <!-- รูปภาพ + Overlay -->
    <div class="aspect-video overflow-hidden">
        <img src="/images/product.jpg"
             alt="สินค้า"
             class="w-full h-full object-cover
                    transition-transform duration-700
                    group-hover:scale-110"/>
        <div class="absolute inset-0 bg-gradient-to-t
                    from-black/50 to-transparent
                    opacity-0 group-hover:opacity-100
                    transition-opacity"></div>
    </div>

    <!-- Badge -->
    <span class="absolute top-3 right-3
                px-3 py-1 bg-sky-500 text-white
                text-xs font-bold rounded-full">
        ใหม่
    </span>

    <!-- เนื้อหา -->
    <div class="p-5 sm:p-6">
        <h3 class="text-lg font-bold text-slate-800
                    line-clamp-1">ชื่อสินค้า</h3>
        <p class="mt-2 text-sm text-slate-500
                  line-clamp-2">
            รายละเอียดสินค้าที่น่าสนใจ...
        </p>
        <div class="mt-4 flex items-center
                    justify-between">
            <span class="text-xl font-black
                        text-sky-600">฿599</span>
            <button class="px-4 py-2 bg-sky-500
                            text-white text-sm font-semibold
                            rounded-lg hover:bg-sky-600
                            transition-colors">
                ซื้อเลย
            </button>
        </div>
    </div>
</div>
HTML — Tailwind
<!-- Responsive Grid: 1 → 2 → 3 → 4 คอลัมน์ -->
<div class="grid grid-cols-1 sm:grid-cols-2
     lg:grid-cols-3 xl:grid-cols-4 gap-6">
    <!-- Card items go here -->
</div>

<!-- Centered Flex Container -->
<div class="flex items-center justify-center
     min-h-screen">
    <div class="w-full max-w-md">Login Form</div>
</div>

<!-- Sidebar Layout -->
<div class="flex flex-col lg:flex-row gap-8">
    <aside class="lg:w-64 shrink-0">
        Sidebar
    </aside>
    <main class="flex-1 min-w-0">
        Main Content
    </main>
</div>

<!-- Sticky Header + Scrollable Content -->
<div class="h-screen flex flex-col">
    <header class="sticky top-0 z-50 bg-white/80
                       backdrop-blur-md border-b
                       px-6 py-3">
        Navigation
    </header>
    <main class="flex-1 overflow-y-auto p-6">
        Scrollable Content
    </main>
</div>
Py

Python

ภาษายอดนิยมสำหรับ AI, Data Science & Automation

Python
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum

class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

@dataclass
class Task:
    title: str
    description: str
    priority: Priority = Priority.MEDIUM
    completed: bool = False
    tags: list[str] = field(default_factory=list)
    created_at: datetime = field(
        default_factory=datetime.now
    )

    def mark_done(self) -> None:
        self.completed = True

    def is_urgent(self) -> bool:
        return self.priority in (
            Priority.HIGH, Priority.CRITICAL
        )

# การใช้งาน
task = Task(
    title="Deploy v2.0",
    description="อัพเดทเวอร์ชันใหม่",
    priority=Priority.HIGH,
    tags=["deploy", "production"],
)
Python — FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(title="XMAN API")

class ProductCreate(BaseModel):
    name: str
    price: float
    category: str = "general"

class ProductResponse(ProductCreate):
    id: int

@app.get("/products")
async def list_products(
    category: str | None = None,
    limit: int = 20,
) -> list[ProductResponse]:
    query = select(Product)
    if category:
        query = query.where(
            Product.category == category
        )
    return await db.execute(
        query.limit(limit)
    )

@app.post("/products", status_code=201)
async def create_product(
    data: ProductCreate,
) -> ProductResponse:
    product = Product(**data.model_dump())
    db.add(product)
    await db.commit()
    return product
FL

Flutter / Dart

สร้างแอป Cross-Platform จากโค้ดเดียว / One codebase, all platforms

Dart — Flutter
class ProductListScreen extends StatefulWidget {
  const ProductListScreen({super.key});

  @override
  State<ProductListScreen> createState() =>
      _ProductListScreenState();
}

class _ProductListScreenState
    extends State<ProductListScreen> {
  List<Product> _products = [];
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _loadProducts();
  }

  Future<void> _loadProducts() async {
    final data = await ApiService.getProducts();
    if (!mounted) return; // สำคัญมาก!
    setState(() {
      _products = data;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    return ListView.builder(
      itemCount: _products.length,
      itemBuilder: (ctx, i) => ProductCard(
        product: _products[i],
        onTap: () => _navigateToDetail(_products[i]),
      ),
    );
  }
}
Dart — Flutter
import 'package:http/http.dart' as http;
import 'dart:convert';

class ApiService {
  static const _baseUrl = 'https://api.xman.studio';

  static Future<List<Product>> getProducts({
    String? category,
    int page = 1,
  }) async {
    final uri = Uri.parse('$_baseUrl/products')
        .replace(queryParameters: {
      if (category != null) 'category': category,
      'page': page.toString(),
    });

    final response = await http.get(
      uri,
      headers: {'Accept': 'application/json'},
    );

    if (response.statusCode != 200) {
      throw ApiException(
        'โหลดสินค้าไม่สำเร็จ',
        response.statusCode,
      );
    }

    final List data = jsonDecode(
      response.body
    )['data'];

    return data
        .map((json) => Product.fromJson(json))
        .toList();
  }
}

SQL / Database

ภาษาจัดการฐานข้อมูล / Database management & optimization

SQL
-- ดึงข้อมูลสินค้าพร้อมหมวดหมู่ (JOIN)
SELECT
    p.id,
    p.name,
    p.price,
    c.name AS category_name,
    COUNT(r.id) AS review_count,
    AVG(r.rating) AS avg_rating
FROM products p
LEFT JOIN categories c
    ON p.category_id = c.id
LEFT JOIN reviews r
    ON r.product_id = p.id
WHERE p.is_active = 1
GROUP BY p.id, p.name, p.price, c.name
HAVING AVG(r.rating) >= 4.0
ORDER BY avg_rating DESC
LIMIT 20;

-- INSERT พร้อมป้องกัน duplicate
INSERT INTO products (name, slug, price)
VALUES ('สินค้าใหม่', 'new-product', 599.00)
ON DUPLICATE KEY UPDATE
    price = VALUES(price);

-- UPDATE พร้อมเงื่อนไขหลายตัว
UPDATE products
SET price = price * 0.9  -- ลด 10%
WHERE category_id = 5
  AND is_active = 1;
SQL
-- สร้าง Index ที่เหมาะสม
CREATE INDEX idx_products_active_created
ON products (is_active, created_at DESC);

-- CTE (Common Table Expression) — อ่านง่าย
WITH monthly_sales AS (
    SELECT
        product_id,
        SUM(amount) AS total,
        COUNT(*) AS orders
    FROM order_items
    WHERE created_at >= DATE_SUB(
        NOW(), INTERVAL 30 DAY
    )
    GROUP BY product_id
)
SELECT
    p.name,
    ms.total,
    ms.orders,
    RANK() OVER(
        ORDER BY ms.total DESC
    ) AS sales_rank
FROM monthly_sales ms
JOIN products p
    ON p.id = ms.product_id
ORDER BY sales_rank
LIMIT 10;

Git

ระบบจัดการเวอร์ชัน / Version control system

Bash — Git
# สร้าง branch ใหม่จาก main
git checkout -b feature/payment-gateway

# ดูสถานะไฟล์ที่เปลี่ยนแปลง
git status
git diff --staged  # ดูเฉพาะที่ staged

# เพิ่มไฟล์ทีละไฟล์ (ปลอดภัยกว่า git add .)
git add app/Services/PaymentService.php
git add tests/PaymentTest.php

# Commit พร้อมข้อความที่ชัดเจน
git commit -m "feat: add Stripe payment gateway

- Add PaymentService with charge/refund
- Add webhook handler for payment events
- Add unit tests for payment flows"

# Push และสร้าง Pull Request
git push -u origin feature/payment-gateway

# อัพเดท branch กับ main (rebase)
git fetch origin
git rebase origin/main

# Stash — เก็บงานชั่วคราว
git stash push -m "WIP: payment form"
git stash pop  # เอากลับมา

# ดูประวัติแบบสวยงาม
git log --oneline --graph --all -20
Bash — Git
# Cherry-pick — เลือก commit เฉพาะ
git cherry-pick abc1234

# Bisect — หา commit ที่ทำให้พัง
git bisect start
git bisect bad           # ตอนนี้พัง
git bisect good v1.0.0  # ตรงนี้ยังดี
# Git จะหา commit ต้นเหตุให้อัตโนมัติ
git bisect reset         # เสร็จแล้วรีเซ็ต

# Reflog — กู้ commit ที่หายไป
git reflog
git checkout HEAD@{3}  # กลับไปจุดนั้น

# Squash commits (รวม commit ก่อน merge)
git merge --squash feature/xyz
git commit -m "feat: add XYZ feature"

# Blame — ดูว่าใครแก้บรรทัดไหน
git blame app/Models/User.php

# Clean — ลบไฟล์ที่ไม่ได้ track
git clean -fd --dry-run  # ดูก่อนลบ
git clean -fd            # ลบจริง

REST API Design

ออกแบบ API ที่ดี / Professional API design patterns

REST API Design
# ===== RESTful API Convention =====

GET    /api/v1/products          # รายการสินค้า
GET    /api/v1/products/42       # สินค้า ID 42
POST   /api/v1/products          # สร้างสินค้าใหม่
PUT    /api/v1/products/42       # แก้ไขทั้งหมด
PATCH  /api/v1/products/42       # แก้ไขบางส่วน
DELETE /api/v1/products/42       # ลบสินค้า

# Nested Resources
GET    /api/v1/products/42/reviews
POST   /api/v1/products/42/reviews

# Filtering, Sorting, Pagination
GET    /api/v1/products?category=software
         &sort=-price
         &page=2
         &per_page=20

# Response Format (JSON)
{
  "data": [...],
  "meta": {
    "current_page": 2,
    "total": 150,
    "per_page": 20
  },
  "links": {
    "next": "/api/v1/products?page=3",
    "prev": "/api/v1/products?page=1"
  }
}
JSON — API Errors
// 200 OK — สำเร็จ
{ "data": { "id": 42, "name": "Product" } }

// 201 Created — สร้างสำเร็จ
{ "data": { ... }, "message": "สร้างเรียบร้อย" }

// 400 Bad Request — ข้อมูลไม่ถูกต้อง
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "ข้อมูลไม่ถูกต้อง",
    "details": {
      "name": ["กรุณากรอกชื่อสินค้า"],
      "price": ["ราคาต้องมากกว่า 0"]
    }
  }
}

// 401 Unauthorized — ไม่ได้ล็อกอิน
{
  "error": {
    "code": "UNAUTHENTICATED",
    "message": "กรุณาเข้าสู่ระบบ"
  }
}

// 404 Not Found
{
  "error": {
    "code": "NOT_FOUND",
    "message": "ไม่พบสินค้าที่ต้องการ"
  }
}

// 429 Too Many Requests
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "คำขอมากเกินไป กรุณารอสักครู่",
    "retry_after": 60
  }
}
🐳

Docker

Containerization & Deployment / คอนเทนเนอร์สำหรับ Deploy

Dockerfile
# Stage 1: Build assets
FROM node:20-alpine AS assets
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY vite.config.js tailwind.config.js ./
COPY resources/ resources/
RUN npm run build

# Stage 2: PHP production
FROM php:8.3-fpm-alpine
WORKDIR /var/www

# Install extensions
RUN docker-php-ext-install \
    pdo_mysql bcmath opcache

# Install Composer
COPY --from=composer:latest \
    /usr/bin/composer /usr/bin/composer

# Install dependencies (no dev)
COPY composer.* ./
RUN composer install --no-dev \
    --optimize-autoloader --no-scripts

# Copy application code
COPY . .
COPY --from=assets /app/public_html/build \
    public_html/build

# Optimize Laravel
RUN php artisan config:cache \
 && php artisan route:cache \
 && php artisan view:cache

EXPOSE 9000
CMD ["php-fpm"]
YAML — docker-compose.yml
services:
  app:
    build: .
    volumes:
      - ./storage:/var/www/storage
    depends_on:
      - mysql
      - redis
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: xmanstudio
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql_data:

LINE OA — Messaging API

เชื่อมต่อ LINE Official Account กับ Laravel / LINE Notification & Chatbot Integration

ขั้นตอนภาพรวม — Overview Steps

1 สร้าง LINE OA + Messaging API Channel
2 ตั้งค่า .env + สร้าง Service Class
3 สร้าง Webhook รับข้อความ
4 ส่งแจ้งเตือนอัตโนมัติ (Push / Flex)

ต้องมี: LINE Official Account (ฟรี), LINE Developers Console, Laravel 10+, PHP 8.1+, HTTPS domain (สำหรับ Webhook)

.env
# =============================================
# LINE OA — Messaging API Configuration
# =============================================
# ได้จาก https://developers.line.biz/console/
# → เลือก Provider → เลือก Channel
# =============================================

# Channel Secret (Basic settings tab)
LINE_CHANNEL_SECRET=your_channel_secret_here

# Channel Access Token (Messaging API tab)
# กด "Issue" เพื่อสร้าง Long-lived token
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token_here

# (Optional) สำหรับ LINE Login
LINE_LOGIN_CHANNEL_ID=your_login_channel_id
LINE_LOGIN_CHANNEL_SECRET=your_login_channel_secret
LINE_LOGIN_CALLBACK_URL=https://yoursite.com/auth/line/callback
PHP — config/services.php
// เพิ่มใน config/services.php
'line' => [
    'channel_secret'       => env('LINE_CHANNEL_SECRET'),
    'channel_access_token' => env('LINE_CHANNEL_ACCESS_TOKEN'),
    'api_endpoint'         => 'https://api.line.me/v2/bot',
],
PHP — app/Services/LineService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class LineService
{
    private string $accessToken;
    private string $channelSecret;
    private string $apiBase;

    public function __construct()
    {
        $this->accessToken   = config('services.line.channel_access_token');
        $this->channelSecret = config('services.line.channel_secret');
        $this->apiBase       = config('services.line.api_endpoint');
    }

    // ======================================
    // Push Message — ส่งข้อความหาคนเฉพาะเจาะจง
    // ======================================
    public function pushMessage(
        string $userId,
        array  $messages,
    ): bool {
        return $this->sendRequest('/message/push', [
            'to'       => $userId,
            'messages' => $messages,
        ]);
    }

    // ======================================
    // Reply Message — ตอบกลับจาก Webhook
    // ======================================
    public function replyMessage(
        string $replyToken,
        array  $messages,
    ): bool {
        return $this->sendRequest('/message/reply', [
            'replyToken' => $replyToken,
            'messages'   => $messages,
        ]);
    }

    // ======================================
    // Multicast — ส่งหาหลายคนพร้อมกัน
    // (สูงสุด 500 userId ต่อครั้ง)
    // ======================================
    public function multicast(
        array $userIds,
        array $messages,
    ): bool {
        return $this->sendRequest('/message/multicast', [
            'to'       => $userIds,
            'messages' => $messages,
        ]);
    }

    // ======================================
    // Broadcast — ส่งหาทุกคนที่ add เพื่อน
    // ======================================
    public function broadcast(array $messages): bool
    {
        return $this->sendRequest('/message/broadcast', [
            'messages' => $messages,
        ]);
    }

    // ======================================
    // ดึงข้อมูลโปรไฟล์ผู้ใช้
    // ======================================
    public function getProfile(string $userId): ?array
    {
        $response = Http::withToken($this->accessToken)
            ->get("{$this->apiBase}/profile/{$userId}");

        return $response->successful()
            ? $response->json()
            : null;
    }

    // ======================================
    // ตรวจสอบ Signature จาก Webhook
    // (สำคัญมาก! ป้องกัน Spoofing)
    // ======================================
    public function validateSignature(
        string $body,
        string $signature,
    ): bool {
        $hash = hash_hmac(
            'sha256',
            $body,
            $this->channelSecret,
            true
        );

        return hash_equals(
            $signature,
            base64_encode($hash)
        );
    }

    // ======================================
    // Private: ส่ง HTTP Request ไป LINE API
    // ======================================
    private function sendRequest(
        string $endpoint,
        array  $data,
    ): bool {
        $response = Http::withToken($this->accessToken)
            ->post($this->apiBase . $endpoint, $data);

        if ($response->failed()) {
            Log::error('LINE API Error', [
                'endpoint' => $endpoint,
                'status'   => $response->status(),
                'body'     => $response->json(),
            ]);
        }

        return $response->successful();
    }
}
PHP — app/Http/Controllers/LineWebhookController.php
<?php

namespace App\Http\Controllers;

use App\Services\LineService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;

class LineWebhookController extends Controller
{
    public function __construct(
        private readonly LineService $line,
    ) {}

    public function handle(Request $request): JsonResponse
    {
        // 1) ตรวจสอบ Signature (ป้องกัน Spoofing)
        $signature = $request->header('X-Line-Signature', '');
        $body      = $request->getContent();

        if (!$this->line->validateSignature($body, $signature)) {
            Log::warning('LINE Webhook: Invalid signature');
            return response()->json(['error' => 'Invalid'], 403);
        }

        // 2) วนลูปรับ Event ทั้งหมด
        $events = $request->input('events', []);

        foreach ($events as $event) {
            match ($event['type']) {
                'message'  => $this->handleMessage($event),
                'follow'   => $this->handleFollow($event),
                'unfollow' => $this->handleUnfollow($event),
                'postback' => $this->handlePostback($event),
                default    => Log::info('LINE event: ' . $event['type']),
            };
        }

        return response()->json(['status' => 'ok']);
    }

    // =============================
    // จัดการข้อความที่ส่งเข้ามา
    // =============================
    private function handleMessage(array $event): void
    {
        $replyToken = $event['replyToken'];
        $userId     = $event['source']['userId'];
        $msgType    = $event['message']['type'];
        $text       = $event['message']['text'] ?? '';

        // ตอบตามคำสั่ง
        $reply = match (mb_strtolower(trim($text))) {
            'สวัสดี', 'hello' => $this->textMsg(
                'สวัสดีครับ! 👋 ยินดีต้อนรับสู่ XMAN Studio'
            ),
            'เมนู', 'menu' => $this->menuMessage(),
            'ราคา', 'price' => $this->textMsg(
                "💰 ราคาบริการ\n\n"
                . "🔹 เว็บไซต์ เริ่มต้น 15,000฿\n"
                . "🔹 แอปมือถือ เริ่มต้น 30,000฿\n"
                . "🔹 ระบบ AI เริ่มต้น 50,000฿\n\n"
                . "📞 ติดต่อเพิ่มเติม: พิมพ์ 'ติดต่อ'"
            ),
            'ติดต่อ', 'contact' => $this->textMsg(
                "📞 ช่องทางติดต่อ\n\n"
                . "🌐 เว็บ: xman.studio\n"
                . "📧 อีเมล: [email protected]"
            ),
            default => $this->textMsg(
                "ขอบคุณสำหรับข้อความครับ 🙏\n"
                . "พิมพ์ 'เมนู' เพื่อดูรายการคำสั่ง"
            ),
        };

        $this->line->replyMessage($replyToken, [$reply]);
    }

    // มีคนเพิ่มเพื่อน (follow)
    private function handleFollow(array $event): void
    {
        $userId = $event['source']['userId'];
        $profile = $this->line->getProfile($userId);

        // บันทึกลง Database
        LineFollower::updateOrCreate(
            ['line_user_id' => $userId],
            [
                'display_name' => $profile['displayName'] ?? null,
                'picture_url'  => $profile['pictureUrl'] ?? null,
                'status'       => 'active',
            ],
        );

        // ส่งข้อความต้อนรับ
        $this->line->replyMessage($event['replyToken'], [
            $this->textMsg(
                "🎉 ยินดีต้อนรับสู่ XMAN Studio!\n\n"
                . "พิมพ์ 'เมนู' เพื่อดูสิ่งที่เราทำได้"
            ),
        ]);
    }

    // มีคน unfollow
    private function handleUnfollow(array $event): void
    {
        LineFollower::where('line_user_id', $event['source']['userId'])
            ->update(['status' => 'inactive']);
    }

    // Postback จาก Flex Message / Rich Menu
    private function handlePostback(array $event): void
    {
        parse_str($event['postback']['data'], $params);

        match ($params['action'] ?? '') {
            'view_order'  => $this->sendOrderDetail($event, $params),
            'track'       => $this->sendTrackingInfo($event, $params),
            default       => null,
        };
    }

    // Helper: สร้าง Text Message Object
    private function textMsg(string $text): array
    {
        return ['type' => 'text', 'text' => $text];
    }
}
PHP — routes/api.php
// routes/api.php
// API routes จะไม่มี CSRF token อยู่แล้ว
use App\Http\Controllers\LineWebhookController;

Route::post(
    '/webhook/line',
    [LineWebhookController::class, 'handle']
)->name('webhook.line');
PHP — bootstrap/app.php (Laravel 11)
// ถ้าใช้ routes/web.php แทน api.php
// ต้องยกเว้น CSRF สำหรับ Webhook URL
// ======================================

// Laravel 11 — bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(
        except: [
            'webhook/line',
            'webhook/*',  // หรือยกเว้นทั้งหมด
        ]
    );
})

// ======================================
// Laravel 10 — app/Http/Middleware/
//               VerifyCsrfToken.php
// ======================================
protected $except = [
    'webhook/line',
];
⚠️
สำคัญ! Webhook URL ที่ตั้งใน LINE Developers Console ต้องเป็น HTTPS
ตัวอย่าง: https://yoursite.com/api/webhook/line
สำหรับทดสอบในเครื่อง ใช้ ngrok: ngrok http 8000
PHP — Message Objects
// ======================================
// 1) Text Message — ข้อความธรรมดา
// ======================================
$textMsg = [
    'type' => 'text',
    'text' => 'สวัสดีครับ! 🎉',
];

// ======================================
// 2) Text + Emoji
// ======================================
$emojiMsg = [
    'type'   => 'text',
    'text'   => '$ สั่งซื้อสำเร็จ $ ขอบคุณครับ',
    'emojis' => [
        ['index' => 0, 'productId' => '5ac1bfd5040ab15980c9b435',
         'emojiId' => '001'],
        ['index' => 12, 'productId' => '5ac1bfd5040ab15980c9b435',
         'emojiId' => '002'],
    ],
];

// ======================================
// 3) Image Message — ส่งรูปภาพ
// ======================================
$imageMsg = [
    'type'               => 'image',
    'originalContentUrl' => 'https://yoursite.com/img/promo.jpg',
    'previewImageUrl'    => 'https://yoursite.com/img/promo-sm.jpg',
];

// ======================================
// 4) Sticker Message — สติกเกอร์
// ======================================
$stickerMsg = [
    'type'      => 'sticker',
    'packageId' => '446',
    'stickerId' => '1988',
];

// ======================================
// 5) Quick Reply — ปุ่มตอบด่วน
// ======================================
$quickReply = [
    'type' => 'text',
    'text' => 'เลือกสิ่งที่ต้องการ:',
    'quickReply' => [
        'items' => [
            [
                'type'   => 'action',
                'action' => [
                    'type'  => 'message',
                    'label' => '💰 ดูราคา',
                    'text'  => 'ราคา',
                ],
            ],
            [
                'type'   => 'action',
                'action' => [
                    'type'  => 'message',
                    'label' => '📞 ติดต่อ',
                    'text'  => 'ติดต่อ',
                ],
            ],
            [
                'type'   => 'action',
                'action' => [
                    'type'  => 'uri',
                    'label' => '🌐 เว็บไซต์',
                    'uri'   => 'https://xman.studio',
                ],
            ],
        ],
    ],
];
PHP — Flex Message Builder
// สร้าง Flex Message ใบเสร็จคำสั่งซื้อ
public function orderReceiptFlex(Order $order): array
{
    // รายการสินค้า
    $items = $order->items->map(fn ($item) => [
        'type'     => 'box',
        'layout'   => 'horizontal',
        'contents' => [
            [
                'type'   => 'text',
                'text'   => $item->name,
                'size'   => 'sm',
                'color'  => '#555555',
                'flex'   => 0,
            ],
            [
                'type'  => 'text',
                'text'  => "฿" . number_format($item->price, 2),
                'size'  => 'sm',
                'color' => '#111111',
                'align' => 'end',
            ],
        ],
    ])->toArray();

    return [
        'type'    => 'flex',
        'altText' => "ใบเสร็จ #{$order->order_number}",
        'contents' => [
            'type' => 'bubble',
            'size' => 'mega',

            // === Header ===
            'header' => [
                'type'            => 'box',
                'layout'          => 'vertical',
                'backgroundColor' => '#0ea5e9',
                'paddingAll'      => '20px',
                'contents'        => [
                    [
                        'type'   => 'text',
                        'text'   => 'XMAN STUDIO',
                        'color'  => '#ffffff',
                        'size'   => 'lg',
                        'weight' => 'bold',
                    ],
                    [
                        'type'  => 'text',
                        'text'  => 'ใบเสร็จรับเงิน',
                        'color' => '#ffffffcc',
                        'size'  => 'xs',
                    ],
                ],
            ],

            // === Body (รายการสินค้า) ===
            'body' => [
                'type'     => 'box',
                'layout'   => 'vertical',
                'contents' => [
                    [
                        'type'   => 'text',
                        'text'   => "Order #{$order->order_number}",
                        'weight' => 'bold',
                        'size'   => 'xl',
                    ],
                    ['type' => 'separator', 'margin' => 'lg'],

                    // รายการสินค้า
                    [
                        'type'     => 'box',
                        'layout'   => 'vertical',
                        'margin'   => 'lg',
                        'spacing'  => 'sm',
                        'contents' => $items,
                    ],

                    ['type' => 'separator', 'margin' => 'lg'],

                    // ยอมรวม
                    [
                        'type'     => 'box',
                        'layout'   => 'horizontal',
                        'margin'   => 'lg',
                        'contents' => [
                            [
                                'type'   => 'text',
                                'text'   => 'รวมทั้งหมด',
                                'size'   => 'md',
                                'weight' => 'bold',
                            ],
                            [
                                'type'   => 'text',
                                'text'   => "฿" . number_format(
                                    $order->total, 2
                                ),
                                'size'   => 'lg',
                                'weight' => 'bold',
                                'color'  => '#0ea5e9',
                                'align'  => 'end',
                            ],
                        ],
                    ],
                ],
            ],

            // === Footer (ปุ่มกด) ===
            'footer' => [
                'type'     => 'box',
                'layout'   => 'vertical',
                'spacing'  => 'sm',
                'contents' => [
                    [
                        'type'   => 'button',
                        'style'  => 'primary',
                        'color'  => '#0ea5e9',
                        'action' => [
                            'type'  => 'uri',
                            'label' => 'ดูรายละเอียดคำสั่งซื้อ',
                            'uri'   => route(
                                'orders.show',
                                $order
                            ),
                        ],
                    ],
                ],
            ],
        ],
    ];
}
PHP — app/Notifications/OrderPaidNotification.php
<?php

namespace App\Notifications;

use App\Channels\LineChannel;
use Illuminate\Notifications\Notification;

class OrderPaidNotification extends Notification
{
    public function __construct(
        public readonly Order $order,
    ) {}

    // กำหนดช่องทางส่ง
    public function via($notifiable): array
    {
        return ['mail', LineChannel::class];
    }

    // ข้อความสำหรับ LINE
    public function toLine($notifiable): array
    {
        return [
            [
                'type' => 'text',
                'text' => "✅ ชำระเงินสำเร็จ!\n\n"
                    . "📦 คำสั่งซื้อ: #{$this->order->order_number}\n"
                    . "💰 ยอดชำระ: ฿" . number_format(
                        $this->order->total, 2
                    ) . "\n"
                    . "📅 วันที่: " . now()->format('d/m/Y H:i')
                    . "\n\nขอบคุณที่ใช้บริการครับ 🙏",
            ],
        ];
    }
}
PHP — app/Channels/LineChannel.php
<?php

namespace App\Channels;

use App\Services\LineService;
use Illuminate\Notifications\Notification;

class LineChannel
{
    public function __construct(
        private readonly LineService $line,
    ) {}

    public function send(
        $notifiable,
        Notification $notification,
    ): void {
        // ดึง LINE User ID จาก Model
        $lineUserId = $notifiable->routeNotificationFor(
            'line'
        );

        if (!$lineUserId) {
            return;
        }

        $messages = $notification->toLine($notifiable);
        $this->line->pushMessage($lineUserId, $messages);
    }
}
PHP — app/Models/User.php (เพิ่ม method)
// เพิ่มใน User Model
public function routeNotificationForLine(): ?string
{
    return $this->line_user_id; // คอลัมน์ในตาราง users
}

// การใช้งาน
$user->notify(new OrderPaidNotification($order));
PHP — Rich Menu API
// ======================================
// สร้าง Rich Menu (ทำครั้งเดียว)
// ======================================
public function createRichMenu(): ?string
{
    $response = Http::withToken($this->accessToken)
        ->post('{$this->apiBase}/richmenu', [
            'size' => [
                'width'  => 2500,
                'height' => 1686, // หรือ 843
            ],
            'selected'    => true,
            'name'        => 'Main Menu',
            'chatBarText' => 'เมนูหลัก',
            'areas' => [
                // ปุ่มซ้ายบน — ดูสินค้า
                [
                    'bounds' => [
                        'x' => 0, 'y' => 0,
                        'width' => 833, 'height' => 843,
                    ],
                    'action' => [
                        'type'  => 'uri',
                        'uri'   => 'https://yoursite.com/products',
                        'label' => 'ดูสินค้า',
                    ],
                ],
                // ปุ่มกลางบน — ดูราคา
                [
                    'bounds' => [
                        'x' => 833, 'y' => 0,
                        'width' => 834, 'height' => 843,
                    ],
                    'action' => [
                        'type'  => 'message',
                        'text'  => 'ราคา',
                        'label' => 'ดูราคา',
                    ],
                ],
                // ปุ่มขวาบน — ติดต่อเรา
                [
                    'bounds' => [
                        'x' => 1667, 'y' => 0,
                        'width' => 833, 'height' => 843,
                    ],
                    'action' => [
                        'type'  => 'message',
                        'text'  => 'ติดต่อ',
                        'label' => 'ติดต่อเรา',
                    ],
                ],
            ],
        ]);

    return $response->json('richMenuId');
}

// อัพโหลดรูป Rich Menu (2500x1686 px)
public function uploadRichMenuImage(
    string $richMenuId,
    string $imagePath,
): bool {
    $response = Http::withToken($this->accessToken)
        ->withHeaders([
            'Content-Type' => 'image/png',
        ])
        ->withBody(
            file_get_contents($imagePath), 'image/png'
        )
        ->post(
            "https://api-data.line.me/v2/bot/richmenu"
            . "/{$richMenuId}/content"
        );

    return $response->successful();
}

// ตั้งเป็น Default สำหรับทุกคน
public function setDefaultRichMenu(
    string $richMenuId,
): bool {
    return Http::withToken($this->accessToken)
        ->post(
            "{$this->apiBase}/user/all/richmenu/{$richMenuId}"
        )->successful();
}
PHP — app/Listeners/SendLineOrderNotification.php
<?php

namespace App\Listeners;

use App\Events\OrderPaid;
use App\Services\LineService;
use Illuminate\Contracts\Queue\ShouldQueue;

// ใช้ Queue เพื่อไม่ให้ block ผู้ใช้
class SendLineOrderNotification
    implements ShouldQueue
{
    public int $tries = 3;
    public int $backoff = 10; // วินาที

    public function __construct(
        private readonly LineService $line,
    ) {}

    public function handle(OrderPaid $event): void
    {
        $order = $event->order;
        $user  = $order->user;

        // ส่งแจ้งลูกค้า
        if ($user->line_user_id) {
            $this->line->pushMessage(
                $user->line_user_id,
                [$this->orderReceiptFlex($order)]
            );
        }

        // ส่งแจ้ง Admin (Broadcast กลุ่ม admin)
        $adminLineId = config('services.line.admin_user_id');
        if ($adminLineId) {
            $this->line->pushMessage($adminLineId, [
                [
                    'type' => 'text',
                    'text' => "🔔 คำสั่งซื้อใหม่!\n\n"
                        . "📦 #{$order->order_number}\n"
                        . "👤 {$user->name}\n"
                        . "💰 ฿" . number_format(
                            $order->total, 2
                        ),
                ],
            ]);
        }
    }
}
PHP — app/Providers/EventServiceProvider.php
// ลงทะเบียน Event → Listener
protected $listen = [
    OrderPaid::class => [
        SendLineOrderNotification::class,
        SendEmailReceipt::class,
        UpdateInventory::class,
    ],
    OrderShipped::class => [
        SendLineShippingNotification::class,
    ],
    LicenseActivated::class => [
        SendLineLicenseInfo::class,
    ],
];
PHP — LINE Notify (ง่ายมาก!)
// ======================================
// LINE Notify — ส่งแจ้งเตือนแบบง่ายที่สุด
// ขอ Token: https://notify-bot.line.me/
// ======================================

// .env
// LINE_NOTIFY_TOKEN=your_token_here

class LineNotifyService
{
    private const API_URL =
        'https://notify-api.line.me/api/notify';

    public function send(
        string  $message,
        ?string $imageUrl = null,
        ?string $stickerPackageId = null,
        ?string $stickerId = null,
    ): bool {
        $data = ['message' => $message];

        if ($imageUrl) {
            $data['imageThumbnail'] = $imageUrl;
            $data['imageFullsize']  = $imageUrl;
        }

        if ($stickerPackageId && $stickerId) {
            $data['stickerPackageId'] = $stickerPackageId;
            $data['stickerId']        = $stickerId;
        }

        return Http::asForm()
            ->withToken(config('services.line.notify_token'))
            ->post(self::API_URL, $data)
            ->successful();
    }
}

// ======================================
// ตัวอย่างการใช้งาน
// ======================================
$notify = app(LineNotifyService::class);

// แจ้งข้อความธรรมดา
$notify->send("\n🛒 คำสั่งซื้อใหม่ #1234\n💰 ฿5,990");

// แจ้งพร้อมรูปภาพ
$notify->send(
    "\n📦 สินค้าจัดส่งแล้ว",
    imageUrl: 'https://yoursite.com/tracking.jpg',
);

// แจ้งพร้อมสติกเกอร์
$notify->send(
    "\n✅ ชำระเงินเรียบร้อย",
    stickerPackageId: '446',
    stickerId: '1988',
);
PHP — app/Console/Commands/LineBroadcast.php
<?php

namespace App\Console\Commands;

use App\Services\LineService;
use Illuminate\Console\Command;

class LineBroadcast extends Command
{
    protected $signature =
        'line:broadcast {message : ข้อความที่จะส่ง}';

    protected $description =
        'ส่งข้อความ LINE ถึงผู้ติดตามทุกคน';

    public function handle(LineService $line): int
    {
        $message = $this->argument('message');

        if (!$this->confirm("ส่ง: \"{$message}\" ?")) {
            $this->info('ยกเลิก');
            return 0;
        }

        $success = $line->broadcast([
            ['type' => 'text', 'text' => $message],
        ]);

        $success
            ? $this->info('✅ ส่งเรียบร้อย!')
            : $this->error('❌ ส่งไม่สำเร็จ');

        return $success ? 0 : 1;
    }
}

// ======================================
// ใช้งาน:
// php artisan line:broadcast "สวัสดีปีใหม่!"
// ======================================
PHP — app/Console/Commands/LineExpiryReminder.php
class LineExpiryReminder extends Command
{
    protected $signature = 'line:expiry-reminder';
    protected $description = 'แจ้ง License ใกล้หมดอายุ';

    public function handle(LineService $line): int
    {
        // หา License ที่จะหมดอายุใน 7 วัน
        $expiring = License::with('user')
            ->where('status', 'active')
            ->whereBetween('expires_at', [
                now(),
                now()->addDays(7),
            ])
            ->get();

        foreach ($expiring as $license) {
            $user = $license->user;
            if (!$user->line_user_id) continue;

            $days = now()->diffInDays($license->expires_at);

            $line->pushMessage($user->line_user_id, [
                [
                    'type' => 'text',
                    'text' => "⏰ แจ้งเตือน License ใกล้หมดอายุ\n\n"
                        . "🔑 {$license->product->name}\n"
                        . "📅 หมดอายุอีก {$days} วัน\n"
                        . "({$license->expires_at->format('d/m/Y')})\n\n"
                        . "ต่ออายุได้ที่ xman.studio/licenses",
                ],
            ]);

            $this->info("ส่งถึง {$user->name}");
        }

        $this->info("ส่งทั้งหมด {$expiring->count()} คน");
        return 0;
    }
}
PHP — routes/console.php (Laravel 11)
// ตั้งเวลาให้ทำงานทุกวันตอน 9 โมงเช้า
Schedule::command('line:expiry-reminder')
    ->dailyAt('09:00')
    ->timezone('Asia/Bangkok');

// รายงานยอดขายรายวันให้ Admin
Schedule::command('line:daily-report')
    ->dailyAt('20:00')
    ->timezone('Asia/Bangkok');

// ======================================
// อย่าลืม! ตั้ง cron บน server:
// * * * * * cd /path && php artisan schedule:run
// ======================================

สรุป: เลือกใช้วิธีไหนดี? / Which approach to use?

LINE Notify
ง่ายที่สุด, ใช้ได้เลย
  • - แจ้งเตือนทีมงาน
  • - Server monitoring
  • - ไม่ต้องมี OA
  • - ส่งได้ 1,000 ครั้ง/ชม.
Messaging API (Push)
ยืดหยุ่น, หลายฟีเจอร์
  • - แจ้งลูกค้ารายคน
  • - Flex Message สวยๆ
  • - Rich Menu
  • - Push 500 msg/req
Webhook (Reply)
Chatbot, Auto-reply
  • - ตอบกลับอัตโนมัติ
  • - Chatbot สั่งงานได้
  • - ฟรี! ไม่จำกัดจำนวน
  • - ต้องมี HTTPS domain

Apps Script + ระบบจองคิว + LINE

ระบบจองคิวออนไลน์ผ่าน Google Forms/Sheets + แจ้งเตือน LINE อัตโนมัติ

Flow ทั้งระบบ — End-to-End Architecture

1 ลูกค้ากรอกฟอร์มจอง
(เว็บ / Google Forms)
2 บันทึกลง Google Sheets
(หรือ Database)
3 Apps Script Trigger
ดักจับข้อมูลใหม่
4 ส่งแจ้งเตือน LINE
(ลูกค้า + แอดมิน)
5 ส่งเตือนก่อนถึงคิว
(Cron / Time Trigger)

เหมาะสำหรับ: คลินิก, ร้านเสริมสวย, ร้านซ่อม, ร้านอาหาร, สำนักงาน — ทุกธุรกิจที่มีระบบจองคิว

Google Apps Script — Code.gs
// =============================================
// Google Apps Script — ส่งแจ้งเตือน LINE
// เมื่อมีการจองคิวใหม่จาก Google Forms
// =============================================

// ⚙️ ตั้งค่า — แก้ไขตรงนี้
const LINE_NOTIFY_TOKEN = 'YOUR_LINE_NOTIFY_TOKEN';
const LINE_NOTIFY_URL   = 'https://notify-api.line.me/api/notify';

/**
 * Trigger: ทำงานเมื่อมีการส่ง Google Form
 * ตั้งค่า: Extensions → Apps Script → Triggers
 *         → เลือก onFormSubmit → From spreadsheet
 *         → On form submit
 */
function onFormSubmit(e) {
  const values    = e.values;
  const timestamp = values[0]; // Timestamp
  const name      = values[1]; // ชื่อผู้จอง
  const phone     = values[2]; // เบอร์โทร
  const service   = values[3]; // บริการที่เลือก
  const date      = values[4]; // วันที่จอง
  const time      = values[5]; // เวลาที่จอง
  const note      = values[6] || '-'; // หมายเหตุ

  // สร้างหมายเลขคิว
  const sheet   = SpreadsheetApp.getActiveSheet();
  const queueNo = 'Q-' + String(
    sheet.getLastRow()
  ).padStart(4, '0');

  // เขียนหมายเลขคิวกลับลง Sheet (คอลัมน์ H)
  sheet.getRange(
    sheet.getLastRow(), 8
  ).setValue(queueNo);

  // สร้างข้อความ LINE
  const message = `
🔔 การจองคิวใหม่!

📋 หมายเลขคิว: ${queueNo}
👤 ชื่อ: ${name}
📱 โทร: ${phone}
💼 บริการ: ${service}
📅 วันที่: ${date}
⏰ เวลา: ${time}
📝 หมายเหตุ: ${note}

⏱️ จองเมื่อ: ${timestamp}`;

  // ส่ง LINE Notify
  sendLineNotify(message);

  // (Optional) ส่ง Email ยืนยันให้ลูกค้า
  const email = values[7]; // ถ้ามีช่อง email
  if (email) {
    MailApp.sendEmail({
      to: email,
      subject: `ยืนยันการจองคิว ${queueNo}`,
      body: `สวัสดีคุณ ${name}\n\n`
        + `หมายเลขคิวของคุณคือ: ${queueNo}\n`
        + `บริการ: ${service}\n`
        + `วันที่: ${date} เวลา ${time}\n\n`
        + `ขอบคุณที่ใช้บริการครับ`,
    });
  }
}
Google Apps Script — LINE Notify
/**
 * ส่งข้อความผ่าน LINE Notify
 * @param {string} message ข้อความที่จะส่ง
 * @param {string} [imageUrl] URL รูปภาพ (optional)
 * @returns {boolean} สำเร็จหรือไม่
 */
function sendLineNotify(message, imageUrl) {
  const payload = { message: message };

  if (imageUrl) {
    payload.imageThumbnail = imageUrl;
    payload.imageFullsize  = imageUrl;
  }

  const options = {
    method:  'post',
    headers: {
      'Authorization': `Bearer ${LINE_NOTIFY_TOKEN}`,
    },
    payload: payload,
    muteHttpExceptions: true,
  };

  try {
    const response = UrlFetchApp.fetch(
      LINE_NOTIFY_URL, options
    );
    const code = response.getResponseCode();

    if (code !== 200) {
      Logger.log(
        `LINE Error: ${code} - ${response.getContentText()}`
      );
      return false;
    }

    Logger.log('LINE Notify ส่งสำเร็จ');
    return true;
  } catch (error) {
    Logger.log(`LINE Error: ${error.message}`);
    return false;
  }
}

/**
 * ส่งข้อความผ่าน LINE Messaging API (Push)
 * สำหรับส่งหาลูกค้ารายคนที่รู้ userId
 */
function sendLinePush(userId, message) {
  const ACCESS_TOKEN =
    'YOUR_CHANNEL_ACCESS_TOKEN';

  const payload = {
    to: userId,
    messages: [
      { type: 'text', text: message }
    ],
  };

  UrlFetchApp.fetch(
    'https://api.line.me/v2/bot/message/push',
    {
      method:      'post',
      contentType: 'application/json',
      headers: {
        'Authorization': `Bearer ${ACCESS_TOKEN}`
      },
      payload: JSON.stringify(payload),
    }
  );
}
Google Apps Script — Reminder
/**
 * ส่งเตือนคิวที่จะมาถึงใน 1 ชั่วโมง
 * ตั้ง Time-driven trigger: ทุก 30 นาที
 *
 * Triggers → Add Trigger:
 *   Function: checkUpcomingQueues
 *   Event: Time-driven → Minutes timer → 30 min
 */
function checkUpcomingQueues() {
  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName('การจอง');
  const data  = sheet.getDataRange().getValues();
  const now   = new Date();

  // เริ่มจากแถว 2 (ข้ามหัวตาราง)
  for (let i = 1; i < data.length; i++) {
    const row        = data[i];
    const name       = row[1]; // ชื่อ
    const phone      = row[2]; // เบอร์
    const service    = row[3]; // บริการ
    const dateStr    = row[4]; // วันที่
    const timeStr    = row[5]; // เวลา
    const queueNo   = row[7]; // หมายเลขคิว
    const reminded  = row[8]; // เคยเตือนแล้ว?

    if (reminded === '✅') continue; // ข้าม

    // รวมวันที่+เวลา
    const appointmentDate = parseDateTime(
      dateStr, timeStr
    );
    if (!appointmentDate) continue;

    // คำนวณเวลาที่เหลือ (นาที)
    const diffMs  = appointmentDate - now;
    const diffMin = diffMs / 60000;

    // เตือนเมื่อเหลืออีก 30-90 นาที
    if (diffMin > 30 && diffMin <= 90) {
      const msg = `
⏰ แจ้งเตือนคิวของคุณ!

📋 คิว: ${queueNo}
👤 คุณ ${name}
💼 บริการ: ${service}
📅 ${dateStr} เวลา ${timeStr}

🕐 อีกประมาณ ${Math.round(diffMin)} นาที
กรุณามาถึงก่อนเวลา 10 นาทีครับ 🙏`;

      sendLineNotify(msg);

      // บันทึกว่าเตือนแล้ว (คอลัมน์ I)
      sheet.getRange(i + 1, 9).setValue('✅');
    }
  }
}

/**
 * Parse วันที่ + เวลา → Date object
 * รองรับ: "31/03/2026", "14:30"
 */
function parseDateTime(dateStr, timeStr) {
  try {
    const [d, m, y] = String(dateStr).split('/');
    const [h, min] = String(timeStr).split(':');
    return new Date(y, m - 1, d, h, min);
  } catch (e) {
    return null;
  }
}
Google Apps Script — Daily Report
/**
 * สรุปการจองของวันถัดไป
 * ตั้ง Trigger: Time-driven → Day timer → 8am-9am
 */
function sendDailySummary() {
  const sheet    = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName('การจอง');
  const data     = sheet.getDataRange().getValues();
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);

  const tmrStr = Utilities.formatDate(
    tomorrow,
    'Asia/Bangkok',
    'dd/MM/yyyy'
  );

  // กรองเฉพาะจองของพรุ่งนี้
  const bookings = data.filter((row, i) =>
    i > 0 && formatDate(row[4]) === tmrStr
  );

  if (bookings.length === 0) {
    sendLineNotify(
      `\n📅 สรุปวันพรุ่งนี้ (${tmrStr})\n\n`
      + `✨ ไม่มีการจอง — วันว่าง!`
    );
    return;
  }

  // จัดเรียงตามเวลา
  bookings.sort((a, b) =>
    String(a[5]).localeCompare(String(b[5]))
  );

  // สร้างข้อความ
  let msg = `\n📅 สรุปวันพรุ่งนี้ (${tmrStr})`
    + `\n📊 ทั้งหมด ${bookings.length} คิว\n`;

  bookings.forEach((row, idx) => {
    msg += `\n${idx + 1}. ⏰ ${row[5]}`
      + ` | 👤 ${row[1]}`
      + ` | 💼 ${row[3]}`;
  });

  sendLineNotify(msg);
}

// Helper: format Date object เป็น dd/MM/yyyy
function formatDate(date) {
  if (date instanceof Date) {
    return Utilities.formatDate(
      date, 'Asia/Bangkok', 'dd/MM/yyyy'
    );
  }
  return String(date);
}
Google Apps Script — Web App API
/**
 * Deploy → Manage deployments → New → Web app
 * Execute as: Me
 * Who has access: Anyone
 *
 * เว็บไซต์สามารถเรียก POST ไปที่ URL นี้
 * เพื่อจองคิวโดยไม่ต้องผ่าน Google Forms
 */
function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);

    const sheet = SpreadsheetApp
      .getActiveSpreadsheet()
      .getSheetByName('การจอง');

    // สร้างหมายเลขคิว
    const queueNo = 'Q-' + String(
      sheet.getLastRow() + 1
    ).padStart(4, '0');

    // เขียนลง Sheet
    sheet.appendRow([
      new Date(),      // Timestamp
      data.name,       // ชื่อ
      data.phone,      // เบอร์โทร
      data.service,    // บริการ
      data.date,       // วันที่
      data.time,       // เวลา
      data.note || '', // หมายเหตุ
      queueNo,        // หมายเลขคิว
    ]);

    // แจ้งเตือน LINE
    sendLineNotify(`
🔔 จองคิวใหม่จากเว็บไซต์!

📋 คิว: ${queueNo}
👤 ${data.name}
📱 ${data.phone}
💼 ${data.service}
📅 ${data.date} ⏰ ${data.time}`);

    // ส่งกลับ JSON
    return ContentService
      .createTextOutput(JSON.stringify({
        success: true,
        queueNo: queueNo,
        message: 'จองคิวสำเร็จ',
      }))
      .setMimeType(
        ContentService.MimeType.JSON
      );

  } catch (error) {
    return ContentService
      .createTextOutput(JSON.stringify({
        success: false,
        message: error.message,
      }))
      .setMimeType(
        ContentService.MimeType.JSON
      );
  }
}

// GET — ดูคิวของวันนี้ (JSON)
function doGet(e) {
  const date = e.parameter.date
    || Utilities.formatDate(
      new Date(), 'Asia/Bangkok', 'dd/MM/yyyy'
    );

  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName('การจอง');
  const data = sheet.getDataRange().getValues();

  const queues = data
    .filter((r, i) =>
      i > 0 && formatDate(r[4]) === date
    )
    .map(r => ({
      queue:   r[7],
      name:    r[1],
      service: r[3],
      time:    r[5],
    }));

  return ContentService
    .createTextOutput(JSON.stringify({
      date, total: queues.length, queues,
    }))
    .setMimeType(
      ContentService.MimeType.JSON
    );
}
PHP — app/Services/QueueBookingService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class QueueBookingService
{
    private string $appsScriptUrl;

    public function __construct()
    {
        // URL ของ Apps Script Web App
        $this->appsScriptUrl = config(
            'services.google.apps_script_url'
        );
    }

    /**
     * จองคิวจากเว็บไซต์
     * → ส่งไป Google Sheets + แจ้ง LINE อัตโนมัติ
     */
    public function createBooking(array $data): array
    {
        $response = Http::timeout(15)
            ->post($this->appsScriptUrl, [
                'name'    => $data['name'],
                'phone'   => $data['phone'],
                'service' => $data['service'],
                'date'    => $data['date'],
                'time'    => $data['time'],
                'note'    => $data['note'] ?? '',
            ]);

        if ($response->failed()) {
            Log::error('Queue booking failed', [
                'status' => $response->status(),
            ]);

            return [
                'success' => false,
                'message' => 'จองคิวไม่สำเร็จ กรุณาลองใหม่',
            ];
        }

        return $response->json();
    }

    /**
     * ดึงคิววันนี้ (สำหรับแสดงผลบนเว็บ)
     */
    public function getTodayQueues(): array
    {
        $today = now()->format('d/m/Y');

        $response = Http::timeout(10)
            ->get($this->appsScriptUrl, [
                'date' => $today,
            ]);

        return $response->successful()
            ? $response->json()
            : ['total' => 0, 'queues' => []];
    }
}
PHP — QueueBookingController.php
class QueueBookingController extends Controller
{
    public function __construct(
        private readonly QueueBookingService $booking,
        private readonly LineService $line,
    ) {}

    // หน้าฟอร์มจองคิว
    public function create()
    {
        $todayQueues = $this->booking->getTodayQueues();

        return view('queue.create', [
            'todayTotal' => $todayQueues['total'],
        ]);
    }

    // บันทึกการจอง
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name'    => 'required|string|max:100',
            'phone'   => 'required|string|max:20',
            'service' => 'required|string',
            'date'    => 'required|date|after:today',
            'time'    => 'required|string',
            'note'    => 'nullable|string|max:500',
        ]);

        // ส่งไป Google Sheets (ผ่าน Apps Script)
        $result = $this->booking->createBooking($validated);

        if (!$result['success']) {
            return back()
                ->withErrors(['error' => $result['message']])
                ->withInput();
        }

        // (Optional) ส่ง LINE Push ตรงถึงลูกค้า
        // ถ้าลูกค้าเชื่อม LINE กับ Account
        if (auth()->user()?->line_user_id) {
            $this->line->pushMessage(
                auth()->user()->line_user_id,
                [[
                    'type' => 'text',
                    'text' => "✅ จองคิวสำเร็จ!\n\n"
                        . "📋 คิว: {$result['queueNo']}\n"
                        . "💼 {$validated['service']}\n"
                        . "📅 {$validated['date']}\n"
                        . "⏰ {$validated['time']}\n\n"
                        . "เราจะแจ้งเตือนก่อนถึงคิวครับ 🔔",
                ]],
            );
        }

        return redirect()
            ->route('queue.success')
            ->with('queueNo', $result['queueNo']);
    }
}
Setup Guide — Step by Step
╔══════════════════════════════════════════╗
║  ขั้นตอนที่ 1: ขอ LINE Notify Token      ║
╚══════════════════════════════════════════╝

1. ไปที่ https://notify-bot.line.me/
2. Login ด้วย LINE Account
3. My Page → Generate Token
4. เลือกห้องแชทที่จะรับแจ้งเตือน
      - "1-on-1 chat" = ส่งมาหาตัวเอง
      - เลือก Group = ส่งเข้ากลุ่ม
5. Copy Token เก็บไว้

╔══════════════════════════════════════════╗
║  ขั้นตอนที่ 2: สร้าง Google Sheets        ║
╚══════════════════════════════════════════╝

1. สร้าง Google Sheets ใหม่
2. ตั้งชื่อ Sheet = "การจอง"
3. สร้างหัวตาราง Row 1:
   A: Timestamp
   B: ชื่อ
   C: เบอร์โทร
   D: บริการ
   E: วันที่
   F: เวลา
   G: หมายเหตุ
   H: หมายเลขคิว
   I: แจ้งเตือน

╔══════════════════════════════════════════╗
║  ขั้นตอนที่ 3: เพิ่ม Apps Script          ║
╚══════════════════════════════════════════╝

1. Extensions → Apps Script
2. ลบโค้ดเดิม → วาง Code.gs จากด้านบน
3. แก้ LINE_NOTIFY_TOKEN = Token ของคุณ
4. กด Save (Ctrl+S)

╔══════════════════════════════════════════╗
║  ขั้นตอนที่ 4: ตั้ง Trigger               ║
╚══════════════════════════════════════════╝

1. คลิก ⏰ (Triggers) ที่เมนูซ้าย
2. + Add Trigger:
   Function: onFormSubmit
   Event source: From spreadsheet
   Event type: On form submit
3. + Add Trigger อีกตัว:
   Function: checkUpcomingQueues
   Event source: Time-driven
   Type: Minutes timer → 30 minutes
4. + Add Trigger อีกตัว:
   Function: sendDailySummary
   Event source: Time-driven
   Type: Day timer → 8am to 9am

╔══════════════════════════════════════════╗
║  ขั้นตอนที่ 5: (Optional) Deploy Web App ║
╚══════════════════════════════════════════╝

1. Deploy → New deployment
2. Type: Web app
3. Execute as: Me
4. Who has access: Anyone
5. Copy URL → ใส่ใน Laravel .env:
   GOOGLE_APPS_SCRIPT_URL=https://script.google.com/...

╔══════════════════════════════════════════╗
║  ทดสอบ: ส่ง Google Form → ดู LINE! 🎉     ║
╚══════════════════════════════════════════╝

XMAN Code Academy © 2026 — แหล่งเรียนรู้โค้ดมืออาชีพ

AI Assistant
ออนไลน์