Nhảy tới nội dung

URL

Component Url cung cấp một value object bất biến (immutable) để phân tích, xây dựng và thao tác URL. Tất cả các phép thay đổi đều trả về instance mới, instance gốc không bị ảnh hưởng.

Tạo Url

Phân tích URL có sẵn

use Vietiso\Core\Url\Url;

$url = Url::parse('https://example.com/path?page=2#section');

$url->scheme(); // 'https'
$url->host(); // 'example.com'
$url->path(); // '/path'
$url->fragment(); // 'section'

Từ request hiện tại

// URL của request đang xử lý
$url = Url::current();

// Từ một Request cụ thể
$url = Url::fromRequest($request);

// Base URL của ứng dụng
$base = Url::base();

Tạo từ components

$url = Url::create(
scheme: 'https',
host: 'example.com',
port: 8080,
path: '/api/v1',
query: ['key' => 'value'],
fragment: 'section',
);

// https://example.com:8080/api/v1?key=value#section
echo $url;

Tạo URL từ path tương đối

// Ghép với base URL của ứng dụng
$url = Url::to('/tours', ['page' => 1]);

// HTTPS buộc
$secureUrl = Url::to('/checkout', [], secure: true);

Helper Functions

// Lấy URL hiện tại (không truyền path)
$current = url();

// Tạo URL từ path
$url = url('/tours', ['page' => 1]);

// Luôn dùng HTTPS
$url = secure_url('/payment');

// URL cho file asset (public/)
$asset = asset('images/logo.png');

// Signed URL (xem phần Signed URL bên dưới)
$signed = signed_url('tour.verify', ['id' => 42]);

Đọc Components

$url = Url::parse('https://admin:secret@example.com:8080/api/v1?page=2&sort=name#results');

$url->scheme(); // 'https'
$url->host(); // 'example.com'
$url->port(); // 8080
$url->user(); // 'admin'
$url->pass(); // 'secret'
$url->path(); // '/api/v1'
$url->queryString(); // 'page=2&sort=name'
$url->fragment(); // 'results'

// Authority = user:pass@host:port (bỏ qua port mặc định)
$url->authority(); // 'admin:secret@example.com:8080'

// Port thực dùng (fallback về port mặc định của scheme)
$url->effectivePort(); // 8080 (hoặc 443 nếu không set)

// Scheme dạng enum
$url->schemeEnum(); // Scheme::Https

Thay Đổi Immutable

Mọi phương thức with* đều trả về instance mới, instance gốc không đổi:

$original = Url::parse('http://example.com/old?a=1');

$new = $original
->withScheme('https')
->withHost('api.example.com')
->withPort(9090)
->withPath('/v2/tours')
->withQueryParam('page', 1)
->withFragment('list');

// $original không thay đổi
echo $original; // http://example.com/old?a=1
echo $new; // https://api.example.com:9090/v2/tours?a=1&page=1#list

Thay đổi scheme

use Vietiso\Core\Url\Scheme;

$url->withScheme('https');
$url->withScheme(Scheme::Https); // dùng enum

Thay đổi path

// Thay toàn bộ path
$url->withPath('/new/path');

// Tự động thêm / ở đầu khi có host
$url->withPath('no-slash'); // → '/no-slash'

// Nối thêm vào path hiện tại
$url->appendPath('v2/users'); // /api → /api/v2/users

// Trailing slash
$url->withTrailingSlash(); // /path → /path/
$url->withoutTrailingSlash(); // /path/ → /path

Thao Tác Query String

$url = Url::parse('https://example.com?page=1&sort=name');

// Thêm/cập nhật một param
$url->withQueryParam('limit', 20);

// Thêm nhiều param cùng lúc
$url->withQueryParams(['page' => 2, 'limit' => 10]);

// Thay toàn bộ query (string, array hoặc QueryBag)
$url->withQuery('a=1&b=2');
$url->withQuery(['a' => 1, 'b' => 2]);

// Xóa param
$url->withoutQueryParam('sort');
$url->withoutQueryParam(['page', 'sort']);

// Xóa toàn bộ query
$url->withoutQuery();

// Đọc param
$url->getQueryParam('page'); // '1'
$url->getQueryParam('missing', 'default');
$url->hasQueryParam('sort'); // true

// Lấy QueryBag object
$bag = $url->query();

Thao Tác Path Segments

$url = Url::parse('https://example.com/blog/2024/my-post.html');

// Tất cả segments (index 0-based)
$url->segments(); // ['blog', '2024', 'my-post.html']

// Lấy segment theo vị trí (1-based)
$url->segment(1); // 'blog'
$url->segment(2); // '2024'
$url->segment(99, 'default'); // 'default'

// Thư mục và tên file
$url->dirname(); // '/blog/2024'
$url->basename(); // 'my-post.html'
$url->extension(); // 'html'

Kiểm Tra URL

$url = Url::parse('https://example.com');

$url->isValid(); // true (có host hoặc path)
$url->isAbsolute(); // true (có cả scheme và host)
$url->isRelative(); // false

$url->isSecure(); // true (https, wss, ftps, sftp, ssh)
$url->isDefaultPort(); // true (port 443 với https)

// Kiểm tra định dạng URL hợp lệ (static)
Url::isValidUrl('https://example.com'); // true
Url::isValidUrl('mailto:user@example.com', ['mailto']); // true

QueryBag

QueryBag là object bất biến đại diện cho tập hợp query parameters. Có thể dùng độc lập:

