Version 1.0.0
This commit is contained in:
22
.editorconfig
Normal file
22
.editorconfig
Normal file
@@ -0,0 +1,22 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
|
||||
.phpunit.cache/
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2025 Viktor S. <thinlineseverywhere@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the “Software”),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
171
README.md
Normal file
171
README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# PHP Url
|
||||
## Description
|
||||
|
||||
A simple library for interacting with URLs using an object-oriented approach.
|
||||
|
||||
Provides tools to build, modify, and parse URLs.
|
||||
Can be used in API client classes or in dependency injection contexts.
|
||||
|
||||
Requires PHP 8.2 or higher.
|
||||
|
||||
* [Objects API](./docs/OBJECTS.md)
|
||||
|
||||
## Components
|
||||
|
||||
* **Builder** - URL instance builder
|
||||
* **Facade** - Facade simplify usage
|
||||
* **Parser** - Raw string url parser
|
||||
* **Serializer** - URL instance serializer
|
||||
* **Url** - URL instance representation
|
||||
* **Util** - Inner utilities
|
||||
|
||||
#### Builders
|
||||
|
||||
* **HostRelative** - Builds a url relative to the hostname, setup scheme and port
|
||||
* **ReplaceAttributes** - Builds new url instance with passed params replacement
|
||||
|
||||
#### Serializers
|
||||
|
||||
* **RFC3986** - Serializes url instance to RFC3986 string
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require diffhead/php-url
|
||||
composer test
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```php
|
||||
use Diffhead\PHP\Url\Facade;
|
||||
use Diffhead\PHP\Url\Dto\Replace;
|
||||
|
||||
/**
|
||||
* @var \Diffhead\PHP\Url\Url $url
|
||||
*/
|
||||
$url = Facade::parse('www.google.com');
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
$string = Facade::toRfc3986String($url);
|
||||
|
||||
/**
|
||||
* Parameters are optionally.
|
||||
* If null passed then will not
|
||||
* be replaced.
|
||||
*/
|
||||
$dto = new Replace(
|
||||
scheme: 'https',
|
||||
hostname: 'www.github.com',
|
||||
port: 443,
|
||||
path: '/',
|
||||
parameters: []
|
||||
);
|
||||
|
||||
/**
|
||||
* @var \Diffhead\PHP\Url\Url $replaced
|
||||
*/
|
||||
$replaced = Facade::replace($url, $dto);
|
||||
```
|
||||
|
||||
#### Parsing URL
|
||||
```php
|
||||
use Diffhead\PHP\Url\Parser;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsPort;
|
||||
|
||||
$parser = new Parser('ftp://localhost/some/entity?public=1');
|
||||
|
||||
/**
|
||||
* @var string $scheme
|
||||
*/
|
||||
$scheme = $parser->getScheme();
|
||||
|
||||
/**
|
||||
* @var string $hostname
|
||||
*/
|
||||
$hostname = $parser->getHostname();
|
||||
|
||||
/**
|
||||
* @var int $port
|
||||
*/
|
||||
try {
|
||||
$port = $parser->getPort();
|
||||
} catch (UrlNotContainsPort $e) {
|
||||
$port = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string $path
|
||||
*/
|
||||
$path = $parser->getPath();
|
||||
|
||||
/**
|
||||
* @var array{public:string}
|
||||
*/
|
||||
$query = $parser->getParameters();
|
||||
```
|
||||
|
||||
#### Using with the DI container
|
||||
```php
|
||||
use Diffhead\PHP\Url\Builder;
|
||||
use Diffhead\PHP\Url\Builder\HostRelative;
|
||||
use Diffhead\PHP\Url\Port;
|
||||
use Diffhead\PHP\Url\Scheme;
|
||||
use Diffhead\PHP\Url\Serializer;
|
||||
use Diffhead\PHP\Url\Serializer\RFC3986;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class Url
|
||||
{
|
||||
public function __construct(
|
||||
private Builder $builder,
|
||||
private Serializer $serializer
|
||||
) {}
|
||||
|
||||
public function get(string $path, array $query = []): string
|
||||
{
|
||||
return $this->serializer->serialize(
|
||||
$this->builder->build($path, $query)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Client
|
||||
{
|
||||
public function __construct(
|
||||
private Client $http,
|
||||
private Url $url
|
||||
) {}
|
||||
|
||||
public function items(FetchItemsRequest $request): FetchItemsResponse
|
||||
{
|
||||
$response = $this->http->post(
|
||||
$this->url->get('/v1/things'),
|
||||
$this->getJsonRequestOptions($request)
|
||||
);
|
||||
|
||||
return FetchItemsResponse::fromArray(
|
||||
json_decode((string) $response->getBody(), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \DI\Container $container
|
||||
*/
|
||||
$container->set(Url::class, function (): Url {
|
||||
$domain = getenv('API_DOMAIN');
|
||||
$scheme = Scheme::Https->value;
|
||||
$port = Port::WebSecure->value;
|
||||
|
||||
$builder = new HostRelative($domain, $scheme, $port);
|
||||
$serializer = new RFC3986();
|
||||
|
||||
return new Url($builder, $serializer);
|
||||
});
|
||||
|
||||
|
||||
$container->get(Client::class);
|
||||
```
|
||||
29
composer.json
Normal file
29
composer.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "diffhead/php-url",
|
||||
"description": "PHP url interaction library using object-oriented style",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Diffhead\\PHP\\Url\\": "src/",
|
||||
"Diffhead\\PHP\\Url\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Viktor S.",
|
||||
"email": "thinlineseverywhere@gmail.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
1635
composer.lock
generated
Normal file
1635
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
152
docs/OBJECTS.md
Normal file
152
docs/OBJECTS.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Objects API
|
||||
|
||||
#### Diffhead\PHP\Url\Facade
|
||||
```php
|
||||
class Facade
|
||||
{
|
||||
public static function parse(string $url): Url;
|
||||
public static function toRfc3986String(Url $url): string;
|
||||
public static function replace(Url $url, Replace $replacements): Url;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Url
|
||||
```php
|
||||
class Url
|
||||
{
|
||||
public function __construct(
|
||||
string $scheme,
|
||||
string $hostname,
|
||||
string $path,
|
||||
int $port,
|
||||
array $parameters
|
||||
);
|
||||
|
||||
public function scheme(): string;
|
||||
public function hostname(): string;
|
||||
public function path(): string;
|
||||
public function port(): int;
|
||||
public function parameters(): array;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Builder
|
||||
```php
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
use Diffhead\PHP\Url\Url;
|
||||
|
||||
interface Builder
|
||||
{
|
||||
public function build(string $resource, array $parameters = []): Url;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Builder\HostRelative
|
||||
```php
|
||||
namespace Diffhead\PHP\Url\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use Diffhead\PHP\Url\Builder;
|
||||
|
||||
class HostRelative implements Builder
|
||||
{
|
||||
public function __construct(string $hostname, string $scheme, int $port);
|
||||
public function build(string $path, array $query = []): Url;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Builder\ReplaceAttributes
|
||||
```php
|
||||
namespace Diffhead\PHP\Url\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use Diffhead\PHP\Url\Builder;
|
||||
|
||||
class ReplaceAttributes implements Builder
|
||||
{
|
||||
public function __construct(Url $url);
|
||||
|
||||
/**
|
||||
* Pass the hostname and/or another params
|
||||
* to replace it in the URL instance.
|
||||
*
|
||||
* Empty hostname argument means
|
||||
* it will not be replaced.
|
||||
*
|
||||
* @param string $hostname
|
||||
* @param array{scheme?:string,port?:int,path?:string,parameters?:array} $parameters
|
||||
*/
|
||||
public function build(string $hostname = '', array $parameters = []): Url;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Parser
|
||||
```php
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
class Parser
|
||||
{
|
||||
public function __construct(string $url);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsScheme
|
||||
*/
|
||||
public function getScheme(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsHostname
|
||||
*/
|
||||
public function getHostname(): string;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPort
|
||||
*/
|
||||
public function getPort(): int;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPath
|
||||
*/
|
||||
public function getPath(): string;
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsQuery
|
||||
*/
|
||||
public function getParameters(): array;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Serializer
|
||||
```php
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
use Diffhead\PHP\Url\Url;
|
||||
|
||||
interface Serializer
|
||||
{
|
||||
public function toString(Url $url): string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Diffhead\PHP\Url\Serializer\RFC3986
|
||||
```php
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
use Diffhead\PHP\Url\Serializer;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
|
||||
class RFC3986 implements Serializer
|
||||
{
|
||||
public function toString(Url $url): string;
|
||||
}
|
||||
```
|
||||
27
phpunit.xml
Normal file
27
phpunit.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
executionOrder="depends,defects"
|
||||
requireCoverageMetadata="true"
|
||||
beStrictAboutCoverageMetadata="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnPhpunitDeprecations="true"
|
||||
failOnPhpunitDeprecation="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
testdox="true"
|
||||
colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
8
src/Builder.php
Normal file
8
src/Builder.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
interface Builder
|
||||
{
|
||||
public function build(string $resource, array $parameters = []): Url;
|
||||
}
|
||||
28
src/Builder/HostRelative.php
Normal file
28
src/Builder/HostRelative.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Builder;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
|
||||
class HostRelative implements Builder
|
||||
{
|
||||
public function __construct(
|
||||
private string $hostname,
|
||||
private string $scheme = '',
|
||||
private int $port = 0
|
||||
) {}
|
||||
|
||||
public function build(string $path, array $parameters = []): Url
|
||||
{
|
||||
return new Url(
|
||||
$this->scheme,
|
||||
$this->hostname,
|
||||
$path,
|
||||
$this->port,
|
||||
$parameters
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/Builder/ReplaceAttributes.php
Normal file
36
src/Builder/ReplaceAttributes.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Builder;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
|
||||
class ReplaceAttributes implements Builder
|
||||
{
|
||||
public function __construct(
|
||||
private Url $url
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Pass the hostname and/or another params
|
||||
* to replace it in the URL instance.
|
||||
*
|
||||
* Empty hostname argument means
|
||||
* it will not be replaced.
|
||||
*
|
||||
* @param string $hostname
|
||||
* @param array{scheme?:string,port?:int,path?:string,parameters?:array} $parameters
|
||||
*/
|
||||
public function build(string $hostname = '', array $parameters = []): Url
|
||||
{
|
||||
$scheme = $parameters['scheme'] ?? $this->url->scheme();
|
||||
$hostname = $hostname ?: $this->url->hostname();
|
||||
$path = $parameters['path'] ?? $this->url->path();
|
||||
$port = $parameters['port'] ?? $this->url->port();
|
||||
$params = $parameters['parameters'] ?? $this->url->parameters();
|
||||
|
||||
return Url::create($scheme, $hostname, $path, $port, $params);
|
||||
}
|
||||
}
|
||||
41
src/Dto/Replace.php
Normal file
41
src/Dto/Replace.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Dto;
|
||||
|
||||
class Replace
|
||||
{
|
||||
public function __construct(
|
||||
private ?string $scheme = null,
|
||||
private ?string $hostname = null,
|
||||
private ?string $path = null,
|
||||
private ?int $port = null,
|
||||
private ?array $parameters = null,
|
||||
) {}
|
||||
|
||||
public function scheme(): ?string
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
public function hostname(): ?string
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
public function path(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function port(): ?int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function parameters(): ?array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
}
|
||||
11
src/Exception/UrlNotContainsHostname.php
Normal file
11
src/Exception/UrlNotContainsHostname.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
class UrlNotContainsHostname extends UrlRuntimeException
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url, 'not contains hostname');
|
||||
}
|
||||
}
|
||||
11
src/Exception/UrlNotContainsPath.php
Normal file
11
src/Exception/UrlNotContainsPath.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
class UrlNotContainsPath extends UrlRuntimeException
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url, 'not contains path');
|
||||
}
|
||||
}
|
||||
11
src/Exception/UrlNotContainsPort.php
Normal file
11
src/Exception/UrlNotContainsPort.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
class UrlNotContainsPort extends UrlRuntimeException
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url, 'not contains port');
|
||||
}
|
||||
}
|
||||
11
src/Exception/UrlNotContainsQuery.php
Normal file
11
src/Exception/UrlNotContainsQuery.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
class UrlNotContainsQuery extends UrlRuntimeException
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url, 'not contains query');
|
||||
}
|
||||
}
|
||||
11
src/Exception/UrlNotContainsScheme.php
Normal file
11
src/Exception/UrlNotContainsScheme.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
class UrlNotContainsScheme extends UrlRuntimeException
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url, 'not contains scheme');
|
||||
}
|
||||
}
|
||||
13
src/Exception/UrlRuntimeException.php
Normal file
13
src/Exception/UrlRuntimeException.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class UrlRuntimeException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $url, string $description)
|
||||
{
|
||||
parent::__construct(sprintf('"%s" %s', $url, $description));
|
||||
}
|
||||
}
|
||||
73
src/Facade.php
Normal file
73
src/Facade.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
use Closure;
|
||||
use Diffhead\PHP\Url\Builder\ReplaceAttributes;
|
||||
use Diffhead\PHP\Url\Dto\Replace;
|
||||
use Diffhead\PHP\Url\Exception\UrlRuntimeException;
|
||||
use Diffhead\PHP\Url\Serializer\RFC3986;
|
||||
|
||||
class Facade
|
||||
{
|
||||
public static function parse(string $url): Url
|
||||
{
|
||||
$parser = new Parser($url);
|
||||
|
||||
return new Url(
|
||||
self::valueOrDefault(fn () => $parser->getScheme(), ''),
|
||||
self::valueOrDefault(fn () => $parser->getHostname(), ''),
|
||||
self::valueOrDefault(fn () => $parser->getPath(), ''),
|
||||
self::valueOrDefault(fn () => $parser->getPort(), 0),
|
||||
self::valueOrDefault(fn () => $parser->getParameters(), []),
|
||||
);
|
||||
}
|
||||
|
||||
public static function toRfc3986String(Url $url): string
|
||||
{
|
||||
$serializer = new RFC3986();
|
||||
return $serializer->toString($url);
|
||||
}
|
||||
|
||||
public static function replace(Url $url, Replace $replacements): Url
|
||||
{
|
||||
$builder = new ReplaceAttributes($url);
|
||||
|
||||
$parameters = [];
|
||||
|
||||
if ($replacements->scheme() !== null) {
|
||||
$parameters['scheme'] = $replacements->scheme();
|
||||
}
|
||||
|
||||
if ($replacements->path() !== null) {
|
||||
$parameters['path'] = $replacements->path();
|
||||
}
|
||||
|
||||
if ($replacements->port() !== null) {
|
||||
$parameters['port'] = $replacements->port();
|
||||
}
|
||||
|
||||
if ($replacements->parameters() !== null) {
|
||||
$parameters['parameters'] = $replacements->parameters();
|
||||
}
|
||||
|
||||
return $builder->build($replacements->hostname() ?? '', $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure():mixed $getter
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private static function valueOrDefault(Closure $getter, mixed $default = null): mixed
|
||||
{
|
||||
try {
|
||||
return $getter();
|
||||
} catch (UrlRuntimeException $exception) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/Parser.php
Normal file
200
src/Parser.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsHostname;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsPath;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsPort;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsQuery;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsScheme;
|
||||
|
||||
class Parser
|
||||
{
|
||||
private string $url;
|
||||
private ?string $scheme = null;
|
||||
private ?string $hostname = null;
|
||||
private ?int $port = null;
|
||||
private ?string $path = null;
|
||||
private ?array $parameters = null;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$this->url = urldecode($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsScheme
|
||||
*/
|
||||
public function getScheme(): string
|
||||
{
|
||||
if (is_null($this->scheme)) {
|
||||
if (! preg_match(Regex::Scheme->value, $this->url, $matches)) {
|
||||
throw new UrlNotContainsScheme($this->url);
|
||||
}
|
||||
|
||||
$this->scheme = strtolower($matches[1]);
|
||||
}
|
||||
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsHostname
|
||||
*/
|
||||
public function getHostname(): string
|
||||
{
|
||||
if (is_null($this->hostname)) {
|
||||
$matched = preg_match(Regex::Hostname->value, $this->url, $matches);
|
||||
|
||||
switch (true) {
|
||||
case $matched === false:
|
||||
case empty($matches[1]):
|
||||
case str_starts_with($this->url, sprintf('%s://', $matches[1])):
|
||||
$this->throwUrlNotContainsHostname();
|
||||
}
|
||||
|
||||
$this->hostname = $matches[1];
|
||||
}
|
||||
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPort
|
||||
*/
|
||||
public function getPort(): int
|
||||
{
|
||||
if (is_null($this->port)) {
|
||||
$matched = preg_match(
|
||||
Regex::Port->value,
|
||||
$this->url,
|
||||
$matches,
|
||||
PREG_UNMATCHED_AS_NULL
|
||||
);
|
||||
|
||||
if (! $matched) {
|
||||
throw new UrlNotContainsPort($this->url);
|
||||
}
|
||||
|
||||
$this->port = (int) $matches[2];
|
||||
}
|
||||
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsPath
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
if (is_null($this->path)) {
|
||||
if (! preg_match(Regex::Path->value, $this->url, $matches)) {
|
||||
$this->throwUrlNotContainsPath();
|
||||
}
|
||||
|
||||
if (empty($matches[1])) {
|
||||
$this->throwUrlNotContainsPath();
|
||||
}
|
||||
|
||||
$this->path = $matches[1];
|
||||
}
|
||||
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*
|
||||
* @throws \Diffhead\PHP\Url\Exception\UrlNotContainsQuery
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
if (is_null($this->parameters)) {
|
||||
if (! preg_match(Regex::Query->value, $this->url, $matches)) {
|
||||
throw new UrlNotContainsQuery($this->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array<int,string> $parameters
|
||||
*/
|
||||
$parametersRaw = explode('&', $matches[1]);
|
||||
$parameters = [];
|
||||
|
||||
foreach ($parametersRaw as $parameter) {
|
||||
[$key, $value] = explode('=', $parameter);
|
||||
|
||||
if ($this->isNestedKey($key)) {
|
||||
$keys = $this->splitNestedKey($key);
|
||||
|
||||
$this->setNestedValue($keys, $value, $parameters);
|
||||
} else {
|
||||
$this->setFlatValue($key, $value, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
private function isNestedKey(string $key): bool
|
||||
{
|
||||
return is_numeric(strpos($key, '['));
|
||||
}
|
||||
|
||||
private function splitNestedKey(string $nestedKey): array
|
||||
{
|
||||
preg_match_all(Regex::QueryArrayKeys->value, $nestedKey, $matches);
|
||||
|
||||
return array_map(fn (string $key) => $key, $matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $keys
|
||||
* @param string $value
|
||||
* @param array<string|int,string|array> $parameters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setNestedValue(array $keys, string $value, array &$parameters): void
|
||||
{
|
||||
$current = array_shift($keys);
|
||||
|
||||
if (! empty($keys)) {
|
||||
$temporary = [];
|
||||
|
||||
$this->setNestedValue($keys, $value, $temporary);
|
||||
|
||||
$persisted = $parameters[$current] ?? [];
|
||||
$parameters[$current] = array_merge($persisted, $temporary);
|
||||
} else if (empty($current)) {
|
||||
$parameters[] = $value;
|
||||
} else {
|
||||
$parameters[$current] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function setFlatValue(string $key, string $value, array &$parameters): void
|
||||
{
|
||||
$parameters[$key] = $value;
|
||||
}
|
||||
|
||||
private function throwUrlNotContainsHostname(): void
|
||||
{
|
||||
throw new UrlNotContainsHostname($this->url);
|
||||
}
|
||||
|
||||
private function throwUrlNotContainsPath(): void
|
||||
{
|
||||
throw new UrlNotContainsPath($this->url);
|
||||
}
|
||||
}
|
||||
31
src/Port.php
Normal file
31
src/Port.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
enum Port: int
|
||||
{
|
||||
case Cassandra = 9042;
|
||||
case CouchDb = 5984;
|
||||
case ElasticSearch = 9200;
|
||||
case FileTransfer = 21;
|
||||
case FileTransferData = 20;
|
||||
case FileTransferOverSsh = 22;
|
||||
case FileTransferSecure = 990;
|
||||
case Imap = 143;
|
||||
case ImapSecure = 993;
|
||||
case MailTo = 25;
|
||||
case MongoDb = 27017;
|
||||
case MySql = 3306;
|
||||
case OracleDb = 1521;
|
||||
case Pop3 = 110;
|
||||
case Pop3Secure = 995;
|
||||
case PostgreSql = 5432;
|
||||
case RabbitMQ = 5672;
|
||||
case RabbitMQManagement = 15672;
|
||||
case Redis = 6379;
|
||||
case SqlServer = 1433;
|
||||
case Smtp = 587;
|
||||
case SmtpSecure = 465;
|
||||
case Web = 80;
|
||||
case WebSecure = 443;
|
||||
}
|
||||
13
src/Regex.php
Normal file
13
src/Regex.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
enum Regex: string
|
||||
{
|
||||
case Scheme = '/^(\w+):\/\//';
|
||||
case Hostname = '/^(?:(?:[A-Za-z][A-Za-z0-9+\-.]*):\/\/)?(?:(?:[A-Za-z0-9\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*@)?(\[(?:[0-9A-Fa-f:.]+|v[0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&\'()*+,;=:]+)\]|(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2}|[!$&\'()*+,;=])+)(?=[:\/?#]|$)/';
|
||||
case Port = '/^(\w+:\/\/){0,1}[\w\d]+[\w\d\.-_]{0,}?:?(\d+)[\/]?/';
|
||||
case Path = '/^(?:[a-z][a-z0-9+\-.]*:\/\/)?[^\/\?#]+(?:\:\d+)?(\/[^?\#]*)?/';
|
||||
case Query = '/\?(.*?)?(?=#|$)/';
|
||||
case QueryArrayKeys = '/\[([^\]]*)\]/';
|
||||
}
|
||||
20
src/Scheme.php
Normal file
20
src/Scheme.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
enum Scheme: string
|
||||
{
|
||||
case File = 'file';
|
||||
case FileTransfer = 'ftp';
|
||||
case FileTransferOverSsh = 'sftp';
|
||||
case FileTransferSecure = 'ftps';
|
||||
case Http = 'http';
|
||||
case Https = 'https';
|
||||
case Imap = 'imap';
|
||||
case MailTo = 'mailto';
|
||||
case Pop3 = 'pop3';
|
||||
case SecureCopy = 'scp';
|
||||
case Smtp = 'smtp';
|
||||
case WebSocket = 'ws';
|
||||
case WebSocketSecure = 'wss';
|
||||
}
|
||||
8
src/Serializer.php
Normal file
8
src/Serializer.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
interface Serializer
|
||||
{
|
||||
public function toString(Url $url): string;
|
||||
}
|
||||
64
src/Serializer/RFC3986.php
Normal file
64
src/Serializer/RFC3986.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Serializer;
|
||||
|
||||
use Diffhead\PHP\Url\Serializer;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use Diffhead\PHP\Url\Util;
|
||||
|
||||
class RFC3986 implements Serializer
|
||||
{
|
||||
public function toString(Url $url): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s://%s%s%s',
|
||||
$url->scheme(),
|
||||
$this->getHost($url),
|
||||
$this->getPath($url),
|
||||
$this->getQueryString($url)
|
||||
);
|
||||
}
|
||||
|
||||
private function getHost(Url $url): string
|
||||
{
|
||||
switch (true) {
|
||||
/**
|
||||
* If http protocol with 80 port
|
||||
* or https protocol with 443 port
|
||||
*/
|
||||
case Util::isDefaultHttpUrl($url):
|
||||
/**
|
||||
* If ws protocol with 80 port
|
||||
* or wss protocol with 443 port
|
||||
*/
|
||||
case Util::isDefaultWebSocketUrl($url):
|
||||
return $url->hostname();
|
||||
|
||||
default:
|
||||
return sprintf('%s:%d', $url->hostname(), $url->port());
|
||||
}
|
||||
}
|
||||
|
||||
private function getPath(Url $url): string
|
||||
{
|
||||
$pathIsEmpty = empty($url->path());
|
||||
$pathStartsWithSlash = str_starts_with($url->path(), '/');
|
||||
|
||||
if ($pathIsEmpty || $pathStartsWithSlash) {
|
||||
return $url->path();
|
||||
}
|
||||
|
||||
return sprintf('/%s', $url->path());
|
||||
}
|
||||
|
||||
private function getQueryString(Url $url): string
|
||||
{
|
||||
if ($parameters = $url->parameters()) {
|
||||
return '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
61
src/Url.php
Normal file
61
src/Url.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
class Url
|
||||
{
|
||||
public static function create(
|
||||
string $scheme,
|
||||
string $hostname,
|
||||
string $path,
|
||||
int $port,
|
||||
array $parameters
|
||||
): Url {
|
||||
return new static($scheme, $hostname, $path, $port, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $scheme
|
||||
* @param string $hostname
|
||||
* @param string $path
|
||||
* @param int $port
|
||||
* @param array<string,string|int|float|array> $parameters
|
||||
*/
|
||||
public function __construct(
|
||||
private string $scheme,
|
||||
private string $hostname,
|
||||
private string $path,
|
||||
private int $port,
|
||||
private array $parameters
|
||||
) {}
|
||||
|
||||
public function scheme(): string
|
||||
{
|
||||
return $this->scheme;
|
||||
}
|
||||
|
||||
public function hostname(): string
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function port(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string|array>
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
}
|
||||
54
src/Util.php
Normal file
54
src/Util.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url;
|
||||
|
||||
class Util
|
||||
{
|
||||
public static function isDefaultHttpUrl(Url $url): bool
|
||||
{
|
||||
return static::httpWithoutTls($url)
|
||||
|| static::httpWithTls($url);
|
||||
}
|
||||
|
||||
public static function isDefaultWebSocketUrl(Url $url): bool
|
||||
{
|
||||
return static::webSocketWithoutTls($url)
|
||||
|| static::webSocketWithTls($url);
|
||||
}
|
||||
|
||||
private static function httpWithoutTls(Url $url): bool
|
||||
{
|
||||
return $url->scheme() === Scheme::Http->value
|
||||
&& static::webOrEmptyPort($url);
|
||||
}
|
||||
|
||||
private static function httpWithTls(Url $url): bool
|
||||
{
|
||||
return $url->scheme() === Scheme::Https->value
|
||||
&& static::webSecureOrEmptyPort($url);
|
||||
}
|
||||
|
||||
private static function webSocketWithoutTls(Url $url): bool
|
||||
{
|
||||
return $url->scheme() === Scheme::WebSocket->value
|
||||
&& static::webOrEmptyPort($url);
|
||||
}
|
||||
|
||||
private static function webSocketWithTls(Url $url): bool
|
||||
{
|
||||
return $url->scheme() === Scheme::WebSocketSecure->value
|
||||
&& static::webSecureOrEmptyPort($url);
|
||||
}
|
||||
|
||||
private static function webOrEmptyPort(Url $url): bool
|
||||
{
|
||||
return $url->port() === Port::Web->value || empty($url->port());
|
||||
}
|
||||
|
||||
private static function webSecureOrEmptyPort(Url $url): bool
|
||||
{
|
||||
return $url->port() === Port::WebSecure->value || empty($url->port());
|
||||
}
|
||||
}
|
||||
36
tests/Builder/HostRelativeTest.php
Normal file
36
tests/Builder/HostRelativeTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Builder\HostRelative;
|
||||
use Diffhead\PHP\Url\Port;
|
||||
use Diffhead\PHP\Url\Scheme;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(HostRelative::class)]
|
||||
#[CoversMethod(HostRelative::class, 'build')]
|
||||
class HostRelativeTest extends TestCase
|
||||
{
|
||||
public function testUrlBuilding(): void
|
||||
{
|
||||
$hostname = 'www.google.com';
|
||||
$scheme = Scheme::Https;
|
||||
$port = Port::WebSecure;
|
||||
$path = '/api/users';
|
||||
$query = ['active' => true, 'page' => 2, 'perPage' => 100];
|
||||
|
||||
$builder = new HostRelative($hostname, $scheme->value, $port->value);
|
||||
|
||||
$url = $builder->build($path, $query);
|
||||
|
||||
$this->assertSame($hostname, $url->hostname());
|
||||
$this->assertSame($scheme->value, $url->scheme());
|
||||
$this->assertSame($port->value, $url->port());
|
||||
$this->assertSame($path, $url->path());
|
||||
$this->assertEquals($query, $url->parameters());
|
||||
}
|
||||
}
|
||||
83
tests/Builder/ReplaceAttributesTest.php
Normal file
83
tests/Builder/ReplaceAttributesTest.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests\Builder;
|
||||
|
||||
use Diffhead\PHP\Url\Builder\ReplaceAttributes;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use TypeError;
|
||||
|
||||
#[CoversClass(ReplaceAttributes::class)]
|
||||
#[CoversMethod(ReplaceAttributes::class, 'build')]
|
||||
class ReplaceAttributesTest extends TestCase
|
||||
{
|
||||
public function testReplacementOnEmptyData(): void
|
||||
{
|
||||
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
|
||||
|
||||
$builder = new ReplaceAttributes($origin);
|
||||
|
||||
$replaced = $builder->build();
|
||||
|
||||
$this->assertSame('http', $replaced->scheme());
|
||||
$this->assertSame('example.com', $replaced->hostname());
|
||||
$this->assertSame('/path', $replaced->path());
|
||||
$this->assertSame(80, $replaced->port());
|
||||
$this->assertSame(['a' => '1'], $replaced->parameters());
|
||||
}
|
||||
|
||||
public function testPartiallyReplacement(): void
|
||||
{
|
||||
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
|
||||
|
||||
$builder = new ReplaceAttributes($origin);
|
||||
|
||||
$replaced = $builder->build('new-example.com', [
|
||||
'scheme' => 'https',
|
||||
'port' => 443,
|
||||
]);
|
||||
|
||||
$this->assertSame('https', $replaced->scheme());
|
||||
$this->assertSame('new-example.com', $replaced->hostname());
|
||||
$this->assertSame('/path', $replaced->path());
|
||||
$this->assertSame(443, $replaced->port());
|
||||
$this->assertSame(['a' => '1'], $replaced->parameters());
|
||||
}
|
||||
|
||||
public function testFullReplacement(): void
|
||||
{
|
||||
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
|
||||
|
||||
$builder = new ReplaceAttributes($origin);
|
||||
|
||||
$replaced = $builder->build('new-example.com', [
|
||||
'scheme' => 'https',
|
||||
'path' => '/new-path',
|
||||
'port' => 443,
|
||||
'parameters' => ['b' => '2'],
|
||||
]);
|
||||
|
||||
$this->assertSame('https', $replaced->scheme());
|
||||
$this->assertSame('new-example.com', $replaced->hostname());
|
||||
$this->assertSame('/new-path', $replaced->path());
|
||||
$this->assertSame(443, $replaced->port());
|
||||
$this->assertSame(['b' => '2'], $replaced->parameters());
|
||||
}
|
||||
|
||||
public function testInvalidParametersTypes(): void
|
||||
{
|
||||
$this->expectException(TypeError::class);
|
||||
|
||||
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
|
||||
|
||||
$builder = new ReplaceAttributes($origin);
|
||||
|
||||
$builder->build('new-example.com', [
|
||||
'scheme' => 123,
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
tests/Dto/ReplaceTest.php
Normal file
36
tests/Dto/ReplaceTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests\Dto;
|
||||
|
||||
use Diffhead\PHP\Url\Dto\Replace;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Replace::class)]
|
||||
#[CoversMethod(Replace::class, 'scheme')]
|
||||
#[CoversMethod(Replace::class, 'hostname')]
|
||||
#[CoversMethod(Replace::class, 'path')]
|
||||
#[CoversMethod(Replace::class, 'port')]
|
||||
#[CoversMethod(Replace::class, 'parameters')]
|
||||
class ReplaceTest extends TestCase
|
||||
{
|
||||
public function testReplaceDtoProperlyInitializes(): void
|
||||
{
|
||||
$replace = new Replace(
|
||||
scheme: 'https',
|
||||
hostname: 'example.com',
|
||||
path: '/path',
|
||||
port: 443,
|
||||
parameters: ['param' => 'value'],
|
||||
);
|
||||
|
||||
$this->assertSame('https', $replace->scheme());
|
||||
$this->assertSame('example.com', $replace->hostname());
|
||||
$this->assertSame('/path', $replace->path());
|
||||
$this->assertSame(443, $replace->port());
|
||||
$this->assertSame(['param' => 'value'], $replace->parameters());
|
||||
}
|
||||
}
|
||||
86
tests/FacadeTest.php
Normal file
86
tests/FacadeTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests;
|
||||
|
||||
use Diffhead\PHP\Url\Dto\Replace;
|
||||
use Diffhead\PHP\Url\Facade;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Facade::class)]
|
||||
#[CoversMethod(Facade::class, 'parse')]
|
||||
#[CoversMethod(Facade::class, 'toRfc3986String')]
|
||||
#[CoversMethod(Facade::class, 'replace')]
|
||||
class FacadeTest extends TestCase
|
||||
{
|
||||
public function testParsingOnEmptyString(): void
|
||||
{
|
||||
$url = Facade::parse('');
|
||||
|
||||
$this->assertSame('', $url->scheme());
|
||||
$this->assertSame('', $url->hostname());
|
||||
$this->assertSame('', $url->path());
|
||||
$this->assertSame(0, $url->port());
|
||||
$this->assertSame([], $url->parameters());
|
||||
}
|
||||
|
||||
public function testParsingOnProperlyWebUrl(): void
|
||||
{
|
||||
$url = Facade::parse('https://example.com:8080/path/to/resource?param1=value1¶m2=value2');
|
||||
|
||||
$this->assertSame('https', $url->scheme());
|
||||
$this->assertSame('example.com', $url->hostname());
|
||||
$this->assertSame('/path/to/resource', $url->path());
|
||||
$this->assertSame(8080, $url->port());
|
||||
$this->assertSame(['param1' => 'value1', 'param2' => 'value2'], $url->parameters());
|
||||
}
|
||||
|
||||
public function testParsingOnDsn(): void
|
||||
{
|
||||
$url = Facade::parse('mysql://user:password@localhost:3306/database_name');
|
||||
|
||||
$this->assertSame('mysql', $url->scheme());
|
||||
$this->assertSame('localhost', $url->hostname());
|
||||
$this->assertSame('/database_name', $url->path());
|
||||
$this->assertSame(3306, $url->port());
|
||||
$this->assertSame([], $url->parameters());
|
||||
}
|
||||
|
||||
public function testStringConversionToRfc3986(): void
|
||||
{
|
||||
$originUrl = 'https://example.com:8080/path/to/resource?param1=value1¶m2=value2';
|
||||
$url = Facade::parse($originUrl);
|
||||
$convertedUrl = Facade::toRfc3986String($url);
|
||||
|
||||
$this->assertSame($originUrl, $convertedUrl);
|
||||
}
|
||||
|
||||
public function testUrlAttributesReplacement(): void
|
||||
{
|
||||
$raw = 'http://example.com/path?param=oldValue';
|
||||
$origin = Facade::parse($raw);
|
||||
|
||||
$replace = new Replace(
|
||||
scheme: 'https',
|
||||
hostname: 'new-example.com',
|
||||
path: '/new-path',
|
||||
port: 443,
|
||||
parameters: ['param' => 'newValue', 'addedParam' => 'addedValue'],
|
||||
);
|
||||
|
||||
$replaced = Facade::replace($origin, $replace);
|
||||
|
||||
$this->assertSame('https', $replaced->scheme());
|
||||
$this->assertSame('new-example.com', $replaced->hostname());
|
||||
$this->assertSame('/new-path', $replaced->path());
|
||||
$this->assertSame(443, $replaced->port());
|
||||
|
||||
$this->assertSame(
|
||||
['param' => 'newValue', 'addedParam' => 'addedValue'],
|
||||
$replaced->parameters()
|
||||
);
|
||||
}
|
||||
}
|
||||
118
tests/ParserTest.php
Normal file
118
tests/ParserTest.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests;
|
||||
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsHostname;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsPath;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsPort;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsQuery;
|
||||
use Diffhead\PHP\Url\Exception\UrlNotContainsScheme;
|
||||
use Diffhead\PHP\Url\Parser;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Parser::class)]
|
||||
#[CoversMethod(Parser::class, 'getScheme')]
|
||||
#[CoversMethod(Parser::class, 'getHostname')]
|
||||
#[CoversMethod(Parser::class, 'getPath')]
|
||||
#[CoversMethod(Parser::class, 'getPort')]
|
||||
#[CoversMethod(Parser::class, 'getParameters')]
|
||||
class ParserTest extends TestCase
|
||||
{
|
||||
public function testExistingSchemeParsing(): void
|
||||
{
|
||||
$this->assertSame('http', $this->getParser('http://example.info')->getScheme());
|
||||
$this->assertSame('https', $this->getParser('https://example.info')->getScheme());
|
||||
}
|
||||
|
||||
public function testEmptySchemeParsing(): void
|
||||
{
|
||||
$this->expectException(UrlNotContainsScheme::class);
|
||||
$this->getParser('example.info')->getScheme();
|
||||
}
|
||||
|
||||
public function testExistingHostnameParsing(): void
|
||||
{
|
||||
$this->assertSame('example.info', $this->getParser('example.info')->getHostname());
|
||||
$this->assertSame('example.info', $this->getParser('https://example.info')->getHostname());
|
||||
}
|
||||
|
||||
public function testEmptyHostnameParsing(): void
|
||||
{
|
||||
$this->expectException(UrlNotContainsHostname::class);
|
||||
$this->getParser('https:///api/webhooks/event')->getHostname();
|
||||
}
|
||||
|
||||
public function testExistingPortParsing(): void
|
||||
{
|
||||
$this->assertSame(80, $this->getParser('localhost:80')->getPort());
|
||||
$this->assertSame(443, $this->getParser('https://google.com:443/index')->getPort());
|
||||
}
|
||||
|
||||
public function testEmptyPortParsing(): void
|
||||
{
|
||||
$this->expectException(UrlNotContainsPort::class);
|
||||
$this->getParser('localhost')->getPort();
|
||||
}
|
||||
|
||||
public function testExistingPathParsing(): void
|
||||
{
|
||||
$this->assertSame('/', $this->getParser('https://google.com/')->getPath());
|
||||
$this->assertSame('/api/endpoint', $this->getParser('https://something.org/api/endpoint')->getPath());
|
||||
$this->assertSame('/api/endpoint', $this->getParser('something.org:1234/api/endpoint')->getPath());
|
||||
}
|
||||
|
||||
public function testEmptyPathParsing(): void
|
||||
{
|
||||
$this->expectException(UrlNotContainsPath::class);
|
||||
$this->getParser('https://google.com')->getPath();
|
||||
}
|
||||
|
||||
public function testExistingFlatParametersParsing(): void
|
||||
{
|
||||
$url = 'https://example.com?parameter=value&secondParameter=secondValue&a=100';
|
||||
$parser = $this->getParser($url);
|
||||
|
||||
$urlParameters = [
|
||||
'parameter' => 'value',
|
||||
'secondParameter' => 'secondValue',
|
||||
'a' => '100'
|
||||
];
|
||||
|
||||
$this->assertEquals($urlParameters, $parser->getParameters());
|
||||
}
|
||||
|
||||
public function testExistingNestedParametersParsing(): void
|
||||
{
|
||||
$url = 'https://example.com/search?query=book&filters%5Bprice%5D%5Bmin%5D=10&filters%5Bprice%5D%5Bmax%5D=100&filters%5Bcategories%5D%5B%5D=fiction&filters%5Bcategories%5D%5B%5D=history';
|
||||
$parser = $this->getParser($url);
|
||||
|
||||
$urlParameters = [
|
||||
'query' => 'book',
|
||||
'price' => [
|
||||
'min' => '10',
|
||||
'max' => '100'
|
||||
],
|
||||
'categories' => [
|
||||
'fiction',
|
||||
'history'
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($urlParameters, $parser->getParameters());
|
||||
}
|
||||
|
||||
public function testNonExistingParametersParsing(): void
|
||||
{
|
||||
$this->expectException(UrlNotContainsQuery::class);
|
||||
$this->getParser('https://google.com')->getParameters();
|
||||
}
|
||||
|
||||
private function getParser(string $url): Parser
|
||||
{
|
||||
return new Parser($url);
|
||||
}
|
||||
}
|
||||
183
tests/Serializer/RFC3986Test.php
Normal file
183
tests/Serializer/RFC3986Test.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests\Serializer;
|
||||
|
||||
use Diffhead\PHP\Url\Port;
|
||||
use Diffhead\PHP\Url\Scheme;
|
||||
use Diffhead\PHP\Url\Serializer\RFC3986;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(RFC3986::class)]
|
||||
#[CoversMethod(RFC3986::class, 'toString')]
|
||||
class RFC3986Test extends TestCase
|
||||
{
|
||||
public function testSerializeHttpUrlWithDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::Http->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::Web->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'http://localhost/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeHttpUrlWithNonDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::Http->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::WebSecure->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'http://localhost:443/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeHttpsUrlWithDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::Https->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::WebSecure->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'https://localhost/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeHttpsUrlWithNonDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::Https->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::MySql->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'https://localhost:3306/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSerializeWebSocketUrlWithDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::WebSocket->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::Web->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'ws://localhost/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeWebSocketUrlWithNonDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::Http->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::WebSecure->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'ws://localhost:443/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeWebSocketSecureUrlWithDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::WebSocketSecure->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::WebSecure->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'wss://localhost/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
public function serializeWebSocketSecureUrlWithNonDefaultPort(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::WebSocketSecure->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::Web->value,
|
||||
[
|
||||
'isActive' => true,
|
||||
'page' => 1,
|
||||
'perPage' => 100
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'wss://localhost:80/api/endpoint?isActive=1&page=1&perPage=100',
|
||||
$this->getSerializer()->toString($url)
|
||||
);
|
||||
}
|
||||
|
||||
private function getSerializer(): RFC3986
|
||||
{
|
||||
return new RFC3986();
|
||||
}
|
||||
}
|
||||
55
tests/UrlTest.php
Normal file
55
tests/UrlTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests;
|
||||
|
||||
use Diffhead\PHP\Url\Port;
|
||||
use Diffhead\PHP\Url\Scheme;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Url::class)]
|
||||
#[CoversMethod(Url::class, 'scheme')]
|
||||
#[CoversMethod(Url::class, 'hostname')]
|
||||
#[CoversMethod(Url::class, 'path')]
|
||||
#[CoversMethod(Url::class, 'port')]
|
||||
#[CoversMethod(Url::class, 'parameters')]
|
||||
class UrlTest extends TestCase
|
||||
{
|
||||
public function testInitialization(): void
|
||||
{
|
||||
$url = new Url(
|
||||
Scheme::FileTransferOverSsh->value,
|
||||
'file.storage.info',
|
||||
'/home/user/Downloads',
|
||||
Port::FileTransferOverSsh->value,
|
||||
['file' => 'something.txt']
|
||||
);
|
||||
|
||||
$this->assertSame(Scheme::FileTransferOverSsh->value, $url->scheme());
|
||||
$this->assertSame('file.storage.info', $url->hostname());
|
||||
$this->assertSame('/home/user/Downloads', $url->path());
|
||||
$this->assertSame(Port::FileTransferOverSsh->value, $url->port());
|
||||
$this->assertEquals(['file' => 'something.txt'], $url->parameters());
|
||||
}
|
||||
|
||||
public function testStaticInitialization(): void
|
||||
{
|
||||
$url = Url::create(
|
||||
Scheme::WebSocketSecure->value,
|
||||
'localhost',
|
||||
'/api/endpoint',
|
||||
Port::WebSecure->value,
|
||||
['token' => 'token1234']
|
||||
);
|
||||
|
||||
$this->assertSame(Scheme::WebSocketSecure->value, $url->scheme());
|
||||
$this->assertSame('localhost', $url->hostname());
|
||||
$this->assertSame('/api/endpoint', $url->path());
|
||||
$this->assertSame(Port::WebSecure->value, $url->port());
|
||||
$this->assertEquals(['token' => 'token1234'], $url->parameters());
|
||||
}
|
||||
}
|
||||
108
tests/UtilTest.php
Normal file
108
tests/UtilTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Diffhead\PHP\Url\Tests;
|
||||
|
||||
use Diffhead\PHP\Url\Port;
|
||||
use Diffhead\PHP\Url\Scheme;
|
||||
use Diffhead\PHP\Url\Url;
|
||||
use Diffhead\PHP\Url\Util;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(Util::class)]
|
||||
#[CoversMethod(Util::class, 'isDefaultHttpUrl')]
|
||||
#[CoversMethod(Util::class, 'isDefaultWebSocketUrl')]
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public function testHttpHttpsWithDefaultPortsTesting(): void
|
||||
{
|
||||
$httpWith80Port = new Url(
|
||||
Scheme::Http->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::Web->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$httpsWith443Port = new Url(
|
||||
Scheme::Https->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::WebSecure->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertTrue(Util::isDefaultHttpUrl($httpWith80Port));
|
||||
$this->assertTrue(Util::isDefaultHttpUrl($httpsWith443Port));
|
||||
}
|
||||
|
||||
public function testHttpHttpsWithNonDefaultPortsTesting(): void
|
||||
{
|
||||
|
||||
$httpWith443Port = new Url(
|
||||
Scheme::Http->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::WebSecure->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$httpsWith80Port = new Url(
|
||||
Scheme::Https->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::Web->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertFalse(Util::isDefaultHttpUrl($httpWith443Port));
|
||||
$this->assertFalse(Util::isDefaultHttpUrl($httpsWith80Port));
|
||||
}
|
||||
|
||||
public function testWebSocketWithDefaultPortsTesting(): void
|
||||
{
|
||||
$webSocketWith80Port = new Url(
|
||||
Scheme::WebSocket->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::Web->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$webSocketSecureWith443Port = new Url(
|
||||
Scheme::WebSocketSecure->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::WebSecure->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertTrue(Util::isDefaultWebSocketUrl($webSocketWith80Port));
|
||||
$this->assertTrue(Util::isDefaultWebSocketUrl($webSocketSecureWith443Port));
|
||||
}
|
||||
|
||||
public function testWebSocketWithNonDefaultPortsTesting(): void
|
||||
{
|
||||
$webSocketWith443Port = new Url(
|
||||
Scheme::WebSocket->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::WebSecure->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$webSocketSecureWith80Port = new Url(
|
||||
Scheme::WebSocketSecure->value,
|
||||
'localhost',
|
||||
'/',
|
||||
Port::Web->value,
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertFalse(Util::isDefaultWebSocketUrl($webSocketWith443Port));
|
||||
$this->assertFalse(Util::isDefaultWebSocketUrl($webSocketSecureWith80Port));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user