Skip to main content

Routing

Framework sử dụng PHP Attributes để định nghĩa routes trực tiếp trên controller methods. Routes được parse bằng Reflection và dispatch bằng FastRoute.

Route Attributes

Mỗi public method trong controller có thể gắn một route attribute:

<?php

namespace Vietiso\Modules\User\Controllers;

use Vietiso\Core\Route\Attributes\Get;
use Vietiso\Core\Route\Attributes\Post;
use Vietiso\Core\Route\Attributes\Put;
use Vietiso\Core\Route\Attributes\Delete;

class UserController
{
#[Get(uri: '')]
public function index(Request $request) {}

#[Get(uri: '{id:\d+}')]
public function show(int $id) {}

#[Post(uri: '')]
public function store(CreateUserDTO $dto) {}

#[Put(uri: '{id:\d+}')]
public function update(int $id, UpdateUserDTO $dto) {}

#[Delete(uri: '{id:\d+}')]
public function destroy(int $id) {}
}

Tham số đầy đủ của Route Attribute

Tất cả HTTP method attributes (Get, Post, Put, Delete, Patch, Head) đều có chung signature:

#[Get(
uri: '/users', // Bắt buộc: URI path
name: 'users.index', // Route name (dùng cho URL generation)
summary: 'Danh sách users', // OpenAPI summary
description: 'Lấy danh sách tất cả users trong hệ thống.', // Mô tả chi tiết
deprecated: false, // Đánh dấu API đã deprecated
middlewares: [AuthMiddleware::class], // Thêm middleware cho route này
excludedMiddlewares: [], // Loại bỏ middleware từ group/global
responses: [], // Định nghĩa responses cho API docs
)]

Route Attribute cho nhiều HTTP Methods

Dùng #[Route] khi cần match nhiều methods cùng lúc:

use Vietiso\Core\Route\Attributes\Route;

#[Route(
methods: ['GET', 'POST'], // Bắt buộc: một hoặc nhiều HTTP methods
uri: '/webhook',
description: 'Handle webhook callbacks.',
)]
public function webhook(Request $request) {}

HTTP Methods

AttributeHTTP Method
#[Get]GET
#[Post]POST
#[Put]PUT
#[Patch]PATCH
#[Delete]DELETE
#[Head]HEAD
#[Route]Custom (một hoặc nhiều)

Group - Nhóm Routes trên Controller

Dùng #[Group] trên class để gom tất cả routes với chung prefix, middleware và metadata:

<?php

use Vietiso\Core\Route\Attributes\Group;
use Vietiso\Core\Route\Attributes\Get;
use Vietiso\Core\Route\Attributes\Post;
use Vietiso\Core\Route\Attributes\Delete;
use Vietiso\Core\Route\Attributes\Patch;

#[Group(
prefix: 'api/tour',
middlewares: [AuthMiddleware::class],
)]
class TourController
{
#[Get(uri: '')] // => /api/tour
public function index() {}

#[Post(uri: '')] // => /api/tour
public function store() {}

#[Get(uri: '{tour:\d+}')] // => /api/tour/{tour}
public function show() {}

#[Delete(uri: '{tour:\d+}')] // => /api/tour/{tour}
public function destroy() {}
}

Tham số đầy đủ của Group

#[Group(
prefix: 'api/booking', // URL prefix cho tất cả routes
name: 'booking.', // Prefix cho route names
summary: 'Booking API', // OpenAPI summary
description: 'Quản lý đặt phòng.', // Mô tả
deprecated: false, // true = bỏ qua toàn bộ controller
middlewares: [AuthMiddleware::class], // Middleware mặc định cho tất cả routes
excludedMiddlewares: [], // Middleware loại bỏ mặc định
)]
class BookingController { }

Prefix hoàn chỉnh

Prefix cuối cùng của route được ghép từ: module prefix + group prefix + route URI:

Module prefix:  bookings          (từ .module.php → 'prefix' => 'bookings')
Group prefix: api/booking (từ #[Group(prefix: 'api/booking')])
Route URI: {id:\d+}/items (từ #[Get(uri: '{id:\d+}/items')])

→ Final: /bookings/api/booking/{id}/items

Route Parameters

Tham số cơ bản

#[Get(uri: '{id}')]
public function show(int $id) {}

Regex Constraints

Dùng regex trực tiếp trong URI để validate tham số:

// Chỉ chấp nhận số
#[Get(uri: '{id:\d+}')]
public function show(int $id) {}

// Chỉ chấp nhận slug
#[Get(uri: '{slug:[a-z0-9-]+}')]
public function showBySlug(string $slug) {}

// Nhiều tham số với constraints
#[Get(uri: '{userId:\d+}/posts/{postId:\d+}')]
public function showPost(int $userId, int $postId) {}

Optional Segments

Sử dụng [...] cho phần tùy chọn:

#[Get(uri: '/users[/{id:\d+}]')]
public function index(?int $id = null) {}
// Match: /users và /users/123

Middleware trên Route

Middleware từ Group

Middleware khai báo trong #[Group] áp dụng cho tất cả routes trong controller:

#[Group(
prefix: 'api/booking',
middlewares: [AuthMiddleware::class],
)]
class BookingController
{
// AuthMiddleware áp dụng
#[Get(uri: '')]
public function index(Request $request) {}

// AuthMiddleware áp dụng
#[Post(uri: '')]
public function store(BookingDTO $dto) {}
}