use Vietiso\Core\Url\QueryBag;

// Tạo
$bag = new QueryBag(['page' => '1', 'sort' => 'name']);
$bag = QueryBag::fromString('page=1&sort=name');
$bag = QueryBag::fromArray(['page' => '1']);

// Đọc
$bag->get('page'); // '1'
$bag->get('missing', '0'); // '0'
$bag->has('sort'); // true
$bag->all(); // ['page' => '1', 'sort' => 'name']
$bag->keys(); // ['page', 'sort']
$bag->isEmpty(); // false
$bag->count(); // 2

// Thay đổi (immutable)
$bag->with('limit', '20');
$bag->withMany(['page' => '2', 'limit' => '20']);
$bag->without('sort');
$bag->without(['page', 'sort']);
$bag->only(['page']);

// Merge
$bag->merge(['extra' => 'value']);
$bag->merge($anotherBag);

// Functional
$bag->filter(fn($value) => $value !== '');
$bag->map(fn($value) => strtoupper($value));

// Xuất
$bag->toString(); // 'page=1&sort=name' (RFC 3986)
$bag->toString(QueryBag::ENC_RFC1738); // dùng + thay space
$bag->toArray();
json_encode($bag); // {"page":"1","sort":"name"}

// ArrayAccess (chỉ đọc)
$bag['page']; // '1'
isset($bag['sort']); // true
foreach ($bag as $key => $value) { ... }

QueryBag là immutable — gán trực tiếp $bag['key'] = 'value' hoặc unset($bag['key']) sẽ ném LogicException.

Scheme Enum

Scheme enum liệt kê các scheme được hỗ trợ với thông tin port và bảo mật:

use Vietiso\Core\Url\Scheme;

// Các scheme hỗ trợ
Scheme::Http // 'http'
Scheme::Https // 'https'
Scheme::Ftp // 'ftp'
Scheme::Ftps // 'ftps'
Scheme::Sftp // 'sftp'
Scheme::Ws // 'ws'
Scheme::Wss // 'wss'
Scheme::Mailto // 'mailto'
Scheme::Tel // 'tel'
Scheme::Ssh // 'ssh'
Scheme::File // 'file'

// Port mặc định
Scheme::Http->defaultPort(); // 80
Scheme::Https->defaultPort(); // 443
Scheme::Ftp->defaultPort(); // 21
Scheme::Sftp->defaultPort(); // 22

// Kiểm tra bảo mật
Scheme::Https->isSecure(); // true
Scheme::Http->isSecure(); // false

// Chuyển sang phiên bản secure
Scheme::Http->toSecure(); // Scheme::Https
Scheme::Ws->toSecure(); // Scheme::Wss
Scheme::Ftp->toSecure(); // Scheme::Ftps

// Parse từ string (case-insensitive)
Scheme::tryFromString('HTTPS'); // Scheme::Https
Scheme::tryFromString('xxx'); // null

Signed URL

Signed URL dùng HMAC-SHA256 để xác minh URL không bị giả mạo:

$url = Url::parse('https://example.com/verify?user=42');

// Ký URL (dùng app.key từ config)
$signed = $url->signed();

// Dùng key tùy chỉnh
$signed = $url->signed('my-secret-key');

// Xác minh
$signed->hasValidSignature(); // true
$signed->hasValidSignature('my-secret-key'); // true

// URL có thời hạn (giây hoặc DateTimeInterface)
$signed = $url->temporarySignedUrl(3600); // hết hạn sau 1 giờ
$signed = $url->temporarySignedUrl(now()->addDay()); // hết hạn sau 1 ngày

Tạo signed URL từ named route

// Dùng helper function (route phải được đặt tên)
$url = signed_url('tour.verify', ['id' => 42]);

// Có thời hạn
$url = signed_url('download.file', ['id' => 5], expiration: 3600);
$url = signed_url('download.file', ['id' => 5], expiration: now()->addHour());

Xác minh trong controller/middleware

$url = Url::fromRequest($request);

if (!$url->hasValidSignature()) {
return Response::json(['error' => 'Invalid or expired link'], 403);
}

Encode / Decode

// Encode theo RFC 3986 (rawurlencode)
Url::encode('hello world/foo&bar'); // 'hello%20world%2Ffoo%26bar'

// Decode
Url::decode('hello%20world'); // 'hello world'

Xuất Dữ Liệu

$url = Url::parse('https://example.com:8080/path?a=1#frag');

// Chuỗi URL
$url->toString(); // 'https://example.com:8080/path?a=1#frag'
(string) $url; // tương đương toString()

// Port mặc định bị ẩn
Url::parse('https://example.com:443/path')->toString();
// → 'https://example.com/path'

// Array
$url->toArray();
// [
// 'scheme' => 'https',
// 'host' => 'example.com',
// 'port' => 8080,
// 'user' => null,
// 'pass' => null,
// 'path' => '/path',
// 'query' => ['a' => '1'],
// 'fragment' => 'frag',
// ]

// JSON
$url->toJson();
json_encode($url); // serialize thành chuỗi URL

Fluent Chaining

Vì tất cả with* đều trả về instance mới, có thể chain liên tiếp:

$url = Url::parse('http://example.com')
->withScheme('https')
->withHost('api.example.com')
->withPort(9090)
->withPath('/v2/tours')
->withQueryParams(['page' => 1, 'limit' => 20])
->withFragment('results');

// https://api.example.com:9090/v2/tours?page=1&limit=20#results
echo $url;