feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support

BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
This commit is contained in:
2025-10-27 09:31:28 +01:00
parent 799f74f00a
commit c8b47e647d
81 changed files with 6988 additions and 601 deletions

View File

@@ -0,0 +1,373 @@
# Native PHP 8.5 URL System
**Zero-dependency URL parsing and manipulation** using native PHP 8.5 `Uri\Rfc3986\Uri` and `Uri\WhatWg\Url`.
## Features
**Native PHP 8.5 API** - No external dependencies
**Dual Spec Support** - RFC 3986 and WHATWG URL Standard
**Smart Factory** - Automatic spec selection based on use case
**Immutable Withers** - Framework-compliant readonly pattern
**Type Safe** - Full type safety with enums and value objects
**IDNA/Punycode** - Native international domain name support
## Installation
**Requirement**: PHP 8.5+
No composer packages needed - uses native PHP URL API!
## Quick Start
### Basic Usage
```php
use App\Framework\Http\Url\UrlFactory;
// Automatic spec selection
$url = UrlFactory::parse('https://example.com/path');
// API Client (RFC 3986)
$apiUrl = UrlFactory::forApiClient('https://api.example.com/users');
// Browser Redirect (WHATWG)
$redirect = UrlFactory::forBrowserRedirect('https://app.example.com/dashboard');
```
### URL Manipulation (Immutable)
```php
$url = UrlFactory::forApiClient('https://api.example.com/users');
// Immutable withers return new instances
$withAuth = $url->withUserInfo('api_key', 'secret');
$withQuery = $url->withQuery('filter=active&sort=name');
$withPath = $url->withPath('/v2/users');
// Component access
echo $url->getScheme(); // 'https'
echo $url->getHost(); // 'api.example.com'
echo $url->getPath(); // '/users'
```
### Use Case Factory Methods
```php
// API & Server-Side (RFC 3986)
UrlFactory::forApiClient($url); // REST, GraphQL, SOAP
UrlFactory::forCurlRequest($url); // cURL operations
UrlFactory::forSignature($url); // OAuth, AWS signing
UrlFactory::forCanonical($url); // SEO, duplicate detection
// Browser & Client-Side (WHATWG)
UrlFactory::forBrowserRedirect($url); // HTTP redirects
UrlFactory::forDeepLink($url); // Universal links
UrlFactory::forFormAction($url); // HTML form actions
UrlFactory::forClientSide($url); // JavaScript fetch()
```
## Architecture
### URL Specifications
**RFC 3986** - Server-side URL handling:
- Strict parsing rules
- No automatic encoding
- Preserves exact structure
- Best for: API clients, signatures, cURL
**WHATWG** - Browser-compatible URL handling:
- Living standard (matches browsers)
- Automatic percent-encoding
- URL normalization
- Best for: Redirects, deep links, forms
### Components
```
src/Framework/Http/Url/
├── Url.php # Unified URL interface
├── UrlSpec.php # RFC3986 vs WHATWG enum
├── UrlUseCase.php # Use case categories
├── Rfc3986Url.php # RFC 3986 implementation
├── WhatwgUrl.php # WHATWG implementation
└── UrlFactory.php # Smart factory
```
## Examples
### API Client with Authentication
```php
$apiUrl = UrlFactory::forApiClient('https://api.example.com/resource');
$withAuth = $apiUrl->withUserInfo('api_key', 'secret_token');
echo $withAuth->toString();
// https://api_key:secret_token@api.example.com/resource
```
### URL Signature Generation
```php
$url = UrlFactory::forSignature('https://api.example.com/resource');
$withParams = $url->withQuery('timestamp=1234567890&user_id=42');
// Generate signature from canonical URL string
$canonical = $withParams->toString();
$signature = hash_hmac('sha256', $canonical, $secretKey);
$signed = $withParams->withQuery(
$withParams->getQuery() . "&signature={$signature}"
);
```
### Browser Redirect with Parameters
```php
$redirect = UrlFactory::forBrowserRedirect('https://app.example.com/login');
$withReturn = $redirect->withQuery('return_url=/dashboard&status=success');
// Browser-compatible encoding
header('Location: ' . $withReturn->toString());
```
### Deep Link with Fallback
```php
$deepLink = UrlFactory::forDeepLink('myapp://open/article/123');
$fallback = UrlFactory::forBrowserRedirect('https://example.com/article/123');
// Try deep link first, fallback to web
$targetUrl = $isNativeApp ? $deepLink : $fallback;
```
### IDNA/Punycode Support
```php
$unicode = UrlFactory::parse('https://例え.jp/path');
$ascii = $unicode->toAsciiString();
echo $ascii; // https://xn--r8jz45g.jp/path
```
### URL Comparison
```php
$url1 = UrlFactory::parse('https://example.com/path#frag1');
$url2 = UrlFactory::parse('https://example.com/path#frag2');
// Ignore fragment by default
$url1->equals($url2); // true
// Include fragment in comparison
$url1->equals($url2, includeFragment: true); // false
```
### Relative URL Resolution
```php
$base = UrlFactory::parse('https://example.com/base/path');
$resolved = $base->resolve('../other/resource');
echo $resolved->toString();
// https://example.com/other/resource
```
### Spec Conversion
```php
$rfc = UrlFactory::forApiClient('https://example.com/path');
$whatwg = UrlFactory::convert($rfc, UrlSpec::WHATWG);
// Now browser-compatible with normalization
```
## Use Case Guide
### When to use RFC 3986
✅ REST API requests
✅ URL signature generation (OAuth, AWS)
✅ cURL operations
✅ Canonical URL generation (SEO)
✅ Webhook URLs
✅ FTP/SFTP URLs
### When to use WHATWG
✅ HTTP redirects
✅ Deep links / universal links
✅ HTML form actions
✅ JavaScript fetch() compatibility
✅ Browser-side URL generation
✅ Mobile app URLs
## Testing
Comprehensive Pest tests included:
```bash
./vendor/bin/pest tests/Unit/Framework/Http/Url/
```
Test coverage:
- RFC 3986 parsing and manipulation
- WHATWG parsing and normalization
- Factory method selection
- URL comparison and resolution
- IDNA/Punycode handling
- Immutability guarantees
- Edge cases and error handling
## Framework Integration
### Readonly Pattern
All URL classes are `final readonly` with immutable withers:
```php
final readonly class Rfc3986Url implements Url
{
private function __construct(
private NativeRfc3986Uri $uri
) {}
// Withers return new instances
public function withPath(string $path): self
{
return new self($this->uri->withPath($path));
}
}
```
### Value Object Pattern
URLs are value objects with value semantics:
```php
$url1 = UrlFactory::parse('https://example.com/path');
$url2 = UrlFactory::parse('https://example.com/path');
$url1->equals($url2); // true
```
### DI Container Integration
Register in container initializer:
```php
final readonly class UrlServiceInitializer implements Initializer
{
#[Initializer]
public function __invoke(): UrlService
{
return new UrlService(UrlFactory::class);
}
}
```
## Performance
Native PHP 8.5 implementation = **C-level performance**:
- ✅ Zero external dependencies
- ✅ No reflection overhead
- ✅ Optimized memory usage
- ✅ Fast parsing and manipulation
- ✅ Native IDNA conversion
## Migration from Legacy Code
### Before (primitive strings)
```php
function generateApiUrl(string $baseUrl, array $params): string
{
$query = http_build_query($params);
return $baseUrl . '?' . $query;
}
```
### After (type-safe URLs)
```php
function generateApiUrl(Url $baseUrl, array $params): Url
{
$query = http_build_query($params);
return $baseUrl->withQuery($query);
}
// Usage
$apiUrl = UrlFactory::forApiClient('https://api.example.com/resource');
$withParams = generateApiUrl($apiUrl, ['filter' => 'active']);
```
## Best Practices
1. **Use factory methods** for automatic spec selection
2. **Prefer specific use case methods** over generic parse()
3. **Type hint with Url** for flexibility
4. **Use equals() for comparison** instead of string comparison
5. **Leverage immutability** - withers are safe for concurrent use
6. **Choose correct spec** - RFC 3986 for server, WHATWG for browser
## Advanced Features
### Native URL Access
Access underlying native PHP URL objects when needed:
```php
$url = UrlFactory::forApiClient('https://example.com');
$nativeUri = $url->getNativeUrl(); // \Uri\Rfc3986\Uri
```
### Custom URL Schemes
Both specs support custom schemes:
```php
$custom = UrlFactory::parse('myscheme://resource/path');
echo $custom->getScheme(); // 'myscheme'
```
## Troubleshooting
### Invalid URL Errors
```php
try {
$url = UrlFactory::parse('invalid://url');
} catch (\InvalidArgumentException $e) {
// Handle parse error
}
```
### Spec Mismatch
Convert between specs when needed:
```php
$rfc = UrlFactory::forApiClient('https://example.com');
$whatwg = UrlFactory::convert($rfc, UrlSpec::WHATWG);
```
### IDNA Issues
Native PHP 8.5 handles IDNA automatically:
```php
$url = UrlFactory::parse('https://例え.jp');
$ascii = $url->toAsciiString(); // Automatic Punycode
```
## Resources
- [RFC 3986 Specification](https://www.rfc-editor.org/rfc/rfc3986)
- [WHATWG URL Standard](https://url.spec.whatwg.org/)
- [PHP 8.5 URL API Documentation](https://www.php.net/manual/en/book.uri.php)
- [IDNA/Punycode Reference](https://www.rfc-editor.org/rfc/rfc5891)
## License
Part of Custom PHP Framework - Internal Use

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
use InvalidArgumentException;
use Throwable;
use Uri\Rfc3986\Uri as NativeRfc3986Uri;
/**
* RFC 3986 compliant URL implementation
*
* Wraps native PHP 8.5+ Uri\Rfc3986\Uri for server-side URL handling.
*
* Use Cases:
* - API clients (REST, GraphQL, SOAP)
* - URL signatures (OAuth, AWS, etc.)
* - cURL requests
* - Server-side canonicalization
*
* Characteristics:
* - Strict RFC 3986 compliance
* - No automatic encoding
* - Preserves exact URL structure
* - Deterministic formatting for signatures
*/
final readonly class Rfc3986Url implements Url
{
/**
* @param NativeRfc3986Uri $uri Native PHP RFC 3986 URI instance
*/
private function __construct(
private NativeRfc3986Uri $uri
) {
}
/**
* Parse RFC 3986 URI from string
*
* @param string $input URI string to parse
* @param Url|null $base Optional base URI for relative resolution
* @return self New RFC 3986 URL instance
* @throws InvalidArgumentException If URI is invalid
*/
public static function parse(string $input, ?Url $base = null): self
{
try {
if ($base instanceof self) {
// RFC 3986 reference resolution with base URI
$uri = $base->uri->resolve($input);
} else {
$uri = NativeRfc3986Uri::parse($input);
}
return new self($uri);
} catch (Throwable $e) {
throw new InvalidArgumentException(
"Failed to parse RFC 3986 URI: {$input}",
previous: $e
);
}
}
public function getSpec(): UrlSpec
{
return UrlSpec::RFC3986;
}
// Component Getters
public function getScheme(): string
{
return $this->uri->getScheme() ?? '';
}
public function getHost(): string
{
return $this->uri->getHost() ?? '';
}
public function getPort(): ?int
{
return $this->uri->getPort();
}
public function getPath(): string
{
return $this->uri->getPath() ?? '';
}
public function getQuery(): string
{
return $this->uri->getQuery() ?? '';
}
public function getFragment(): string
{
return $this->uri->getFragment() ?? '';
}
public function getUserInfo(): string
{
return $this->uri->getUserInfo() ?? '';
}
// Immutable Withers (delegate to native withers)
public function withScheme(string $scheme): self
{
return new self($this->uri->withScheme($scheme));
}
public function withHost(string $host): self
{
return new self($this->uri->withHost($host));
}
public function withPort(?int $port): self
{
return new self($this->uri->withPort($port));
}
public function withPath(string $path): self
{
return new self($this->uri->withPath($path));
}
public function withQuery(string $query): self
{
return new self($this->uri->withQuery($query));
}
public function withFragment(string $fragment): self
{
return new self($this->uri->withFragment($fragment));
}
public function withUserInfo(string $user, ?string $password = null): self
{
$userInfo = $password !== null ? "{$user}:{$password}" : $user;
return new self($this->uri->withUserInfo($userInfo));
}
// Serialization
public function toString(): string
{
return $this->uri->toString();
}
public function toAsciiString(): string
{
// Native PHP 8.5 handles IDNA/Punycode conversion
return $this->uri->toRawString();
}
// Utilities
public function resolve(string $relative): self
{
$resolved = $this->uri->resolve($relative);
return new self($resolved);
}
public function equals(Url $other, bool $includeFragment = false): bool
{
if (! $other instanceof self) {
return false;
}
if ($includeFragment) {
return $this->uri->equals($other->uri);
}
// Compare without fragments
$thisWithoutFragment = $this->uri->withFragment(null);
$otherWithoutFragment = $other->uri->withFragment(null);
return $thisWithoutFragment->equals($otherWithoutFragment);
}
public function getNativeUrl(): NativeRfc3986Uri
{
return $this->uri;
}
/**
* String representation (allows string casting)
*/
public function __toString(): string
{
return $this->uri->toString();
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
/**
* Unified URL interface for native PHP 8.5 URL API
*
* Abstracts RFC 3986 and WHATWG URL implementations
* providing a unified API surface for URL manipulation.
*
* Replaces old Uri class with full-featured URL handling.
*
* All implementations must be immutable - modification methods
* return new instances (wither pattern).
*/
interface Url
{
/**
* Parse URL from string with optional base URL
*
* @param string $input URL string to parse
* @param self|null $base Optional base URL for relative resolution
* @return self New URL instance
* @throws \InvalidArgumentException If URL is invalid
*/
public static function parse(string $input, ?self $base = null): self;
/**
* Get URL specification this instance conforms to
*
* @return UrlSpec Either RFC3986 or WHATWG
*/
public function getSpec(): UrlSpec;
// Component Getters
/**
* Get URL scheme (e.g., 'https', 'ftp')
*
* @return string Scheme without trailing colon, empty string if absent
*/
public function getScheme(): string;
/**
* Get host component (domain or IP address)
*
* @return string Host, empty string if absent
*/
public function getHost(): string;
/**
* Get port number
*
* @return int|null Port number, null if default or absent
*/
public function getPort(): ?int;
/**
* Get path component
*
* @return string Path, empty string if absent
*/
public function getPath(): string;
/**
* Get query string
*
* @return string Query without leading '?', empty string if absent
*/
public function getQuery(): string;
/**
* Get fragment identifier
*
* @return string Fragment without leading '#', empty string if absent
*/
public function getFragment(): string;
/**
* Get user info (username:password)
*
* @return string User info, empty string if absent
*/
public function getUserInfo(): string;
// Immutable Withers (Framework Pattern)
/**
* Return instance with specified scheme
*
* @param string $scheme New scheme
* @return self New instance with updated scheme
*/
public function withScheme(string $scheme): self;
/**
* Return instance with specified host
*
* @param string $host New host
* @return self New instance with updated host
*/
public function withHost(string $host): self;
/**
* Return instance with specified port
*
* @param int|null $port New port, null for default
* @return self New instance with updated port
*/
public function withPort(?int $port): self;
/**
* Return instance with specified path
*
* @param string $path New path
* @return self New instance with updated path
*/
public function withPath(string $path): self;
/**
* Return instance with specified query
*
* @param string $query New query string (without leading '?')
* @return self New instance with updated query
*/
public function withQuery(string $query): self;
/**
* Return instance with specified fragment
*
* @param string $fragment New fragment (without leading '#')
* @return self New instance with updated fragment
*/
public function withFragment(string $fragment): self;
/**
* Return instance with specified user info
*
* @param string $user Username
* @param string|null $password Optional password
* @return self New instance with updated user info
*/
public function withUserInfo(string $user, ?string $password = null): self;
// Serialization
/**
* Convert URL to string representation
*
* @return string Complete URL string
*/
public function toString(): string;
/**
* Convert URL to ASCII-compatible string (Punycode/IDNA)
*
* @return string ASCII-encoded URL for international domain names
*/
public function toAsciiString(): string;
// Utilities
/**
* Resolve relative URL against this URL as base
*
* @param string $relative Relative URL to resolve
* @return self New URL instance with resolved URL
*/
public function resolve(string $relative): self;
/**
* Check equality with another URL
*
* @param self $other URL to compare
* @param bool $includeFragment Whether to include fragment in comparison
* @return bool True if URLs are equal
*/
public function equals(self $other, bool $includeFragment = false): bool;
/**
* Get underlying native PHP URL object
*
* Provides access to native \Uri\Rfc3986\Uri or \Uri\WhatWg\Url
* for advanced use cases requiring direct native API access.
*
* @return object Native PHP URL object
*/
public function getNativeUrl(): object;
}

View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
/**
* Smart URL factory with automatic spec selection
*
* Provides convenient factory methods that automatically select
* the appropriate URL specification (RFC 3986 vs WHATWG) based
* on the intended use case.
*
* Usage:
* ```php
* // API Client (RFC 3986)
* $apiUrl = UrlFactory::forApiClient('https://api.example.com/users');
*
* // Browser Redirect (WHATWG)
* $redirect = UrlFactory::forBrowserRedirect('https://example.com/dashboard');
*
* // Automatic selection
* $url = UrlFactory::forUseCase(UrlUseCase::DEEP_LINK, 'myapp://open');
* ```
*/
final readonly class UrlFactory
{
/**
* Parse URL with automatic spec detection
*
* Attempts to select appropriate spec based on scheme and structure.
* Defaults to RFC 3986 for ambiguous cases.
*
* @param string $input URL string to parse
* @param Url|null $base Optional base URL
* @return Url Parsed URL with selected spec
*/
public static function parse(string $input, ?Url $base = null): Url
{
// Heuristic: Use WHATWG for http/https URLs, RFC 3986 for others
$scheme = parse_url($input, PHP_URL_SCHEME);
if (in_array($scheme, ['http', 'https', 'file', 'ws', 'wss'], true)) {
return WhatwgUrl::parse($input, $base);
}
return Rfc3986Url::parse($input, $base);
}
/**
* Parse URL for specific use case with automatic spec selection
*
* @param UrlUseCase $useCase Intended use case
* @param string $input URL string to parse
* @param Url|null $base Optional base URL
* @return Url Parsed URL with spec matching use case
*/
public static function forUseCase(UrlUseCase $useCase, string $input, ?Url $base = null): Url
{
$spec = UrlSpec::forUseCase($useCase);
return match ($spec) {
UrlSpec::RFC3986 => Rfc3986Url::parse($input, $base),
UrlSpec::WHATWG => WhatwgUrl::parse($input, $base),
};
}
/**
* Create URL for API client use (RFC 3986)
*
* Best for:
* - REST API calls
* - GraphQL endpoints
* - SOAP services
* - Webhook URLs
*
* @param string $input API endpoint URL
* @param Url|null $base Optional base URL
* @return Rfc3986Url RFC 3986 compliant URL
*/
public static function forApiClient(string $input, ?Url $base = null): Rfc3986Url
{
return Rfc3986Url::parse($input, $base);
}
/**
* Create URL for cURL request (RFC 3986)
*
* Best for:
* - cURL operations
* - HTTP client requests
* - File transfers
*
* @param string $input Request URL
* @param Url|null $base Optional base URL
* @return Rfc3986Url RFC 3986 compliant URL
*/
public static function forCurlRequest(string $input, ?Url $base = null): Rfc3986Url
{
return Rfc3986Url::parse($input, $base);
}
/**
* Create URL for signature generation (RFC 3986)
*
* Best for:
* - OAuth signatures
* - AWS request signing
* - HMAC-based authentication
* - Webhook signature verification
*
* @param string $input URL to sign
* @param Url|null $base Optional base URL
* @return Rfc3986Url RFC 3986 compliant URL
*/
public static function forSignature(string $input, ?Url $base = null): Rfc3986Url
{
return Rfc3986Url::parse($input, $base);
}
/**
* Create canonical URL (RFC 3986)
*
* Best for:
* - SEO canonical URLs
* - Duplicate content detection
* - URL normalization
* - Sitemap generation
*
* @param string $input URL to canonicalize
* @param Url|null $base Optional base URL
* @return Rfc3986Url RFC 3986 compliant URL
*/
public static function forCanonical(string $input, ?Url $base = null): Rfc3986Url
{
return Rfc3986Url::parse($input, $base);
}
/**
* Create URL for browser redirect (WHATWG)
*
* Best for:
* - HTTP redirects (302, 301, etc.)
* - Location headers
* - User-facing redirects
*
* @param string $input Redirect target URL
* @param Url|null $base Optional base URL
* @return WhatwgUrl WHATWG compliant URL
*/
public static function forBrowserRedirect(string $input, ?Url $base = null): WhatwgUrl
{
return WhatwgUrl::parse($input, $base);
}
/**
* Create URL for deep link (WHATWG)
*
* Best for:
* - Universal links
* - App deep links
* - Mobile-to-web links
* - Cross-platform navigation
*
* @param string $input Deep link URL
* @param Url|null $base Optional base URL
* @return WhatwgUrl WHATWG compliant URL
*/
public static function forDeepLink(string $input, ?Url $base = null): WhatwgUrl
{
return WhatwgUrl::parse($input, $base);
}
/**
* Create URL for HTML form action (WHATWG)
*
* Best for:
* - Form submission targets
* - HTML5 form actions
* - Browser form handling
*
* @param string $input Form action URL
* @param Url|null $base Optional base URL
* @return WhatwgUrl WHATWG compliant URL
*/
public static function forFormAction(string $input, ?Url $base = null): WhatwgUrl
{
return WhatwgUrl::parse($input, $base);
}
/**
* Create URL for client-side JavaScript (WHATWG)
*
* Best for:
* - JavaScript fetch() API
* - XMLHttpRequest URLs
* - Browser URL API compatibility
* - Client-side routing
*
* @param string $input JavaScript URL
* @param Url|null $base Optional base URL
* @return WhatwgUrl WHATWG compliant URL
*/
public static function forClientSide(string $input, ?Url $base = null): WhatwgUrl
{
return WhatwgUrl::parse($input, $base);
}
/**
* Convert between URL specs
*
* @param Url $url URL to convert
* @param UrlSpec $targetSpec Target specification
* @return Url Converted URL
*/
public static function convert(Url $url, UrlSpec $targetSpec): Url
{
if ($url->getSpec() === $targetSpec) {
return $url;
}
$urlString = $url->toString();
return match ($targetSpec) {
UrlSpec::RFC3986 => Rfc3986Url::parse($urlString),
UrlSpec::WHATWG => WhatwgUrl::parse($urlString),
};
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
/**
* URL Specification identifier
*
* Distinguishes between RFC 3986 and WHATWG URL Standard
* for different parsing and handling semantics.
*/
enum UrlSpec: string
{
/**
* RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
*
* Use for:
* - Server-side URL canonicalization
* - API clients (REST, GraphQL, SOAP)
* - URL signatures and validation
* - cURL compatibility
* - File system paths
*
* Characteristics:
* - Strict parsing rules
* - No automatic encoding
* - No URL normalization
* - Preserves original structure
*
* Example:
* ```php
* $uri = Rfc3986Url::parse('https://api.example.com/users?id=123');
* ```
*/
case RFC3986 = 'rfc3986';
/**
* WHATWG URL Standard (Living Standard)
*
* Use for:
* - Browser-like URL handling
* - Deep links and redirects
* - Client-side generated URLs
* - HTML form actions
* - JavaScript fetch() API compatibility
*
* Characteristics:
* - Living standard (matches modern browsers)
* - Automatic percent-encoding
* - URL normalization
* - Special scheme handling (http, https, file, etc.)
*
* Example:
* ```php
* $url = WhatwgUrl::parse('https://example.com/redirect');
* ```
*/
case WHATWG = 'whatwg';
/**
* Get recommended spec for specific use case
*
* Automatically selects the appropriate URL specification
* based on the intended usage pattern.
*/
public static function forUseCase(UrlUseCase $useCase): self
{
return match ($useCase) {
UrlUseCase::API_CLIENT,
UrlUseCase::CURL_REQUEST,
UrlUseCase::SIGNATURE_GENERATION,
UrlUseCase::CANONICAL_URL => self::RFC3986,
UrlUseCase::BROWSER_REDIRECT,
UrlUseCase::DEEP_LINK,
UrlUseCase::HTML_FORM_ACTION,
UrlUseCase::CLIENT_SIDE_URL => self::WHATWG,
};
}
/**
* Check if this spec is RFC 3986
*/
public function isRfc3986(): bool
{
return $this === self::RFC3986;
}
/**
* Check if this spec is WHATWG
*/
public function isWhatwg(): bool
{
return $this === self::WHATWG;
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
/**
* URL Use Case categories for automatic spec selection
*
* Helps determine whether RFC 3986 or WHATWG URL Standard
* should be used based on the intended usage pattern.
*/
enum UrlUseCase
{
/**
* API Client requests (REST, GraphQL, SOAP)
*
* Recommended Spec: RFC 3986
* - Strict parsing for API endpoints
* - URL signature compatibility
* - Predictable canonicalization
*/
case API_CLIENT;
/**
* cURL requests and HTTP client operations
*
* Recommended Spec: RFC 3986
* - Compatible with cURL expectations
* - No automatic normalization
* - Preserves exact URL structure
*/
case CURL_REQUEST;
/**
* URL signature generation (OAuth, AWS, etc.)
*
* Recommended Spec: RFC 3986
* - Deterministic URL formatting
* - No automatic encoding changes
* - Critical for signature validation
*/
case SIGNATURE_GENERATION;
/**
* Canonical URL generation (SEO, duplicate detection)
*
* Recommended Spec: RFC 3986
* - Consistent URL representation
* - Reliable comparison
* - SEO-friendly formatting
*/
case CANONICAL_URL;
/**
* Browser redirect URLs
*
* Recommended Spec: WHATWG
* - Browser-compatible behavior
* - Automatic encoding
* - Matches browser expectations
*/
case BROWSER_REDIRECT;
/**
* Deep links (app-to-web, universal links)
*
* Recommended Spec: WHATWG
* - Mobile browser compatibility
* - Modern URL handling
* - Cross-platform consistency
*/
case DEEP_LINK;
/**
* HTML form action URLs
*
* Recommended Spec: WHATWG
* - HTML5 specification compliance
* - Browser form submission compatibility
* - Automatic encoding of form data
*/
case HTML_FORM_ACTION;
/**
* Client-side generated URLs (JavaScript compatibility)
*
* Recommended Spec: WHATWG
* - Matches JavaScript URL API
* - Compatible with fetch()
* - Consistent with browser behavior
*/
case CLIENT_SIDE_URL;
/**
* Get human-readable description of this use case
*/
public function description(): string
{
return match ($this) {
self::API_CLIENT => 'API client requests (REST, GraphQL, SOAP)',
self::CURL_REQUEST => 'cURL requests and HTTP client operations',
self::SIGNATURE_GENERATION => 'URL signature generation (OAuth, AWS)',
self::CANONICAL_URL => 'Canonical URL generation (SEO)',
self::BROWSER_REDIRECT => 'Browser redirect URLs',
self::DEEP_LINK => 'Deep links and universal links',
self::HTML_FORM_ACTION => 'HTML form action URLs',
self::CLIENT_SIDE_URL => 'Client-side JavaScript URLs',
};
}
/**
* Get recommended URL spec for this use case
*/
public function recommendedSpec(): UrlSpec
{
return UrlSpec::forUseCase($this);
}
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Url;
use InvalidArgumentException;
use Uri\WhatWg\Url as NativeWhatwgUrl;
/**
* WHATWG URL Standard implementation
*
* Wraps native PHP 8.5+ Uri\WhatWg\Url for browser-compatible URL handling.
*
* Use Cases:
* - Browser redirects
* - Deep links and universal links
* - HTML form actions
* - Client-side JavaScript compatibility
*
* Characteristics:
* - Living Standard (matches modern browsers)
* - Automatic percent-encoding
* - URL normalization
* - Special scheme handling (http, https, file, etc.)
*/
final readonly class WhatwgUrl implements Url
{
/**
* @param NativeWhatwgUrl $url Native PHP WHATWG URL instance
*/
private function __construct(
private NativeWhatwgUrl $url
) {
}
/**
* Parse WHATWG URL from string
*
* @param string $input URL string to parse
* @param Url|null $base Optional base URL for relative resolution
* @return self New WHATWG URL instance
* @throws InvalidArgumentException If URL is invalid
*/
public static function parse(string $input, ?Url $base = null): self
{
try {
if ($base instanceof self) {
// WHATWG URL resolution with base
$url = $base->url->resolve($input);
} else {
$url = new NativeWhatwgUrl($input);
}
return new self($url);
} catch (\Throwable $e) {
throw new InvalidArgumentException(
"Failed to parse WHATWG URL: {$input}",
previous: $e
);
}
}
public function getSpec(): UrlSpec
{
return UrlSpec::WHATWG;
}
// Component Getters (WHATWG methods)
public function getScheme(): string
{
return $this->url->getScheme() ?? '';
}
public function getHost(): string
{
// Prefer Unicode host for display
return $this->url->getUnicodeHost() ?? '';
}
public function getPort(): ?int
{
return $this->url->getPort();
}
public function getPath(): string
{
return $this->url->getPath() ?? '';
}
public function getQuery(): string
{
return $this->url->getQuery() ?? '';
}
public function getFragment(): string
{
return $this->url->getFragment() ?? '';
}
public function getUserInfo(): string
{
$user = $this->url->getUsername() ?? '';
$pass = $this->url->getPassword() ?? '';
if ($user === '') {
return '';
}
return $pass !== '' ? "{$user}:{$pass}" : $user;
}
// Immutable Withers (delegate to native withers)
public function withScheme(string $scheme): self
{
return new self($this->url->withScheme($scheme));
}
public function withHost(string $host): self
{
return new self($this->url->withHost($host));
}
public function withPort(?int $port): self
{
return new self($this->url->withPort($port));
}
public function withPath(string $path): self
{
return new self($this->url->withPath($path));
}
public function withQuery(string $query): self
{
return new self($this->url->withQuery($query !== '' ? $query : null));
}
public function withFragment(string $fragment): self
{
return new self($this->url->withFragment($fragment !== '' ? $fragment : null));
}
public function withUserInfo(string $user, ?string $password = null): self
{
$withUser = $this->url->withUsername($user);
return new self($withUser->withPassword($password ?? ''));
}
// Serialization
public function toString(): string
{
return $this->url->toUnicodeString();
}
public function toAsciiString(): string
{
// WHATWG URLs with Punycode encoding
return $this->url->toAsciiString();
}
// Utilities
public function resolve(string $relative): self
{
$resolved = $this->url->resolve($relative);
return new self($resolved);
}
public function equals(Url $other, bool $includeFragment = false): bool
{
if (! $other instanceof self) {
return false;
}
if ($includeFragment) {
return $this->url->equals($other->url);
}
// Compare without fragments
$thisWithoutFragment = $this->url->withFragment(null);
$otherWithoutFragment = $other->url->withFragment(null);
return $thisWithoutFragment->equals($otherWithoutFragment);
}
public function getNativeUrl(): NativeWhatwgUrl
{
return $this->url;
}
/**
* String representation (allows string casting)
*/
public function __toString(): string
{
return $this->url->toUnicodeString();
}
}