From 97db02cfa135cec2a792d90488e6aced9b023d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 21 Oct 2026 10:38:38 +0200 Subject: [PATCH 01/15] Update README.md to reflect branch changes and improve clarity --- README.md | 126 +++++++++++++++++++----------------------------------- 1 file changed, 43 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 5072594..4323e78 100644 --- a/README.md +++ b/README.md @@ -22,114 +22,74 @@ /> +[![PHPStan](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPStan.yml) +[![PHPUnit](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPUnit.yml) +[![Codecov](https://codecov.io/gh/The-FireHub-Project/Foundation/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Foundation/tree/develop) +

- - GitHub created at - + GitHub last commit (branch) - + GitHub activity (branch) - - GitHub contributors - - GitHub language count - - GitHub top language - + GitHub repo size - - GitHub code size

-Provides the high-level foundation layer of the FireHub ecosystem. This repository contains developer-focused APIs and reusable building blocks that simplify common programming tasks while maintaining consistency, type safety, and architectural principles across FireHub applications. - -The Foundation layer is built on top of the Runtime layer and transforms low-level execution primitives into expressive object-oriented APIs. It provides commonly used components such as string manipulation, date and time handling, mathematical utilities, and other essential application-level abstractions. - -This repository represents the developer-facing layer between the FireHub Runtime and application code, enabling clean, readable, and maintainable PHP development. +The essential PHP core for building domain-driven, clean architecture applications. +Standard provides the base classes, kernel, domain layer, and minimal runtime utilities needed to start any FireHub project. --- -## FireHub Icon Repository Purpose - -FireHub Foundation is the high-level developer API layer of the FireHub ecosystem. - -Built on top of FireHub Runtime, it provides expressive, object-oriented abstractions for common programming tasks. The purpose of this layer is to expose simple, consistent, and powerful APIs while hiding low-level implementation details. +## FireHub Icon Development Branch -Foundation contains reusable building blocks such as: +⚠️ **This is the `develop` branch** -- String manipulation APIs -- Date and time handling -- Mathematical utilities -- Common application-level abstractions -- Developer-oriented helper objects +- Unstable +- APIs may change without notice +- Not intended for production use -The Foundation layer follows the FireHub architectural principles by separating responsibilities between definition, execution, and usage: +## FireHub Icon Branch Purpose -## FireHub Icon Security +The `develop` branch is the **primary integration branch** for all ongoing development. -For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. +It serves as the staging area where: +- Feature branches are merged +- Bug fixes are integrated +- Experimental work is stabilized +- Code is prepared for upcoming releases -## FireHub Icon Community +All **release branches** are created **from `develop`**. -![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Foundation?style=social) -![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Foundation?style=social) -![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Foundation?style=social) +## Stability Guarantee -- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). -- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. -- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. -- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. +❌ No backward compatibility guarantee +❌ APIs may change without notice +❌ Behavior may be incomplete or inconsistent +❌ Breaking changes are expected -## FireHub Icon Support +This branch is intended **only for contributors and advanced testers**. -

- - GitHub in-progress issues - - GitHub total issues - - GitHub opened pull requests -

+## FireHub Icon Composer Usage (Not Recommended) -For help with FireHub products, please refer to the following resources: +For internal testing only: -- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) -- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Foundation/issues) -- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) -- **Commercial support** (Pro/Enterprise): contact your account manager +```json +{ + "require": { + "the-firehub-project/foundation": "dev-develop" + } +} +``` +⚠️ Never use dev-develop in production. ## FireHub Icon Authors and Contributors @@ -146,4 +106,4 @@ Architecture guidelines, design principles, and ecosystem documentation are avai This software is licensed under the Apache-2.0 License. -For more details, read the full license [here](./LICENSE). +For more details, read the full license [here](./LICENSE). \ No newline at end of file From a67f27efd8ca0d504c179dc0a9d6f11be65e5b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 29 Oct 2026 14:59:49 +0100 Subject: [PATCH 02/15] **Refactor `chunk()`, `zip()`, and range/init sources for type annotation clarity and PHPStan compatibility** - **Changes**: - Updated `Stream::chunk()` return type PHPDoc to reflect `list>` for improved alignment with expected stream result structure. - Adjusted `chunk()` implementation: - Added explicit casting of `ChunkSource` with detailed type annotations. - Annotated `Stream::zip()` with PHPStan suppression (`@phpstan-ignore argument.type`) to address minor type mismatches with strict static analysis. - Enhanced `RangeInit` and `FillInit` generator methods: - Suppressed PHPStan complaints about native type assumptions for `$key` and `$value` using `@phpstan-ignore varTag.nativeType`. - **Rationale**: - Improves code readability and long-term maintainability by ensuring accurate PHPDoc annotations and compatibility with PHPStan static analysis. - Aligns with FireHub's emphasis on robust type safety and developer tooling integration. --- .../Storage/Initialization/FillInit.php | 2 +- .../Storage/Initialization/RangeInit.php | 2 +- src/DataStructure/Stream.php | 15 ++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/DataStructure/Storage/Initialization/FillInit.php b/src/DataStructure/Storage/Initialization/FillInit.php index 81f1df7..786d3b5 100644 --- a/src/DataStructure/Storage/Initialization/FillInit.php +++ b/src/DataStructure/Storage/Initialization/FillInit.php @@ -62,7 +62,7 @@ public function initialize ():Generator { for ($key = 0; $key < $this->length; $key++) /** @var TKey $key */ - yield $key => $this->value; + yield $key => $this->value; // @phpstan-ignore varTag.nativeType } diff --git a/src/DataStructure/Storage/Initialization/RangeInit.php b/src/DataStructure/Storage/Initialization/RangeInit.php index 0acb9af..9ea50b1 100644 --- a/src/DataStructure/Storage/Initialization/RangeInit.php +++ b/src/DataStructure/Storage/Initialization/RangeInit.php @@ -74,7 +74,7 @@ public function initialize ():Generator { * @var TKey $key * @var TValue $value */ - yield $key++ => $value; + yield $key++ => $value; // @phpstan-ignore varTag.nativeType $value += $this->step; diff --git a/src/DataStructure/Stream.php b/src/DataStructure/Stream.php index 9f44c1b..7626d7c 100644 --- a/src/DataStructure/Stream.php +++ b/src/DataStructure/Stream.php @@ -326,17 +326,18 @@ public function skip ():Skip { * @throws \FireHub\Foundation\DataStructure\Exception\ChunkSizeException If the specified chunk size is less * than one. * - * @return static> The chunked Stream. + * @return static>> The chunked Stream. */ public function chunk (int $size):static { - return new static( - new ChunkSource( - $this->source, - $size - ) + /** @var \FireHub\Foundation\DataStructure\Stream\Source\ChunkSource> $source*/ + $source = new ChunkSource( + $this->source, + $size ); + return new static($source); + } /** @@ -384,7 +385,7 @@ public function concat (iterable $values):static { public function zip (iterable $values):static { return new static( - new ZipSource( + new ZipSource( // @phpstan-ignore argument.type $this->source, $values ) From e05d9bdc9ba79f72528ab2bc9e3c4a4ecf48e0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 10:29:00 +0100 Subject: [PATCH 03/15] **Introduce URI Value Object Components and Validation Exceptions** - **New Additions**: - Introduced `Uri` component classes under `Foundation\Uri\Component` namespace: - `Scheme`, `Authority`, `Host`, `Port`, `Path`, `Query`, `Fragment`, `UserInfo`. - Each class encapsulates the corresponding URI component, enforcing immutability and strict validation rules. - Created custom exception classes under `Foundation\Uri\Exception` to handle invalid value scenarios: - `InvalidSchemeException`, `InvalidAuthorityException`, `InvalidHostException`, `InvalidPortException`, `InvalidPathException`, `InvalidQueryException`, `InvalidFragmentException`, `InvalidUserInfoException`. - **Details**: - Each URI component ensures type consistency, validation, and normalization. - E.g., `Scheme` enforces valid schemes like `https`, `ftp`; `Host` supports registered names and IP literals. - Validation utilizes regex patterns and PHP filter functions for strict compliance. - Validation exceptions are thrown for invalid patterns, maintaining robust error handling. - PHPDoc and version annotations included for clarity, compatibility, and guidance. - **Rationale**: - Establishes a modular, extensible foundation for URI handling in the FireHub ecosystem. - Provides strict validation and normalization for consistent, error-free URI representation and processing. - Aligns with FireHub's principles of type safety, immutability, and developer-friendly APIs. --- src/Uri/Component/Authority.php | 111 ++++++++++++++ src/Uri/Component/Fragment.php | 72 ++++++++++ src/Uri/Component/Host.php | 136 ++++++++++++++++++ src/Uri/Component/Path.php | 100 +++++++++++++ src/Uri/Component/Port.php | 72 ++++++++++ src/Uri/Component/Query.php | 86 +++++++++++ src/Uri/Component/Scheme.php | 103 +++++++++++++ src/Uri/Component/UserInfo.php | 72 ++++++++++ .../Exception/InvalidFragmentException.php | 31 ++++ src/Uri/Exception/InvalidHostException.php | 31 ++++ src/Uri/Exception/InvalidPathException.php | 31 ++++ src/Uri/Exception/InvalidPortException.php | 31 ++++ src/Uri/Exception/InvalidQueryException.php | 31 ++++ src/Uri/Exception/InvalidSchemeException.php | 31 ++++ .../Exception/InvalidUserInfoException.php | 31 ++++ 15 files changed, 969 insertions(+) create mode 100644 src/Uri/Component/Authority.php create mode 100644 src/Uri/Component/Fragment.php create mode 100644 src/Uri/Component/Host.php create mode 100644 src/Uri/Component/Path.php create mode 100644 src/Uri/Component/Port.php create mode 100644 src/Uri/Component/Query.php create mode 100644 src/Uri/Component/Scheme.php create mode 100644 src/Uri/Component/UserInfo.php create mode 100644 src/Uri/Exception/InvalidFragmentException.php create mode 100644 src/Uri/Exception/InvalidHostException.php create mode 100644 src/Uri/Exception/InvalidPathException.php create mode 100644 src/Uri/Exception/InvalidPortException.php create mode 100644 src/Uri/Exception/InvalidQueryException.php create mode 100644 src/Uri/Exception/InvalidSchemeException.php create mode 100644 src/Uri/Exception/InvalidUserInfoException.php diff --git a/src/Uri/Component/Authority.php b/src/Uri/Component/Authority.php new file mode 100644 index 0000000..0363c9e --- /dev/null +++ b/src/Uri/Component/Authority.php @@ -0,0 +1,111 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; + +/** + * ### Represents a URI authority + * + * Combines user information, host, and port into a single immutable URI authority component. + * @since 1.0.0 + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Authority extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Host $host

+ * URI host. + *

+ * @param null|\FireHub\Foundation\Uri\Component\Port$port [optional]

+ * Network port. + *

+ * @param null|\FireHub\Foundation\Uri\Component\UserInfo $user_info [optional]

+ * User information. + *

+ * + * @return void + */ + public function __construct ( + private Host $host, + private ?Port $port = null, + private ?UserInfo $user_info = null + ) {} + + /** + * ### Gets the host + * @since 1.0.0 + * + * @return \FireHub\Foundation\Uri\Component\Host The host. + */ + public function host ():Host { + + return $this->host; + + } + + /** + * ### Gets the port + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\Port The port. + */ + public function port ():?Port { + + return $this->port; + + } + + /** + * ### Gets the user information + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\UserInfo The user information. + */ + public function userInfo ():?UserInfo { + + return $this->user_info; + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\UserInfo::value() To get the user information. + * @uses \FireHub\Foundation\Uri\Component\Host::value() To get the host. + * @uses \FireHub\Foundation\Uri\Component\Port::value() To get the port. + */ + public function value ():string { + + $authority = ''; + + if ($this->user_info !== null) + $authority .= $this->user_info->value().'@'; + + $authority .= $this->host->value(); + + if ($this->port !== null) + $authority .= ':'.$this->port->value(); + + return $authority; + + } + +} diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php new file mode 100644 index 0000000..a8ecd4b --- /dev/null +++ b/src/Uri/Component/Fragment.php @@ -0,0 +1,72 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidFragmentException; +use FireHub\Runtime; + +/** + * ### Represents a URI fragment + * @since 1.0.0 + * + * @template TValue of string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Fragment extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the fragment. + * + * @param TValue $fragment

+ * URI fragment. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidFragmentException If fragment has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private string $fragment + ) { + + $this->guard( + fn() => Runtime\Str\SB\Regex::match( + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/D', + $fragment + ), + fn() => new InvalidFragmentException + ); + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + public function value ():string { + + return $this->fragment; + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php new file mode 100644 index 0000000..c1db9d9 --- /dev/null +++ b/src/Uri/Component/Host.php @@ -0,0 +1,136 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidHostException; +use FireHub\Runtime; + +/** + * ### Represents a URI host + * + * Supports registered names, IPv4 addresses, and bracketed IPv6 literals. + * @since 1.0.0 + * + * @template TValue of string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Host extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Foundation\Uri\Component\Host::isValid() To validate the host. + * + * @param TValue $host

+ * URI host. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidHostException If host has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private string $host + ) { + + $this->guard( + fn() => self::isValid($host), + fn() => new InvalidHostException + ); + + } + + /** + * ### Returns the normalized host + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Casing::toLower() To convert the host to lowercase. + * + * @return lowercase-string Normalized host. + */ + public function normalized ():string { + + return Runtime\Str\SB\Casing::toLower($this->host); + + } + + /** + * ### Returns the original host + */ + public function value ():string { + + return $this->host; + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Host::normalized() To get the normalized host. + */ + protected function comparisonValue ():string { + + return $this->normalized(); + + } + + /** + * ### Validates a URI host + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Search::endsWith() To check if the host ends with a closing bracket. + * @uses \FireHub\Runtime\Str\SB\Access::part() To get the address part of the host. + * @uses \FireHub\Runtime\Filter::value() To validate the address part of the host. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the host. + * + * @param string $host

+ * URI host. + *

+ * + * @return bool True if the host is valid, false otherwise. + */ + private static function isValid (string $host):bool { + + if ($host === '') return true; + + if ($host[0] === '[') { + + if (!Runtime\Str\SB\Search::endsWith(']', $host)) return false; + + $address = Runtime\Str\SB\Access::part($host, 1, -1); + + return Runtime\Filter::value( + $address, + FILTER_VALIDATE_IP, + FILTER_FLAG_IPV6 + ) !== false; + + } + + return Runtime\Str\SB\Regex::match( + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=-]|%[A-Fa-f0-9]{2})*\z/', + $host + ); + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/Path.php b/src/Uri/Component/Path.php new file mode 100644 index 0000000..9e79b34 --- /dev/null +++ b/src/Uri/Component/Path.php @@ -0,0 +1,100 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidPathException; +use FireHub\Runtime; + +/** + * ### Represents a URI path + * + * Preserves the original path and percent-encoded sequences. + * @since 1.0.0 + * + * @template TValue of string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Path extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the path. + * + * @param TValue $path

+ * URI path. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidPathException If path has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct( + private string $path + ) { + + $this->guard( + fn() => Runtime\Str\SB\Regex::match( + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/-]|%[A-Fa-f0-9]{2})*\z/D', + $path + ), + fn() => new InvalidPathException + ); + + } + + /** + * ### Checks whether the path is absolute + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Search::startsWith() To check if the path starts with a slash. + * + * @return bool True if the path is absolute, false otherwise. + */ + public function isAbsolute ():bool { + + return Runtime\Str\SB\Search::startsWith('/', $this->path); + + } + + /** + * ### Checks whether the path is empty + * @since 1.0.0 + * + * @return bool True if the path is empty, false otherwise. + */ + public function isEmpty ():bool { + + return $this->path === ''; + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + public function value ():string { + + return $this->path; + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/Port.php b/src/Uri/Component/Port.php new file mode 100644 index 0000000..7c06547 --- /dev/null +++ b/src/Uri/Component/Port.php @@ -0,0 +1,72 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidPortException; + +/** + * ### Represents a network port + * @since 1.0.0 + * + * @template TValue of int + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Port extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * + * @param int $port

+ * Network port. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidPortException If port is not between 0 and 65535. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private int $port + ) { + + $this->guard( + fn() => $port >= 0 && $port <= 65535, + fn() => new InvalidPortException( + 'Port must be between 0 and 65535.' + ) + ); + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + * + * @return int<0, 65535> Network port. + */ + public function value ():int { + + /** @var int<0, 65535> */ + return $this->port; + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/Query.php b/src/Uri/Component/Query.php new file mode 100644 index 0000000..7c86260 --- /dev/null +++ b/src/Uri/Component/Query.php @@ -0,0 +1,86 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidQueryException; +use FireHub\Runtime; + +/** + * ### Represents a URI query component + * + * Preserves the raw query string, including repeated parameters and percent-encoded characters. + * @since 1.0.0 + * + * @template TValue of string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Query extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the query. + * + * @param TValue $query

+ * Raw URI query. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidQueryException If query has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct( + private string $query + ) { + + $this->guard( + fn() => Runtime\Str\SB\Regex::match( + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/D', + $query + ), + fn() => new InvalidQueryException + ); + + } + + /** + * ### Checks whether the query is empty + * @since 1.0.0 + * + * @return bool True if the query is empty, false otherwise. + */ + public function isEmpty(): bool { + + return $this->query === ''; + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function value(): string { + + return $this->query; + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/Scheme.php b/src/Uri/Component/Scheme.php new file mode 100644 index 0000000..32dd491 --- /dev/null +++ b/src/Uri/Component/Scheme.php @@ -0,0 +1,103 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidSchemeException; +use FireHub\Runtime; + +/** + * ### Represents a URI scheme + * + * Preserves the original scheme representation. + * @since 1.0.0 + * + * @template TValue of non-empty-string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Scheme extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the scheme. + * + * @param TValue $scheme

+ * URI scheme. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidSchemeException If scheme has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private string $scheme + ) { + + $this->guard( + fn() => Runtime\Str\SB\Regex::match( + '/\A[A-Za-z][A-Za-z0-9+.-]*\z/', + $scheme + ), + fn() => new InvalidSchemeException + ); + + } + + /** + * ### Returns the normalized scheme + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Casing::toLower() To convert the scheme to lower case. + * + * @return lowercase-string&non-empty-string Normalized scheme. + */ + public function normalized ():string { + + /** @var lowercase-string&non-empty-string */ + return Runtime\Str\SB\Casing::toLower($this->scheme); + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function value ():string { + + return $this->scheme; + + } + + /** + * ### Compares schemes case-insensitively + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Internal\Scheme::normalized() To get the normalized scheme. + * + * @return non-empty-string Normalized scheme. + */ + protected function comparisonValue ():string { + + return $this->normalized(); + + } + +} \ No newline at end of file diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php new file mode 100644 index 0000000..3ccb0a0 --- /dev/null +++ b/src/Uri/Component/UserInfo.php @@ -0,0 +1,72 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Component; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Exception\InvalidUserInfoException; +use FireHub\Runtime; + +/** + * ### Represents URI user information + * @since 1.0.0 + * + * @template TValue of non-empty-string + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class UserInfo extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the userinfo. + * + * @param TValue $userinfo

+ * Raw URI user information. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUserInfoException If userinfo has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private string $userinfo + ) { + + $this->guard( + fn() => Runtime\Str\SB\Regex::match( + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:-]|%[A-Fa-f0-9]{2})*\z/', + $userinfo + ), + fn() => new InvalidUserInfoException + ); + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + public function value ():string { + + return $this->userinfo; + + } + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidFragmentException.php b/src/Uri/Exception/InvalidFragmentException.php new file mode 100644 index 0000000..2d67b5a --- /dev/null +++ b/src/Uri/Exception/InvalidFragmentException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI fragment + * @since 1.0.0 + */ +final class InvalidFragmentException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI fragment.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidHostException.php b/src/Uri/Exception/InvalidHostException.php new file mode 100644 index 0000000..9719dd9 --- /dev/null +++ b/src/Uri/Exception/InvalidHostException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI host + * @since 1.0.0 + */ +final class InvalidHostException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI host.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidPathException.php b/src/Uri/Exception/InvalidPathException.php new file mode 100644 index 0000000..cdfe73f --- /dev/null +++ b/src/Uri/Exception/InvalidPathException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI path + * @since 1.0.0 + */ +final class InvalidPathException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI path.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidPortException.php b/src/Uri/Exception/InvalidPortException.php new file mode 100644 index 0000000..c0dcda9 --- /dev/null +++ b/src/Uri/Exception/InvalidPortException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI port + * @since 1.0.0 + */ +final class InvalidPortException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI port.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidQueryException.php b/src/Uri/Exception/InvalidQueryException.php new file mode 100644 index 0000000..f9d9f32 --- /dev/null +++ b/src/Uri/Exception/InvalidQueryException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI query + * @since 1.0.0 + */ +final class InvalidQueryException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI query.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidSchemeException.php b/src/Uri/Exception/InvalidSchemeException.php new file mode 100644 index 0000000..516ad73 --- /dev/null +++ b/src/Uri/Exception/InvalidSchemeException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI scheme + * @since 1.0.0 + */ +final class InvalidSchemeException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI scheme.'; + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidUserInfoException.php b/src/Uri/Exception/InvalidUserInfoException.php new file mode 100644 index 0000000..f763fa1 --- /dev/null +++ b/src/Uri/Exception/InvalidUserInfoException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents invalid URI user information + * @since 1.0.0 + */ +final class InvalidUserInfoException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI user information.'; + +} \ No newline at end of file From 0d949cd26701c4cbd902045077304633b5a0d9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 10:58:02 +0100 Subject: [PATCH 04/15] **Refactor and enhance URI `Host` normalization and validation logic** - **Changes to `Host`**: - Updated the `normalized()` method: - Added checks for empty and IPv6 literal hosts. - Incorporated regex-based validation and percent-encoding normalization for host values. - Ensured case-insensitive comparison by normalizing to lowercase. - Enhanced `isValid()` method: - Introduced additional regex patterns to validate IPv6 and other naming schemes. - Simplified conditional flows for clarity and efficiency. - **Fixes**: - Corrected PHPDoc `@uses` annotations in `Scheme` to reference the proper namespace. - Addressed namespace typos in method references for consistency. - **Rationale**: - Improves URI normalization by handling edge cases and enforcing strict compliance with standards. - Strengthens validation mechanisms and ensures proper percent-encoding and decoding for host names. - Enhances code maintainability and readability for future extensions. --- src/Uri/Component/Host.php | 49 +++++++++++++++++++++++++++++------- src/Uri/Component/Scheme.php | 2 +- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php index c1db9d9..0f5b73e 100644 --- a/src/Uri/Component/Host.php +++ b/src/Uri/Component/Host.php @@ -62,12 +62,38 @@ public function __construct ( * @since 1.0.0 * * @uses \FireHub\Runtime\Str\SB\Casing::toLower() To convert the host to lowercase. + * @uses \FireHub\Runtime\Str\SB\Casing::toUpper() To convert the host to uppercase. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the host. + * @uses \FireHub\Runtime\Str\SB\Regex::replaceUsing() To replace the host with the normalized version. + * @uses \FireHub\Runtime\Char\SB::chr() To get the character at the specified position. + * @uses \FireHub\Runtime\Number::baseConverter() To convert the hexadecimal value to decimal. * * @return lowercase-string Normalized host. */ public function normalized ():string { - return Runtime\Str\SB\Casing::toLower($this->host); + if ($this->host === '') + return ''; + + if ($this->host[0] === '[') + return Runtime\Str\SB\Casing::toLower($this->host); + + $normalized = Runtime\Str\SB\Regex::replaceUsing( + '/%([A-Fa-f0-9]{2})/', + static function (array $matches):string { + + $character = Runtime\Char\SB::chr((int)Runtime\Number::baseConverter($matches[1], 16, 10)); // @phpstan-ignore-line + + if (Runtime\Str\SB\Regex::match('/\A[A-Za-z0-9._~-]\z/', $character)) + return $character; + + return '%'.Runtime\Str\SB\Casing::toUpper($matches[1]); // @phpstan-ignore offsetAccess.notFound + + }, + $this->host + ); + + return Runtime\Str\SB\Casing::toLower($normalized); } @@ -118,18 +144,23 @@ private static function isValid (string $host):bool { $address = Runtime\Str\SB\Access::part($host, 1, -1); - return Runtime\Filter::value( - $address, - FILTER_VALIDATE_IP, - FILTER_FLAG_IPV6 - ) !== false; + if (Runtime\Filter::value( + $address, + FILTER_VALIDATE_IP, + FILTER_FLAG_IPV6 + ) !== false) return true; + + return Runtime\Str\SB\Regex::match( + '/\Av[0-9A-Fa-f]+\.[A-Za-z0-9._~!$&\'()*+,;=:-]+\z/i', + $address + ); } return Runtime\Str\SB\Regex::match( - '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=-]|%[A-Fa-f0-9]{2})*\z/', - $host - ); + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=-]|%[A-Fa-f0-9]{2})*\z/', + $host + ); } diff --git a/src/Uri/Component/Scheme.php b/src/Uri/Component/Scheme.php index 32dd491..cc530a6 100644 --- a/src/Uri/Component/Scheme.php +++ b/src/Uri/Component/Scheme.php @@ -90,7 +90,7 @@ public function value ():string { * ### Compares schemes case-insensitively * @since 1.0.0 * - * @uses \FireHub\Foundation\Uri\Internal\Scheme::normalized() To get the normalized scheme. + * @uses \FireHub\Foundation\Uri\Component\Scheme::normalized() To get the normalized scheme. * * @return non-empty-string Normalized scheme. */ From e440f301759404e7dd9a8f275498f6ce33d2e896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 12:05:17 +0100 Subject: [PATCH 05/15] **Refactor `UserInfo` and `Host` normalization logic** - **Changes**: - Updated `UserInfo`: - Relaxed `@template TValue` constraint from `non-empty-string` to `string` for broader compatibility. - Improved `Host`: - Refactored `normalized()` method: - Added case normalization for hosts based on context (e.g., IPv6 literal handling, percent-encoding). - Applied consistent lowercase transformation logic while preserving relevant uppercase encodings. - Enhanced regex-based normalization to target accurate hexadecimal percent-escaping patterns. - **Rationale**: - Improves flexibility of `UserInfo` for handling various string inputs. - Strengthens `Host` normalization with better consideration for edge cases, ensuring compliance with URI standards. - Enhances maintainability and correctness through code clarity and logical flow. --- src/Uri/Component/Host.php | 26 +++++++++++++++++--------- src/Uri/Component/UserInfo.php | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php index 0f5b73e..d294746 100644 --- a/src/Uri/Component/Host.php +++ b/src/Uri/Component/Host.php @@ -68,33 +68,41 @@ public function __construct ( * @uses \FireHub\Runtime\Char\SB::chr() To get the character at the specified position. * @uses \FireHub\Runtime\Number::baseConverter() To convert the hexadecimal value to decimal. * - * @return lowercase-string Normalized host. + * @return string Normalized host. */ public function normalized ():string { if ($this->host === '') return ''; - if ($this->host[0] === '[') + if ($this->host[0] === '[') { + + if ($this->host[1] === 'v' || $this->host[1] === 'V') + return $this->host; + return Runtime\Str\SB\Casing::toLower($this->host); - $normalized = Runtime\Str\SB\Regex::replaceUsing( - '/%([A-Fa-f0-9]{2})/', + } + + $normalized = Runtime\Str\SB\Casing::toLower($this->host); + + return Runtime\Str\SB\Regex::replaceUsing( + '/%([a-f0-9]{2})/', static function (array $matches):string { - $character = Runtime\Char\SB::chr((int)Runtime\Number::baseConverter($matches[1], 16, 10)); // @phpstan-ignore-line + $character = Runtime\Char\SB::chr( + (int) Runtime\Number::baseConverter($matches[1], 16, 10) // @phpstan-ignore-line + ); if (Runtime\Str\SB\Regex::match('/\A[A-Za-z0-9._~-]\z/', $character)) - return $character; + return Runtime\Str\SB\Casing::toLower($character); return '%'.Runtime\Str\SB\Casing::toUpper($matches[1]); // @phpstan-ignore offsetAccess.notFound }, - $this->host + $normalized ); - return Runtime\Str\SB\Casing::toLower($normalized); - } /** diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php index 3ccb0a0..b4a3ba8 100644 --- a/src/Uri/Component/UserInfo.php +++ b/src/Uri/Component/UserInfo.php @@ -21,7 +21,7 @@ * ### Represents URI user information * @since 1.0.0 * - * @template TValue of non-empty-string + * @template TValue of string * * @extends \FireHub\Core\Type\ValueObject */ From 763559624a76282a22dc9ed08c65576ed8eeefe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 13:06:52 +0100 Subject: [PATCH 06/15] **Add utility methods for URI components and fix regex patterns** - **Changes**: - Added `isEmpty()` checks to several URI component classes: - `Path`, `UserInfo`, `Authority`, `Fragment`, `Host`. - Each check ensures components can reliably determine if their value is empty. - Introduced `isRoot()` method to `Path` to check if the path represents the root (i.e., `/`). - Updated regex patterns: - Removed unnecessary `D` modifier in `Query` and `Fragment` validation to improve clarity. - **Rationale**: - Enhances usability with utility methods for common operations (`isEmpty()`, `isRoot()`). - Strengthens component consistency with added checks for emptiness. - Simplifies and streamlines regex definitions while maintaining validation integrity. --- src/Uri/Component/Authority.php | 22 +++++++++++++++++++--- src/Uri/Component/Fragment.php | 14 +++++++++++++- src/Uri/Component/Host.php | 12 ++++++++++++ src/Uri/Component/Path.php | 12 ++++++++++++ src/Uri/Component/Query.php | 2 +- src/Uri/Component/UserInfo.php | 20 ++++++++++++++++---- 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/Uri/Component/Authority.php b/src/Uri/Component/Authority.php index 0363c9e..3c6dec2 100644 --- a/src/Uri/Component/Authority.php +++ b/src/Uri/Component/Authority.php @@ -32,10 +32,10 @@ * @param \FireHub\Foundation\Uri\Component\Host $host

* URI host. *

- * @param null|\FireHub\Foundation\Uri\Component\Port$port [optional]

+ * @param null|\FireHub\Foundation\Uri\Component\Port $port [optional]

* Network port. *

- * @param null|\FireHub\Foundation\Uri\Component\UserInfo $user_info [optional]

+ * @param null|\FireHub\Foundation\Uri\Component\UserInfo $user_info [optional]

* User information. *

* @@ -75,7 +75,7 @@ public function port ():?Port { * ### Gets the user information * @since 1.0.0 * - * @return null|\FireHub\Foundation\Uri\Component\UserInfo The user information. + * @return null|\FireHub\Foundation\Uri\Component\UserInfo The user information. */ public function userInfo ():?UserInfo { @@ -83,6 +83,22 @@ public function userInfo ():?UserInfo { } + /** + * ### Checks whether the authority is empty + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Host::isEmpty() To check if the host is empty. + * + * @return bool True if the authority is empty, false otherwise. + */ + public function isEmpty ():bool { + + return $this->host->isEmpty() + && $this->port === null + && $this->user_info === null; + + } + /** * @inheritdoc * diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php index a8ecd4b..5480c25 100644 --- a/src/Uri/Component/Fragment.php +++ b/src/Uri/Component/Fragment.php @@ -50,7 +50,7 @@ public function __construct ( $this->guard( fn() => Runtime\Str\SB\Regex::match( - '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/D', + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/', $fragment ), fn() => new InvalidFragmentException @@ -58,6 +58,18 @@ public function __construct ( } + /** + * ### Checks whether the fragment is empty + * @since 1.0.0 + * + * @return bool True if the fragment is empty, false otherwise. + */ + public function isEmpty ():bool { + + return $this->fragment === ''; + + } + /** * @inheritdoc * diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php index d294746..75ae676 100644 --- a/src/Uri/Component/Host.php +++ b/src/Uri/Component/Host.php @@ -57,6 +57,18 @@ public function __construct ( } + /** + * ### Checks whether the host is empty + * @since 1.0.0 + * + * @return bool True if the host is empty, false otherwise. + */ + public function isEmpty ():bool { + + return $this->host === ''; + + } + /** * ### Returns the normalized host * @since 1.0.0 diff --git a/src/Uri/Component/Path.php b/src/Uri/Component/Path.php index 9e79b34..2b7accb 100644 --- a/src/Uri/Component/Path.php +++ b/src/Uri/Component/Path.php @@ -60,6 +60,18 @@ public function __construct( } + /** + * ### Checks whether the path represents the root + * @since 1.0.0 + * + * @return bool True if the path is exactly "/", false otherwise. + */ + public function isRoot ():bool { + + return $this->path === '/'; + + } + /** * ### Checks whether the path is absolute * @since 1.0.0 diff --git a/src/Uri/Component/Query.php b/src/Uri/Component/Query.php index 7c86260..42d5fb1 100644 --- a/src/Uri/Component/Query.php +++ b/src/Uri/Component/Query.php @@ -52,7 +52,7 @@ public function __construct( $this->guard( fn() => Runtime\Str\SB\Regex::match( - '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/D', + '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:@\/?-]|%[A-Fa-f0-9]{2})*\z/', $query ), fn() => new InvalidQueryException diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php index b4a3ba8..27572ce 100644 --- a/src/Uri/Component/UserInfo.php +++ b/src/Uri/Component/UserInfo.php @@ -34,7 +34,7 @@ * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the userinfo. * - * @param TValue $userinfo

+ * @param TValue $user_info

* Raw URI user information. *

* @@ -45,19 +45,31 @@ * @return void */ public function __construct ( - private string $userinfo + private string $user_info ) { $this->guard( fn() => Runtime\Str\SB\Regex::match( '/\A(?:[A-Za-z0-9._~!$&\'()*+,;=:-]|%[A-Fa-f0-9]{2})*\z/', - $userinfo + $user_info ), fn() => new InvalidUserInfoException ); } + /** + * ### Checks whether the user information is empty + * @since 1.0.0 + * + * @return bool True if the user information is empty, false otherwise. + */ + public function isEmpty(): bool { + + return $this->user_info === ''; + + } + /** * @inheritdoc * @@ -65,7 +77,7 @@ public function __construct ( */ public function value ():string { - return $this->userinfo; + return $this->user_info; } From 0f143771670183209211ed33a92d6621d3a43497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 13:41:39 +0100 Subject: [PATCH 07/15] **Introduce `Uri` core class, `InvalidUriException`, and `KnownScheme` enum** - **New Additions**: - Added `Uri` class: - Represents complete, immutable URI references. - Combines components (`Scheme`, `Authority`, `Path`, `Query`, `Fragment`) into a unified value object. - Enforces strict RFC 3986 compliance with validation and normalization. - Supports utility methods like `isAbsolute()`, `isRelative()`, and `value()` for functional operations. - Introduced `InvalidUriException`: - Custom exception for invalid URI component combinations. - Integrated as part of the URI validation process. - Added `KnownScheme` enum: - Enumerates standard URI schemes (e.g., `http`, `ftp`, `mailto`). - Facilitates standardization and type safety for schemes. - **Rationale**: - Establishes a robust foundation for URI handling in the FireHub Project. - Aligns with the modular and type-safe architectural principles of the ecosystem. - Enables seamless integration and processing of normalized, validated URI references. --- src/Uri.php | 233 ++++++++++++++++++++++ src/Uri/Exception/InvalidUriException.php | 31 +++ src/Uri/KnownScheme.php | 153 ++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 src/Uri.php create mode 100644 src/Uri/Exception/InvalidUriException.php create mode 100644 src/Uri/KnownScheme.php diff --git a/src/Uri.php b/src/Uri.php new file mode 100644 index 0000000..7d84058 --- /dev/null +++ b/src/Uri.php @@ -0,0 +1,233 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Foundation + */ + +namespace FireHub\Foundation; + +use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Component\ { + Authority, Fragment, Path, Query, Scheme +}; +use FireHub\Foundation\Uri\Exception\InvalidUriException; +use FireHub\Runtime; + +/** + * ### Represents a URI reference + * + * Combines URI components into an immutable URI reference according to RFC 3986. + * + * Supports both absolute URIs and relative references. + * @since 1.0.0 + * + * @extends \FireHub\Core\Type\ValueObject + */ +final readonly class Uri extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\ValueObject::guard() As a guard. + * @uses \FireHub\Foundation\Uri::isValidCombination() To validate the URI component combination. + * + * @param null|\FireHub\Foundation\Uri\Component\Scheme $scheme [optional]

+ * URI scheme. + *

+ * @param null|\FireHub\Foundation\Uri\Component\Authority $authority [optional]

+ * URI authority. + *

+ * @param \FireHub\Foundation\Uri\Component\Path $path [optional]

+ * URI path. + *

+ * @param null|\FireHub\Foundation\Uri\Component\Query $query [optional]

+ * URI query. + *

+ * @param null|\FireHub\Foundation\Uri\Component\Fragment $fragment [optional]

+ * URI fragment. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct ( + private ?Scheme $scheme = null, + private ?Authority $authority = null, + private Path $path = new Path(''), // @phpstan-ignore parameter.defaultValue + private ?Query $query = null, + private ?Fragment $fragment = null + ) { + + $this->guard( + fn() => $this->isValidCombination(), + fn() => new InvalidUriException + ); + + } + + /** + * ### Gets the scheme + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\Scheme URI scheme. + */ + public function scheme ():?Scheme { + + return $this->scheme; + + } + + /** + * ### Gets the authority + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\Authority URI authority. + */ + public function authority ():?Authority { + + return $this->authority; + + } + + /** + * ### Gets the path + * @since 1.0.0 + * + * @return \FireHub\Foundation\Uri\Component\Path URI path. + */ + public function path ():Path { + + return $this->path; + + } + + /** + * ### Gets the query + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\Query URI query. + */ + public function query ():?Query { + + return $this->query; + + } + + /** + * ### Gets the fragment + * @since 1.0.0 + * + * @return null|\FireHub\Foundation\Uri\Component\Fragment URI fragment. + */ + public function fragment ():?Fragment { + + return $this->fragment; + + } + + /** + * ### Checks whether the URI is absolute + * + * An absolute URI contains a scheme and does not contain a fragment. + * @since 1.0.0 + * + * @return bool True if the URI is absolute, false otherwise. + */ + public function isAbsolute ():bool { + + return $this->scheme !== null && $this->fragment === null; + + } + + /** + * ### Checks whether the URI reference is relative + * @since 1.0.0 + * + * @return bool True if the URI reference has no scheme, false otherwise. + */ + public function isRelative ():bool { + + return $this->scheme === null; + + } + + /** + * @inheritdoc + * + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Scheme::value() To get the scheme value. + * @uses \FireHub\Foundation\Uri\Component\Authority::value() To get the authority value. + * @uses \FireHub\Foundation\Uri\Component\Path::value() To get the path value. + * @uses \FireHub\Foundation\Uri\Component\Query::value() To get the query value. + * @uses \FireHub\Foundation\Uri\Component\Fragment::value() To get the fragment value. + */ + public function value ():string { + + $uri = ''; + + if ($this->scheme !== null) + $uri .= $this->scheme->value().':'; + + if ($this->authority !== null) + $uri .= '//'.$this->authority->value(); + + $uri .= $this->path->value(); + + if ($this->query !== null) + $uri .= '?'.$this->query->value(); + + if ($this->fragment !== null) + $uri .= '#'.$this->fragment->value(); + + return $uri; + + } + + /** + * ### Validates the URI component combination + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Path::value() To get the path value. + * @uses \FireHub\Foundation\Uri\Component\Path::isAbsolute() To check if the path is absolute. + * @uses \FireHub\Runtime\Str\SB\Search::startsWith() To check if the path starts with "//". + * @uses \FireHub\Runtime\Str\SB\Search::contains() To check if the path contains a colon. + * @uses \FireHub\Runtime\Str\SB\Delimiter::explode() To split the path into segments. + * + * @return bool True if the combination is valid, false otherwise. + */ + private function isValidCombination ():bool { + + $path = $this->path->value(); + + if ($this->authority !== null) + return $path === '' || $this->path->isAbsolute(); + + if (Runtime\Str\SB\Search::startsWith('//', $path)) + return false; + + if ($this->scheme === null && $path !== '' && $path[0] !== '/') { + + $first_segment = Runtime\Str\SB\Delimiter::explode($path, '/', 2)[0]; // @phpstan-ignore offsetAccess.notFound + + if (Runtime\Str\SB\Search::contains(':', $first_segment)) + return false; + + } + + return true; + + } + +} \ No newline at end of file diff --git a/src/Uri/Exception/InvalidUriException.php b/src/Uri/Exception/InvalidUriException.php new file mode 100644 index 0000000..dae81a1 --- /dev/null +++ b/src/Uri/Exception/InvalidUriException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.3 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid URI + * @since 1.0.0 + */ +final class InvalidUriException extends InvalidArgumentException { + + /** + * @inheritdoc + * + * @since 1.0.0 + */ + protected const string DEFAULT_MESSAGE = 'Invalid URI.'; + +} \ No newline at end of file diff --git a/src/Uri/KnownScheme.php b/src/Uri/KnownScheme.php new file mode 100644 index 0000000..dd60a55 --- /dev/null +++ b/src/Uri/KnownScheme.php @@ -0,0 +1,153 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri; + +/** + * ### Known URI schemes + * + * Defines commonly used URI schemes for identifying resources across network protocols, communication services, and + * other resource identification systems. + * + * This enumeration is not exhaustive. Custom URI schemes are supported through the Scheme value object. + * @since 1.0.0 + */ +enum KnownScheme:string { + + /** + * Hypertext Transfer Protocol. + * @since 1.0.0 + */ + case HTTP = 'http'; + + /** + * Hypertext Transfer Protocol Secure. + * @since 1.0.0 + */ + case HTTPS = 'https'; + + /** + * WebSocket Protocol. + * @since 1.0.0 + */ + case WS = 'ws'; + + /** + * WebSocket Secure Protocol. + * @since 1.0.0 + */ + case WSS = 'wss'; + + /** + * File Transfer Protocol. + * @since 1.0.0 + */ + case FTP = 'ftp'; + + /** + * Secure Shell Protocol. + * @since 1.0.0 + */ + case SSH = 'ssh'; + + /** + * Local file resources. + * @since 1.0.0 + */ + case FILE = 'file'; + + /** + * Electronic mail addresses. + * @since 1.0.0 + */ + case MAILTO = 'mailto'; + + /** + * Telephone numbers. + * @since 1.0.0 + */ + case TEL = 'tel'; + + /** + * Short Message Service. + * @since 1.0.0 + */ + case SMS = 'sms'; + + /** + * Uniform Resource Names. + * @since 1.0.0 + */ + case URN = 'urn'; + + /** + * Inline data resources. + * @since 1.0.0 + */ + case DATA = 'data'; + + /** + * Lightweight Directory Access Protocol. + * @since 1.0.0 + */ + case LDAP = 'ldap'; + + /** + * Lightweight Directory Access Protocol over TLS. + * @since 1.0.0 + */ + case LDAPS = 'ldaps'; + + /** + * Session Initiation Protocol. + * @since 1.0.0 + */ + case SIP = 'sip'; + + /** + * Secure Session Initiation Protocol. + * @since 1.0.0 + */ + case SIPS = 'sips'; + + /** + * Internet Relay Chat. + * @since 1.0.0 + */ + case IRC = 'irc'; + + /** + * Secure Internet Relay Chat. + * @since 1.0.0 + */ + case IRCS = 'ircs'; + + /** + * Network News Transfer Protocol. + * @since 1.0.0 + */ + case NEWS = 'news'; + + /** + * Network News Transfer Protocol. + * @since 1.0.0 + */ + case NNTP = 'nntp'; + + /** + * Git repository resources. + * @since 1.0.0 + */ + case GIT = 'git'; + +} \ No newline at end of file From daff5a27a4256ada347342fcb54c6500035db22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 14:16:19 +0100 Subject: [PATCH 08/15] Refactor `Uri` class to extend `BaseUri` for improved core integration - **Changes**: - Replaced inheritance of `ValueObject` with `BaseUri` in `Foundation\Uri`. - Adjusted namespace import to reflect `Uri` as `BaseUri`. - **Rationale**: - Aligns `Uri` class with the core `BaseUri` structure for consistency and better integration. - Simplifies future enhancements by unifying foundational URI behavior under `BaseUri`. --- src/Uri.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Uri.php b/src/Uri.php index 7d84058..b639d19 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -13,7 +13,7 @@ namespace FireHub\Foundation; -use FireHub\Core\Type\ValueObject; +use FireHub\Core\Type\Uri as BaseUri; use FireHub\Foundation\Uri\Component\ { Authority, Fragment, Path, Query, Scheme }; @@ -28,9 +28,9 @@ * Supports both absolute URIs and relative references. * @since 1.0.0 * - * @extends \FireHub\Core\Type\ValueObject + * @extends \FireHub\Core\Type\Uri */ -final readonly class Uri extends ValueObject { +final readonly class Uri extends BaseUri { /** * ### Constructor From 86768d7e06fb96245357f106f455806848601984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 30 Oct 2026 14:53:00 +0100 Subject: [PATCH 09/15] Add percent-encoding support to URI components - **New Features**: - Introduced `Percent` class in `Foundation\Uri\Encoding` for percent-encoding and decoding based on RFC 3986: - Added methods: `encodePath()`, `encodeQuery()`, `encodeFragment()`, `encodeUserInfo()`, and `decode()`. - Ensures safe handling of all URI components with disallowed bytes percent-encoded. - **Updates**: - Extended `Query`, `Path`, `UserInfo`, and `Fragment` components: - Added `fromDecoded()` and `decoded()` methods to simplify encoding from raw strings and decoding back to raw values. - Utilized `Percent` class for encoding/decoding logic. - **Rationale**: - Improves URI component handling with robust percent-encoding and decoding. - Enhances usability by abstracting encoding logic into a dedicated utility class, making component APIs cleaner and more consistent. --- src/Uri/Component/Fragment.php | 37 +++++++ src/Uri/Component/Path.php | 37 +++++++ src/Uri/Component/Query.php | 37 +++++++ src/Uri/Component/UserInfo.php | 37 +++++++ src/Uri/Encoding/Percent.php | 188 +++++++++++++++++++++++++++++++++ 5 files changed, 336 insertions(+) create mode 100644 src/Uri/Encoding/Percent.php diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php index 5480c25..852f772 100644 --- a/src/Uri/Component/Fragment.php +++ b/src/Uri/Component/Fragment.php @@ -14,6 +14,7 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Encoding\Percent; use FireHub\Foundation\Uri\Exception\InvalidFragmentException; use FireHub\Runtime; @@ -58,6 +59,42 @@ public function __construct ( } + /** + * ### Creates a URI fragment from a decoded string + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeQuery() To encode the fragment. + * + * @param string $fragment

+ * Decoded URI fragment. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidFragmentException If fragment has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Encoded URI fragment. + */ + public static function fromDecoded (string $fragment):self { + + return new self(Percent::encodeQuery($fragment)); + + } + + /** + * ### Gets the decoded URI fragment + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\PercentEncoder::decode() To decode the fragment. + * + * @return string Decoded URI fragment. + */ + public function decoded ():string { + + return Percent::decode($this->fragment); + + } + /** * ### Checks whether the fragment is empty * @since 1.0.0 diff --git a/src/Uri/Component/Path.php b/src/Uri/Component/Path.php index 2b7accb..375d63d 100644 --- a/src/Uri/Component/Path.php +++ b/src/Uri/Component/Path.php @@ -14,6 +14,7 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Encoding\Percent; use FireHub\Foundation\Uri\Exception\InvalidPathException; use FireHub\Runtime; @@ -60,6 +61,42 @@ public function __construct( } + /** + * ### Creates a URI path from a decoded string + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodePath() To encode the path. + * + * @param string $path

+ * Decoded URI path. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidPathException If path has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Encoded URI path. + */ + public static function fromDecoded (string $path):self { + + return new self(Percent::encodePath($path)); + + } + + /** + * ### Gets the decoded URI path + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\PercentEncoder::decode() To decode the path. + * + * @return string Decoded URI path. + */ + public function decoded ():string { + + return Percent::decode($this->path); + + } + /** * ### Checks whether the path represents the root * @since 1.0.0 diff --git a/src/Uri/Component/Query.php b/src/Uri/Component/Query.php index 42d5fb1..63614de 100644 --- a/src/Uri/Component/Query.php +++ b/src/Uri/Component/Query.php @@ -14,6 +14,7 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Encoding\Percent; use FireHub\Foundation\Uri\Exception\InvalidQueryException; use FireHub\Runtime; @@ -60,6 +61,42 @@ public function __construct( } + /** + * ### Creates a URI query from a decoded string + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeQuery() To encode the query. + * + * @param string $query

+ * Decoded URI query. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidQueryException If query has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Encoded URI query. + */ + public static function fromDecoded (string $query):self { + + return new self(Percent::encodeQuery($query)); + + } + + /** + * ### Gets the decoded URI query + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\PercentEncoder::decode() To decode the query. + * + * @return string Decoded URI query. + */ + public function decoded ():string { + + return Percent::decode($this->query); + + } + /** * ### Checks whether the query is empty * @since 1.0.0 diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php index 27572ce..76c8eda 100644 --- a/src/Uri/Component/UserInfo.php +++ b/src/Uri/Component/UserInfo.php @@ -14,6 +14,7 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Encoding\Percent; use FireHub\Foundation\Uri\Exception\InvalidUserInfoException; use FireHub\Runtime; @@ -58,6 +59,42 @@ public function __construct ( } + /** + * ### Creates a URI user info from a decoded string + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeQuery() To encode the user info. + * + * @param string $user_info

+ * Decoded URI user info. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUserInfoException If userinfo has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Encoded URI user info. + */ + public static function fromDecoded (string $user_info):self { + + return new self(Percent::encodeQuery($user_info)); + + } + + /** + * ### Gets the decoded URI user info + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\PercentEncoder::decode() To decode the user info. + * + * @return string Decoded URI user info. + */ + public function decoded ():string { + + return Percent::decode($this->user_info); + + } + /** * ### Checks whether the user information is empty * @since 1.0.0 diff --git a/src/Uri/Encoding/Percent.php b/src/Uri/Encoding/Percent.php new file mode 100644 index 0000000..9f48e7f --- /dev/null +++ b/src/Uri/Encoding/Percent.php @@ -0,0 +1,188 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Foundation + */ + +namespace FireHub\Foundation\Uri\Encoding; + +use FireHub\Runtime; + +/** + * ### URI percent encoder + * + * Provides component-specific percent-encoding and decoding according to RFC 3986. + * + * Encoding preserves characters allowed by the target URI component and percent-encodes all other bytes. + * + * Input values are treated as decoded data. Existing percent signs are encoded rather than interpreted as escape + * sequences. + * @since 1.0.0 + */ +final readonly class Percent { + + /** + * ### Encodes a URI path + * + * Preserves forward slashes as path separators. + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encode() To percent-encode disallowed bytes. + * + * @param string $value

+ * Decoded URI path. + *

+ * + * @return string Percent-encoded URI path. + */ + public static function encodePath (string $value):string { + + return self::encode( + $value, + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@/" + ); + + } + + /** + * ### Encodes a URI query + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encode() To percent-encode disallowed bytes. + * + * @param string $value

+ * Decoded URI query. + *

+ * + * @return string Percent-encoded URI query. + */ + public static function encodeQuery (string $value):string { + + return self::encode( + $value, + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@/?" + ); + + } + + /** + * ### Encodes a URI fragment + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encode() To percent-encode disallowed bytes. + * + * @param string $value

+ * Decoded URI fragment. + *

+ * + * @return string Percent-encoded URI fragment. + */ + public static function encodeFragment (string $value):string { + + return self::encode( + $value, + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:@/?" + ); + + } + + /** + * ### Encodes URI user information + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encode() To percent-encode disallowed bytes. + * + * @param string $value

+ * Decoded URI user information. + *

+ * + * @return string Percent-encoded URI user information. + */ + public static function encodeUserInfo (string $value):string { + + return self::encode( + $value, + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$&'()*+,;=:" + ); + + } + + /** + * ### Decodes a percent-encoded URI component + * + * Decodes percent-encoded octets without interpreting component delimiters or applying form-style decoding. + * + * Plus signs are preserved. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Encoding::rawUrlDecode() To decode percent-encoded octets. + * + * @param string $value

+ * Percent-encoded URI component. + *

+ * + * @return string Decoded component. + */ + public static function decode (string $value):string { + + return Runtime\Encoding::rawUrlDecode($value); + + } + + /** + * ### Percent-encodes disallowed bytes + * + * Preserves only bytes explicitly listed in the allowed character set. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Inspection::length() To get the length of the input value. + * @uses \FireHub\Runtime\Str\SB\Search::contains() To check if a character is in the allowed set. + * @uses \FireHub\Runtime\Char\SB::ord() To get the byte value of a character. + * @uses \FireHub\Runtime\Str\SB\Transform::format() To format a string with a placeholder. + * + * @param string $value

+ * Input value. + *

+ * @param string $allowed

+ * Allowed ASCII characters. + *

+ * + * @return string Percent-encoded value. + */ + private static function encode (string $value, string $allowed):string { + + $encoded = ''; + + $length = Runtime\Str\SB\Inspection::length($value); + + for ($index = 0; $index < $length; $index++) { + + $character = $value[$index]; + + if (Runtime\Str\SB\Search::contains($character, $allowed)) { + + $encoded .= $character; + + continue; + + } + + $encoded .= Runtime\Str\SB\Transform::format( + '%%%02X', + Runtime\Char\SB::ord($character) + ); + + } + + return $encoded; + + } + +} \ No newline at end of file From bbe5bda929bd4e4df4a48c6bc0ea0bb5d9d6b881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 2 Nov 2026 09:00:24 +0100 Subject: [PATCH 10/15] Introduce URI parsing methods for Authority and Reference components - **New Features**: - Added `Authority::parse` method: - Parses user info, host, and optional port from a URI authority string. - Validates URI authorities according to RFC 3986, including IPv6 literals and port ranges. - Introduced `Uri::parse` method: - Parses absolute and relative URI references. - Validates components (scheme, authority, path, query, fragment) with detailed checks for compliance. - Preserves original representations of URI components, such as percent-encoded sequences. - **Updates**: - Included `parsePort` as a utility for strict port validation in `Authority`. - Enhanced error handling with appropriate exceptions for invalid URIs. - **Rationale**: - Establishes core URI parsing capabilities, enabling seamless handling of structured and validated URI references. - Ensures strict adherence to RFC 3986 while improving ease of integration for URI components. --- src/Uri.php | 54 +++++++++++++++ src/Uri/Component/Authority.php | 118 +++++++++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/Uri.php b/src/Uri.php index b639d19..73a063f 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -76,6 +76,60 @@ public function __construct ( } + /** + * ### Parses a URI reference + * + * Parses an absolute URI or relative reference according to RFC 3986. + * + * Preserves the original component representations, including percent-encoded sequences and empty query or + * fragment components. + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Authority::parse() To parse the authority. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To match the URI pattern. + * + * @param string $uri

+ * URI reference to parse. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI is invalid. + * @throws \FireHub\Foundation\Uri\Exception\InvalidSchemeException If scheme has invalid pattern. + * @throws \FireHub\Foundation\Uri\Exception\InvalidPortException If port is not between 0 and 65535. + * @throws \FireHub\Foundation\Uri\Exception\InvalidPathException If path has invalid pattern. + * @throws \FireHub\Foundation\Uri\Exception\InvalidQueryException If query has invalid pattern. + * @throws \FireHub\Foundation\Uri\Exception\InvalidFragmentException If fragment has invalid pattern. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Parsed URI reference. + */ + public static function parse (string $uri):self { + + if (!Runtime\Str\SB\Regex::match( + '/\A(?:([A-Za-z][A-Za-z0-9+.-]*):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?\z/s', + $uri, + result: $matches, + flags: PREG_UNMATCHED_AS_NULL + )) throw new InvalidUriException; + + return new self( + $matches[1] !== null // @phpstan-ignore offsetAccess.notFound + ? new Scheme($matches[1]) // @phpstan-ignore argument.type + : null, + $matches[2] !== null // @phpstan-ignore offsetAccess.notFound + ? Authority::parse($matches[2]) // @phpstan-ignore argument.type + : null, + new Path($matches[3]), // @phpstan-ignore offsetAccess.notFound, argument.type + $matches[4] !== null // @phpstan-ignore offsetAccess.notFound + ? new Query($matches[4]) // @phpstan-ignore argument.type + : null, + $matches[5] !== null // @phpstan-ignore offsetAccess.notFound + ? new Fragment($matches[5]) // @phpstan-ignore argument.type + : null + ); + + } + /** * ### Gets the scheme * @since 1.0.0 diff --git a/src/Uri/Component/Authority.php b/src/Uri/Component/Authority.php index 3c6dec2..bcfcb8f 100644 --- a/src/Uri/Component/Authority.php +++ b/src/Uri/Component/Authority.php @@ -14,6 +14,9 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Core\Meta\Enum\Side; +use FireHub\Foundation\Uri\Exception\InvalidUriException; +use FireHub\Runtime; /** * ### Represents a URI authority @@ -47,6 +50,88 @@ public function __construct ( private ?UserInfo $user_info = null ) {} + /** + * ### Parses a URI authority + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Search::lastPosition() To find the last occurrence of a specific character. + * @uses \FireHub\Runtime\Str\SB\Search::firstPosition() To find the first occurrence of a specific character. + * @uses \FireHub\Runtime\Str\SB\Access::part() To get the part of the string. + * @uses \FireHub\Runtime\Str\SB\Search::startsWith() To check if the string starts with a specific character. + * @uses \FireHub\Runtime\Str\SB\Search::contains() To check if the string contains a specific character. + * @uses \FireHub\Foundation\Uri\Authority::parsePort() To parse the port. + * + * @param string $authority

+ * URI authority to parse. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the authority or port is invalid. + * @throws \FireHub\Foundation\Uri\Exception\InvalidPortException If port is not between 0 and 65535. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self Parsed URI authority. + */ + public static function parse (string $authority):self { + + $user_info = null; + + $separator = Runtime\Str\SB\Search::lastPosition('@', $authority); + + if ($separator !== false) { + + $user_info = new UserInfo( + Runtime\Str\SB\Access::part($authority, 0, $separator) + ); + + $authority = Runtime\Str\SB\Access::part($authority, $separator + 1); + + } + + $host = $authority; $port = null; + if (Runtime\Str\SB\Search::startsWith('[', $authority)) { + + $closing = Runtime\Str\SB\Search::firstPosition(']', $authority); + + if ($closing === false) + throw new InvalidUriException; + + $host = Runtime\Str\SB\Access::part($authority, 0, $closing + 1); + + $remaining = Runtime\Str\SB\Access::part($authority, $closing + 1); + + if ($remaining !== '') { + + if (!Runtime\Str\SB\Search::startsWith(':', $remaining)) + throw new InvalidUriException; + + $port = self::parsePort(Runtime\Str\SB\Access::part($remaining, 1)); + + } + + } else if (Runtime\Str\SB\Search::contains(':', $authority)) { + + $separator = Runtime\Str\SB\Search::lastPosition(':', $authority) ?: 0; + + $host = Runtime\Str\SB\Access::part($authority, 0, $separator); + + if (Runtime\Str\SB\Search::contains(':', $host)) + throw new InvalidUriException; + + $port = self::parsePort( + Runtime\Str\SB\Access::part($authority, $separator + 1) + ); + + } + + return new self( + new Host($host), + $port, + $user_info + ); + + } + /** * ### Gets the host * @since 1.0.0 @@ -124,4 +209,35 @@ public function value ():string { } -} + /** + * ### Parses a URI port + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Regex::match() To check if the port is valid. + * @uses \FireHub\Runtime\Str\SB\Transform::trim() To trim the port. + * + * @param string $port

+ * Raw URI port. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the port is invalid. + * @throws \FireHub\Foundation\Uri\Exception\InvalidPortException If port is not between 0 and 65535. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return null|\FireHub\Foundation\Uri\Component\Port Parsed port. + */ + private static function parsePort (string $port):?Port { + + if ($port === '') return null; + + if (!Runtime\Str\SB\Regex::match('/\A[0-9]+\z/', $port)) + throw new InvalidUriException; + + $normalized = Runtime\Str\SB\Transform::trim($port, Side::LEFT, '0'); + + return new Port((int)$normalized); + + } + +} \ No newline at end of file From 92a958742cfb49bdd5e32402f61d4266bd99ea77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 2 Nov 2026 09:04:50 +0100 Subject: [PATCH 11/15] Update `UserInfo` and `Fragment` components to use correct percent-encoding methods - **Changes**: - Replaced `Percent::encodeQuery()` with `Percent::encodeUserInfo()` in `UserInfo::fromDecoded()` for accurate encoding of user info. - Updated `Fragment::fromDecoded()` to use `Percent::encodeFragment()` instead of `Percent::encodeQuery()`. - **Rationale**: - Ensures the correct encoding methods are used for each URI component, adhering to RFC 3986 specifications. - Improves clarity and consistency in the encoding logic for user info and fragment components. --- src/Uri/Component/Fragment.php | 4 ++-- src/Uri/Component/UserInfo.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php index 852f772..efd7770 100644 --- a/src/Uri/Component/Fragment.php +++ b/src/Uri/Component/Fragment.php @@ -63,7 +63,7 @@ public function __construct ( * ### Creates a URI fragment from a decoded string * @since 1.0.0 * - * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeQuery() To encode the fragment. + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeFragment() To encode the fragment. * * @param string $fragment

* Decoded URI fragment. @@ -77,7 +77,7 @@ public function __construct ( */ public static function fromDecoded (string $fragment):self { - return new self(Percent::encodeQuery($fragment)); + return new self(Percent::encodeFragment($fragment)); } diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php index 76c8eda..126ea9b 100644 --- a/src/Uri/Component/UserInfo.php +++ b/src/Uri/Component/UserInfo.php @@ -63,7 +63,7 @@ public function __construct ( * ### Creates a URI user info from a decoded string * @since 1.0.0 * - * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeQuery() To encode the user info. + * @uses \FireHub\Foundation\Uri\Encoding\Percent::encodeUserInfo() To encode the user info. * * @param string $user_info

* Decoded URI user info. @@ -77,7 +77,7 @@ public function __construct ( */ public static function fromDecoded (string $user_info):self { - return new self(Percent::encodeQuery($user_info)); + return new self(Percent::encodeUserInfo($user_info)); } From 90ca4c9219d0c9207b798bc4581afdaeaf86c004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 2 Nov 2026 09:56:34 +0100 Subject: [PATCH 12/15] Add normalization methods for URI components and overall URI normalization - **New Features**: - Introduced `normalized()` methods for URI components (`Query`, `Path`, `UserInfo`, `Fragment`, `Host`): - Normalizes percent-encoded sequences for compatibility with RFC 3986. - Preserves original logical structure (e.g., query parameter order, path segments). - Added `Uri::normalize()` method for complete URI normalization: - Applies component-level normalization (`Scheme`, `Authority`, `Path`, `Query`, `Fragment`, `Host`). - Integrates dot-segment removal for paths. - Removes default ports for supported schemes. - **Enhancements**: - Refactored `Host` normalization to utilize `Percent::normalize()` for consistent percent-encoding handling. - Added `Path::removeDotSegments()` to handle relative and absolute path normalization per RFC 3986 Section 5.2.4. - **Rationale**: - Standardizes normalization logic across all URI components. - Ensures adherence to URI standards, improving reliability and interoperability of parsed and normalized URI references. - Enhances code clarity and maintainability by consolidating normalization logic into reusable methods. --- src/Uri.php | 64 ++++++++++++++++++++++++- src/Uri/Component/Fragment.php | 14 ++++++ src/Uri/Component/Host.php | 34 ++------------ src/Uri/Component/Path.php | 86 ++++++++++++++++++++++++++++++++++ src/Uri/Component/Query.php | 16 +++++++ src/Uri/Component/UserInfo.php | 14 ++++++ src/Uri/Encoding/Percent.php | 39 +++++++++++++++ 7 files changed, 237 insertions(+), 30 deletions(-) diff --git a/src/Uri.php b/src/Uri.php index 73a063f..a332159 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -15,7 +15,7 @@ use FireHub\Core\Type\Uri as BaseUri; use FireHub\Foundation\Uri\Component\ { - Authority, Fragment, Path, Query, Scheme + Authority, Fragment, Host, Path, Query, Scheme, UserInfo }; use FireHub\Foundation\Uri\Exception\InvalidUriException; use FireHub\Runtime; @@ -216,6 +216,68 @@ public function isRelative ():bool { } + /** + * ### Normalizes the URI + * + * Applies syntax-based normalization according to RFC 3986 and removes default ports for supported schemes. + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Component\Scheme::normalized() To normalize the scheme. + * @uses \FireHub\Foundation\Uri\Component\Authority::normalized() To normalize the authority. + * @uses \FireHub\Foundation\Uri\Component\Path::normalized() To normalize the path. + * @uses \FireHub\Foundation\Uri\Component\Query::normalized() To normalize the query. + * @uses \FireHub\Foundation\Uri\Component\Fragment::normalized() To normalize the fragment. + * @uses \FireHub\Foundation\Uri\Component\Path::removeDotSegments() To remove dot segments. + * + * @return self Normalized URI. + */ + public function normalize ():self { + + $scheme = $this->scheme !== null + ? new Scheme($this->scheme->normalized()) + : null; + + $authority = null; + + if ($this->authority !== null) { + + $host = new Host( + $this->authority->host()->normalized() + ); + + $port = $this->authority->port(); + + $user_info = $this->authority->userInfo(); + + $authority = new Authority( + $host, + $port, + $user_info !== null + ? new UserInfo($user_info->normalized()) + : null + ); + + } + + $path = new Path($this->path->normalized()); + + if ($scheme !== null || $authority !== null) + $path = $path->removeDotSegments(); + + return new self( + $scheme, + $authority, + $path, + $this->query !== null + ? new Query($this->query->normalized()) + : null, + $this->fragment !== null + ? new Fragment($this->fragment->normalized()) + : null + ); + + } + /** * @inheritdoc * diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php index efd7770..828a7f2 100644 --- a/src/Uri/Component/Fragment.php +++ b/src/Uri/Component/Fragment.php @@ -107,6 +107,20 @@ public function isEmpty ():bool { } + /** + * ### Gets the normalized fragment + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::normalize() To normalize the fragment. + * + * @return string Normalized fragment. + */ + public function normalized ():string { + + return Percent::normalize($this->fragment); + + } + /** * @inheritdoc * diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php index 75ae676..808671b 100644 --- a/src/Uri/Component/Host.php +++ b/src/Uri/Component/Host.php @@ -14,6 +14,7 @@ namespace FireHub\Foundation\Uri\Component; use FireHub\Core\Type\ValueObject; +use FireHub\Foundation\Uri\Encoding\Percent; use FireHub\Foundation\Uri\Exception\InvalidHostException; use FireHub\Runtime; @@ -74,11 +75,7 @@ public function isEmpty ():bool { * @since 1.0.0 * * @uses \FireHub\Runtime\Str\SB\Casing::toLower() To convert the host to lowercase. - * @uses \FireHub\Runtime\Str\SB\Casing::toUpper() To convert the host to uppercase. - * @uses \FireHub\Runtime\Str\SB\Regex::match() To validate the host. - * @uses \FireHub\Runtime\Str\SB\Regex::replaceUsing() To replace the host with the normalized version. - * @uses \FireHub\Runtime\Char\SB::chr() To get the character at the specified position. - * @uses \FireHub\Runtime\Number::baseConverter() To convert the hexadecimal value to decimal. + * @uses \FireHub\Foundation\Uri\Encoding\Percent::normalize() To normalize the host. * * @return string Normalized host. */ @@ -87,32 +84,11 @@ public function normalized ():string { if ($this->host === '') return ''; - if ($this->host[0] === '[') { - - if ($this->host[1] === 'v' || $this->host[1] === 'V') - return $this->host; - + if ($this->host[0] === '[') return Runtime\Str\SB\Casing::toLower($this->host); - } - - $normalized = Runtime\Str\SB\Casing::toLower($this->host); - - return Runtime\Str\SB\Regex::replaceUsing( - '/%([a-f0-9]{2})/', - static function (array $matches):string { - - $character = Runtime\Char\SB::chr( - (int) Runtime\Number::baseConverter($matches[1], 16, 10) // @phpstan-ignore-line - ); - - if (Runtime\Str\SB\Regex::match('/\A[A-Za-z0-9._~-]\z/', $character)) - return Runtime\Str\SB\Casing::toLower($character); - - return '%'.Runtime\Str\SB\Casing::toUpper($matches[1]); // @phpstan-ignore offsetAccess.notFound - - }, - $normalized + return Runtime\Str\SB\Casing::toLower( + Percent::normalize($this->host) ); } diff --git a/src/Uri/Component/Path.php b/src/Uri/Component/Path.php index 375d63d..e51335c 100644 --- a/src/Uri/Component/Path.php +++ b/src/Uri/Component/Path.php @@ -135,6 +135,22 @@ public function isEmpty ():bool { } + /** + * ### Gets the normalized path + * + * Normalizes percent-encoded sequences without changing the path segment structure. + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::normalize() To normalize the path. + * + * @return string Normalized path. + */ + public function normalized ():string { + + return Percent::normalize($this->path); + + } + /** * @inheritdoc * @@ -146,4 +162,74 @@ public function value ():string { } + /** + * ### Removes dot segments + * + * Removes "." and ".." segments according to RFC 3986 Section 5.2.4. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Search::startsWith() To check if a segment starts with charaters. + * @uses \FireHub\Runtime\Str\SB\Access::part() To get a part of a string. + * @uses \FireHub\Runtime\Str\SB\Regex::replace() To replace a substring. + * @uses \FireHub\Runtime\Str\SB\Search::firstPosition() To find the first occurrence of a character. + * + * @return self Path without dot segments. + */ + public function removeDotSegments ():self { + + $input = $this->path; $output = ''; + while ($input !== '') { + + if (Runtime\Str\SB\Search::startsWith('../', $input)) + $input = Runtime\Str\SB\Access::part($input, 3); + + else if (Runtime\Str\SB\Search::startsWith('./', $input)) + $input = Runtime\Str\SB\Access::part($input, 2); + + else if (Runtime\Str\SB\Search::startsWith('/./', $input)) + $input = Runtime\Str\SB\Access::part($input, 2); + + else if ($input === '/.') + $input = '/'; + + else if (Runtime\Str\SB\Search::startsWith('/../', $input)) { + + $input = Runtime\Str\SB\Access::part($input, 3); + $output = Runtime\Str\SB\Regex::replace('~/[^/]*$~', '', $output); + + } else if ($input === '/..') { + + $input = '/'; + $output = Runtime\Str\SB\Regex::replace('~/[^/]*$~', '', $output); + + } else if ($input === '.' || $input === '..') { + + $input = ''; + + } else { + + $position = $input[0] === '/' + ? Runtime\Str\SB\Search::firstPosition('/', $input, offset: 1) + : Runtime\Str\SB\Search::firstPosition('/', $input); + + if ($position === false) { + + $output .= $input; + $input = ''; + + } else { + + $output .= Runtime\Str\SB\Access::part($input, 0, $position); + $input = Runtime\Str\SB\Access::part($input, $position); + + } + + } + + } + + return new self($output); + + } + } \ No newline at end of file diff --git a/src/Uri/Component/Query.php b/src/Uri/Component/Query.php index 63614de..30a6d9e 100644 --- a/src/Uri/Component/Query.php +++ b/src/Uri/Component/Query.php @@ -109,6 +109,22 @@ public function isEmpty(): bool { } + /** + * ### Gets the normalized query + * + * Normalizes percent-encoded sequences without changing query parameter ordering or interpretation. + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::normalize() To normalize the query. + * + * @return string Normalized query. + */ + public function normalized ():string { + + return Percent::normalize($this->query); + + } + /** * @inheritDoc * diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php index 126ea9b..fbf0046 100644 --- a/src/Uri/Component/UserInfo.php +++ b/src/Uri/Component/UserInfo.php @@ -107,6 +107,20 @@ public function isEmpty(): bool { } + /** + * ### Gets the normalized user information + * @since 1.0.0 + * + * @uses \FireHub\Foundation\Uri\Encoding\Percent::normalize() To normalize the user information. + * + * @return string Normalized user information. + */ + public function normalized ():string { + + return Percent::normalize($this->user_info); + + } + /** * @inheritdoc * diff --git a/src/Uri/Encoding/Percent.php b/src/Uri/Encoding/Percent.php index 9f48e7f..5f95b95 100644 --- a/src/Uri/Encoding/Percent.php +++ b/src/Uri/Encoding/Percent.php @@ -136,6 +136,45 @@ public static function decode (string $value):string { } + /** + * ### Normalizes percent-encoded sequences + * + * Decodes unreserved characters and converts remaining percent-encoded hexadecimal digits to uppercase. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Str\SB\Regex::replaceUsing() To replace percent-encoded sequences. + * @uses \FireHub\Runtime\Str\SB\Regex::match() To check if a character is unreserved. + * @uses \FireHub\Runtime\Str\SB\Casing::toUpper() To convert hexadecimal digits to uppercase. + * @uses \FireHub\Runtime\Encoding::rawUrlDecode() To decode percent-encoded octets. + * + * @param string $value

+ * URI component. + *

+ * + * @return string Normalized URI component. + */ + public static function normalize (string $value):string { + + return Runtime\Str\SB\Regex::replaceUsing( + '/%([0-9A-Fa-f]{2})/', + static function (array $matches):string { + + $character = Runtime\Encoding::rawUrlDecode($matches[0]); // @phpstan-ignore offsetAccess.notFound + + if (Runtime\Str\SB\Regex::match( + '/\A[A-Za-z0-9._~-]\z/', + $character + )) + return $character; + + return '%'.Runtime\Str\SB\Casing::toUpper($matches[1]); // @phpstan-ignore offsetAccess.notFound + + }, + $value + ); + + } + /** * ### Percent-encodes disallowed bytes * From 058e3bb8216abb5d7d49901d169dbb7608b631cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 2 Nov 2026 10:05:16 +0100 Subject: [PATCH 13/15] Add `with*` mutator methods for URI and Authority components - **New Features**: - Introduced `withHost()`, `withPort()`, and `withUserInfo()` in `Authority` to enable immutability-preserving updates to host, port, and user info. - Added `withScheme()`, `withAuthority()`, `withPath()`, `withQuery()`, and `withFragment()` in `Uri` to allow similar updates for URI components. - **Details**: - Each method creates a new immutable instance with the specified updated component while preserving other values. - Includes strict type annotations to ensure consistency across components. - PHPDoc comments and exceptions provided for clarity and enhanced error handling. - **Rationale**: - Improves flexibility of `Uri` and `Authority` classes by supporting the construction of updated variants without mutating existing instances. - Aligns with the project's focus on immutability, type safety, and compliance with URI standards. --- src/Uri.php | 130 ++++++++++++++++++++++++++++++++ src/Uri/Component/Authority.php | 60 +++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/src/Uri.php b/src/Uri.php index a332159..d6169ad 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -130,6 +130,136 @@ public static function parse (string $uri):self { } + /** + * Returns a URI with the specified scheme. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Scheme $scheme

+ * URI scheme. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI with the specified scheme. + */ + public function withScheme (Scheme $scheme):self { + + return new self( + $scheme, + $this->authority, + $this->path, + $this->query, + $this->fragment + ); + + } + + /** + * Returns a URI with the specified authority. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Authority $authority

+ * URI authority. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI with the specified authortity. + */ + public function withAuthority (Authority $authority):self { + + return new self( + $this->scheme, + $authority, + $this->path, + $this->query, + $this->fragment + ); + + } + + /** + * Returns a URI with the specified path. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Path $path

+ * URI path. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI with the specified path. + */ + public function withPath (Path $path):self { + + return new self( + $this->scheme, + $this->authority, + $path, + $this->query, + $this->fragment + ); + + } + + /** + * Returns a URI with the specified query. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Query $query

+ * URI query. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI with the specified query. + */ + public function withQuery (Query $query):self { + + return new self( + $this->scheme, + $this->authority, + $this->path, + $query, + $this->fragment + ); + + } + + /** + * Returns a URI with the specified fragment. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Fragment $fragment [optional]

+ * URI fragment. + *

+ * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI with the specified fragment. + */ + public function withFragment (Fragment $fragment):self { + + return new self( + $this->scheme, + $this->authority, + $this->path, + $this->query, + $fragment + ); + + } + /** * ### Gets the scheme * @since 1.0.0 diff --git a/src/Uri/Component/Authority.php b/src/Uri/Component/Authority.php index bcfcb8f..1652315 100644 --- a/src/Uri/Component/Authority.php +++ b/src/Uri/Component/Authority.php @@ -132,6 +132,66 @@ public static function parse (string $authority):self { } + /** + * Returns an authority with the specified host. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Host $host

+ * Host information. + *

+ * + * @return self New authority with the specified host information. + */ + public function withHost (Host $host):self { + + return new self( + $host, + $this->port, + $this->user_info + ); + + } + + /** + * Returns an authority with the specified port. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\Port $port

+ * Port information. + *

+ * + * @return self New authority with the specified port information. + */ + public function withPort (Port $port):self { + + return new self( + $this->host, + $port, + $this->user_info + ); + + } + + /** + * Returns an authority with the specified user information. + * @since 1.0.0 + * + * @param \FireHub\Foundation\Uri\Component\UserInfo $user_info

+ * User information. + *

+ * + * @return self New authority with the specified user information. + */ + public function withUserInfo (UserInfo $user_info):self { + + return new self( + $this->host, + $this->port, + $user_info + ); + + } + /** * ### Gets the host * @since 1.0.0 From 262473692a9c1a84f559ec53d9b3621fc89b3353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 2 Nov 2026 10:14:30 +0100 Subject: [PATCH 14/15] Add `without*` mutator methods for URI and Authority components - **New Features**: - Introduced `withoutPort()` and `withoutUserInfo()` in `Authority` to enable removal of port and user info while preserving immutability. - Added `withoutScheme()`, `withoutAuthority()`, `withoutQuery()`, and `withoutFragment()` in `Uri` for creating new URI instances without these components. - **Details**: - Each method creates a new immutable instance by omitting the specified component and preserving other values. - Includes comprehensive PHPDoc comments and exception handling for clarity and proper usage. - **Rationale**: - Enhances flexibility of `Uri` and `Authority` classes by supporting the removal of specific components immutably. - Aligns with the project's principles of immutability, type safety, and adherence to URI standards. --- CHANGELOG.md | 5 ++ README.md | 2 +- src/Uri.php | 88 +++++++++++++++++++++++++++++++++ src/Uri/Component/Authority.php | 32 ++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844e276..50791ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [[v0.2.0](https://github.com/The-FireHub-Project/Foundation/compare/v0.2.0...v0.1.0)] – 2026-11-02 + +### Added +* Create URI by @dieselxxx + ## [v0.1.0] – 2026-10-21 ### Added diff --git a/README.md b/README.md index 4323e78..0b6323c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ /> version diff --git a/src/Uri.php b/src/Uri.php index d6169ad..2e9147d 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -156,6 +156,28 @@ public function withScheme (Scheme $scheme):self { } + /** + * Returns a URI without the scheme. + * @since 1.0.0 + * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI without the scheme. + */ + public function withoutScheme ():self { + + return new self( + null, + $this->authority, + $this->path, + $this->query, + $this->fragment + ); + + } + /** * Returns a URI with the specified authority. * @since 1.0.0 @@ -182,6 +204,28 @@ public function withAuthority (Authority $authority):self { } + /** + * Returns a URI without the authority. + * @since 1.0.0 + * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI without the authority. + */ + public function withoutAuthority ():self { + + return new self( + $this->scheme, + null, + $this->path, + $this->query, + $this->fragment + ); + + } + /** * Returns a URI with the specified path. * @since 1.0.0 @@ -234,6 +278,28 @@ public function withQuery (Query $query):self { } + /** + * Returns a URI without the query. + * @since 1.0.0 + * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI without the query. + */ + public function withoutQuery ():self { + + return new self( + $this->scheme, + $this->authority, + $this->path, + null, + $this->fragment + ); + + } + /** * Returns a URI with the specified fragment. * @since 1.0.0 @@ -260,6 +326,28 @@ public function withFragment (Fragment $fragment):self { } + /** + * Returns a URI without the fragment. + * @since 1.0.0 + * + * @throws \FireHub\Foundation\Uri\Exception\InvalidUriException If the URI component combination is invalid. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return self New URI without the fragment. + */ + public function withoutFragment ():self { + + return new self( + $this->scheme, + $this->authority, + $this->path, + $this->query, + null + ); + + } + /** * ### Gets the scheme * @since 1.0.0 diff --git a/src/Uri/Component/Authority.php b/src/Uri/Component/Authority.php index 1652315..9375edd 100644 --- a/src/Uri/Component/Authority.php +++ b/src/Uri/Component/Authority.php @@ -172,6 +172,22 @@ public function withPort (Port $port):self { } + /** + * Returns an authority without the port. + * @since 1.0.0 + * + * @return self New authority without the port. + */ + public function withoutPort ():self { + + return new self( + $this->host, + null, + $this->user_info + ); + + } + /** * Returns an authority with the specified user information. * @since 1.0.0 @@ -192,6 +208,22 @@ public function withUserInfo (UserInfo $user_info):self { } + /** + * Returns an authority without the user information. + * @since 1.0.0. + * + * @return self New authority without the user information. + */ + public function withoutUserInfo ():self { + + return new self( + $this->host, + $this->port, + null + ); + + } + /** * ### Gets the host * @since 1.0.0 From a541a12b81de820b3d0c1de5052737cafee7f361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:16:03 +0100 Subject: [PATCH 15/15] Update README.md --- README.md | 126 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 0b6323c..c6f4724 100644 --- a/README.md +++ b/README.md @@ -22,74 +22,114 @@ /> -[![PHPStan](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPStan.yml) -[![PHPUnit](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Foundation/actions/workflows/PHPUnit.yml) -[![Codecov](https://codecov.io/gh/The-FireHub-Project/Foundation/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Foundation/tree/develop) -

- + + GitHub created at + GitHub last commit (branch) - + GitHub activity (branch) - + + GitHub contributors + + GitHub language count + + GitHub top language + GitHub commit difference between two branches + + GitHub code size

-The essential PHP core for building domain-driven, clean architecture applications. -Standard provides the base classes, kernel, domain layer, and minimal runtime utilities needed to start any FireHub project. +Provides the high-level foundation layer of the FireHub ecosystem. This repository contains developer-focused APIs and reusable building blocks that simplify common programming tasks while maintaining consistency, type safety, and architectural principles across FireHub applications. + +The Foundation layer is built on top of the Runtime layer and transforms low-level execution primitives into expressive object-oriented APIs. It provides commonly used components such as string manipulation, date and time handling, mathematical utilities, and other essential application-level abstractions. + +This repository represents the developer-facing layer between the FireHub Runtime and application code, enabling clean, readable, and maintainable PHP development. --- -## FireHub Icon Development Branch +## FireHub Icon Repository Purpose + +FireHub Foundation is the high-level developer API layer of the FireHub ecosystem. + +Built on top of FireHub Runtime, it provides expressive, object-oriented abstractions for common programming tasks. The purpose of this layer is to expose simple, consistent, and powerful APIs while hiding low-level implementation details. -⚠️ **This is the `develop` branch** +Foundation contains reusable building blocks such as: -- Unstable -- APIs may change without notice -- Not intended for production use +- String manipulation APIs +- Date and time handling +- Mathematical utilities +- Common application-level abstractions +- Developer-oriented helper objects -## FireHub Icon Branch Purpose +The Foundation layer follows the FireHub architectural principles by separating responsibilities between definition, execution, and usage: -The `develop` branch is the **primary integration branch** for all ongoing development. +## FireHub Icon Security -It serves as the staging area where: -- Feature branches are merged -- Bug fixes are integrated -- Experimental work is stabilized -- Code is prepared for upcoming releases +For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. -All **release branches** are created **from `develop`**. +## FireHub Icon Community -## Stability Guarantee +![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Foundation?style=social) +![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Foundation?style=social) +![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Foundation?style=social) -❌ No backward compatibility guarantee -❌ APIs may change without notice -❌ Behavior may be incomplete or inconsistent -❌ Breaking changes are expected +- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). +- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. +- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. +- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. -This branch is intended **only for contributors and advanced testers**. +## FireHub Icon Support -## FireHub Icon Composer Usage (Not Recommended) +

+ + GitHub in-progress issues + + GitHub total issues + + GitHub opened pull requests +

-For internal testing only: +For help with FireHub products, please refer to the following resources: -```json -{ - "require": { - "the-firehub-project/foundation": "dev-develop" - } -} -``` -⚠️ Never use dev-develop in production. +- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) +- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Foundation/issues) +- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) +- **Commercial support** (Pro/Enterprise): contact your account manager ## FireHub Icon Authors and Contributors @@ -106,4 +146,4 @@ Architecture guidelines, design principles, and ecosystem documentation are avai This software is licensed under the Apache-2.0 License. -For more details, read the full license [here](./LICENSE). \ No newline at end of file +For more details, read the full license [here](./LICENSE).