├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractValueObject.php ├── Boolean.php ├── ClassString.php ├── Contracts └── Arrayable.php ├── Coordinate.php ├── Double.php ├── Email.php ├── Exceptions └── ValueObjectInvalidArgumentException.php ├── IPv4.php ├── IPv6.php ├── Integer.php ├── Json.php ├── NonNegativeInteger.php ├── Percentage.php ├── PositiveInteger.php ├── RangeInteger.php ├── Resolution.php ├── SemVer.php ├── Support ├── EmailAddressValidator.php ├── FloatStringValidator.php ├── IntegerStringValidator.php ├── TypeCast.php └── UrlValidator.php ├── Text.php ├── Timestamp.php ├── Url.php └── Uuid.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Alexander Tebiev / / https://github.com/beeyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ValueObjects PHP Package 🫗 2 | 3 | ![Latest Version on Packagist](https://img.shields.io/packagist/v/beeyev/value-objects-php) 4 | ![Supported PHP Versions](https://img.shields.io/packagist/dependency-v/beeyev/value-objects-php/php.svg) 5 | 6 | ## ℹ️ Introduction 7 | 8 | This package provides a collection of immutable value objects that you can use in your PHP applications. 9 | 10 | Value objects are a key concept in Domain-Driven Design (DDD). 11 | They are simple objects whose equality is based on their value rather than their identity. 12 | Using value objects can help you write more expressive, reliable, and maintainable code. 13 | 14 | ## 🧾 Benefits of using value objects 15 | 16 | - **Immutability**: Ensures objects remain consistent throughout their lifecycle. 17 | - **Expressiveness**: Represents domain concepts naturally. 18 | - **Validation**: Encapsulates validation logic, reducing errors. 19 | - **Reusability**: Promotes DRY principles. 20 | - **Ease of Testing**: Self-contained and simple to test. 21 | 22 | ## 📎 Why use value objects over primitives 23 | 24 | - **Validation**: ❗ Guarantees valid data. 25 | - **Self-Documenting Code**: Enhances readability. 26 | - **Encapsulation**: Contains logic related to the value. 27 | - **Consistency**: Ensures uniform handling of data. 28 | - **Ease of Refactoring**: Centralizes changes to logic or validation rules. 29 | 30 | ## 📦 Installation 31 | 32 | Use Composer to install this package. Run the following command: 33 | 34 | ```bash 35 | composer require beeyev/value-objects-php 36 | ``` 37 | 38 | ## ▶️ Usage 39 | 40 | Here are examples of how to use the value objects provided by this package: 41 | 42 | ### Email 43 | 44 | ```php 45 | use Beeyev\ValueObject\Email; 46 | 47 | $email = new Email('abc@gmail.com'); 48 | echo $email->value; // Output: 'abc@gmail.com' 49 | echo $email->username; // Output: 'abc' 50 | echo $email->domain; // Output: 'gmail.com' 51 | ``` 52 | 53 | ### URL 54 | 55 | ```php 56 | use Beeyev\ValueObject\Url; 57 | 58 | $url = new Url('https://example.com'); 59 | echo $url->value; // Output: 'https://example.com' 60 | 61 | // Every value object can be cast to a string 62 | echo (string) $url; // Output: 'https://example.com' 63 | ``` 64 | 65 | ### UUID 66 | 67 | ```php 68 | use Beeyev\ValueObject\Uuid; 69 | 70 | $uuid = new Uuid('550e8400-e29b-41d4-a716-446655440000'); 71 | echo $uuid->value; // Output: '550e8400-e29b-41d4-a716-446655440000' 72 | ``` 73 | 74 | ### IPv4 Address 75 | 76 | ```php 77 | use Beeyev\ValueObject\IPv4; 78 | 79 | $ip = new IPv4('172.20.13.13'); 80 | echo $ip->value; // Output: '172.20.13.13' 81 | ``` 82 | 83 | ### IPv6 Address 84 | 85 | ```php 86 | use Beeyev\ValueObject\IPv6; 87 | 88 | $ip = new IPv6('2606:4700:4700::1111'); 89 | echo $ip->value; // Output: '2606:4700:4700::1111' 90 | ``` 91 | 92 | ### Coordinates 93 | 94 | Represents a geographic coordinate (latitude and longitude). 95 | 96 | ```php 97 | use Beeyev\ValueObject\Coordinate; 98 | 99 | $coordinate = new Coordinate(37.7749, -122.4194); 100 | echo $coordinate->latitude; // Output: 37.7749 101 | echo $coordinate->longitude; // Output: -122.4194 102 | $coordinate->toArray(); // Array: [37.7749, -122.4194] 103 | 104 | // Coordinate object can be created from a string 105 | // Supported formats: '37.7749,-122.4194', '37.7749, -122.4194', '37.7749 122.4194', '37.7749/122.4194' 106 | $coordinate = Coordinate::fromString('37.7749,-122.4194'); 107 | 108 | echo $coordinate->toString(); // Output: '37.7749, -122.4194' 109 | // Or cast to a string 110 | echo (string) $coordinate; // Output: '37.7749, -122.4194' 111 | ``` 112 | 113 | ### Json 114 | 115 | Represents a JSON string. 116 | 117 | ```php 118 | use Beeyev\ValueObject\Json; 119 | 120 | $json = new Json('{"name": "John", "age": 30}'); 121 | echo $json->value; // Output: '{"name": "John", "age": 30}' 122 | echo $json->toArray(); // Output: ['name' => 'John', 'age' => 30] 123 | ``` 124 | 125 | ### Percentage 126 | 127 | Represents a percentage integer value from 0 to 100. 128 | 129 | ```php 130 | use Beeyev\ValueObject\Percentage; 131 | 132 | $percentage = new Percentage(50); 133 | echo $percentage->value; // Output: 50 134 | ``` 135 | 136 | ### RangeInteger 137 | 138 | Represents a range of integer values. 139 | 140 | ```php 141 | use Beeyev\ValueObject\RangeInteger; 142 | 143 | $range = new RangeInteger(-5, 10); 144 | echo $range->start; // Output: -5 145 | echo $range->end; // Output: 10 146 | $range->toArray(); // Array: [-5, 10] 147 | echo (string) $range; // Output: '-5 - 10' 148 | 149 | 150 | // Range object can be created from a string 151 | $range = RangeInteger::fromString('-5 - 10'); 152 | 153 | // If you try to create a range object with the start value greater than the end value, an exception will be thrown 154 | try { 155 | $range = new RangeInteger(10, -5); 156 | } catch (ValueObjectInvalidArgumentException $e) { 157 | echo $e->getMessage(); // Output: 'Start value cannot be greater than the end value.' 158 | } 159 | ``` 160 | 161 | ### Resolution 162 | 163 | Represents resolution (width and height). 164 | 165 | ```php 166 | use Beeyev\ValueObject\Resolution; 167 | 168 | // Only positive integers are allowed 169 | $resolution = new Resolution(1920, 1080); 170 | echo $resolution->width; // Output: 1920 171 | echo $resolution->height; // Output: 1080 172 | $resolution->toArray(); // Array: [1920, 1080] 173 | echo (string) $resolution; // Output: '1920x1080' 174 | ``` 175 | 176 | ### Semantic Version 177 | 178 | Represents a semantic version number (SemVer). 179 | 180 | ```php 181 | use Beeyev\ValueObject\SemVer; 182 | 183 | $version = new SemVer('1.0.3'); 184 | echo $version->value; // Output: '1.0.3' 185 | echo $version->major; // Output: 1 186 | echo $version->minor; // Output: 0 187 | echo $version->patch; // Output: 3 188 | 189 | // Is supports semver with pre-release and build metadata 190 | $version = new SemVer('1.0.3-beta+exp.sha.5114f85'); 191 | echo $version->value; // Output: '1.0.3-beta+exp.sha.5114f85' 192 | echo $version->releaseVersion; // Output: '1.0.3' 193 | echo $version->build; // Output: 'exp.sha.5114f85' 194 | echo $version->preRelease; // Output: 'beta' 195 | 196 | // SemVer value objects can be compared 197 | $version1 = new SemVer('1.0.5'); 198 | $version2 = new SemVer('1.0.1-alpha+001'); 199 | 200 | $version1->greaterThan($version2); // true 201 | $version1->lowerThan($version2); // false 202 | 203 | $version1->equalTo($version2); // false 204 | $version1->notEqualTo($version2); // true 205 | 206 | $version1->greaterThanOrEqualTo($version2); // true 207 | $version1->lowerThanOrEqualTo($version2); // false 208 | ``` 209 | 210 | ### Timestamp 211 | 212 | Represents a unix timestamp. 213 | 214 | ```php 215 | use Beeyev\ValueObject\Timestamp; 216 | 217 | $timestamp = new Timestamp(1631535600); 218 | echo $timestamp->value; // Output: 1631535600 219 | echo $timestamp->dateTime // Returns DateTimeImmutable object 220 | ``` 221 | 222 | ### Class string 223 | 224 | Represents a PHP class string. 225 | 226 | ```php 227 | use Beeyev\ValueObject\ClassString; 228 | 229 | $classString = new ClassString('App\Models\User'); 230 | // Same as 231 | $classString = new ClassString(User::class); 232 | 233 | echo $classString->value; // Output: 'App\Models\User' 234 | 235 | // Returns true if the class exists 236 | $classString->isClassExist(); // true 237 | 238 | // Returns true if the object is an instance of this class string. 239 | $classString->isInstanceOf($user); // true 240 | 241 | // It is possible to instantiate an object from the class string 242 | $classString = new ClassString(\DateTimeImmutable::class); 243 | $instance = $classString->instantiate(); 244 | assert($instance instanceof \DateTimeImmutable); 245 | 246 | // It is possible to instantiate an object from the class string with arguments 247 | $classString = new ClassString(\DateTimeImmutable::class); 248 | $instance = $classString->instantiateWith('2021-01-01 00:00:00', new \DateTimeZone('UTC')); 249 | assert($instance instanceof \DateTimeImmutable); 250 | echo $instance->format('Y-m-d H:i:s'); // Output: '2021-01-01 00:00:00' 251 | 252 | // It is possible to check if the interface exists 253 | $classString = new ClassString(\DateTimeInterface::class); 254 | $classString->isInterfaceExist(); // true 255 | ``` 256 | 257 | ## 🐒 Primitive Value Objects 258 | 259 | ### Text 260 | 261 | Represents a non-empty text string. 262 | 263 | ```php 264 | use Beeyev\ValueObject\Text; 265 | use Beeyev\ValueObject\Exceptions\ValueObjectInvalidArgumentException; 266 | 267 | $text = new Text('Hello, World!'); 268 | echo $text->value; // Output: 'Hello, World!' 269 | echo (string) $text; // Output: 'Hello, World!' 270 | echo $text->length(); // Output: 13 271 | 272 | // If you try to create an empty text object, an exception will be thrown 273 | try { 274 | $text = new Text(''); 275 | } catch (ValueObjectInvalidArgumentException $e) { 276 | echo $e->getMessage(); // Output: 'Text value cannot be empty.' 277 | } 278 | ``` 279 | 280 | ### Boolean 281 | 282 | ```php 283 | use Beeyev\ValueObject\Boolean; 284 | 285 | $boolean = new Boolean(true); 286 | // It is also possible to create a boolean object from non-boolean values 287 | // Supported values: 'true', 'false', '1', '0', 'yes', 'no', 'on', 'off' 288 | // $boolean = new Boolean('on'); 289 | 290 | echo $boolean->value; // Output: true 291 | echo $boolean->toString(); // Output: 'true' 292 | echo (string) $boolean; // Output: 'true' 293 | ``` 294 | 295 | ### Integer 296 | 297 | ```php 298 | use Beeyev\ValueObject\Integer; 299 | 300 | $integer = new Integer(42); 301 | // It is also possible to create an integer object from a string 302 | // $integer = new Integer('42'); 303 | 304 | echo $integer->value; // Output: 42 305 | ``` 306 | 307 | ### Positive Integer 308 | 309 | Represents a positive integer greater than zero. 310 | Useful for storing values that must always be positive. 311 | For example, a database row ID. 312 | 313 | ```php 314 | use Beeyev\ValueObject\PositiveInteger; 315 | use Beeyev\ValueObject\Exceptions\ValueObjectInvalidArgumentException; 316 | 317 | $positiveInteger = new PositiveInteger(42); 318 | echo $positiveInteger->value; // Output: 42 319 | 320 | // If you try to create a positive integer object from a negative value or equal to zero, an exception will be thrown 321 | try { 322 | $positiveInteger = new PositiveInteger(0); 323 | } catch (ValueObjectInvalidArgumentException $e) { 324 | echo $e->getMessage(); // Output: 'Provided number is not a positive integer. Given value: `0`.' 325 | } 326 | ``` 327 | 328 | ### Non-Negative Integer 329 | 330 | Represents a non-negative integer, greater than or equal to zero. 331 | 332 | ```php 333 | use Beeyev\ValueObject\NonNegativeInteger; 334 | use Beeyev\ValueObject\Exceptions\ValueObjectInvalidArgumentException; 335 | 336 | $positiveInteger = new NonNegativeInteger(96); 337 | echo $positiveInteger->value; // Output: 96 338 | ``` 339 | 340 | ### Double (float) 341 | 342 | Represents a double-precision floating-point number. 343 | 344 | ```php 345 | use Beeyev\ValueObject\Double; 346 | 347 | $double = new Double(3.14); 348 | // It is also possible to create a double object from a string 349 | // $double = new Double('3.14'); 350 | 351 | echo $double->value; // Output: 3.14 352 | echo $double->toString(); // Output: '3.14' 353 | echo (string) $double; // Output: '3.14' 354 | ``` 355 | 356 | ## Common functionality 357 | 358 | Every value object has the following functionality: 359 | 360 | ```php 361 | // Every value object can be cast to a string and supports \Stringable interface 362 | $vo->toString(); // Returns the value of the object as a string 363 | (string) $vo; // Returns the value of the object as a string 364 | 365 | // Value objects can be compared 366 | $vo1->sameAs($vo2); // Returns true if the values are equal 367 | $vo1->notSameAs($vo2); // Returns true if the values are not equal 368 | ``` 369 | 370 | ## 🏗 Creating your own value objects 371 | 372 | It is possible to create your own value objects by extending the `AbstractValueObject` class. 373 | 374 | ## 📚 Extending functionality 375 | 376 | Feel free to extend the functionality of the value objects by creating your own classes that inherit from the provided value objects. 377 | 378 | ## 🐛 Contributions 379 | 380 | If you have suggestions for improvements or wish to create your own custom value object to be included as a built-in feature, please submit a Pull Request. 381 | Additionally, bug reports and feature requests can be submitted via the [GitHub Issue Tracker](https://github.com/beeyev/value-objects-php/issues). 382 | 383 | ## © License 384 | 385 | The MIT License (MIT). Please see [License File](https://github.com/beeyev/value-objects-php/blob/master/LICENSE.md) for more information. 386 | 387 | --- 388 | 389 | If you love this project, please consider giving me a ⭐ 390 | 391 | ![](https://visitor-badge.laobi.icu/badge?page_id=beeyev.value-objects-php) 392 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beeyev/value-objects-php", 3 | "description": "Immutable value objects for PHP, designed for Domain-Driven Design (DDD). Enhance your applications with expressive, reliable, and maintainable code.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "value objects", 8 | "valueObject", 9 | "ddd" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Alexander Tebiev", 14 | "email": "alexander.tebiev@gmail.com", 15 | "homepage": "https://github.com/beeyev/" 16 | } 17 | ], 18 | "homepage": "https://github.com/beeyev/value-objects-php", 19 | "support": { 20 | "issues": "https://github.com/beeyev/value-objects-php/issues", 21 | "source": "https://github.com/beeyev/value-objects-php.git", 22 | "docs": "https://github.com/beeyev/value-objects-php/", 23 | "rss": "https://github.com/beeyev/value-objects-php/releases.atom" 24 | }, 25 | "require": { 26 | "php": "^8.2" 27 | }, 28 | "require-dev": { 29 | "friendsofphp/php-cs-fixer": "^3.59", 30 | "infection/infection": "^0.29.5", 31 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.21", 32 | "phpstan/phpstan": "^1.11", 33 | "phpstan/phpstan-phpunit": "^1.4", 34 | "phpstan/phpstan-strict-rules": "^1.6", 35 | "phpunit/phpunit": "^11.2", 36 | "symplify/phpstan-rules": "^13.0" 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true, 40 | "autoload": { 41 | "psr-4": { 42 | "Beeyev\\ValueObject\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Beeyev\\ValueObject\\Tests\\": "tests/" 48 | } 49 | }, 50 | "config": { 51 | "allow-plugins": { 52 | "infection/extension-installer": true 53 | }, 54 | "sort-packages": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AbstractValueObject.php: -------------------------------------------------------------------------------- 1 | sameAs($object); 25 | } 26 | 27 | /** 28 | * Returns string representation of the value object. 29 | * 30 | * @return non-empty-string 31 | */ 32 | public function toString(): string 33 | { 34 | return $this->__toString(); // @phpstan-ignore return.type 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Boolean.php: -------------------------------------------------------------------------------- 1 | true, 18 | 'true' => true, 19 | 'on' => true, 20 | 'yes' => true, 21 | ]; 22 | protected const FALSE_VALUES = [ 23 | '0' => true, 24 | 'false' => true, 25 | 'off' => true, 26 | 'no' => true, 27 | ]; 28 | 29 | public bool $value; 30 | 31 | /** 32 | * @throws ValueObjectInvalidArgumentException 33 | */ 34 | public function __construct(bool|int|string|\Stringable $value) 35 | { 36 | if ($value instanceof \Stringable) { 37 | $value = (string) $value; 38 | } 39 | 40 | $this->validate($value); 41 | 42 | $this->value = is_bool($value) ? $value : $this->convertToBoolean($value); 43 | } 44 | 45 | /** 46 | * @throws ValueObjectInvalidArgumentException 47 | */ 48 | protected function convertToBoolean(int|string $value): bool 49 | { 50 | $value = strtolower((string) $value); 51 | 52 | if (isset(static::TRUE_VALUES[$value])) { 53 | return true; 54 | } 55 | 56 | if (isset(static::FALSE_VALUES[$value])) { 57 | return false; 58 | } 59 | 60 | throw new ValueObjectInvalidArgumentException("Value could not be converted to boolean, given value: `{$value}`."); 61 | } 62 | 63 | /** 64 | * @throws ValueObjectInvalidArgumentException 65 | */ 66 | protected function validate(bool|int|string $value): void 67 | { 68 | if ($value === '') { 69 | throw new ValueObjectInvalidArgumentException('Provided string value cannot be empty.'); 70 | } 71 | } 72 | 73 | /** 74 | * Returns string representation of the value object. 75 | * 76 | * @return non-empty-string 77 | */ 78 | #[\Override] 79 | public function __toString(): string 80 | { 81 | return $this->value ? 'true' : 'false'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ClassString.php: -------------------------------------------------------------------------------- 1 | value); 22 | } 23 | 24 | /** 25 | * Returns true if the object is an instance of this class string. 26 | */ 27 | public function isInstanceOf(object|string $object): bool 28 | { 29 | return is_a($this->value, is_object($object) ? $object::class : $object, true); 30 | } 31 | 32 | /** 33 | * Instantiate the class string without constructor parameters if possible. 34 | */ 35 | public function instantiate(): object 36 | { 37 | $classString = $this->value; 38 | 39 | return new $classString(); 40 | } 41 | 42 | /** 43 | * Instantiate the class string with constructor parameters if possible. 44 | * 45 | * @param mixed ...$parameters 46 | */ 47 | public function instantiateWith(... $parameters): object 48 | { 49 | $classString = $this->value; 50 | 51 | return new $classString(...$parameters); 52 | } 53 | 54 | /** 55 | * Returns true if the interface exists for this class string. 56 | */ 57 | public function isInterfaceExist(): bool 58 | { 59 | return interface_exists($this->value); 60 | } 61 | 62 | /** 63 | * @throws ValueObjectInvalidArgumentException 64 | */ 65 | #[\Override] 66 | protected function validate(string $value): void 67 | { 68 | parent::validate($value); 69 | 70 | if (preg_match('/^\d/', $value) === 1) { 71 | throw new ValueObjectInvalidArgumentException("Class string value cannot start with a digit. Given value: `{$value}`"); 72 | } 73 | 74 | if (str_contains($value, ' ')) { 75 | throw new ValueObjectInvalidArgumentException("Class string value cannot contain spaces. Given value: `{$value}`"); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Contracts/Arrayable.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function toArray(): array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Coordinate.php: -------------------------------------------------------------------------------- 1 | validate($latitude, $longitude); 32 | 33 | $this->latitude = $latitude; 34 | $this->longitude = $longitude; 35 | } 36 | 37 | /** 38 | * Converts string to Coordinate object. 39 | * Supported delimiters: @see static::DELIMITERS 40 | */ 41 | public static function fromString(string|\Stringable $latitudeAndLongitude): self 42 | { 43 | $latitudeAndLongitudeString = (string) $latitudeAndLongitude; 44 | 45 | if ($latitudeAndLongitudeString === '') { 46 | throw new ValueObjectInvalidArgumentException('Coordinate string cannot be empty.'); 47 | } 48 | 49 | $normalized = str_ireplace(static::DELIMITERS, '/', $latitudeAndLongitudeString); 50 | 51 | $coordinateParts = explode('/', $normalized); 52 | 53 | if (count($coordinateParts) !== 2) { 54 | throw new ValueObjectInvalidArgumentException("Coordinate string can not be parsed. Given value: `{$latitudeAndLongitudeString}`."); 55 | } 56 | 57 | return new self($coordinateParts[0], $coordinateParts[1]); 58 | } 59 | 60 | /** 61 | * @return array{0: float, 1: float} 62 | */ 63 | public function toArray(): array 64 | { 65 | return [ 66 | $this->latitude, 67 | $this->longitude, 68 | ]; 69 | } 70 | 71 | /** 72 | * @throws ValueObjectInvalidArgumentException 73 | */ 74 | protected function validate(float $latitude, float $longitude): void 75 | { 76 | if ($latitude < -90.0 || $latitude > 90.0) { 77 | throw new ValueObjectInvalidArgumentException("Latitude must be between `-90` and `90` degrees. Given value: `{$latitude}`"); 78 | } 79 | 80 | if ($longitude < -180.0 || $longitude > 180.0) { 81 | throw new ValueObjectInvalidArgumentException("Longitude must be between `-180` and `180` degrees. Given value: `{$longitude}`"); 82 | } 83 | } 84 | 85 | public function __toString(): string 86 | { 87 | return $this->latitude . ', ' . $this->longitude; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Double.php: -------------------------------------------------------------------------------- 1 | validate($value); 23 | 24 | $this->value = $value; 25 | } 26 | 27 | protected function validate(float $inputValue): void {} 28 | 29 | /** 30 | * Returns string representation of the value object. 31 | * 32 | * @return non-empty-string 33 | */ 34 | public function __toString(): string 35 | { 36 | return (string) $this->value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Email.php: -------------------------------------------------------------------------------- 1 | username = $this->extractUsername(); 31 | $this->domain = $this->extractDomain(); 32 | } 33 | 34 | /** 35 | * @throws ValueObjectInvalidArgumentException 36 | */ 37 | #[\Override] 38 | protected function validate(string $value): void 39 | { 40 | parent::validate($value); 41 | 42 | if (EmailAddressValidator::isEmailAddressValid($value) === false) { 43 | throw new ValueObjectInvalidArgumentException("Provided email address is incorrect. Given value: `{$value}`"); 44 | } 45 | } 46 | 47 | /** 48 | * @return non-empty-string 49 | */ 50 | protected function extractUsername(): string 51 | { 52 | $result = explode('@', $this->value)[0]; 53 | assert($result !== ''); 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * @return non-empty-string 60 | */ 61 | protected function extractDomain(): string 62 | { 63 | $result = explode('@', $this->value)[1]; 64 | assert($result !== ''); 65 | 66 | return $result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Exceptions/ValueObjectInvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | validate(); 17 | } 18 | 19 | protected function validate(): void {} 20 | 21 | /** 22 | * Returns string representation of the value object. 23 | * 24 | * @return non-empty-string 25 | */ 26 | public function __toString(): string 27 | { 28 | return (string) $this->value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Json.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function toArray(): array 23 | { 24 | return json_decode($this->value, true, 512, JSON_THROW_ON_ERROR); // @phpstan-ignore return.type 25 | } 26 | 27 | /** 28 | * @throws ValueObjectInvalidArgumentException 29 | */ 30 | #[\Override] 31 | protected function validate(string $value): void 32 | { 33 | parent::validate($value); 34 | 35 | try { 36 | json_decode($value, true, 512, JSON_THROW_ON_ERROR); 37 | } catch (\JsonException $e) { 38 | throw new ValueObjectInvalidArgumentException("Provided string is not valid JSON. Given value: `{$value}`"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NonNegativeInteger.php: -------------------------------------------------------------------------------- 1 | value < 0) { 25 | throw new ValueObjectInvalidArgumentException("Provided number is not a non-negative integer. Given value: `{$this->value}`."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Percentage.php: -------------------------------------------------------------------------------- 1 | value < 0 || $this->value > 100) { 25 | throw new ValueObjectInvalidArgumentException("Provided number is out of percentage range. Given value: `{$this->value}`."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PositiveInteger.php: -------------------------------------------------------------------------------- 1 | value < 1) { 25 | throw new ValueObjectInvalidArgumentException("Provided number is not a positive integer. Given value: `{$this->value}`."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RangeInteger.php: -------------------------------------------------------------------------------- 1 | validate($start, $end); 32 | 33 | $this->start = $start; 34 | $this->end = $end; 35 | } 36 | 37 | /** 38 | * @return array{0: int, 1: int} 39 | */ 40 | public function toArray(): array 41 | { 42 | return [$this->start, $this->end]; 43 | } 44 | 45 | /** 46 | * Converts string to RangeInteger object. 47 | * Supported delimiters: @see static::DELIMITERS 48 | */ 49 | public static function fromString(string|\Stringable $range): self 50 | { 51 | $range = (string) $range; 52 | 53 | if ($range === '') { 54 | throw new ValueObjectInvalidArgumentException('Range string string cannot be empty.'); 55 | } 56 | 57 | $normalized = str_replace(static::DELIMITERS, '/', $range); 58 | 59 | $rangeParts = explode('/', $normalized); 60 | 61 | if (count($rangeParts) !== 2) { 62 | throw new ValueObjectInvalidArgumentException("Range string can not be parsed. Given value: `{$range}`."); 63 | } 64 | 65 | return new self($rangeParts[0], $rangeParts[1]); 66 | } 67 | 68 | /** 69 | * @throws ValueObjectInvalidArgumentException 70 | */ 71 | protected function validate(int $start, int $end): void 72 | { 73 | if ($start > $end) { 74 | throw new ValueObjectInvalidArgumentException("Range Start value cannot be greater than End value. Given values: `{$start}` and `{$end}`"); 75 | } 76 | } 77 | 78 | public function __toString(): string 79 | { 80 | return $this->start . ' - ' . $this->end; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Resolution.php: -------------------------------------------------------------------------------- 1 | validate($width, $height); 35 | 36 | $this->width = $width; 37 | $this->height = $height; 38 | } 39 | 40 | /** 41 | * Converts string to RangeInteger object. 42 | * Supported delimiters: @see static::DELIMITERS 43 | */ 44 | public static function fromString(string|\Stringable $resolution): self 45 | { 46 | $resolution = (string) $resolution; 47 | 48 | if ($resolution === '') { 49 | throw new ValueObjectInvalidArgumentException('Range string string cannot be empty.'); 50 | } 51 | 52 | $normalized = str_ireplace(static::DELIMITERS, '/', $resolution); 53 | 54 | $resolutionParts = explode('/', $normalized); 55 | 56 | if (count($resolutionParts) !== 2) { 57 | throw new ValueObjectInvalidArgumentException("Resolution string can not be parsed. Given value: `{$resolution}`."); 58 | } 59 | 60 | return new self($resolutionParts[0], $resolutionParts[1]); 61 | } 62 | 63 | /** 64 | * @return array{0: positive-int, 1: positive-int} 65 | */ 66 | public function toArray(): array 67 | { 68 | return [$this->width, $this->height]; 69 | } 70 | 71 | /** 72 | * @throws ValueObjectInvalidArgumentException 73 | * 74 | * @phpstan-assert positive-int $width 75 | * @phpstan-assert positive-int $height 76 | */ 77 | protected function validate(int $width, int $height): void 78 | { 79 | if ($width <= 0) { 80 | throw new ValueObjectInvalidArgumentException("Width value must be a positive integer. Given value: `{$width}`"); 81 | } 82 | 83 | if ($height <= 0) { 84 | throw new ValueObjectInvalidArgumentException("Height value must be a positive integer. Given value: `{$height}`"); 85 | } 86 | } 87 | 88 | public function __toString(): string 89 | { 90 | return $this->width . 'x' . $this->height; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SemVer.php: -------------------------------------------------------------------------------- 1 | 0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/'; 20 | 21 | /** @var non-negative-int */ 22 | public int $major; 23 | 24 | /** @var non-negative-int */ 25 | public int $minor; 26 | 27 | /** @var non-negative-int */ 28 | public int $patch; 29 | 30 | public string $preRelease; 31 | 32 | public string $build; 33 | 34 | /** 35 | * Returns the release version major.minor.patch 36 | * This string contains only the major, minor, and patch versions, excluding any pre-release and build metadata. 37 | * 38 | * @return non-empty-string 39 | */ 40 | public string $releaseVersion; 41 | 42 | /** 43 | * @throws ValueObjectInvalidArgumentException 44 | */ 45 | public function __construct(string|\Stringable $value) 46 | { 47 | $value = $this->trimVPrefix((string) $value); 48 | parent::__construct($value); 49 | 50 | $match = $this->matchSemVer($this->value); 51 | 52 | $this->major = (int) $match['major']; // @phpstan-ignore assign.propertyType 53 | $this->minor = (int) $match['minor']; // @phpstan-ignore assign.propertyType 54 | $this->patch = (int) $match['patch']; // @phpstan-ignore assign.propertyType 55 | $this->preRelease = $match['preRelease'] ?? ''; 56 | $this->build = $match['buildmetadata'] ?? ''; 57 | 58 | $this->releaseVersion = $this->major . '.' . $this->minor . '.' . $this->patch; 59 | } 60 | 61 | /** 62 | * Returns true if the version is equal to the provided version. 63 | */ 64 | public function equalTo(self $semVer): bool 65 | { 66 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '=='); 67 | } 68 | 69 | /** 70 | * Returns true if the version is not equal to the provided version. 71 | */ 72 | public function notEqualTo(self $semVer): bool 73 | { 74 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '!='); 75 | } 76 | 77 | /** 78 | * Returns true if the version is lower than the provided version. 79 | */ 80 | public function lowerThan(self $semVer): bool 81 | { 82 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '<'); 83 | } 84 | 85 | /** 86 | * Returns true if the version is lower than or equal to the provided version. 87 | */ 88 | public function lowerThanOrEqualTo(self $semVer): bool 89 | { 90 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '<='); 91 | } 92 | 93 | /** 94 | * Returns true if the version is greater than the provided version. 95 | */ 96 | public function greaterThan(self $semVer): bool 97 | { 98 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '>'); 99 | } 100 | 101 | /** 102 | * Returns true if the version is greater than or equal to the provided version. 103 | */ 104 | public function greaterThanOrEqualTo(self $semVer): bool 105 | { 106 | return version_compare($this->releaseVersion, $semVer->releaseVersion, '>='); 107 | } 108 | 109 | /** 110 | * Trims the 'v' prefix if exists from the semver. 111 | */ 112 | protected function trimVPrefix(string $inputValue): string 113 | { 114 | if ($inputValue === '') { 115 | return $inputValue; 116 | } 117 | 118 | if (mb_stripos($inputValue, 'v') === 0) { 119 | return mb_substr($inputValue, 1); 120 | } 121 | 122 | return $inputValue; 123 | } 124 | 125 | /** 126 | * @throws ValueObjectInvalidArgumentException 127 | */ 128 | #[\Override] 129 | protected function validate(string $value): void 130 | { 131 | parent::validate($value); 132 | 133 | $this->matchSemVer($value); 134 | } 135 | 136 | /** 137 | * @param non-empty-string $semVer 138 | * 139 | * @return non-empty-array 140 | * 141 | * @throws ValueObjectInvalidArgumentException 142 | */ 143 | protected function matchSemVer(string $semVer): array 144 | { 145 | if (!preg_match(self::SEMVER_VALIDATION_REGEX, $semVer, $match)) { // @phpstan-ignore booleanNot.exprNotBoolean 146 | throw new ValueObjectInvalidArgumentException("Provided string is not a valid semantic version. Given value: `{$semVer}`"); 147 | } 148 | 149 | return $match; // @phpstan-ignore return.type 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Support/EmailAddressValidator.php: -------------------------------------------------------------------------------- 1 | (?:"?([^"]*)"?\s)?)(?:\s+)?((?P.+)@(?P[^>]+))(?P>?))/'; 18 | private const PATTERN2 = '/^[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/'; 19 | 20 | public static function isEmailAddressValid(string $emailAddress): bool 21 | { 22 | if ((bool) preg_match(self::PATTERN1, $emailAddress, $matches) === false) { 23 | $isValid = false; 24 | } elseif (is_string($matches['local']) && strlen($matches['local']) > 64) { 25 | // The maximum total length of a username or other local-part is 64 octets. RFC 5322 section 4.5.3.1.1 26 | // https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.1 27 | $isValid = false; 28 | } elseif ( 29 | is_string($matches['local']) 30 | && strlen($matches['local']) + strlen((string) $matches['domain']) > 253 31 | ) { 32 | // There is a restriction in RFC 2821 on the length of an address in MAIL and RCPT commands 33 | // of 254 characters. Since addresses that do not fit in those fields are not normally useful, the 34 | // upper limit on address lengths should normally be considered to be 254. 35 | // 36 | // Dominic Sayers, RFC 3696 erratum 1690 37 | // https://www.rfc-editor.org/errata_search.php?eid=1690 38 | $isValid = false; 39 | } else { 40 | $isValid = (bool) preg_match(self::PATTERN2, $emailAddress); 41 | } 42 | 43 | return $isValid; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/FloatStringValidator.php: -------------------------------------------------------------------------------- 1 | 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Support/IntegerStringValidator.php: -------------------------------------------------------------------------------- 1 | 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Support/TypeCast.php: -------------------------------------------------------------------------------- 1 | $valueString) { 35 | throw new ValueObjectInvalidArgumentException("Provided {$valueName} value is too small and cannot be represented as integer. Given value: `{$valueString}`"); 36 | } 37 | 38 | return (int) $valueString; 39 | } 40 | 41 | public static function toFloat(float|int|string|\Stringable $value, string $valueName): float 42 | { 43 | if (is_float($value) || is_int($value)) { 44 | return (float) $value; 45 | } 46 | 47 | $valueString = (string) $value; 48 | 49 | if ($valueString === '') { 50 | throw new ValueObjectInvalidArgumentException('Value cannot be empty.'); 51 | } 52 | 53 | if (FloatStringValidator::isValid($valueString) === false) { 54 | throw new ValueObjectInvalidArgumentException("Provided {$valueName} string is not numeric. Given value: `{$valueString}`"); 55 | } 56 | 57 | $valueFloat = (float) $valueString; 58 | 59 | if ($valueString !== (string) $valueFloat) { 60 | throw new ValueObjectInvalidArgumentException("Provided {$valueName} cannot be represented as float. Given value: `{$valueString}`"); 61 | } 62 | 63 | return $valueFloat; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Support/UrlValidator.php: -------------------------------------------------------------------------------- 1 | 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Text.php: -------------------------------------------------------------------------------- 1 | validate($value); 26 | $this->value = $value; 27 | } 28 | 29 | /** 30 | * Returns the length of the text. 31 | * 32 | * @return positive-int 33 | */ 34 | public function length(): int 35 | { 36 | return mb_strlen($this->value); 37 | } 38 | 39 | /** 40 | * @throws ValueObjectInvalidArgumentException 41 | * 42 | * @phpstan-assert non-empty-string $value 43 | */ 44 | protected function validate(string $value): void 45 | { 46 | if ($value === '') { 47 | throw new ValueObjectInvalidArgumentException('Provided value cannot be empty.'); 48 | } 49 | } 50 | 51 | /** 52 | * Returns string representation of the value object. 53 | * 54 | * @return non-empty-string 55 | */ 56 | public function __toString(): string 57 | { 58 | return $this->value; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Timestamp.php: -------------------------------------------------------------------------------- 1 | dateTime = new \DateTimeImmutable('@' . $this->value); 26 | } 27 | 28 | /** 29 | * @throws ValueObjectInvalidArgumentException 30 | */ 31 | #[\Override] 32 | protected function validate(): void 33 | { 34 | if (strtotime(date('Y-m-d H:i:s', $this->value)) !== $this->value) { 35 | throw new ValueObjectInvalidArgumentException("Provided value is not valid timestamp. Given value: `{$this->value}`."); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Url.php: -------------------------------------------------------------------------------- 1 |