Thêm Middleware cho Route cụ thể

#[Group(prefix: 'api/user')]
class UserController
{
// Thêm AuthMiddleware chỉ cho route này
#[Get(
uri: '',
middlewares: [AuthMiddleware::class],
)]
public function index(Request $request) {}

// Không có middleware đặc biệt
#[Post(uri: 'login')]
public function login(LoginDTO $loginDTO) {}
}

Loại bỏ Middleware cho Route cụ thể

#[Group(
prefix: 'api/booking',
middlewares: [AuthMiddleware::class],
)]
class BookingController
{
// AuthMiddleware áp dụng (từ Group)
#[Post(uri: '')]
public function store(BookingDTO $dto) {}

// Thay thế AuthMiddleware bằng AuthCustomer
#[Get(
uri: 'public',
middlewares: [AuthCustomer::class],
excludedMiddlewares: [AuthMiddleware::class],
)]
public function publicList(Request $request) {}

// Loại bỏ hoàn toàn auth
#[Get(
uri: 'categories',
excludedMiddlewares: [AuthMiddleware::class],
)]
public function categories() {}
}

Thứ tự merge Middleware

Global middlewares (đăng ký trong file vietiso)
+ Module middlewares (đăng ký cho module cụ thể)
+ Group middlewares (từ #[Group(middlewares: [...])])
+ Route middlewares (từ #[Get(middlewares: [...])])
- Excluded middlewares (từ #[Group/Get(excludedMiddlewares: [...])])

Named Routes & URL Generation

Đặt tên Route

#[Group(
prefix: 'api/tour',
name: 'tour.', // Prefix cho tên route
)]
class TourController
{
#[Get(uri: '', name: 'index')]
// Tên cuối cùng: tour.index
public function index() {}

#[Get(uri: '{id:\d+}', name: 'show')]
// Tên cuối cùng: tour.show
public function show(int $id) {}
}

Tạo URL từ Route Name

// Sử dụng helper route()
$url = route('tour.index');
// => http://localhost:9501/api/tour

$url = route('tour.show', ['id' => 5]);
// => http://localhost:9501/api/tour/5

// Thêm query params
$url = route('tour.index', ['page' => 2, 'limit' => 10]);
// => http://localhost:9501/api/tour?page=2&limit=10

// Relative URL (không có domain)
$url = route('tour.show', ['id' => 5], absolute: false);
// => /api/tour/5

Parameter Resolution trong Controller Method

Controller method chỉ hỗ trợ inject: Request, ValidatedDTO, Model (route model binding), Enum, và scalar route params. Services phải inject qua constructor.

Request

use Vietiso\Core\Http\Request;

#[Get(uri: '')]
public function index(Request $request)
{
$page = $request->query('page', 1);
}

ValidatedDTO

Request data tự động validate và tạo DTO instance:

#[Post(uri: '')]
public function store(CreateBookingDTO $dto)
{
// $dto đã validate xong
// Nếu validation fail → trả về 422 tự động
}

// Kết hợp Request và DTO
#[Put(uri: '{id:\d+}')]
public function update(Request $request, UpdateBookingDTO $dto)
{
// DTO nhận data từ request body + route params
}

Route Model Binding

Khi type-hint là Model, framework tự động query findOrFail():

// Tham số {tour} match với tên biến $tour
// Framework gọi: Tour::query()->findOrFail($routeParams['tour'])
#[Get(uri: '{tour:\d+}')]
public function show(Tour $tour)
{
return $tour;
}

#[Delete(uri: '{tour:\d+}')]
public function destroy(Tour $tour)
{
$tour->delete();
}

Nếu record không tồn tại → tự động throw 404 Not Found.

FindBy - Binding theo field tùy chỉnh

Mặc định Model binding tìm theo primary key. Dùng #[FindBy] để tìm theo field khác:

use Vietiso\Core\Database\Attributes\FindBy;

#[Get(uri: '{slug}')]
public function showBySlug(
#[FindBy('slug')] Tour $tour
)
{
// Framework gọi: Tour::query()->where('slug', $routeParams['slug'])->first()
return $tour;
}

WithoutGlobalScopes

Tắt Global Scopes khi resolve Model binding:

use Vietiso\Core\Database\Attributes\WithoutGlobalScopes;

#[Get(uri: '{id:\d+}')]
public function showIncludingDeleted(
#[WithoutGlobalScopes([SoftDeleteScope::class])] Tour $tour
)
{
// Query không áp dụng SoftDeleteScope
return $tour;
}

Enum

Route param tự động cast sang PHP Enum:

enum BookingStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Cancelled = 'cancelled';
}

