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:
373
src/Framework/Http/Url.php85/README.md
Normal file
373
src/Framework/Http/Url.php85/README.md
Normal 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
|
||||
197
src/Framework/Http/Url.php85/Rfc3986Url.php
Normal file
197
src/Framework/Http/Url.php85/Rfc3986Url.php
Normal 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();
|
||||
}
|
||||
}
|
||||
191
src/Framework/Http/Url.php85/Url.php
Normal file
191
src/Framework/Http/Url.php85/Url.php
Normal 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;
|
||||
}
|
||||
229
src/Framework/Http/Url.php85/UrlFactory.php
Normal file
229
src/Framework/Http/Url.php85/UrlFactory.php
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
97
src/Framework/Http/Url.php85/UrlSpec.php
Normal file
97
src/Framework/Http/Url.php85/UrlSpec.php
Normal 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;
|
||||
}
|
||||
}
|
||||
119
src/Framework/Http/Url.php85/UrlUseCase.php
Normal file
119
src/Framework/Http/Url.php85/UrlUseCase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
204
src/Framework/Http/Url.php85/WhatwgUrl.php
Normal file
204
src/Framework/Http/Url.php85/WhatwgUrl.php
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user