#[Get(uri: 'status/{status}')]
public function byStatus(BookingStatus $status)
{
// $status là BookingStatus enum
// Nếu value không hợp lệ → 404 Not Found
}

Scalar Route Params

#[Get(uri: '{agentId:\d+}')]
public function show(int $agentId)
{
// $agentId = giá trị từ URL
}

#[Get(uri: '{userId:\d+}/posts/{postId:\d+}')]
public function showPost(int $userId, int $postId) {}

Thứ tự Resolution

Framework resolve parameters theo thứ tự ưu tiên:

  1. Request → nếu type là Request
  2. FindBy Model → nếu có #[FindBy] attribute
  3. Route param match → nếu tên biến trùng route param:
    • Type là ModelfindOrFail()
    • Type là Enum::tryFrom()
    • Type khác → truyền giá trị trực tiếp
  4. ValidatedDTO → nếu type kế thừa ValidatedDTO

Constructor Injection

Services được inject qua constructor (không phải method):

#[Group(
prefix: 'api/booking',
middlewares: [AuthMiddleware::class],
)]
class BookingController
{
public function __construct(
protected BookingServiceInterface $bookingService,
protected BookingItemServiceInterface $bookingItemService,
) {}

#[Post(uri: '')]
public function store(BookingDTO $dto)
{
return $this->bookingService->create($dto);
}

#[Get(uri: '{booking:\d+}')]
public function show(Request $request, Booking $booking)
{
return $this->bookingService->getDetail($booking);
}
}

Route trong Module

Routes được đăng ký qua Service Provider của module:

<?php
// modules/booking/src/Providers/BookingServiceProvider.php

namespace Vietiso\Modules\Booking\Providers;

use Vietiso\Core\Container\ServiceProvider;

class BookingServiceProvider extends ServiceProvider
{
public function boot(): void
{
$routeCollection = $this->app->get('route.collection');
$module = $this->app->get('module.manager')->get('booking');

// Đăng ký controller → framework tự parse attributes
$routeCollection->register(BookingController::class, $module);
$routeCollection->register(BookingItemController::class, $module);
}
}

Lấy thông tin Route

#[Get(uri: '{id:\d+}')]
public function show(Request $request, int $id)
{
$route = $request->route();

$route->getName(); // Route name
$route->getParams(); // Tất cả route params
$route->getParam('id'); // Một param cụ thể
$route->getAction(); // [ControllerClass, methodName]
$route->getModule(); // Module instance (hoặc null)
$route->getMiddlewares(); // Danh sách middleware đã áp dụng
}

Ví dụ thực tế

API Controller đầy đủ

<?php

namespace Vietiso\Modules\Tour\Controllers;

use Vietiso\Core\Route\Attributes\Group;
use Vietiso\Core\Route\Attributes\Get;
use Vietiso\Core\Route\Attributes\Post;
use Vietiso\Core\Route\Attributes\Delete;
use Vietiso\Core\Route\Attributes\Patch;
use Vietiso\Core\Http\Request;

#[Group(
prefix: 'api/tour',
middlewares: [AuthMiddleware::class],
)]
class TourController
{
public function __construct(
protected TourServiceInterface $tourService,
) {}

#[Get(
uri: 'business-tour',
description: 'Lấy danh sách tour của doanh nghiệp.',
)]
public function getBusinessTours(Request $request)
{
return $this->tourService->getBusinessTours($request);
}

#[Post(
uri: '',
description: 'Thêm mới một tour.',
)]
public function createTour(CreateTourDTO $dto)
{
return $this->tourService->create($dto);
}

#[Delete(
uri: '{tour:\d+}',
description: 'Xóa tour.',
)]
public function deleteTour(Tour $tour)
{
return $this->tourService->delete($tour);
}

#[Patch(
uri: '{tour:\d+}/status',
description: 'Cập nhật trạng thái tour.',
)]
public function updateStatus(Tour $tour, ActiveDTO $activeDTO)
{
return $this->tourService->updateStatus($tour, $activeDTO);
}

// Route với middleware riêng, loại bỏ auth mặc định
#[Post(
uri: 'sync',
description: 'Đồng bộ thông tin tour từ TMS.',
middlewares: [TMSMiddleware::class],
excludedMiddlewares: [AuthMiddleware::class],
)]
public function syncTour(SyncTourDTO $dto)
{
return $this->tourService->sync($dto);
}

// Route public, không cần auth
#[Get(
uri: 'categories',
description: 'Lấy danh sách loại tour.',
excludedMiddlewares: [AuthMiddleware::class],
)]
public function categories(Request $request)
{
return $this->tourService->getCategories($request);
}
}