├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── SECURITY.md └── workflows │ ├── bc-check.yml │ ├── infection.yml │ ├── phpstan.yml │ ├── pint.yml │ └── run-tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── infection.json5 ├── phpstan.neon ├── phpunit.xml.dist ├── pint.json ├── src ├── Artisan │ ├── ValueObjectMakeCommand.php │ └── stubs │ │ └── value-object.stub ├── Collection │ ├── Complex │ │ ├── ClassString.php │ │ ├── Email.php │ │ ├── FullName.php │ │ ├── Name.php │ │ ├── Phone.php │ │ ├── TaxNumber.php │ │ ├── Url.php │ │ └── Uuid.php │ └── Primitive │ │ ├── Boolean.php │ │ ├── Number.php │ │ └── Text.php ├── Concerns │ ├── HandlesCallbacks.php │ └── SanitizesNumbers.php ├── Contracts │ └── Immutable.php ├── ValueObject.php └── ValueObjectServiceProvider.php └── tests ├── Pest.php ├── TestCase.php └── Unit ├── Complex ├── ClassStringTest.php ├── EmailTest.php ├── FullNameTest.php ├── NameTest.php ├── PhoneTest.php ├── TaxNumberTest.php ├── UrlTest.php └── UuidTest.php ├── Primitive ├── BooleanTest.php ├── NumberTest.php └── TextTest.php ├── ValueObjectCommandTest.php ├── ValueObjectEqualityTest.php └── ValueObjectTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - # **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://paypal.com/donate/?hosted_button_id=KHLEL8PFS4AXJ"] 2 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email `contact@observer.name` instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /.github/workflows/bc-check.yml: -------------------------------------------------------------------------------- 1 | name: bc-check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | backwards-compatibility-check: 13 | name: "Backwards Compatibility Check" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: "Install PHP" 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: "8.3" 23 | - name: "Install dependencies" 24 | run: "composer install" 25 | - name: "Install BC check" 26 | run: "composer require --dev roave/backward-compatibility-check" 27 | - name: "Check for BC breaks" 28 | run: "vendor/bin/roave-backward-compatibility-check" 29 | -------------------------------------------------------------------------------- /.github/workflows/infection.yml: -------------------------------------------------------------------------------- 1 | name: infection 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | infection: 11 | name: "Running Infection" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.3' 20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium 21 | coverage: pcov 22 | 23 | - name: Cache composer dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: vendor 27 | key: composer-${{ hashFiles('composer.lock') }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | composer require "laravel/framework:10.*" "pestphp/pest:^1.16" "nesbot/carbon:^2.64.1" --dev --no-interaction --no-update 32 | composer update --prefer-stable --prefer-dist --no-interaction 33 | 34 | - name: Run infection 35 | run: ./vendor/bin/infection --show-mutations --min-msi=100 --min-covered-msi=100 36 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: phpstan 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | larastan: 11 | name: "Running Larastan" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.3' 20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl 21 | coverage: none 22 | 23 | - name: Cache composer dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: vendor 27 | key: composer-${{ hashFiles('composer.lock') }} 28 | 29 | - name: Run composer install 30 | run: composer install -n --prefer-dist 31 | 32 | - name: Run phpstan 33 | run: ./vendor/bin/phpstan 34 | -------------------------------------------------------------------------------- /.github/workflows/pint.yml: -------------------------------------------------------------------------------- 1 | name: pint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | pint: 11 | name: "Running Laravel Pint" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '8.3' 20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl 21 | coverage: none 22 | 23 | - name: Cache composer dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: vendor 27 | key: composer-${{ hashFiles('composer.lock') }} 28 | 29 | - name: Run composer install 30 | run: composer install -n --prefer-dist 31 | 32 | - name: Run Laravel Pint 33 | run: ./vendor/bin/pint --test 34 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: [8.1, 8.2, 8.3, 8.4] 17 | laravel: ['10.*', '11.*'] 18 | stability: [prefer-lowest, prefer-stable] 19 | include: 20 | - laravel: 10.* 21 | testbench: 8.* 22 | - laravel: 11.* 23 | testbench: 9.* 24 | exclude: 25 | - laravel: 11.* 26 | php: 8.1 27 | 28 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v3 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium 39 | coverage: pcov 40 | 41 | - name: Setup problem matchers 42 | run: | 43 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 44 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer require "laravel/framework:${{ matrix.laravel }}" "nesbot/carbon:^2.64.1" --dev --no-interaction --no-update 49 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 50 | 51 | - name: Execute tests 52 | run: ./vendor/bin/pest --coverage-clover build/clover.xml 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .php_cs 3 | .php_cs.cache 4 | .phpunit.result.cache 5 | build 6 | coverage 7 | phpunit.xml 8 | psalm.xml 9 | testbench.yaml 10 | vendor 11 | node_modules 12 | .php-cs-fixer.cache 13 | .phpunit.cache 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Michael Rubél 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Value Objects for Laravel](https://user-images.githubusercontent.com/37669560/200172635-6b2ca8d8-fb2b-4037-a697-b8f6e4c8c615.png) 2 | 3 | # Laravel Value Objects 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/michael-rubel/laravel-value-objects.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/michael-rubel/laravel-value-objects) 5 | [![Tests](https://img.shields.io/github/actions/workflow/status/michael-rubel/laravel-value-objects/run-tests.yml?branch=main&style=flat-square&label=tests&logo=github)](https://github.com/michael-rubel/laravel-value-objects/actions) 6 | [![Code Quality](https://img.shields.io/scrutinizer/quality/g/michael-rubel/laravel-value-objects.svg?style=flat-square&logo=scrutinizer)](https://scrutinizer-ci.com/g/michael-rubel/laravel-value-objects/?branch=main) 7 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/michael-rubel/laravel-value-objects.svg?style=flat-square&logo=scrutinizer)](https://scrutinizer-ci.com/g/michael-rubel/laravel-value-objects/?branch=main) 8 | [![Infection](https://img.shields.io/github/actions/workflow/status/michael-rubel/laravel-value-objects/infection.yml?branch=main&style=flat-square&label=infection&logo=php)](https://github.com/michael-rubel/laravel-value-objects/actions) 9 | [![Larastan](https://img.shields.io/github/actions/workflow/status/michael-rubel/laravel-value-objects/phpstan.yml?branch=main&style=flat-square&label=larastan&logo=laravel)](https://github.com/michael-rubel/laravel-value-objects/actions) 10 | 11 | A bunch of general-purpose value objects you can use in your Laravel application. 12 | 13 | --- 14 | 15 | The package requires `PHP 8.1` or higher and `Laravel 10` or higher. 16 | 17 | ## #StandWithUkraine 18 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 19 | 20 | ## Installation 21 | Install the package using composer: 22 | ```bash 23 | composer require michael-rubel/laravel-value-objects 24 | ``` 25 | 26 | ## Built-in value objects 27 | 28 | - [`Boolean`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Primitive/Boolean.php) 29 | - [`ClassString`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/ClassString.php) 30 | - [`Email`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/Email.php) 31 | - [`FullName`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/FullName.php) 32 | - [`Name`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/Name.php) 33 | - [`Number`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Primitive/Number.php) 34 | - [`Phone`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/Phone.php) 35 | - [`TaxNumber`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/TaxNumber.php) 36 | - [`Text`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Primitive/Text.php) 37 | - [`Url`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/Url.php) 38 | - [`Uuid`](https://github.com/michael-rubel/laravel-value-objects/blob/main/src/Collection/Complex/Uuid.php) 39 | 40 | ### Artisan command 41 | You can generate custom value objects with Artisan command: 42 | ```shell 43 | php artisan make:value-object YourNameValueObject 44 | ``` 45 | 46 | ## Usage 47 | 48 | ### Boolean 49 | ```php 50 | $bool = new Boolean('1'); 51 | $bool = Boolean::make('1'); 52 | $bool = Boolean::from('1'); 53 | 54 | $bool->value(); // true 55 | (string) $bool; // 'true' 56 | $bool->toArray(); // ['true'] 57 | ``` 58 | 59 | --- 60 | 61 | ### Number 62 | ```php 63 | $number = new Number('10.20999', scale: 2); 64 | $number = Number::make('10.20999', scale: 2); 65 | $number = Number::from('10.20999', scale: 2); 66 | 67 | $number->value(); // '10.20' 68 | (string) $number; // '10.20' 69 | $number->toArray(); // ['10.20'] 70 | 71 | // Starting from version `3.5.0` also 72 | // accepts locale-formatted numbers: 73 | $number = new Number('1.230,00'); 74 | $number->value(); // '1230.00' 75 | $number = new Number('1,230.00'); 76 | $number->value(); // '1230.00' 77 | $number = new Number('1 230,00'); 78 | $number->value(); // '1230.00' 79 | $number = new Number('1 230.00'); 80 | $number->value(); // '1230.00' 81 | ``` 82 | 83 | --- 84 | 85 | ### Text 86 | ```php 87 | $text = new Text('Lorem Ipsum is simply dummy text.'); 88 | $text = Text::make('Lorem Ipsum is simply dummy text.'); 89 | $text = Text::from('Lorem Ipsum is simply dummy text.'); 90 | 91 | $text->value(); // 'Lorem Ipsum is simply dummy text.' 92 | (string) $text; // 'Lorem Ipsum is simply dummy text.' 93 | $text->toArray(); // ['Lorem Ipsum is simply dummy text.'] 94 | ``` 95 | 96 | --- 97 | 98 | ### ClassString 99 | ```php 100 | $classString = new ClassString('\Exception'); 101 | $classString = ClassString::make('\Exception'); 102 | $classString = ClassString::from('\Exception'); 103 | 104 | $classString->value(); // '\Exception' 105 | (string) $classString; // '\Exception' 106 | $classString->toArray(); // ['\Exception'] 107 | 108 | $classString->classExists(); // true 109 | $classString->interfaceExists(); // false 110 | $classString->instantiate(); // Exception { ... } 111 | $classString->instantiateWith(['message' => 'My message.']); // Exception { #message: "test" ... } 112 | ``` 113 | 114 | --- 115 | 116 | ### Email 117 | ```php 118 | $email = new Email('michael@laravel.software'); 119 | $email = Email::make('michael@laravel.software'); 120 | $email = Email::from('michael@laravel.software'); 121 | 122 | $email->value(); // 'michael@laravel.software' 123 | (string) $email; // 'michael@laravel.software' 124 | $email->toArray(); // ['email' => 'michael@laravel.software', 'username' => 'michael', 'domain' => 'laravel.software'] 125 | ``` 126 | 127 | --- 128 | 129 | ### FullName 130 | ```php 131 | $name = new FullName(' Taylor Otwell '); 132 | $name = FullName::make(' Taylor Otwell '); 133 | $name = FullName::from(' Taylor Otwell '); 134 | 135 | $name->value(); // 'Taylor Otwell' 136 | (string) $name; // 'Taylor Otwell' 137 | 138 | $name->fullName(); // 'Taylor Otwell' 139 | $name->firstName(); // 'Taylor' 140 | $name->lastName(); // 'Otwell' 141 | 142 | $name = 'Richard Le Poidevin'; 143 | 144 | $fullName = new FullName($name, limit: 2); 145 | 146 | $fullName->toArray(); 147 | 148 | // array:3 [ 149 | // "fullName" => "Richard Le Poidevin" 150 | // "firstName" => "Richard" 151 | // "lastName" => "Le Poidevin" 152 | // ] 153 | ``` 154 | 155 | --- 156 | 157 | ### Name 158 | ```php 159 | $name = new Name(' Company name! '); 160 | $name = Name::make(' Company name! '); 161 | $name = Name::from(' Company name! '); 162 | 163 | $name->value(); // 'Company name!' 164 | (string) $name; // 'Company name!' 165 | $name->toArray(); // ['Company name!'] 166 | ``` 167 | 168 | --- 169 | 170 | ### Phone 171 | ```php 172 | $phone = new Phone(' +38 000 000 00 00 '); 173 | $phone = Phone::make(' +38 000 000 00 00 '); 174 | $phone = Phone::from(' +38 000 000 00 00 '); 175 | 176 | $phone->value(); // '+38 000 000 00 00' 177 | (string) $phone; // '+38 000 000 00 00' 178 | $phone->toArray(); // ['+38 000 000 00 00'] 179 | 180 | $phone->sanitized(); // '+380000000000' 181 | ``` 182 | 183 | --- 184 | 185 | ### TaxNumber 186 | ```php 187 | $taxNumber = new TaxNumber('0123456789', 'PL'); 188 | $taxNumber = TaxNumber::make('0123456789', 'PL'); 189 | $taxNumber = TaxNumber::from('0123456789', 'PL'); 190 | 191 | $taxNumber->value(); // 'PL0123456789' 192 | (string) $taxNumber; // 'PL0123456789' 193 | $taxNumber->toArray(); // ['fullTaxNumber' => 'PL0123456789', 'taxNumber' => '0123456789', 'prefix' => 'PL'] 194 | 195 | $taxNumber->fullTaxNumber(); // 'PL0123456789' 196 | $taxNumber->taxNumber(); // '0123456789' 197 | $taxNumber->prefix(); // 'PL' 198 | ``` 199 | 200 | --- 201 | 202 | ### Url 203 | ```php 204 | $uuid = new Url('my-blog-page'); 205 | $uuid = Url::make('my-blog-page'); 206 | $uuid = Url::from('my-blog-page'); 207 | 208 | $uuid->value(); // 'https://example.com/my-blog-page' 209 | (string) $uuid; // 'https://example.com/my-blog-page' 210 | $uuid->toArray(); // ['https://example.com/my-blog-page'] 211 | ``` 212 | 213 | --- 214 | 215 | ### Uuid 216 | ```php 217 | $uuid = new Uuid('8547d10c-7a37-492a-8d33-be0e5ae6119b', 'Optional name'); 218 | $uuid = Uuid::make('8547d10c-7a37-492a-8d33-be0e5ae6119b', 'Optional name'); 219 | $uuid = Uuid::from('8547d10c-7a37-492a-8d33-be0e5ae6119b', 'Optional name'); 220 | 221 | $uuid->value(); // '8547d10c-7a37-492a-8d33-be0e5ae6119b' 222 | (string) $uuid; // '8547d10c-7a37-492a-8d33-be0e5ae6119b' 223 | $uuid->toArray(); // ['name' => 'Optional name', 'value' => '8547d10c-7a37-492a-8d33-be0e5ae6119b'] 224 | 225 | $uuid->uuid(); // '8547d10c-7a37-492a-8d33-be0e5ae6119b' 226 | $uuid->name(); // 'Optional name' 227 | ``` 228 | 229 | ## Handle failed validation 230 | 231 | If you want to avoid try/catching your value object when the validation fails, you can use `makeOrNull` method: 232 | 233 | ```php 234 | $bool = Boolean::makeOrNull('bad input'); // null 235 | 236 | $bool?->value(); // null 237 | ``` 238 | 239 | ## Extending functionality 240 | All value objects are [Macroable](https://laravel.com/api/9.x/Illuminate/Support/Traits/Macroable.html). 241 | This way you can add new methods dynamically. If you need to extend existing methods, you can create a value object locally with `make:value-object` command and use inheritance. 242 | 243 | For example: 244 | ```php 245 | ValueObject::macro('str', function () { 246 | return str($this->value()); 247 | }); 248 | 249 | $name = new Text('Lorem ipsum'); 250 | 251 | $name->str()->is('Lorem ipsum'); // true 252 | ``` 253 | 254 | ## Conditionable 255 | Value objects utilize a [Conditionable](https://laravel.com/api/9.x/Illuminate/Support/Traits/Conditionable.html) trait. 256 | You can use `when` and `unless` methods. 257 | 258 | ```php 259 | TaxNumber::from('PL0123456789')->when(function ($number) { 260 | return $number->prefix() !== null; 261 | })->prefix(); 262 | ``` 263 | 264 | ## Contributing 265 | If you see any way we can improve the package, or maybe you want to make your own custom value object as built-in, PRs are welcome. 266 | 267 | ## Testing 268 | ```bash 269 | composer test 270 | ``` 271 | 272 | ## License 273 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 274 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michael-rubel/laravel-value-objects", 3 | "description": "It is an example template for Laravel packages. Fill or change it the way you like.", 4 | "keywords": [ 5 | "michael-rubel", 6 | "laravel", 7 | "laravel-value-objects" 8 | ], 9 | "homepage": "https://github.com/michael-rubel/laravel-value-objects", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Michael Rubél", 14 | "email": "contact@observer.name", 15 | "role": "Author" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^10.0|^11.0", 21 | "illuminate/validation": "^10.0|^11.0", 22 | "michael-rubel/laravel-formatters": "^8.0", 23 | "phpmath/bignumber": "^1.2", 24 | "spatie/laravel-package-tools": "^1.12" 25 | }, 26 | "require-dev": { 27 | "infection/infection": "^0.27.3", 28 | "laravel/pint": "^1.0", 29 | "nunomaduro/collision": "^6.0|^7.0|^8.0", 30 | "larastan/larastan": "^2.0", 31 | "orchestra/testbench": "^8.0|^9.0", 32 | "pestphp/pest": "^1.16|^2.0", 33 | "phpunit/phpunit": "^9.5|^10.5" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "MichaelRubel\\ValueObjects\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "MichaelRubel\\ValueObjects\\Tests\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "./vendor/bin/testbench package:test --no-coverage", 47 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 48 | }, 49 | "config": { 50 | "sort-packages": true, 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true, 53 | "infection/extension-installer": true 54 | } 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "MichaelRubel\\ValueObjects\\ValueObjectServiceProvider" 60 | ] 61 | } 62 | }, 63 | "minimum-stability": "dev", 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /infection.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vendor/infection/infection/resources/schema.json", 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "testFramework": "pest", 9 | "logs": { 10 | "text": "php://stderr", 11 | "github": true 12 | }, 13 | // "logs": { 14 | // "text": "infection.log" 15 | // }, 16 | "mutators": { 17 | "@default": true, 18 | "IncrementInteger": { 19 | "ignore": [ 20 | "MichaelRubel\\ValueObjects\\Collection\\Complex\\FullName::__construct" 21 | ] 22 | }, 23 | "ConcatOperandRemoval": { 24 | "ignore": [ 25 | "MichaelRubel\\ValueObjects\\Artisan\\ValueObjectMakeCommand::getDefaultNamespace" 26 | ] 27 | }, 28 | "Concat": { 29 | "ignore": [ 30 | "MichaelRubel\\ValueObjects\\Artisan\\ValueObjectMakeCommand::getDefaultNamespace" 31 | ] 32 | }, 33 | "CastString": { 34 | "ignore": [ 35 | "MichaelRubel\\ValueObjects\\Collection\\Primitive\\Boolean::handleNonBoolean" 36 | ] 37 | }, 38 | "Ternary": { 39 | "ignore": [ 40 | "MichaelRubel\\ValueObjects\\Collection\\Primitive\\Boolean::handleNonBoolean" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - src 8 | 9 | level: max 10 | 11 | ignoreErrors: 12 | - '#Cannot call method (.*) on mixed\.#' 13 | - '#Property (.*) does not accept mixed\.#' 14 | - '#Method (.*) should return (.*) but returns mixed\.#' 15 | - '#Parameter (.*), mixed given.#' 16 | - '#Unsafe usage of new static\(\)\.#' 17 | - '#Cannot cast mixed to string\.#' 18 | - '#Parameter \#1 \$string of function str expects string\|null, int\|string\|null given\.#' 19 | - '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\Email\:\:domain\(\) should return string but returns string\|null\.#' 20 | - '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\Email\:\:username\(\) should return string but returns string\|null\.#' 21 | - '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\FullName\:\:firstName\(\) should return string but returns string\|null\.#' 22 | - '#Method MichaelRubel\\ValueObjects\\Collection\\Complex\\FullName\:\:lastName\(\) should return string but returns string\|null\.#' 23 | - '#Parameter \#1 \$value of class Illuminate\\Support\\Stringable constructor expects string, int\|string given\.#' 24 | - '#Parameter \#1 \$string of function str expects string\|null, float given\.#' 25 | - '#Parameter \#1 \$string of function str expects string\|null, float\|int\|string\|null given\.#' 26 | 27 | checkMissingIterableValueType: false 28 | 29 | reportUnmatchedIgnoredErrors: false 30 | 31 | checkOctaneCompatibility: true 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "phpdoc_separation": false, 5 | "concat_space": { 6 | "spacing": "one" 7 | }, 8 | "class_attributes_separation": { 9 | "elements": { 10 | "const": "only_if_meta" 11 | } 12 | }, 13 | "binary_operator_spaces": { 14 | "default": "single_space", 15 | "operators": {"=>": null, "=": "align_single_space_minimal"} 16 | }, 17 | "no_superfluous_phpdoc_tags": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Artisan/ValueObjectMakeCommand.php: -------------------------------------------------------------------------------- 1 | laravel->basePath('/stubs/value-object.stub')) 35 | ? $customPath // @codeCoverageIgnore 36 | : __DIR__ . '/stubs/value-object.stub'; 37 | } 38 | 39 | /** 40 | * Get the default namespace for the class. 41 | * 42 | * @param string $rootNamespace 43 | * 44 | * @return string 45 | */ 46 | protected function getDefaultNamespace($rootNamespace): string 47 | { 48 | return $rootNamespace . '\\ValueObjects'; 49 | } 50 | 51 | /** 52 | * Get the console command options. 53 | * 54 | * @return array 55 | */ 56 | protected function getOptions(): array 57 | { 58 | return [ 59 | ['value-object', null, InputOption::VALUE_NONE, 'Create a value object'], 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Artisan/stubs/value-object.stub: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class {{ class }} extends ValueObject 20 | { 21 | /** 22 | * Create a new instance of the value object. 23 | * 24 | * @return void 25 | */ 26 | public function __construct() 27 | { 28 | $this->validate(); 29 | } 30 | 31 | /** 32 | * Get the object value. 33 | * 34 | * @return string 35 | */ 36 | public function value(): string 37 | { 38 | // TODO: Implement value() method. 39 | } 40 | 41 | /** 42 | * Get array representation of the value object. 43 | * 44 | * @return array 45 | */ 46 | public function toArray(): array 47 | { 48 | // TODO: Implement value() method. 49 | } 50 | 51 | /** 52 | * Get string representation of the value object. 53 | * 54 | * @return string 55 | */ 56 | public function __toString(): string 57 | { 58 | // TODO: Implement value() method. 59 | } 60 | 61 | /** 62 | * Validate the value object data. 63 | * 64 | * @return void 65 | */ 66 | protected function validate(): void 67 | { 68 | // TODO: Implement validate() method. 69 | 70 | if ($this->value() === '') { 71 | throw ValidationException::withMessages(['Value of {{ class }} cannot be empty.']); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Collection/Complex/ClassString.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(string $string) 28 | * @method static static from(string $string) 29 | * @method static static makeOrNull(string|null $string) 30 | * 31 | * @extends ValueObject 32 | */ 33 | class ClassString extends ValueObject 34 | { 35 | /** 36 | * @var string 37 | */ 38 | protected string $string; 39 | 40 | /** 41 | * Create a new instance of the value object. 42 | * 43 | * @param string $string 44 | */ 45 | public function __construct(string $string) 46 | { 47 | if (isset($this->string)) { 48 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 49 | } 50 | 51 | $this->string = $string; 52 | 53 | $this->validate(); 54 | } 55 | 56 | /** 57 | * Determine if the class exists for this class string. 58 | * 59 | * @return bool 60 | */ 61 | public function classExists(): bool 62 | { 63 | return class_exists($this->value()); 64 | } 65 | 66 | /** 67 | * Determine if the interface exists for this class string. 68 | * 69 | * @return bool 70 | */ 71 | public function interfaceExists(): bool 72 | { 73 | return interface_exists($this->value()); 74 | } 75 | 76 | /** 77 | * Instantiate the class string if possible. 78 | * 79 | * @param array $parameters 80 | * 81 | * @return object 82 | */ 83 | public function instantiate(array $parameters = []): object 84 | { 85 | return app($this->value(), $parameters); 86 | } 87 | 88 | /** 89 | * Instantiate the class string if possible. 90 | * 91 | * @param array $parameters 92 | * 93 | * @return object 94 | */ 95 | public function instantiateWith(array $parameters): object 96 | { 97 | return $this->instantiate($parameters); 98 | } 99 | 100 | /** 101 | * Get the object value. 102 | * 103 | * @return string 104 | */ 105 | public function value(): string 106 | { 107 | return $this->string; 108 | } 109 | 110 | /** 111 | * Validate the value object data. 112 | * 113 | * @return void 114 | */ 115 | protected function validate(): void 116 | { 117 | if ($this->value() === '') { 118 | throw ValidationException::withMessages(['Class string cannot be empty.']); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Collection/Complex/Email.php: -------------------------------------------------------------------------------- 1 | 22 | * 23 | * @template TKey of array-key 24 | * @template TValue 25 | * 26 | * @method static static make(string|Stringable $value) 27 | * @method static static from(string|Stringable $value) 28 | * @method static static makeOrNull(string|Stringable|null $value) 29 | * 30 | * @extends Text 31 | */ 32 | class Email extends Text 33 | { 34 | /** 35 | * @var array 36 | */ 37 | protected array $split; 38 | 39 | /** 40 | * Create a new instance of the value object. 41 | * 42 | * @param string|Stringable $value 43 | */ 44 | public function __construct(string|Stringable $value) 45 | { 46 | parent::__construct($value); 47 | 48 | $this->split(); 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function username(): string 55 | { 56 | return $this->split[0]; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function domain(): string 63 | { 64 | return $this->split[1]; 65 | } 66 | 67 | /** 68 | * Get an array representation of the value object. 69 | * 70 | * @return array 71 | */ 72 | public function toArray(): array 73 | { 74 | return [ 75 | 'email' => $this->value(), 76 | 'username' => $this->username(), 77 | 'domain' => $this->domain(), 78 | ]; 79 | } 80 | 81 | /** 82 | * Validate the email. 83 | * 84 | * @return void 85 | */ 86 | protected function validate(): void 87 | { 88 | $validator = Validator::make( 89 | ['email' => $this->value()], 90 | ['email' => $this->validationRules()], 91 | ); 92 | 93 | if ($validator->fails()) { 94 | throw ValidationException::withMessages(['Your email is invalid.']); 95 | } 96 | } 97 | 98 | /** 99 | * Define the rules for email validator. 100 | * 101 | * @return array 102 | */ 103 | protected function validationRules(): array 104 | { 105 | return ['required', 'email:filter,spoof']; 106 | } 107 | 108 | /** 109 | * Split the value by at symbol. 110 | * 111 | * @return void 112 | */ 113 | protected function split(): void 114 | { 115 | $this->split = explode('@', $this->value()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Collection/Complex/FullName.php: -------------------------------------------------------------------------------- 1 | 24 | * 25 | * @template TKey of array-key 26 | * @template TValue 27 | * 28 | * @method static static make(string|Stringable $value, int $limit = -1) 29 | * @method static static from(string|Stringable $value, int $limit = -1) 30 | * @method static static makeOrNull(string|Stringable|null $value, int $limit = -1) 31 | * 32 | * @extends Name 33 | */ 34 | class FullName extends Name 35 | { 36 | /** 37 | * @var Collection 38 | */ 39 | protected Collection $split; 40 | 41 | /** 42 | * Create a new instance of the value object. 43 | * 44 | * @param string|Stringable $value 45 | * @param int $limit 46 | */ 47 | public function __construct(string|Stringable $value, protected int $limit = -1) 48 | { 49 | static::beforeParentCalls(fn () => $this->split()); 50 | 51 | parent::__construct($value); 52 | } 53 | 54 | /** 55 | * Get the full name. 56 | * 57 | * @return string 58 | */ 59 | public function fullName(): string 60 | { 61 | return $this->value(); 62 | } 63 | 64 | /** 65 | * Get the first name. 66 | * 67 | * @return string 68 | */ 69 | public function firstName(): string 70 | { 71 | return $this->split->first(); 72 | } 73 | 74 | /** 75 | * Get the last name. 76 | * 77 | * @return string 78 | */ 79 | public function lastName(): string 80 | { 81 | return $this->split->last(); 82 | } 83 | 84 | /** 85 | * Get an array representation of the value object. 86 | * 87 | * @return array 88 | */ 89 | public function toArray(): array 90 | { 91 | return [ 92 | 'fullName' => $this->fullName(), 93 | 'firstName' => $this->firstName(), 94 | 'lastName' => $this->lastName(), 95 | ]; 96 | } 97 | 98 | /** 99 | * Validate the value object data. 100 | * 101 | * @return void 102 | */ 103 | protected function validate(): void 104 | { 105 | if ($this->value() === '') { 106 | throw ValidationException::withMessages(['Full name cannot be empty.']); 107 | } 108 | 109 | if (count($this->split) < 2) { 110 | throw ValidationException::withMessages(['Full name should have a first name and last name.']); 111 | } 112 | } 113 | 114 | /** 115 | * Sanitize the value. 116 | * 117 | * @return void 118 | */ 119 | protected function sanitize(): void 120 | { 121 | $this->value = format(FullNameFormatter::class, $this->value()); 122 | } 123 | 124 | /** 125 | * Split the value. 126 | * 127 | * @return void 128 | */ 129 | protected function split(): void 130 | { 131 | $this->split = str($this->value())->split('/\s/', $this->limit); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Collection/Complex/Name.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(string|Stringable $value) 28 | * @method static static from(string|Stringable $value) 29 | * @method static static makeOrNull(string|Stringable|null $value) 30 | * 31 | * @extends Text 32 | */ 33 | class Name extends Text 34 | { 35 | /** 36 | * Create a new instance of the value object. 37 | * 38 | * @param string|Stringable $value 39 | */ 40 | public function __construct(string|Stringable $value) 41 | { 42 | parent::__construct($value); 43 | 44 | $this->sanitize(); 45 | } 46 | 47 | /** 48 | * Sanitize the value. 49 | * 50 | * @return void 51 | */ 52 | protected function sanitize(): void 53 | { 54 | $this->value = format(NameFormatter::class, $this->value()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Collection/Complex/Phone.php: -------------------------------------------------------------------------------- 1 | 21 | * 22 | * @template TKey of array-key 23 | * @template TValue 24 | * 25 | * @method static static make(string|Stringable $value) 26 | * @method static static from(string|Stringable $value) 27 | * @method static static makeOrNull(string|Stringable|null $value) 28 | * 29 | * @extends Text 30 | */ 31 | class Phone extends Text 32 | { 33 | /** 34 | * Create a new instance of the value object. 35 | * 36 | * @param string|Stringable $value 37 | */ 38 | public function __construct(string|Stringable $value) 39 | { 40 | parent::__construct($value); 41 | 42 | $this->trim(); 43 | } 44 | 45 | /** 46 | * Get the sanitized phone number. 47 | * 48 | * @return string 49 | */ 50 | public function sanitized(): string 51 | { 52 | return str($this->value()) 53 | ->replaceMatches('/\p{C}+/u', '') 54 | ->replace(' ', '') 55 | ->value(); 56 | } 57 | 58 | /** 59 | * Validate the phone number. 60 | * 61 | * @return void 62 | */ 63 | protected function validate(): void 64 | { 65 | if (! preg_match('/^[+]?[0-9 ]{5,15}$/', $this->sanitized())) { 66 | throw ValidationException::withMessages(['Your phone number is invalid.']); 67 | } 68 | } 69 | 70 | /** 71 | * Trim the value. 72 | * 73 | * @return void 74 | */ 75 | protected function trim(): void 76 | { 77 | $this->value = trim($this->value()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Collection/Complex/TaxNumber.php: -------------------------------------------------------------------------------- 1 | 24 | * 25 | * @template TKey of array-key 26 | * @template TValue 27 | * 28 | * @method static static make(string $number, string|null $prefix = null) 29 | * @method static static from(string $number, string|null $prefix = null) 30 | * @method static static makeOrNull(string|null $number, string|null $prefix = null) 31 | * 32 | * @extends ValueObject 33 | */ 34 | class TaxNumber extends ValueObject 35 | { 36 | /** 37 | * @var string 38 | */ 39 | protected string $number; 40 | 41 | /** 42 | * @var string|null 43 | */ 44 | protected ?string $prefix = null; 45 | 46 | /** 47 | * Create a new instance of the value object. 48 | * 49 | * @param string $number 50 | * @param string|null $prefix 51 | */ 52 | public function __construct(string $number, ?string $prefix = null) 53 | { 54 | if (isset($this->number)) { 55 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 56 | } 57 | 58 | $this->number = $number; 59 | $this->prefix = $prefix; 60 | 61 | $this->validate(); 62 | $this->sanitize(); 63 | 64 | if ($this->canSplit()) { 65 | $this->split(); 66 | } 67 | } 68 | 69 | /** 70 | * Get the tax number with a country prefix. 71 | * 72 | * @return string 73 | */ 74 | public function fullTaxNumber(): string 75 | { 76 | return $this->prefix() . $this->taxNumber(); 77 | } 78 | 79 | /** 80 | * Get the tax number without country prefix. 81 | * 82 | * @return string 83 | */ 84 | public function taxNumber(): string 85 | { 86 | return str($this->number) 87 | ->upper() 88 | ->value(); 89 | } 90 | 91 | /** 92 | * Get the tax number prefix. 93 | * 94 | * @return string 95 | */ 96 | public function prefix(): string 97 | { 98 | return str($this->prefix) 99 | ->upper() 100 | ->value(); 101 | } 102 | 103 | /** 104 | * Get the country prefix for a given tax number. 105 | * 106 | * @return string 107 | */ 108 | public function country(): string 109 | { 110 | return $this->prefix(); 111 | } 112 | 113 | /** 114 | * Get the object value. 115 | * 116 | * @return string 117 | */ 118 | public function value(): string 119 | { 120 | return $this->fullTaxNumber(); 121 | } 122 | 123 | /** 124 | * Get an array representation of the value object. 125 | * 126 | * @return array 127 | */ 128 | public function toArray(): array 129 | { 130 | return [ 131 | 'fullTaxNumber' => $this->fullTaxNumber(), 132 | 'taxNumber' => $this->taxNumber(), 133 | 'prefix' => $this->prefix(), 134 | ]; 135 | } 136 | 137 | /** 138 | * Validate the value object data. 139 | * 140 | * @return void 141 | */ 142 | protected function validate(): void 143 | { 144 | if ($this->value() === '') { 145 | throw ValidationException::withMessages(['Tax number cannot be empty.']); 146 | } 147 | } 148 | 149 | /** 150 | * Sanitize the value. 151 | * 152 | * @return void 153 | */ 154 | protected function sanitize(): void 155 | { 156 | $this->number = format(TaxNumberFormatter::class, $this->taxNumber(), $this->prefix()); 157 | } 158 | 159 | /** 160 | * Determines whether to split the value. 161 | * 162 | * @return bool 163 | */ 164 | protected function canSplit(): bool 165 | { 166 | return ! is_numeric($this->number); 167 | } 168 | 169 | /** 170 | * Split the value. 171 | * 172 | * @return void 173 | */ 174 | protected function split(): void 175 | { 176 | $this->prefix = str($this->number) 177 | ->substr(0, 2) 178 | ->upper() 179 | ->value(); 180 | 181 | $this->number = str($this->number) 182 | ->substr(2) 183 | ->value(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Collection/Complex/Url.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(string $value) 28 | * @method static static from(string $value) 29 | * @method static static makeOrNull(string|null $value) 30 | * 31 | * @extends Text 32 | */ 33 | class Url extends Text 34 | { 35 | /** 36 | * Create a new instance of the value object. 37 | * 38 | * @param string $value 39 | */ 40 | public function __construct(string $value) 41 | { 42 | parent::__construct($value); 43 | 44 | $this->value = url($value); 45 | 46 | $validator = Validator::make( 47 | ['url' => $this->value()], 48 | ['url' => $this->validationRules()], 49 | ); 50 | 51 | if ($validator->fails()) { 52 | throw ValidationException::withMessages(['Your URL is invalid.']); 53 | } 54 | } 55 | 56 | /** 57 | * Define the rules for email validator. 58 | * 59 | * @return array 60 | */ 61 | protected function validationRules(): array 62 | { 63 | return ['required', 'url']; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Collection/Complex/Uuid.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(string $value, string|null $name = null) 28 | * @method static static from(string $value, string|null $name = null) 29 | * @method static static makeOrNull(string|null $value, string|null $name = null) 30 | * 31 | * @extends ValueObject 32 | */ 33 | class Uuid extends ValueObject 34 | { 35 | /** 36 | * @var string 37 | */ 38 | protected string $value; 39 | 40 | /** 41 | * @var string|null 42 | */ 43 | protected ?string $name = null; 44 | 45 | /** 46 | * Create a new instance of the value object. 47 | * 48 | * @param string $value 49 | * @param string|null $name 50 | */ 51 | public function __construct(string $value, ?string $name = null) 52 | { 53 | if (isset($this->value)) { 54 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 55 | } 56 | 57 | $this->value = $value; 58 | $this->name = $name; 59 | 60 | $this->validate(); 61 | } 62 | 63 | /** 64 | * Get the UUID value. 65 | * 66 | * @return string 67 | */ 68 | public function uuid(): string 69 | { 70 | return $this->value(); 71 | } 72 | 73 | /** 74 | * Get the UUID name if present. 75 | * 76 | * @return string|null 77 | */ 78 | public function name(): ?string 79 | { 80 | return $this->name; 81 | } 82 | 83 | /** 84 | * Get the object value. 85 | * 86 | * @return string 87 | */ 88 | public function value(): string 89 | { 90 | return $this->value; 91 | } 92 | 93 | /** 94 | * Get an array representation of the value object. 95 | * 96 | * @return array 97 | */ 98 | public function toArray(): array 99 | { 100 | return [ 101 | 'name' => $this->name(), 102 | 'value' => $this->value(), 103 | ]; 104 | } 105 | 106 | /** 107 | * Validate the value object data. 108 | * 109 | * @return void 110 | */ 111 | protected function validate(): void 112 | { 113 | if (! str($this->value())->isUuid()) { 114 | throw ValidationException::withMessages(['UUID is invalid.']); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Collection/Primitive/Boolean.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(bool|int|string $value) 28 | * @method static static from(bool|int|string $value) 29 | * @method static static makeOrNull(bool|int|string|null $value) 30 | * 31 | * @extends ValueObject 32 | */ 33 | class Boolean extends ValueObject 34 | { 35 | /** 36 | * @var bool 37 | */ 38 | protected bool $value; 39 | 40 | /** 41 | * Values that represent `true` boolean. 42 | * 43 | * @var array 44 | */ 45 | protected array $trueValues = ['1', 'true', 'on', 'yes']; 46 | 47 | /** 48 | * Values that represent `false` boolean. 49 | * 50 | * @var array 51 | */ 52 | protected array $falseValues = ['0', 'false', 'off', 'no']; 53 | 54 | /** 55 | * Create a new instance of the value object. 56 | * 57 | * @param bool|int|string $value 58 | */ 59 | public function __construct(bool|int|string $value) 60 | { 61 | if (isset($this->value)) { 62 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 63 | } 64 | 65 | ! is_bool($value) ? $this->handleNonBoolean($value) : $this->value = $value; 66 | } 67 | 68 | /** 69 | * Get the object value. 70 | * 71 | * @return bool 72 | */ 73 | public function value(): bool 74 | { 75 | return $this->value; 76 | } 77 | 78 | /** 79 | * Determine if the passed boolean is true. 80 | * 81 | * @param int|string $value 82 | * @return void 83 | */ 84 | protected function handleNonBoolean(int|string $value): void 85 | { 86 | $string = is_string($value) ? $value : (string) $value; 87 | 88 | $this->value = match (true) { 89 | Str::contains($string, $this->trueValues, ignoreCase: true) => true, 90 | Str::contains($string, $this->falseValues, ignoreCase: true) => false, 91 | default => throw new InvalidArgumentException('Invalid boolean.'), 92 | }; 93 | } 94 | 95 | /** 96 | * Get string representation of the value object. 97 | * 98 | * @return string 99 | */ 100 | public function toString(): string 101 | { 102 | return $this->value() ? 'true' : 'false'; 103 | } 104 | 105 | /** 106 | * Get string representation of the value object. 107 | * 108 | * @return string 109 | */ 110 | public function __toString(): string 111 | { 112 | return $this->toString(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Collection/Primitive/Number.php: -------------------------------------------------------------------------------- 1 | 24 | * 25 | * @template TKey of array-key 26 | * @template TValue 27 | * 28 | * @method static static make(int|string|float $number, int $scale = 2) 29 | * @method static static from(int|string|float $number, int $scale = 2) 30 | * @method static static makeOrNull(int|string|float|null $number, int $scale = 2) 31 | * 32 | * @method string abs() 33 | * @method string add(float|int|string|BigNumber $value) 34 | * @method string divide(float|int|string|BigNumber $value) 35 | * @method string multiply(float|int|string|BigNumber $value) 36 | * @method string mod(float|int|string|BigNumber $value) 37 | * @method string pow(int|string $value) 38 | * @method string sqrt() 39 | * @method string subtract(float|int|string|BigNumber $value) 40 | * 41 | * @see \PHP\Math\BigNumber\BigNumber 42 | * 43 | * @extends ValueObject 44 | */ 45 | class Number extends ValueObject 46 | { 47 | use SanitizesNumbers; 48 | 49 | /** 50 | * @var BigNumber 51 | */ 52 | protected BigNumber $bigNumber; 53 | 54 | /** 55 | * Create a new instance of the value object. 56 | * 57 | * @param int|string|float $number 58 | * @param int $scale 59 | */ 60 | public function __construct(int|string|float $number, protected int $scale = 2) 61 | { 62 | if (isset($this->bigNumber)) { 63 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 64 | } 65 | 66 | $this->bigNumber = new BigNumber( 67 | $this->sanitize($number), $this->scale, mutable: false 68 | ); 69 | } 70 | 71 | /** 72 | * Get the object value. 73 | * 74 | * @return string 75 | */ 76 | public function value(): string 77 | { 78 | return $this->bigNumber->getValue(); 79 | } 80 | 81 | /** 82 | * Get the number as an integer. 83 | * 84 | * @return int 85 | */ 86 | public function asInteger(): int 87 | { 88 | return (int) $this->bigNumber->getValue(); 89 | } 90 | 91 | /** 92 | * Get the number as a float. 93 | * 94 | * @return float 95 | */ 96 | public function asFloat(): float 97 | { 98 | return (float) $this->bigNumber->getValue(); 99 | } 100 | 101 | /** 102 | * Get the underlying `BigNumber` object. 103 | * 104 | * @return BigNumber 105 | */ 106 | public function asBigNumber(): BigNumber 107 | { 108 | return $this->bigNumber; 109 | } 110 | 111 | /** 112 | * Forward call to underlying object if the method 113 | * doesn't exist in `Number` and doesn't have a macro. 114 | * 115 | * @param string $method 116 | * @param array $parameters 117 | * 118 | * @return mixed 119 | */ 120 | public function __call($method, $parameters) 121 | { 122 | if (static::hasMacro($method)) { 123 | return parent::__call($method, $parameters); 124 | } 125 | 126 | return $this->bigNumber->{$method}(...$parameters)->getValue(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Collection/Primitive/Text.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | * @template TKey of array-key 25 | * @template TValue 26 | * 27 | * @method static static make(string|Stringable $value) 28 | * @method static static from(string|Stringable $value) 29 | * @method static static makeOrNull(string|Stringable|null $value) 30 | * 31 | * @extends ValueObject 32 | */ 33 | class Text extends ValueObject 34 | { 35 | /** 36 | * @var string|Stringable 37 | */ 38 | protected string|Stringable $value; 39 | 40 | /** 41 | * Create a new instance of the value object. 42 | * 43 | * @param string|Stringable $value 44 | */ 45 | public function __construct(string|Stringable $value) 46 | { 47 | if (isset($this->value)) { 48 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 49 | } 50 | 51 | $this->value = $value; 52 | 53 | if (isset($this->before)) { 54 | ($this->before)(); 55 | } 56 | 57 | $this->validate(); 58 | } 59 | 60 | /** 61 | * Get the object value. 62 | * 63 | * @return string 64 | */ 65 | public function value(): string 66 | { 67 | return (string) $this->value; 68 | } 69 | 70 | /** 71 | * Validate the value object data. 72 | * 73 | * @return void 74 | */ 75 | protected function validate(): void 76 | { 77 | if ($this->value() === '') { 78 | throw new InvalidArgumentException('Text cannot be empty.'); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Concerns/HandlesCallbacks.php: -------------------------------------------------------------------------------- 1 | before = $callback; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Concerns/SanitizesNumbers.php: -------------------------------------------------------------------------------- 1 | isPrecise($number)) { 19 | throw new LengthException('Float precision loss detected.'); 20 | } 21 | 22 | $number = str($number)->replace(',', '.'); 23 | 24 | $dots = $number->substrCount('.'); 25 | 26 | if ($dots >= 2) { 27 | $number = $number 28 | ->replaceLast('.', ',') 29 | ->replace('.', '') 30 | ->replaceLast(',', '.'); 31 | } 32 | 33 | return $number 34 | ->replaceMatches('/(?!^-)[^0-9.]/', '') 35 | ->toString(); 36 | } 37 | 38 | /** 39 | * Determine whether the passed floating point number is precise. 40 | * 41 | * @param float $number 42 | * 43 | * @return bool 44 | */ 45 | protected function isPrecise(float $number): bool 46 | { 47 | $numberAsString = str($number); 48 | 49 | $afterFloatingPoint = $numberAsString 50 | ->explode('.') 51 | ->get(1, ''); 52 | 53 | $precisionPosition = str($afterFloatingPoint)->length(); 54 | 55 | $roundedNumber = round($number, $precisionPosition); 56 | 57 | if ($roundedNumber === $number && $numberAsString->length() <= PHP_FLOAT_DIG) { 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Contracts/Immutable.php: -------------------------------------------------------------------------------- 1 | 27 | * 28 | * @template TKey of array-key 29 | * @template TValue 30 | * 31 | * @implements Arrayable 32 | */ 33 | abstract class ValueObject implements Arrayable, Immutable 34 | { 35 | use Conditionable, HandlesCallbacks, Macroable; 36 | 37 | /** 38 | * Get the object value. 39 | * 40 | * @return mixed 41 | */ 42 | abstract public function value(); 43 | 44 | /** 45 | * Convenient method to create a value object statically. 46 | * 47 | * @param mixed $values 48 | * 49 | * @return static 50 | */ 51 | public static function make(mixed ...$values): static 52 | { 53 | return new static(...$values); 54 | } 55 | 56 | /** 57 | * Convenient method to create a value object statically. 58 | * 59 | * @param mixed $values 60 | * 61 | * @return static 62 | */ 63 | public static function from(mixed ...$values): static 64 | { 65 | return static::make(...$values); 66 | } 67 | 68 | /** 69 | * Create a value object or return null. 70 | * 71 | * @param mixed $values 72 | * 73 | * @return static|null 74 | */ 75 | public static function makeOrNull(mixed ...$values): ?static 76 | { 77 | try { 78 | return static::make(...$values); 79 | } catch (Throwable) { 80 | return null; 81 | } 82 | } 83 | 84 | /** 85 | * Check if objects are instances of same class 86 | * and share the same properties and values. 87 | * 88 | * @param ValueObject $object 89 | * 90 | * @return bool 91 | */ 92 | public function equals(ValueObject $object): bool 93 | { 94 | return $this == $object; 95 | } 96 | 97 | /** 98 | * Inversion for `equals` method. 99 | * 100 | * @param ValueObject $object 101 | * 102 | * @return bool 103 | */ 104 | public function notEquals(ValueObject $object): bool 105 | { 106 | return ! $this->equals($object); 107 | } 108 | 109 | /** 110 | * Get an array representation of the value object. 111 | * 112 | * @return array 113 | */ 114 | public function toArray(): array 115 | { 116 | return (array) $this->value(); 117 | } 118 | 119 | /** 120 | * Get string representation of the value object. 121 | * 122 | * @return string 123 | */ 124 | public function toString(): string 125 | { 126 | return (string) $this->value(); 127 | } 128 | 129 | /** 130 | * Get string representation of the value object. 131 | * 132 | * @return string 133 | */ 134 | public function __toString(): string 135 | { 136 | return $this->toString(); 137 | } 138 | 139 | /** 140 | * Get the internal property value. 141 | * 142 | * @param string $name 143 | * 144 | * @return mixed 145 | */ 146 | public function __get(string $name): mixed 147 | { 148 | return $this->{$name}; 149 | } 150 | 151 | /** 152 | * Make sure value object is immutable. 153 | * 154 | * @param string $name 155 | * @param mixed $value 156 | * 157 | * @return void 158 | * @throws InvalidArgumentException 159 | */ 160 | public function __set(string $name, mixed $value): void 161 | { 162 | throw new InvalidArgumentException(static::IMMUTABLE_MESSAGE); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ValueObjectServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-value-objects') 24 | ->hasCommand(ValueObjectMakeCommand::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature', 'Unit'); 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Expectations 23 | |-------------------------------------------------------------------------- 24 | | 25 | | When you're writing tests, you often need to check that values meet certain conditions. The 26 | | 'expect()' function gives you access to a set of 'expectations' methods that you can use 27 | | to assert different things. Of course, you may extend the Expectation API at any time. 28 | | 29 | */ 30 | 31 | expect()->extend('toBeOne', function () { 32 | return $this->toBe(1); 33 | }); 34 | 35 | expect()->extend('toBeModel', function () { 36 | return $this->toBeInstanceOf(Model::class); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('testing'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Complex/ClassStringTest.php: -------------------------------------------------------------------------------- 1 | expectException(ValidationException::class); 12 | 13 | new ClassString(''); 14 | }); 15 | 16 | test('validation exception message is correct in class string', function () { 17 | try { 18 | new ClassString(''); 19 | } catch (ValidationException $e) { 20 | $this->assertSame('Class string cannot be empty.', $e->getMessage()); 21 | } 22 | }); 23 | 24 | test('class string cannot be null', function () { 25 | $this->expectException(\TypeError::class); 26 | 27 | new ClassString(null); 28 | }); 29 | 30 | test('can get class string', function () { 31 | $classString = new ClassString('My\Test\Class'); 32 | 33 | $this->assertSame('My\Test\Class', $classString->value()); 34 | }); 35 | 36 | test('class string non-instantiable and class and interface are undefined', function () { 37 | $classString = new ClassString('My\Test\Class\NonInstantiable'); 38 | 39 | $this->assertFalse($classString->classExists()); 40 | $this->assertFalse($classString->interfaceExists()); 41 | }); 42 | 43 | test('class string throws binding resolution exception when trying to instantiate non-instantiable class', function () { 44 | $classString = new ClassString('My\Test\Class\NonInstantiable'); 45 | 46 | $this->expectException(BindingResolutionException::class); 47 | 48 | $classString->instantiate(); 49 | }); 50 | 51 | test('class string is exists but interface dont', function () { 52 | $classString = new ClassString(ClassString::class); 53 | 54 | $this->assertTrue($classString->classExists()); 55 | $this->assertFalse($classString->interfaceExists()); 56 | }); 57 | 58 | test('class string is interface & exists but class dont', function () { 59 | $classString = new ClassString(TestCase::class); 60 | 61 | $this->assertTrue($classString->classExists()); 62 | $this->assertFalse($classString->interfaceExists()); 63 | }); 64 | 65 | test('can cast class string to string', function () { 66 | $classString = new ClassString(ClassString::class); 67 | 68 | $this->assertSame('MichaelRubel\ValueObjects\Collection\Complex\ClassString', (string) $classString); 69 | }); 70 | 71 | test('can instantiate a class from class string value', function () { 72 | $classString = new ClassString('Exception'); 73 | 74 | $this->assertEquals(new \Exception, $classString->instantiate()); 75 | $this->assertEquals(new \Exception('test'), $classString->instantiateWith(['message' => 'test'])); 76 | }); 77 | 78 | test('class string is makeable', function () { 79 | $valueObject = ClassString::make('Exception'); 80 | $this->assertSame('Exception', $valueObject->value()); 81 | 82 | $valueObject = ClassString::from('Exception'); 83 | $this->assertSame('Exception', $valueObject->value()); 84 | }); 85 | 86 | test('class string is macroable', function () { 87 | ClassString::macro('getLength', function () { 88 | return str($this->value())->length(); 89 | }); 90 | $valueObject = new ClassString('TestClass\Testing'); 91 | $this->assertSame(17, $valueObject->getLength()); 92 | }); 93 | 94 | test('class string is conditionable', function () { 95 | $valueObject = new ClassString(TestCase::class); 96 | $this->assertTrue($valueObject->when(true)->classExists()); 97 | $this->assertSame($valueObject, $valueObject->when(false)->classExists()); 98 | }); 99 | 100 | test('class string is arrayable', function () { 101 | $valueObject = new ClassString('Throwable'); 102 | $this->assertTrue($valueObject->interfaceExists()); 103 | $this->assertSame([$valueObject->value()], $valueObject->toArray()); 104 | }); 105 | 106 | test('class string is stringable', function () { 107 | $valueObject = new ClassString('Throwable'); 108 | $this->assertSame($valueObject->value(), (string) $valueObject); 109 | $valueObject = new ClassString('Throwable'); 110 | $this->assertSame($valueObject->value(), $valueObject->toString()); 111 | }); 112 | 113 | test('class string has immutable properties', function () { 114 | $this->expectException(\InvalidArgumentException::class); 115 | $valueObject = new ClassString('\Exception'); 116 | $this->assertSame('\Exception', $valueObject->string); 117 | $valueObject->classString = 'immutable'; 118 | }); 119 | 120 | test('class string has immutable constructor', function () { 121 | $this->expectException(\InvalidArgumentException::class); 122 | $valueObject = new ClassString('\Exception'); 123 | $valueObject->__construct('\Throwable'); 124 | }); 125 | 126 | test('can extend protected methods in class string', function () { 127 | $text = new TestClassString('Exception'); 128 | $text->validate(); 129 | $this->assertSame('Exception', $text->value()); 130 | }); 131 | 132 | class TestClassString extends ClassString 133 | { 134 | public function validate(): void 135 | { 136 | parent::validate(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/Unit/Complex/EmailTest.php: -------------------------------------------------------------------------------- 1 | assertSame('michael@laravel.software', $email->value()); 13 | }); 14 | 15 | test('email has username', function () { 16 | $email = new Email('michael@laravel.software'); 17 | 18 | $this->assertSame('michael', $email->username()); 19 | }); 20 | 21 | test('email has domain', function () { 22 | $email = new Email('michael@laravel.software'); 23 | 24 | $this->assertSame('laravel.software', $email->domain()); 25 | }); 26 | 27 | test('email is wrong', function () { 28 | $this->expectException(ValidationException::class); 29 | 30 | new Email('123'); 31 | }); 32 | 33 | test('email is wrong with at', function () { 34 | $this->expectException(ValidationException::class); 35 | 36 | new Email('laravel@framework'); 37 | }); 38 | 39 | test('validation exception message is correct in email', function () { 40 | try { 41 | new Email(''); 42 | } catch (ValidationException $e) { 43 | $this->assertSame('Your email is invalid.', $e->getMessage()); 44 | } 45 | }); 46 | 47 | test('email cannot accept null', function () { 48 | $this->expectException(\TypeError::class); 49 | 50 | new Email(null); 51 | }); 52 | 53 | test('email fails when no argument passed', function () { 54 | $this->expectException(\TypeError::class); 55 | 56 | new Email; 57 | }); 58 | 59 | test('email fails when empty string passed', function () { 60 | $this->expectException(ValidationException::class); 61 | 62 | new Email(''); 63 | }); 64 | 65 | test('email is makeable', function () { 66 | $valueObject = Email::make('michael@laravel.software'); 67 | $this->assertSame('michael@laravel.software', $valueObject->value()); 68 | 69 | $valueObject = Email::from('michael@laravel.software'); 70 | $this->assertSame('michael@laravel.software', $valueObject->value()); 71 | }); 72 | 73 | test('email is macroable', function () { 74 | Email::macro('str', function () { 75 | return str($this->value()); 76 | }); 77 | 78 | $valueObject = new Email('michael@laravel.software'); 79 | 80 | $this->assertTrue($valueObject->str()->is('michael@laravel.software')); 81 | }); 82 | 83 | test('email is conditionable', function () { 84 | $valueObject = new Email('michael@laravel.software'); 85 | $this->assertSame('michael@laravel.software', $valueObject->when(true)->value()); 86 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 87 | }); 88 | 89 | test('email is arrayable', function () { 90 | $array = (new Email('michael@laravel.software'))->toArray(); 91 | $this->assertSame([ 92 | 'email' => 'michael@laravel.software', 93 | 'username' => 'michael', 94 | 'domain' => 'laravel.software', 95 | ], $array); 96 | }); 97 | 98 | test('email is stringable', function () { 99 | $valueObject = new Email('michael@laravel.software'); 100 | $this->assertSame('michael@laravel.software', (string) $valueObject); 101 | 102 | $valueObject = new Email('michael@laravel.software'); 103 | $this->assertSame('michael@laravel.software', $valueObject->toString()); 104 | }); 105 | 106 | test('email accepts stringable', function () { 107 | $valueObject = new Email(str('michael@laravel.software')); 108 | $this->assertSame('michael@laravel.software', $valueObject->value()); 109 | }); 110 | 111 | test('email fails when empty stringable passed', function () { 112 | $this->expectException(ValidationException::class); 113 | 114 | new Email(str('')); 115 | }); 116 | 117 | test('email has immutable properties', function () { 118 | $this->expectException(\InvalidArgumentException::class); 119 | $valueObject = new Email('contact@observer.name'); 120 | $this->assertSame('contact@observer.name', $valueObject->value); 121 | $valueObject->value = 'immutable'; 122 | }); 123 | 124 | test('email has immutable constructor', function () { 125 | $this->expectException(\InvalidArgumentException::class); 126 | $valueObject = new Email('contact@observer.name'); 127 | $valueObject->__construct('contact@observer.com'); 128 | }); 129 | 130 | test('can extend protected methods in email', function () { 131 | $email = new TestEmail('contact@observer.name'); 132 | $this->assertSame(['required', 'email:filter,spoof'], $email->validationRules()); 133 | }); 134 | 135 | class TestEmail extends Email 136 | { 137 | public function __construct(string|Stringable $value) 138 | { 139 | $this->value = $value; 140 | 141 | $this->validate(); 142 | $this->split(); 143 | } 144 | 145 | public function validationRules(): array 146 | { 147 | return parent::validationRules(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/Unit/Complex/FullNameTest.php: -------------------------------------------------------------------------------- 1 | assertSame('Michael', $name->firstName()); 13 | }); 14 | 15 | test('can get last name', function () { 16 | $name = new FullName('Michael Rubél'); 17 | 18 | $this->assertSame('Rubél', $name->lastName()); 19 | }); 20 | 21 | test('can get full name', function () { 22 | $name = new FullName('Michael Rubél'); 23 | 24 | $this->assertSame('Michael Rubél', $name->fullName()); 25 | }); 26 | 27 | test('can get full name minus case', function () { 28 | $name = new FullName('Anna Nowak-Kowalska'); 29 | 30 | $this->assertSame('Anna', $name->firstName()); 31 | $this->assertSame('Nowak-Kowalska', $name->lastName()); 32 | }); 33 | 34 | test('full name with break control', function () { 35 | $name = new FullName('Alicja Bachleda Curuś', 2); 36 | $this->assertSame('Alicja', $name->firstName()); 37 | $this->assertSame('Bachleda Curuś', $name->lastName()); 38 | }); 39 | 40 | test('can get full name with name in between', function () { 41 | $name = new FullName('Anna Ewa Kowalska'); 42 | 43 | $this->assertSame('Anna', $name->firstName()); 44 | $this->assertSame('Kowalska', $name->lastName()); 45 | }); 46 | 47 | test('can break control using word count', function () { 48 | $name = 'Richard Le Poidevin'; 49 | $name = new FullName($name, 2); 50 | $this->assertSame('Richard', $name->firstName()); 51 | $this->assertSame('Le Poidevin', $name->lastName()); 52 | }); 53 | 54 | test('full name covnerts the first letter of each word to uppercase', function ($input, $result) { 55 | $name = new FullName($input); 56 | $this->assertSame($result, $name->fullName()); 57 | })->with([ 58 | ['michael mcKenzie', 'Michael McKenzie'], 59 | ['michael McKenzie', 'Michael McKenzie'], 60 | ['Michael mcKenzie', 'Michael McKenzie'], 61 | ['Michael McKenzie', 'Michael McKenzie'], 62 | ['michael mckenzie', 'Michael Mckenzie'], 63 | ['michael mc-kenzie', 'Michael Mc-kenzie'], 64 | [' michael mc-Kenzie ', 'Michael Mc-Kenzie'], 65 | ]); 66 | 67 | test('can get cast to string', function () { 68 | $name = new FullName('Michael Rubél'); 69 | $this->assertSame('Michael Rubél', (string) $name); 70 | 71 | $name = new FullName('Michael Rubél'); 72 | $this->assertSame('Michael Rubél', $name->toString()); 73 | }); 74 | 75 | test('cannot pass empty string', function () { 76 | $this->expectException(ValidationException::class); 77 | 78 | new FullName(''); 79 | }); 80 | 81 | test('validation exception message is correct in email', function () { 82 | try { 83 | new FullName(''); 84 | } catch (ValidationException $e) { 85 | $this->assertSame('Full name cannot be empty.', $e->getMessage()); 86 | } 87 | 88 | try { 89 | new FullName('Test'); 90 | } catch (ValidationException $e) { 91 | $this->assertSame('Full name should have a first name and last name.', $e->getMessage()); 92 | } 93 | }); 94 | 95 | test('cannot pass null', function () { 96 | $this->expectException(\TypeError::class); 97 | $name = new FullName(null); 98 | $this->assertSame('', $name->fullName()); 99 | }); 100 | 101 | test('full name is makeable', function () { 102 | $name = FullName::make('Michael Rubél'); 103 | $this->assertSame('Michael Rubél', $name->fullName()); 104 | 105 | $name = FullName::from('Michael Rubél'); 106 | $this->assertSame('Michael Rubél', $name->fullName()); 107 | }); 108 | 109 | test('full name is macroable', function () { 110 | FullName::macro('middlename', fn () => $this->split[1]); 111 | $valueObject = new FullName('Anna Ewa Kowalska'); 112 | $this->assertSame('Ewa', $valueObject->middlename()); 113 | 114 | FullName::macro('inverse', fn () => $this->split = $this->split->reverse()); 115 | $valueObject = new FullName('Nowak-Kowalska Ewa Anna'); 116 | $valueObject->inverse(); 117 | $this->assertSame('Anna', $valueObject->firstName()); 118 | $this->assertSame('Nowak-Kowalska', $valueObject->lastName()); 119 | $this->assertSame('Ewa', $valueObject->middlename()); 120 | }); 121 | 122 | test('full name is conditionable', function () { 123 | $valueObject = new FullName('Michael Rubél'); 124 | 125 | $this->assertSame('Michael', $valueObject->when(true)->firstName()); 126 | $this->assertSame($valueObject, $valueObject->when(false)->firstName()); 127 | }); 128 | 129 | test('full name is arrayable', function () { 130 | $valueObject = new FullName('Michael Rubél'); 131 | $this->assertSame([ 132 | 'fullName' => 'Michael Rubél', 133 | 'firstName' => 'Michael', 134 | 'lastName' => 'Rubél', 135 | ], $valueObject->toArray()); 136 | 137 | $valueObject = new FullName('Richard Le Poidevin', 2); 138 | $this->assertSame([ 139 | 'fullName' => 'Richard Le Poidevin', 140 | 'firstName' => 'Richard', 141 | 'lastName' => 'Le Poidevin', 142 | ], $valueObject->toArray()); 143 | }); 144 | 145 | test('full name is stringable', function () { 146 | $valueObject = new FullName('Name Name'); 147 | $this->assertSame($valueObject->value(), (string) $valueObject); 148 | }); 149 | 150 | test('full name accepts stringable', function () { 151 | $valueObject = new FullName(str('Name Name')); 152 | $this->assertSame('Name Name', $valueObject->value()); 153 | }); 154 | 155 | test('full name fails when passed only first name', function () { 156 | $this->expectException(ValidationException::class); 157 | 158 | new FullName('Name'); 159 | }); 160 | 161 | test('full name has immutable properties', function () { 162 | $this->expectException(\InvalidArgumentException::class); 163 | $valueObject = new FullName('Michael Rubél'); 164 | $this->assertSame('Michael Rubél', $valueObject->value); 165 | $valueObject->full_name = 'immutable'; 166 | }); 167 | 168 | test('full name has immutable constructor', function () { 169 | $this->expectException(\InvalidArgumentException::class); 170 | $valueObject = new FullName('Michael Rubél'); 171 | $valueObject->__construct(' Michael Rubél '); 172 | }); 173 | 174 | test('can extend protected methods in email', function () { 175 | $fullName = new TestFullName('First Second Third Fourth Name', 2); 176 | $this->assertSame([ 177 | 'fullName' => 'First Second Third Fourth Name', 178 | 'firstName' => 'First', 179 | 'lastName' => 'Second Third Fourth Name', 180 | ], $fullName->toArray()); 181 | }); 182 | 183 | class TestFullName extends FullName 184 | { 185 | public function __construct(string|Stringable $value, protected int $limit = -1) 186 | { 187 | $this->value = $value; 188 | 189 | $this->split(); 190 | } 191 | 192 | protected function split(): void 193 | { 194 | parent::split(); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /tests/Unit/Complex/NameTest.php: -------------------------------------------------------------------------------- 1 | assertSame('Company Name', $name->value()); 11 | 12 | $name = new Name(' Name!'); 13 | $this->assertSame('Name!', $name->value()); 14 | 15 | $name = new Name('Name@$ '); 16 | $this->assertSame('Name@$', $name->value()); 17 | 18 | $name = new Name('HOTEL GOŁĘBIEWSKI TADEUSZ GOŁĘBIEWSKI,\r\nTAGO PRZEDSIĘBIORSTWO PRZEMYSŁU CUKIERNICZEGO TADEUSZ GOŁĘBIEWSKI'); 19 | $this->assertSame('HOTEL GOŁĘBIEWSKI TADEUSZ GOŁĘBIEWSKI,TAGO PRZEDSIĘBIORSTWO PRZEMYSŁU CUKIERNICZEGO TADEUSZ GOŁĘBIEWSKI', $name->value()); 20 | 21 | $name = new Name('HOTEL GOŁĘBIEWSKI TADEUSZ GOŁĘBIEWSKI, 22 | TAGO PRZEDSIĘBIORSTWO PRZEMYSŁU CUKIERNICZEGO TADEUSZ GOŁĘBIEWSKI'); 23 | $this->assertSame('HOTEL GOŁĘBIEWSKI TADEUSZ GOŁĘBIEWSKI,TAGO PRZEDSIĘBIORSTWO PRZEMYSŁU CUKIERNICZEGO TADEUSZ GOŁĘBIEWSKI', $name->value()); 24 | }); 25 | 26 | test('name cannot accept null', function () { 27 | $this->expectException(\TypeError::class); 28 | 29 | new Name(null); 30 | }); 31 | 32 | test('name fails when no argument passed', function () { 33 | $this->expectException(\TypeError::class); 34 | 35 | new Name; 36 | }); 37 | 38 | test('name fails when empty string passed', function () { 39 | $this->expectException(\InvalidArgumentException::class); 40 | 41 | new Name(''); 42 | }); 43 | 44 | test('name is makeable', function () { 45 | $valueObject = Name::make('1'); 46 | $this->assertSame('1', $valueObject->value()); 47 | 48 | $valueObject = Name::from('1'); 49 | $this->assertSame('1', $valueObject->value()); 50 | }); 51 | 52 | test('name is macroable', function () { 53 | Name::macro('str', function () { 54 | return str($this->value()); 55 | }); 56 | 57 | $valueObject = new Name('Lorem ipsum'); 58 | 59 | $this->assertTrue($valueObject->str()->is('Lorem ipsum')); 60 | }); 61 | 62 | test('text is conditionable', function () { 63 | $valueObject = new Name('1'); 64 | $this->assertSame('1', $valueObject->when(true)->value()); 65 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 66 | }); 67 | 68 | test('text is arrayable', function () { 69 | $array = (new Name('Lorem Ipsum is simply dummy text.'))->toArray(); 70 | $this->assertSame(['Lorem Ipsum is simply dummy text.'], $array); 71 | }); 72 | 73 | test('text is stringable', function () { 74 | $valueObject = new Name('Lorem ipsum'); 75 | $this->assertSame('Lorem ipsum', (string) $valueObject); 76 | 77 | $valueObject = new Name('Lorem ipsum'); 78 | $this->assertSame('Lorem ipsum', $valueObject->toString()); 79 | }); 80 | 81 | test('text accepts stringable', function () { 82 | $valueObject = new Name(str('Lorem ipsum')); 83 | $this->assertSame('Lorem ipsum', $valueObject->value()); 84 | }); 85 | 86 | test('text fails when empty stringable passed', function () { 87 | $this->expectException(\InvalidArgumentException::class); 88 | 89 | new Name(str('')); 90 | }); 91 | 92 | test('name has immutable properties', function () { 93 | $this->expectException(\InvalidArgumentException::class); 94 | $valueObject = new Name('Lorem ipsum'); 95 | $this->assertSame('Lorem ipsum', $valueObject->value); 96 | $valueObject->value = 'immutable'; 97 | }); 98 | 99 | test('name has immutable constructor', function () { 100 | $this->expectException(\InvalidArgumentException::class); 101 | $valueObject = new Name('Lorem ipsum'); 102 | $valueObject->__construct(' Lorem ipsum '); 103 | }); 104 | 105 | test('can extend protected methods in name', function () { 106 | $name = new TestName('Name'); 107 | $this->assertSame('Name', $name->value()); 108 | }); 109 | 110 | class TestName extends Name 111 | { 112 | public function __construct(string|Stringable $value) 113 | { 114 | $this->value = $value; 115 | 116 | $this->sanitize(); 117 | } 118 | 119 | protected function sanitize(): void 120 | { 121 | parent::sanitize(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Unit/Complex/PhoneTest.php: -------------------------------------------------------------------------------- 1 | assertSame('+38 000 000 00 00', $phone->value()); 11 | 12 | $phone = new Phone('+48 00 000 00 00'); 13 | $this->assertSame('+48 00 000 00 00', $phone->value()); 14 | 15 | $phone = new Phone('+48000000000'); 16 | $this->assertSame('+48000000000', $phone->value()); 17 | }); 18 | 19 | test('phone is squished', function () { 20 | $phone = new Phone(' +38 000 000 00 00 '); 21 | $this->assertSame('+38 000 000 00 00', $phone->value()); 22 | }); 23 | 24 | test('phone allows short version', function () { 25 | $phone = new Phone('00000'); 26 | $this->assertSame('00000', $phone->value()); 27 | 28 | $phone = new Phone('000 000 000'); 29 | $this->assertSame('000 000 000', $phone->value()); 30 | 31 | $phone = new Phone('000000000'); 32 | $this->assertSame('000000000', $phone->value()); 33 | }); 34 | 35 | test('phone is sanitized', function () { 36 | $phone = new Phone('+48 00 000 00 00'); 37 | $this->assertSame('+48000000000', $phone->sanitized()); 38 | 39 | $phone = new Phone('00 000 00 00'); 40 | $this->assertSame('000000000', $phone->sanitized()); 41 | }); 42 | 43 | test('phone deals with line-break', function () { 44 | $phone = new Phone('+38 000 45 | 000 00 00'); 46 | $this->assertSame('+380000000000', $phone->sanitized()); 47 | }); 48 | 49 | test('phone accepts only one plus character', function () { 50 | $this->expectException(ValidationException::class); 51 | new Phone('++38 000 000 00'); 52 | }); 53 | 54 | test('phone rejects plus if it is not the first char', function () { 55 | $this->expectException(ValidationException::class); 56 | new Phone('38 000 +000 000'); 57 | }); 58 | 59 | test('phone rejects plus when it is the last char', function () { 60 | $this->expectException(ValidationException::class); 61 | new Phone('38 000 000 000+'); 62 | }); 63 | 64 | test('phone fails when wrong number passed', function () { 65 | $this->expectException(ValidationException::class); 66 | new Phone('123'); 67 | }); 68 | 69 | test('validation exception message is correct in phone', function () { 70 | try { 71 | new Phone('123'); 72 | } catch (ValidationException $e) { 73 | $this->assertSame('Your phone number is invalid.', $e->getMessage()); 74 | } 75 | }); 76 | 77 | test('phone cannot accept null', function () { 78 | $this->expectException(\TypeError::class); 79 | new Phone(null); 80 | }); 81 | 82 | test('phone fails when no argument passed', function () { 83 | $this->expectException(\TypeError::class); 84 | new Phone; 85 | }); 86 | 87 | test('phone fails when empty string passed', function () { 88 | $this->expectException(ValidationException::class); 89 | new Phone(''); 90 | }); 91 | 92 | test('phone is makeable', function () { 93 | $valueObject = Phone::make('+48 00 000 00 00'); 94 | $this->assertSame('+48 00 000 00 00', $valueObject->value()); 95 | 96 | $valueObject = Phone::from('+48 00 000 00 00'); 97 | $this->assertSame('+48 00 000 00 00', $valueObject->value()); 98 | }); 99 | 100 | test('phone is macroable', function () { 101 | Phone::macro('str', function () { 102 | return str($this->value()); 103 | }); 104 | 105 | $valueObject = new Phone('+48 00 000 00 00'); 106 | 107 | $this->assertTrue($valueObject->str()->is('+48 00 000 00 00')); 108 | }); 109 | 110 | test('phone is conditionable', function () { 111 | $valueObject = new Phone('+48 00 000 00 00'); 112 | $this->assertSame('+48 00 000 00 00', $valueObject->when(true)->value()); 113 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 114 | }); 115 | 116 | test('phone is arrayable', function () { 117 | $array = (new Phone('+48 00 000 00 00'))->toArray(); 118 | $this->assertSame(['+48 00 000 00 00'], $array); 119 | }); 120 | 121 | test('phone is stringable', function () { 122 | $valueObject = new Phone('+48 00 000 00 00'); 123 | $this->assertSame('+48 00 000 00 00', (string) $valueObject); 124 | 125 | $valueObject = new Phone('+48 00 000 00 00'); 126 | $this->assertSame('+48 00 000 00 00', $valueObject->toString()); 127 | }); 128 | 129 | test('phone accepts stringable', function () { 130 | $valueObject = new Phone(str('+48 00 000 00 00')); 131 | $this->assertSame('+48 00 000 00 00', $valueObject->value()); 132 | }); 133 | 134 | test('phone fails when empty stringable passed', function () { 135 | $this->expectException(ValidationException::class); 136 | new Phone(str('')); 137 | }); 138 | 139 | test('phone is immutable', function () { 140 | $this->expectException(\InvalidArgumentException::class); 141 | $valueObject = new Phone('+48 00 000 00 00'); 142 | $this->assertSame('+48 00 000 00 00', $valueObject->value); 143 | $valueObject->value = 'immutable'; 144 | }); 145 | 146 | test('phone has immutable properties', function () { 147 | $this->expectException(\InvalidArgumentException::class); 148 | $valueObject = new Phone('+48 00 000 00 00'); 149 | $this->assertSame('+48 00 000 00 00', $valueObject->value); 150 | $valueObject->value = 'immutable'; 151 | }); 152 | 153 | test('phone has immutable constructor', function () { 154 | $this->expectException(\InvalidArgumentException::class); 155 | $valueObject = new Phone('+48 00 000 00 00'); 156 | $valueObject->__construct('+38 000 000 00 00'); 157 | }); 158 | 159 | test('can extend protected methods in phone', function () { 160 | $phone = new TestPhone('+38 000 000 00 00'); 161 | $this->assertSame('+38 000 000 00 00', $phone->value()); 162 | }); 163 | 164 | class TestPhone extends Phone 165 | { 166 | public function __construct(string|Stringable $value) 167 | { 168 | $this->value = $value; 169 | 170 | $this->trim(); 171 | } 172 | 173 | protected function trim(): void 174 | { 175 | parent::trim(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /tests/Unit/Complex/TaxNumberTest.php: -------------------------------------------------------------------------------- 1 | expectException(\TypeError::class); 10 | 11 | new TaxNumber(null); 12 | }); 13 | 14 | test('data in tax number is number plus prefix country is null', function () { 15 | $data = new TaxNumber('PL1234567890'); 16 | $this->assertEquals('PL', $data->prefix()); 17 | $this->assertEquals('PL', $data->country()); 18 | $this->assertEquals('1234567890', $data->taxNumber()); 19 | }); 20 | 21 | test('data in tax number is number plus prefix country is null and prefix lowercase char', function () { 22 | $data = new TaxNumber('pl1234567890'); 23 | $this->assertEquals('PL', $data->prefix()); 24 | $this->assertEquals('PL', $data->country()); 25 | $this->assertEquals('1234567890', $data->taxNumber()); 26 | }); 27 | 28 | test('data in tax number is number without prefix country is null', function () { 29 | $data = new TaxNumber('1234567890'); 30 | $this->assertEquals('', $data->country()); 31 | $this->assertEquals('1234567890', $data->taxNumber()); 32 | }); 33 | 34 | test('data in tax number is number plus prefix country is ok', function () { 35 | $data = new TaxNumber('PL1234567890', 'PL'); 36 | $this->assertEquals('PL', $data->prefix()); 37 | $this->assertEquals('PL', $data->country()); 38 | $this->assertEquals('1234567890', $data->taxNumber()); 39 | }); 40 | 41 | test('data in tax number is number country is added', function () { 42 | $data = new TaxNumber('1234567890', 'pL'); 43 | $this->assertEquals('PL', $data->prefix()); 44 | $this->assertEquals('PL', $data->country()); 45 | $this->assertEquals('1234567890', $data->taxNumber()); 46 | }); 47 | 48 | test('data in tax number is number country is added another', function () { 49 | $data = new TaxNumber('pL1234567890', 'aa'); 50 | $this->assertEquals('AA', $data->country()); 51 | $this->assertEquals('PL1234567890', $data->taxNumber()); 52 | }); 53 | 54 | test('data in tax number is number country is added out full number vat', function () { 55 | $data = new TaxNumber('1234567890', 'pL'); 56 | $this->assertEquals('PL1234567890', $data->fullTaxNumber()); 57 | }); 58 | 59 | test('data in tax number is number country is added and special characters out full number vat', function () { 60 | $data = new TaxNumber(' pl 123-456.78 90 ', 'pL'); 61 | $this->assertEquals('PL1234567890', $data->fullTaxNumber()); 62 | }); 63 | 64 | test('data in tax number is number country is added another static', function () { 65 | $data = new TaxNumber('pL006888nHy', 'aZ'); 66 | $this->assertEquals('AZ', $data->country()); 67 | $this->assertEquals('PL006888NHY', $data->taxNumber()); 68 | }); 69 | 70 | test('data in ta number is number country is added another method to string', function () { 71 | $this->assertEquals('AZPL123ABC456', new TaxNumber('pL123aBc456', 'aZ')); 72 | }); 73 | 74 | test('short data in tax number is number and country', function () { 75 | $this->assertEquals('A6', new TaxNumber('6', 'a')); 76 | }); 77 | 78 | test('tests that are used in the examples in ReadMe', function () { 79 | $this->assertEquals('PL0123456789', new TaxNumber('pl0123456789')); 80 | $this->assertEquals('PL0123456789', new TaxNumber('PL0123456789', 'pL')); 81 | $this->assertEquals('PL0123456789', new TaxNumber('0123456789', 'pL')); 82 | $this->assertEquals('PLAB0123456789', new TaxNumber('Ab0123456789', 'pL')); 83 | $this->assertEquals('PL0123456789', new TaxNumber('PL 012-345 67.89')); 84 | 85 | $multi = new TaxNumber('Ab 012-345 67.89', 'uK'); 86 | $this->assertEquals('UKAB0123456789', $multi); 87 | $this->assertEquals('UKAB0123456789', $multi->fullTaxNumber()); 88 | $this->assertEquals('UK', $multi->country()); 89 | $this->assertEquals('AB0123456789', $multi->taxNumber()); 90 | }); 91 | 92 | test('passed null values to value object', function () { 93 | $data = new TaxNumber('AB0123456789', null); 94 | $this->assertEquals('AB', $data->country()); 95 | $this->assertEquals('0123456789', $data->taxNumber()); 96 | 97 | $data = (new TaxNumber('AB0123456789', null)) 98 | ->fullTaxNumber(); 99 | $this->assertEquals('AB0123456789', $data); 100 | 101 | $data = new TaxNumber('AB0123456789', null); 102 | $this->assertEquals('AB', $data->country()); 103 | $this->assertEquals('0123456789', $data->taxNumber()); 104 | 105 | $data = (new TaxNumber('AB0123456789', null))->fullTaxNumber(); 106 | $this->assertEquals('AB0123456789', $data); 107 | }); 108 | 109 | test('passed empty values to value object', function () { 110 | $data = new TaxNumber('AB0123456789', ''); 111 | $this->assertEquals('AB', $data->country()); 112 | $this->assertEquals('0123456789', $data->taxNumber()); 113 | 114 | $data = (new TaxNumber('AB0123456789', '')) 115 | ->fullTaxNumber(); 116 | $this->assertEquals('AB0123456789', $data); 117 | 118 | $data = new TaxNumber('AB0123456789', ''); 119 | $this->assertEquals('AB', $data->country()); 120 | $this->assertEquals('0123456789', $data->taxNumber()); 121 | 122 | $data = (new TaxNumber('AB0123456789', ''))->fullTaxNumber(); 123 | $this->assertEquals('AB0123456789', $data); 124 | 125 | $this->expectException(ValidationException::class); 126 | new TaxNumber('', ''); 127 | }); 128 | 129 | test('tax number is makeable', function () { 130 | $valueObject = TaxNumber::make('PL0123456789'); 131 | $this->assertSame('PL0123456789', $valueObject->value()); 132 | 133 | $valueObject = TaxNumber::from('PL0123456789'); 134 | $this->assertSame('PL0123456789', $valueObject->value()); 135 | }); 136 | 137 | test('tax number is macroable', function () { 138 | TaxNumber::macro('getLength', function () { 139 | return str($this->fullTaxNumber())->length(); 140 | }); 141 | $valueObject = new TaxNumber('PL0123456789'); 142 | $this->assertSame(12, $valueObject->getLength()); 143 | }); 144 | 145 | test('tax number is conditionable', function () { 146 | $valueObject = new TaxNumber('PL0123456789'); 147 | $this->assertSame('PL', $valueObject->when(function ($vat) { 148 | return $vat->prefix() !== null; 149 | })->prefix()); 150 | $this->assertSame($valueObject, $valueObject->when(function ($vat) { 151 | return $vat->prefix() === null; 152 | })->prefix()); 153 | }); 154 | 155 | test('tax number is arrayable', function () { 156 | $valueObject = new TaxNumber('PL0123456789'); 157 | 158 | $this->assertSame([ 159 | 'fullTaxNumber' => 'PL0123456789', 160 | 'taxNumber' => '0123456789', 161 | 'prefix' => 'PL', 162 | ], $valueObject->toArray()); 163 | }); 164 | 165 | test('tax number is stringable', function () { 166 | $valueObject = new TaxNumber('PL0123456789'); 167 | $this->assertSame($valueObject->value(), (string) $valueObject); 168 | 169 | $valueObject = new TaxNumber('PL0123456789'); 170 | $this->assertSame($valueObject->value(), $valueObject->toString()); 171 | }); 172 | 173 | test('tax number has immutable properties', function () { 174 | $this->expectException(\InvalidArgumentException::class); 175 | $valueObject = new TaxNumber('PL0123456789'); 176 | $this->assertSame('0123456789', $valueObject->number); 177 | $valueObject->tax_number = 'immutable'; 178 | }); 179 | 180 | test('tax number has immutable constructor', function () { 181 | $this->expectException(\InvalidArgumentException::class); 182 | $valueObject = new TaxNumber('PL0123456789'); 183 | $valueObject->__construct(' PL0123456789 '); 184 | }); 185 | 186 | test('can extend protected methods in phone', function () { 187 | $phone = new TestPhone('+38 000 000 00 00'); 188 | $this->assertSame('+38 000 000 00 00', $phone->value()); 189 | }); 190 | 191 | class TestTaxNumber extends TaxNumber 192 | { 193 | public function __construct(string $number, ?string $prefix = null) 194 | { 195 | $this->number = $number; 196 | $this->prefix = $prefix; 197 | 198 | $this->validate(); 199 | $this->sanitize(); 200 | 201 | if ($this->canSplit()) { 202 | $this->split(); 203 | } 204 | } 205 | 206 | protected function validate(): void 207 | { 208 | parent::validate(); 209 | } 210 | 211 | protected function sanitize(): void 212 | { 213 | parent::sanitize(); 214 | } 215 | 216 | protected function canSplit(): bool 217 | { 218 | return parent::canSplit(); 219 | } 220 | 221 | protected function split(): void 222 | { 223 | parent::split(); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /tests/Unit/Complex/UrlTest.php: -------------------------------------------------------------------------------- 1 | assertSame('http://localhost/test-url', $url->value()); 13 | }); 14 | 15 | test('can url accepts query string', function () { 16 | $url = new Url('test-url?query=test&string=test2'); 17 | $this->assertSame('http://localhost/test-url?query=test&string=test2', $url->value()); 18 | }); 19 | 20 | test('can url accepts full url', function () { 21 | $url = new Url('https://example.com/test-url?query=test&string=test2'); 22 | $this->assertSame('https://example.com/test-url?query=test&string=test2', $url->value()); 23 | }); 24 | 25 | test('cannot instantiate invalid url', function () { 26 | $this->expectException(ValidationException::class); 27 | 28 | new Url(' Test Url '); 29 | }); 30 | 31 | test('cannot instantiate invalid url with try/catch', function () { 32 | try { 33 | new Url(' Test Url '); 34 | } catch (ValidationException $exception) { 35 | $this->assertSame('Your URL is invalid.', $exception->getMessage()); 36 | } 37 | }); 38 | 39 | test('can cast url to string', function () { 40 | $url = new Url('test-url'); 41 | $this->assertSame('http://localhost/test-url', (string) $url); 42 | }); 43 | 44 | test('url cannot accept null', function () { 45 | $this->expectException(\TypeError::class); 46 | 47 | new Url(null); 48 | }); 49 | 50 | test('url fails when no argument passed', function () { 51 | $this->expectException(\TypeError::class); 52 | 53 | new Url(); 54 | }); 55 | 56 | test('url fails when empty string passed', function () { 57 | $this->expectException(\InvalidArgumentException::class); 58 | 59 | new Url(''); 60 | }); 61 | 62 | test('url is makeable', function () { 63 | $valueObject = Url::make('1'); 64 | $this->assertSame('http://localhost/1', $valueObject->value()); 65 | }); 66 | 67 | test('url is macroable', function () { 68 | Url::macro('str', function () { 69 | return str($this->value()); 70 | }); 71 | 72 | $valueObject = new Url('test-url'); 73 | 74 | $this->assertTrue($valueObject->str()->is('http://localhost/test-url')); 75 | }); 76 | 77 | test('url is conditionable', function () { 78 | $valueObject = new Url('1'); 79 | $this->assertSame('http://localhost/1', $valueObject->when(true)->value()); 80 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 81 | }); 82 | 83 | test('url is arrayable', function () { 84 | $array = (new Url('test-url'))->toArray(); 85 | $this->assertSame(['http://localhost/test-url'], $array); 86 | }); 87 | 88 | test('url is stringable', function () { 89 | $valueObject = new Url('test-url'); 90 | $this->assertSame('http://localhost/test-url', (string) $valueObject); 91 | 92 | $valueObject = new Url('test-url'); 93 | $this->assertSame('http://localhost/test-url', $valueObject->toString()); 94 | }); 95 | 96 | test('url has immutable properties', function () { 97 | $this->expectException(\InvalidArgumentException::class); 98 | $valueObject = new Url('lorem-ipsum'); 99 | $this->assertSame('http://localhost/lorem-ipsum', $valueObject->value); 100 | $valueObject->value = 'immutable'; 101 | }); 102 | 103 | test('url has immutable constructor', function () { 104 | $this->expectException(\InvalidArgumentException::class); 105 | $valueObject = new Url('test-url'); 106 | $valueObject->__construct(' Lorem ipsum '); 107 | }); 108 | 109 | test('can extend protected methods in url', function () { 110 | $email = new TestUrl('test-url'); 111 | $this->assertSame(['required', 'url'], $email->validationRules()); 112 | }); 113 | 114 | class TestUrl extends Url 115 | { 116 | /** 117 | * Create a new instance of the value object. 118 | * 119 | * @param string|Stringable $value 120 | */ 121 | public function __construct(string|Stringable $value) 122 | { 123 | parent::__construct($value); 124 | 125 | $this->value = url($value); 126 | 127 | $validator = Validator::make( 128 | ['url' => $this->value()], 129 | ['url' => $this->validationRules()], 130 | ); 131 | 132 | if ($validator->fails()) { 133 | throw ValidationException::withMessages(['Your URL is invalid.']); 134 | } 135 | } 136 | 137 | public function validationRules(): array 138 | { 139 | return parent::validationRules(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Unit/Complex/UuidTest.php: -------------------------------------------------------------------------------- 1 | assertSame($uuid, $valueObject->uuid()); 13 | }); 14 | 15 | test('can set uuid name', function () { 16 | $uuid = (string) Str::uuid(); 17 | $result = new Uuid($uuid, 'verification'); 18 | $this->assertSame('verification', $result->name()); 19 | }); 20 | 21 | test('can set uuid value', function () { 22 | $uuid = (string) Str::uuid(); 23 | $result = new Uuid($uuid); 24 | $this->assertSame($uuid, $result->value()); 25 | }); 26 | 27 | test('can cast uuid to string', function () { 28 | $uuid = (string) Str::uuid(); 29 | $string = (string) new Uuid($uuid); 30 | $this->assertSame($uuid, $string); 31 | }); 32 | 33 | test('fails when wrong uuid passed to uuid object', function () { 34 | $this->expectException(ValidationException::class); 35 | 36 | new Uuid('123'); 37 | }); 38 | 39 | test('validation exception message is correct in uuid', function () { 40 | try { 41 | new Uuid('123123'); 42 | } catch (ValidationException $e) { 43 | $this->assertSame('UUID is invalid.', $e->getMessage()); 44 | } 45 | }); 46 | 47 | test('fails when null passed to uuid', function () { 48 | $this->expectException(\TypeError::class); 49 | 50 | new Uuid(null); 51 | }); 52 | 53 | test('full name is makeable', function () { 54 | $uuid = (string) Str::uuid(); 55 | 56 | $valueObject = Uuid::make($uuid); 57 | $this->assertSame($uuid, $valueObject->value()); 58 | 59 | $valueObject = Uuid::from($uuid); 60 | $this->assertSame($uuid, $valueObject->value()); 61 | }); 62 | 63 | test('uuid is macroable', function () { 64 | $uuid = (string) Str::uuid(); 65 | Uuid::macro('getLength', function () { 66 | return str($this->value())->length(); 67 | }); 68 | $valueObject = new Uuid($uuid); 69 | $this->assertSame(36, $valueObject->getLength()); 70 | }); 71 | 72 | test('uuid is conditionable', function () { 73 | $uuid = (string) Str::uuid(); 74 | $valueObject = new Uuid($uuid); 75 | $this->assertSame($uuid, $valueObject->when(true)->value()); 76 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 77 | }); 78 | 79 | test('uuid is arrayable', function () { 80 | $uuid = (string) Str::uuid(); 81 | $array = (new Uuid($uuid, 'name'))->toArray(); 82 | $this->assertSame([ 83 | 'name' => 'name', 84 | 'value' => $uuid, 85 | ], $array); 86 | }); 87 | 88 | test('uuid is stringable', function () { 89 | $uuid = (string) Str::uuid(); 90 | 91 | $valueObject = new Uuid($uuid); 92 | $this->assertSame($valueObject->value(), (string) $valueObject); 93 | 94 | $valueObject = new Uuid($uuid); 95 | $this->assertSame($valueObject->value(), $valueObject->toString()); 96 | }); 97 | 98 | test('uuid has immutable properties', function () { 99 | $this->expectException(\InvalidArgumentException::class); 100 | $uuid = (string) Str::uuid(); 101 | $valueObject = new Uuid($uuid); 102 | $this->assertSame($uuid, $valueObject->value); 103 | $valueObject->tax_number = 'immutable'; 104 | }); 105 | 106 | test('uuid has immutable constructor', function () { 107 | $this->expectException(\InvalidArgumentException::class); 108 | $uuid = (string) Str::uuid(); 109 | $valueObject = new Uuid($uuid); 110 | $valueObject->__construct($uuid, 'test'); 111 | }); 112 | 113 | test('can extend protected methods in uuid', function () { 114 | $uuid = (string) Str::uuid(); 115 | $text = new TestUuid($uuid); 116 | $text->validate(); 117 | $this->assertSame($uuid, $text->value()); 118 | }); 119 | 120 | class TestUuid extends Uuid 121 | { 122 | public function validate(): void 123 | { 124 | parent::validate(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Unit/Primitive/BooleanTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($valueObject->value()); 10 | $valueObject = new Boolean(1); 11 | $this->assertTrue($valueObject->value()); 12 | }); 13 | 14 | test('throws exception on invalid integer', function () { 15 | $this->expectException(InvalidArgumentException::class); 16 | 17 | new Boolean(2); 18 | }); 19 | 20 | test('boolean can accept integer as a string', function () { 21 | $valueObject = new Boolean('0'); 22 | $this->assertFalse($valueObject->value()); 23 | $valueObject = new Boolean('1'); 24 | $this->assertTrue($valueObject->value()); 25 | }); 26 | 27 | test('boolean can accept boolean strings', function () { 28 | $valueObject = new Boolean('false'); 29 | $this->assertFalse($valueObject->value()); 30 | $valueObject = new Boolean('False'); 31 | $this->assertFalse($valueObject->value()); 32 | $valueObject = new Boolean('off'); 33 | $this->assertFalse($valueObject->value()); 34 | $valueObject = new Boolean('no'); 35 | $this->assertFalse($valueObject->value()); 36 | $valueObject = new Boolean('FALSE'); 37 | $this->assertFalse($valueObject->value()); 38 | $valueObject = new Boolean('true'); 39 | $this->assertTrue($valueObject->value()); 40 | $valueObject = new Boolean('True'); 41 | $this->assertTrue($valueObject->value()); 42 | $valueObject = new Boolean('TRUE'); 43 | $this->assertTrue($valueObject->value()); 44 | $valueObject = new Boolean('on'); 45 | $this->assertTrue($valueObject->value()); 46 | $valueObject = new Boolean('yes'); 47 | $this->assertTrue($valueObject->value()); 48 | }); 49 | 50 | test('boolean can accept native booleans', function () { 51 | $valueObject = new Boolean(false); 52 | $this->assertFalse($valueObject->value()); 53 | $valueObject = new Boolean(true); 54 | $this->assertTrue($valueObject->value()); 55 | }); 56 | 57 | test('boolean fails when no argument passed', function () { 58 | $this->expectException(\TypeError::class); 59 | 60 | new Boolean; 61 | }); 62 | 63 | test('boolean fails when null passed', function () { 64 | $this->expectException(\TypeError::class); 65 | 66 | (new Boolean(null))->value(); 67 | }); 68 | 69 | test('boolean fails when empty string passed', function () { 70 | $this->expectException(\InvalidArgumentException::class); 71 | 72 | (new Boolean(''))->value(); 73 | }); 74 | 75 | test('boolean fails when any string passed', function () { 76 | $this->expectException(\InvalidArgumentException::class); 77 | 78 | (new Boolean('asd'))->value(); 79 | }); 80 | 81 | test('boolean is makeable', function () { 82 | $valueObject = Boolean::make(1); 83 | $this->assertTrue($valueObject->value()); 84 | $valueObject = Boolean::make(0); 85 | $this->assertFalse($valueObject->value()); 86 | $valueObject = Boolean::make('1'); 87 | $this->assertTrue($valueObject->value()); 88 | $valueObject = Boolean::make('0'); 89 | $this->assertFalse($valueObject->value()); 90 | $valueObject = Boolean::make('true'); 91 | $this->assertTrue($valueObject->value()); 92 | $valueObject = Boolean::make('false'); 93 | $this->assertFalse($valueObject->value()); 94 | 95 | $valueObject = Boolean::from(1); 96 | $this->assertTrue($valueObject->value()); 97 | $valueObject = Boolean::from(0); 98 | $this->assertFalse($valueObject->value()); 99 | $valueObject = Boolean::from('1'); 100 | $this->assertTrue($valueObject->value()); 101 | $valueObject = Boolean::from('0'); 102 | $this->assertFalse($valueObject->value()); 103 | $valueObject = Boolean::from('true'); 104 | $this->assertTrue($valueObject->value()); 105 | $valueObject = Boolean::from('false'); 106 | $this->assertFalse($valueObject->value()); 107 | }); 108 | 109 | test('boolean is macroable', function () { 110 | Boolean::macro('getPositiveValues', fn () => $this->trueValues); 111 | Boolean::macro('getNegativeValues', fn () => $this->falseValues); 112 | $valueObject = new Boolean(1); 113 | $this->assertSame([ 114 | '1', 'true', 'on', 'yes', 115 | ], $valueObject->getPositiveValues()); 116 | $this->assertSame([ 117 | '0', 'false', 'off', 'no', 118 | ], $valueObject->getNegativeValues()); 119 | }); 120 | 121 | test('boolean is conditionable', function () { 122 | $valueObject = new Boolean('1'); 123 | $this->assertTrue($valueObject->when(true)->value()); 124 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 125 | }); 126 | 127 | test('boolean is arrayable', function () { 128 | $array = (new Boolean(1))->toArray(); 129 | $this->assertSame([true], $array); 130 | $array = (new Boolean(0))->toArray(); 131 | $this->assertSame([false], $array); 132 | $array = (new Boolean('1'))->toArray(); 133 | $this->assertSame([true], $array); 134 | $array = (new Boolean('0'))->toArray(); 135 | $this->assertSame([false], $array); 136 | $array = (new Boolean('true'))->toArray(); 137 | $this->assertSame([true], $array); 138 | $array = (new Boolean('false'))->toArray(); 139 | $this->assertSame([false], $array); 140 | }); 141 | 142 | test('boolean is stringable', function () { 143 | $valueObject = new Boolean(1); 144 | $this->assertSame('true', $valueObject->toString()); 145 | $valueObject = new Boolean(0); 146 | $this->assertSame('false', (string) $valueObject); 147 | $valueObject = new Boolean('1'); 148 | $this->assertSame('true', (string) $valueObject); 149 | $valueObject = new Boolean('0'); 150 | $this->assertSame('false', (string) $valueObject); 151 | $valueObject = new Boolean('true'); 152 | $this->assertSame('true', (string) $valueObject); 153 | $valueObject = new Boolean('false'); 154 | $this->assertSame('false', $valueObject->toString()); 155 | }); 156 | 157 | test('boolean has immutable properties', function () { 158 | $this->expectException(\InvalidArgumentException::class); 159 | $valueObject = new Boolean('1'); 160 | $this->assertTrue($valueObject->value); 161 | $valueObject->value = '0'; 162 | }); 163 | 164 | test('boolean has immutable constructor', function () { 165 | $this->expectException(\InvalidArgumentException::class); 166 | $valueObject = new Boolean('1'); 167 | $valueObject->__construct('false'); 168 | }); 169 | 170 | test('can extend protected methods in boolean', function () { 171 | $bool = new TestBoolean('true'); 172 | $this->assertIsBool($bool->value()); 173 | }); 174 | 175 | class TestBoolean extends Boolean 176 | { 177 | public function __construct(bool|int|string $value) 178 | { 179 | ! is_bool($value) ? $this->handleNonBoolean($value) : $this->value = $value; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/Unit/Primitive/NumberTest.php: -------------------------------------------------------------------------------- 1 | assertSame('1.00', $valueObject->value()); 11 | $valueObject = new Number(2); 12 | $this->assertSame('2.00', $valueObject->value()); 13 | }); 14 | 15 | test('number can cast to integer', function () { 16 | $valueObject = new Number('100'); 17 | $this->assertSame(100, $valueObject->asInteger()); 18 | }); 19 | 20 | test('number can cast to float', function () { 21 | $valueObject = new Number('36000.50'); 22 | $this->assertSame(36000.50, $valueObject->asFloat()); 23 | }); 24 | 25 | test('number as a big number', function () { 26 | $number = new Number('20000.793', 3); 27 | $this->assertEquals(new BigNumber('20000.793', 3, false), $number->asBigNumber()); 28 | }); 29 | 30 | test('number can be divided using magic call', function () { 31 | $number = new Number('20000.793', 4); 32 | $this->assertSame('10000.3965', $number->divide(2)); 33 | }); 34 | 35 | test('number can be multiplied using magic call', function () { 36 | $number = new Number('20000.793', 3); 37 | $this->assertSame('40001.586', $number->multiply(2)); 38 | }); 39 | 40 | test('number strips zeros when the value starts from zero', function ($input, $result) { 41 | $valueObject = new Number($input); 42 | $this->assertSame($result, $valueObject->value()); 43 | })->with([ 44 | ['0000123.987', '123.98'], 45 | ['0000123', '123.00'], 46 | ]); 47 | 48 | test('number can accept string', function ($input, $result) { 49 | $valueObject = new Number($input); 50 | $this->assertSame($result, $valueObject->value()); 51 | })->with([ 52 | ['1', '1.00'], 53 | ['1.2', '1.20'], 54 | ['1.3', '1.30'], 55 | ['1.7', '1.70'], 56 | ['1.8', '1.80'], 57 | ['2', '2.00'], 58 | ['3.1', '3.10'], 59 | [' 100,000 ', '100.00'], 60 | [' 100 000 ,000 ', '100000.00'], 61 | ]); 62 | 63 | test('number accepts formatted value', function ($input, $scale, $result) { 64 | $valueObject = new Number($input, $scale); 65 | $this->assertSame($result, $valueObject->value()); 66 | })->with([ 67 | // Only commas: 68 | ['1,230,00', 2, '1230.00'], 69 | ['123,123,123,5555', 3, '123123123.555'], 70 | 71 | // Only dots: 72 | ['1.230.00', 2, '1230.00'], 73 | ['123.123.123.555', 2, '123123123.55'], 74 | 75 | // Dot-comma convention: 76 | ['1.230,00', 2, '1230.00'], 77 | ['123.123.123,556', 3, '123123123.556'], 78 | 79 | // Comma-dot convention: 80 | ['1,230.00', 2, '1230.00'], 81 | ['123,123,123.555', 2, '123123123.55'], 82 | 83 | // Space-dot convention: 84 | ['1 230.00', 2, '1230.00'], 85 | ['123 123 123.55', 2, '123123123.55'], 86 | 87 | // Space-comma convention: 88 | ['1 230,00', 2, '1230.00'], 89 | ['123 123 123,55', 2, '123123123.55'], 90 | 91 | // Mixed convention: 92 | ['1 230,', 2, '1230.00'], 93 | [',00', 2, '0.00'], 94 | ['.00', 2, '0.00'], 95 | ['123.123 123,55', 2, '123123123.55'], 96 | ['123,123.123,55', 2, '123123123.55'], 97 | ['123 123 123,55', 2, '123123123.55'], 98 | [' 100 000,00 ', 3, '100000.000'], 99 | [' 100 000,000 ', 2, '100000.00'], 100 | ]); 101 | 102 | test('number fails when no argument passed', function () { 103 | $this->expectException(\TypeError::class); 104 | 105 | new Number; 106 | }); 107 | 108 | test('number fails when text provided', function () { 109 | $this->expectException(\InvalidArgumentException::class); 110 | 111 | new Number('asd'); 112 | }); 113 | 114 | test('number fails when empty string passed', function () { 115 | $this->expectException(\InvalidArgumentException::class); 116 | 117 | new Number(''); 118 | }); 119 | 120 | test('number fails when null passed', function () { 121 | $this->expectException(\TypeError::class); 122 | 123 | new Number(null); 124 | }); 125 | 126 | test('number can change decimals as a string input', function ($input, $scale, $result) { 127 | $valueObject = new Number($input, $scale); 128 | $this->assertSame($result, $valueObject->value()); 129 | })->with([ 130 | ['111777999.97', 2, '111777999.97'], 131 | ['111777999,97', 2, '111777999.97'], 132 | ['111777999.99999999997', 11, '111777999.99999999997'], 133 | ['92233720368.547', 3, '92233720368.547'], 134 | 135 | ['7.1', 0, '7'], 136 | ['7.1', 1, '7.1'], 137 | ['7.11', 2, '7.11'], 138 | ['7.99', 3, '7.990'], 139 | ['70.1', 4, '70.1000'], 140 | ['71.1', 5, '71.10000'], 141 | ['17.9', 6, '17.900000'], 142 | ['11.1', 7, '11.1000000'], 143 | ['11.7', 8, '11.70000000'], 144 | ['77.77', 9, '77.770000000'], 145 | ['777.7', 10, '777.7000000000'], 146 | ['777.7', 11, '777.70000000000'], 147 | ['777.77', 12, '777.770000000000'], 148 | ['777.777', 13, '777.7770000000000'], 149 | ['7771.777', 14, '7771.77700000000000'], 150 | ['7771.7771', 15, '7771.777100000000000'], 151 | ['7771.77711', 16, '7771.7771100000000000'], 152 | ['7771.777111', 17, '7771.77711100000000000'], 153 | ['7771.7771119', 18, '7771.777111900000000000'], 154 | ['7771.77711199', 19, '7771.7771119900000000000'], 155 | ['777177711191777.99977777777777777777', 20, '777177711191777.99977777777777777777'], 156 | ]); 157 | 158 | test('number can change decimals as a float input up to 14 characters/digits', function ($input, $scale, $result) { 159 | $valueObject = new Number($input, $scale); 160 | $this->assertSame($result, $valueObject->value()); 161 | })->with([ 162 | [111777999.97, 2, '111777999.97'], 163 | 164 | [7.1, 0, '7'], 165 | [7.1, 1, '7.1'], 166 | [7.11, 2, '7.11'], 167 | [7.99, 3, '7.990'], 168 | [70.1, 4, '70.1000'], 169 | [71.1, 5, '71.10000'], 170 | [17.9, 6, '17.900000'], 171 | [11.1, 7, '11.1000000'], 172 | [11.7, 8, '11.70000000'], 173 | [77.77, 9, '77.770000000'], 174 | [777.7, 10, '777.7000000000'], 175 | [777.7, 11, '777.70000000000'], 176 | [777.77, 12, '777.770000000000'], 177 | [777.777, 13, '777.7770000000000'], 178 | [7771.777, 14, '7771.77700000000000'], 179 | [7771.7771, 15, '7771.777100000000000'], 180 | [7771.77711, 16, '7771.7771100000000000'], 181 | [7771.777111, 17, '7771.77711100000000000'], 182 | [7771.7771119, 18, '7771.777111900000000000'], 183 | [7771.77711199, 19, '7771.7771119900000000000'], 184 | [3210987654321.0, 2, '3210987654321.00'], 185 | [290987654321.78, 2, '290987654321.78'], 186 | [92233720368.987, 2, '92233720368.98'], 187 | [9223372036.8547, 2, '9223372036.85'], 188 | [1.999999999999, 12, '1.999999999999'], 189 | [2.9999999999999, 12, '2.999999999999'], 190 | [290987654321.78, 3, '290987654321.780'], 191 | [92233720368.987, 3, '92233720368.987'], 192 | [9223372036.8547, 3, '9223372036.854'], 193 | [7771.0777110012, 3, '7771.077'], 194 | [9876.100077799, 3, '9876.100'], 195 | [1.543210987671, 3, '1.543'], 196 | [00002.5432109876712, 3, '2.543'], 197 | [3.5432109876789, 3, '3.543'], 198 | [11.543210987671, 3, '11.543'], 199 | [10987654321.789, 3, '10987654321.789'], 200 | [3210987654321.7, 3, '3210987654321.700'], 201 | [44210987654321.0, 3, '44210987654321.000'], 202 | [92233720368.547, 3, '92233720368.547'], 203 | ]); 204 | 205 | test('numeric integer in float form input up to 14 characters/digitss', function ($input, $scale, $result) { 206 | $valueObject = new Number($input, $scale); 207 | $this->assertSame($result, $valueObject->value()); 208 | })->with([ 209 | [1.0, 3, '1.000'], 210 | [2.0000, 3, '2.000'], 211 | [1234567890.0000, 3, '1234567890.000'], 212 | [12345678901234.0000, 3, '12345678901234.000'], 213 | ]); 214 | 215 | test('no conversion of decimal numbers as float input above 14 characters/digits', function ($number) { 216 | try { 217 | new Number($number, 20); 218 | $this->assertFalse(true); 219 | } catch (LengthException $e) { 220 | $this->assertSame(0, $e->getCode()); 221 | $this->assertSame('Float precision loss detected.', $e->getMessage()); 222 | } 223 | })->with([ 224 | 11.5432109876731, 225 | 6667777.1234567890123456789, 226 | 5556666.2345678901234567891, 227 | 4445555.3456789012345678912, 228 | 3334444.4567890123456789123, 229 | 2223333.5678901234567891234, 230 | 1112222.6789012345678912345, 231 | 9991111.7890123456789123456, 232 | 8880000.8901234567891234567, 233 | 8889999.9012345678912345678, 234 | 7778888.0123456789123456789, 235 | 9553543210987654321.77711199, 236 | 777177711191777.99977777777777777777, 237 | ]); 238 | 239 | test('number can handle huge numbers', function ($input, $scale, $result) { 240 | $valueObject = new Number($input, $scale); 241 | $this->assertSame($result, $valueObject->value()); 242 | })->with([ 243 | ['111777999.97', 2, '111777999.97'], 244 | ['111777999,97', 2, '111777999.97'], 245 | ['111777999.99999999997', 11, '111777999.99999999997'], 246 | ['92233720368.547', 3, '92233720368.547'], 247 | ['9876543210111777999.9087', 2, '9876543210111777999.90'], 248 | ['98765432101117779990000.9087', 1, '98765432101117779990000.9'], 249 | ]); 250 | 251 | test('number is makeable', function () { 252 | $valueObject = Number::make('1'); 253 | $this->assertSame('1.00', $valueObject->value()); 254 | $valueObject = Number::make('1.1'); 255 | $this->assertSame('1.10', $valueObject->value()); 256 | $valueObject = Number::make('1'); 257 | $this->assertSame('1.00', $valueObject->value()); 258 | 259 | $valueObject = Number::from('1'); 260 | $this->assertSame('1.00', $valueObject->value()); 261 | $valueObject = Number::from('1.1'); 262 | $this->assertSame('1.10', $valueObject->value()); 263 | $valueObject = Number::from('1'); 264 | $this->assertSame('1.00', $valueObject->value()); 265 | }); 266 | 267 | test('number is macroable', function () { 268 | Number::macro('getLength', function () { 269 | return str($this->value())->length(); 270 | }); 271 | $valueObject = new Number('12.3'); 272 | $this->assertSame(5, $valueObject->getLength()); 273 | }); 274 | 275 | test('number is conditionable', function () { 276 | $valueObject = new Number('1'); 277 | $this->assertSame('1.00', $valueObject->when(true)->value()); 278 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 279 | }); 280 | 281 | test('number is arrayable', function () { 282 | $array = (new Number('1'))->toArray(); 283 | $this->assertSame(['1.00'], $array); 284 | }); 285 | 286 | test('number is stringable', function () { 287 | $valueObject = new Number('1'); 288 | $this->assertSame('1.00', (string) $valueObject); 289 | $valueObject = new Number('1.2'); 290 | $this->assertSame('1.20', (string) $valueObject); 291 | $valueObject = new Number('1.3'); 292 | $this->assertSame('1.30', (string) $valueObject); 293 | $valueObject = new Number('1.7'); 294 | $this->assertSame('1.70', (string) $valueObject); 295 | $valueObject = new Number('1.8'); 296 | $this->assertSame('1.80', (string) $valueObject); 297 | $valueObject = new Number('1230.00'); 298 | $this->assertSame('1230.00', $valueObject->toString()); 299 | }); 300 | 301 | test('number has immutable properties', function () { 302 | $this->expectException(\InvalidArgumentException::class); 303 | $valueObject = new Number('1.2000'); 304 | $this->assertEquals(new BigNumber('1.20', 2, false), $valueObject->bigNumber); 305 | $valueObject->bigNumber = new BigNumber('1.20'); 306 | }); 307 | 308 | test('number has immutable constructor', function () { 309 | $this->expectException(\InvalidArgumentException::class); 310 | $valueObject = new Number('1.2000'); 311 | $valueObject->__construct('1.5000'); 312 | }); 313 | 314 | test('big number is immutable', function () { 315 | Number::macro('isImmutable', function () { 316 | return ! $this->bigNumber->isMutable(); 317 | }); 318 | 319 | $number = new Number('1.2000'); 320 | $this->assertTrue($number->isImmutable()); 321 | }); 322 | 323 | test('number uses sanitizes numbers trait', function () { 324 | $this->assertTrue( 325 | in_array('MichaelRubel\ValueObjects\Concerns\SanitizesNumbers', 326 | class_uses_recursive(Number::class) 327 | ) 328 | ); 329 | }); 330 | 331 | test('can extend protected methods in number', function () { 332 | $number = new TestNumber('1 230,00'); 333 | $this->assertSame('1230.00', $number->value()); 334 | $number = new TestNumber(1230.12); 335 | $this->assertSame('1230.12', $number->value()); 336 | }); 337 | 338 | test('number can accept negative integer', function () { 339 | $valueObject = new Number(-1); 340 | $this->assertSame('-1.00', $valueObject->value()); 341 | $valueObject = new Number(-2); 342 | $this->assertSame('-2.00', $valueObject->value()); 343 | }); 344 | 345 | test('number can cast negative value to integer', function () { 346 | $valueObject = new Number('-100'); 347 | $this->assertSame(-100, $valueObject->asInteger()); 348 | }); 349 | 350 | test('number can cast negative value to float', function () { 351 | $valueObject = new Number('-36000.50'); 352 | $this->assertSame(-36000.50, $valueObject->asFloat()); 353 | }); 354 | 355 | test('negative number as a big number', function () { 356 | $number = new Number('-20000.793', 3); 357 | $this->assertEquals(new BigNumber('-20000.793', 3, false), $number->asBigNumber()); 358 | }); 359 | 360 | test('negative number can be divided using magic call', function () { 361 | $number = new Number('-20000.793', 4); 362 | $this->assertSame('-10000.3965', $number->divide(2)); 363 | }); 364 | 365 | test('negative number can be multiplied using magic call', function () { 366 | $number = new Number('-20000.793', 3); 367 | $this->assertSame('-40001.586', $number->multiply(2)); 368 | }); 369 | 370 | test('negative number strips zeros when the value starts from zero', function ($input, $result) { 371 | $valueObject = new Number($input); 372 | $this->assertSame($result, $valueObject->value()); 373 | })->with([ 374 | ['-0000123.987', '-123.98'], 375 | ['-0000123', '-123.00'], 376 | ]); 377 | 378 | test('negative number accepts formatted value', function ($input, $scale, $result) { 379 | $valueObject = new Number($input, $scale); 380 | $this->assertSame($result, $valueObject->value()); 381 | })->with([ 382 | ['-1,230,00', 2, '-1230.00'], 383 | ['-123.123.123,556', 3, '-123123123.556'], 384 | ['-1 230,00', 2, '-1230.00'], 385 | ['-777.7', 3, '-777.700'], 386 | ]); 387 | 388 | test('negative number fails when invalid text provided', function () { 389 | $this->expectException(\InvalidArgumentException::class); 390 | 391 | new Number('-asd'); 392 | }); 393 | 394 | test('negative number fails when empty string passed', function () { 395 | $this->expectException(\InvalidArgumentException::class); 396 | 397 | new Number('-'); 398 | }); 399 | 400 | test('negative number can change decimals as a string input', function ($input, $scale, $result) { 401 | $valueObject = new Number($input, $scale); 402 | $this->assertSame($result, $valueObject->value()); 403 | })->with([ 404 | ['-111777999.97', 2, '-111777999.97'], 405 | ['-111777999,97', 2, '-111777999.97'], 406 | ['-7.99', 3, '-7.990'], 407 | ['-71.1', 5, '-71.10000'], 408 | ]); 409 | 410 | test('negative number can handle huge numbers', function ($input, $scale, $result) { 411 | $valueObject = new Number($input, $scale); 412 | $this->assertSame($result, $valueObject->value()); 413 | })->with([ 414 | ['-9876543210111777999.9087', 2, '-9876543210111777999.90'], 415 | ['-98765432101117779990000.9087', 1, '-98765432101117779990000.9'], 416 | ]); 417 | 418 | class TestNumber extends Number 419 | { 420 | public function __construct(int|string|float $number, protected int $scale = 2) 421 | { 422 | parent::isPrecise((float) $number); 423 | 424 | $this->bigNumber = new BigNumber($this->sanitize($number), $this->scale); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /tests/Unit/Primitive/TextTest.php: -------------------------------------------------------------------------------- 1 | assertSame('1', $valueObject->value()); 10 | $valueObject = new Text('1.2'); 11 | $this->assertSame('1.2', $valueObject->value()); 12 | $valueObject = new Text('1.3'); 13 | $this->assertSame('1.3', $valueObject->value()); 14 | $valueObject = new Text('1.7'); 15 | $this->assertSame('1.7', $valueObject->value()); 16 | $valueObject = new Text('1.8'); 17 | $this->assertSame('1.8', $valueObject->value()); 18 | $valueObject = new Text('2'); 19 | $this->assertSame('2', $valueObject->value()); 20 | $valueObject = new Text('3.1'); 21 | $this->assertSame('3.1', $valueObject->value()); 22 | }); 23 | 24 | test('text can pass stringable', function () { 25 | $stringable = str('Test'); 26 | $valueObject = new Text($stringable); 27 | $this->assertSame('Test', $valueObject->value()); 28 | }); 29 | 30 | test('text can accept long text', function () { 31 | $text = new Text('Lorem Ipsum is simply dummy text of the printing and typesetting industry.'); 32 | $this->assertSame('Lorem Ipsum is simply dummy text of the printing and typesetting industry.', $text->value()); 33 | $string = " 34 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 35 | Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. 36 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 37 | It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 38 | 39 | It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. 40 | The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. 41 | Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. 42 | Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like). 43 | "; 44 | $text = new Text($string); 45 | $this->assertSame($string, $text->value()); 46 | }); 47 | 48 | test('text cannot accept null', function () { 49 | $this->expectException(\TypeError::class); 50 | 51 | new Text(null); 52 | }); 53 | 54 | test('text fails when no argument passed', function () { 55 | $this->expectException(\TypeError::class); 56 | 57 | new Text; 58 | }); 59 | 60 | test('text fails when empty string passed', function () { 61 | $this->expectException(\InvalidArgumentException::class); 62 | 63 | new Text(''); 64 | }); 65 | 66 | test('text is makeable', function () { 67 | $valueObject = Text::make('1'); 68 | $this->assertSame('1', $valueObject->value()); 69 | 70 | $valueObject = Text::from('1'); 71 | $this->assertSame('1', $valueObject->value()); 72 | }); 73 | 74 | test('text is macroable', function () { 75 | Text::macro('str', function () { 76 | return str($this->value()); 77 | }); 78 | 79 | $valueObject = new Text('Lorem ipsum'); 80 | 81 | $this->assertTrue($valueObject->str()->is('Lorem ipsum')); 82 | }); 83 | 84 | test('text is conditionable', function () { 85 | $valueObject = new Text('1'); 86 | $this->assertSame('1', $valueObject->when(true)->value()); 87 | $this->assertSame($valueObject, $valueObject->when(false)->value()); 88 | }); 89 | 90 | test('text is arrayable', function () { 91 | $array = (new Text('Lorem Ipsum is simply dummy text.'))->toArray(); 92 | $this->assertSame(['Lorem Ipsum is simply dummy text.'], $array); 93 | }); 94 | 95 | test('text is stringable', function () { 96 | $valueObject = new Text('1'); 97 | $this->assertSame('1', (string) $valueObject); 98 | $valueObject = new Text('1.2'); 99 | $this->assertSame('1.2', (string) $valueObject); 100 | $valueObject = new Text('1.3'); 101 | $this->assertSame('1.3', (string) $valueObject); 102 | $valueObject = new Text('1.7'); 103 | $this->assertSame('1.7', (string) $valueObject); 104 | $valueObject = new Text('1.8'); 105 | $this->assertSame('1.8', (string) $valueObject); 106 | $valueObject = new Text('Lorem ipsum'); 107 | $this->assertSame('Lorem ipsum', $valueObject->toString()); 108 | }); 109 | 110 | test('text accepts stringable', function () { 111 | $valueObject = new Text(str('Lorem ipsum')); 112 | $this->assertSame('Lorem ipsum', $valueObject->value()); 113 | }); 114 | 115 | test('text fails when empty stringable passed', function () { 116 | $this->expectException(\InvalidArgumentException::class); 117 | 118 | new Text(str('')); 119 | }); 120 | 121 | test('text has immutable properties', function () { 122 | $this->expectException(\InvalidArgumentException::class); 123 | $valueObject = new Text('Lorem ipsum'); 124 | $this->assertSame('Lorem ipsum', $valueObject->value); 125 | $valueObject->value = 'test'; 126 | }); 127 | 128 | test('text has immutable constructor', function () { 129 | $this->expectException(\InvalidArgumentException::class); 130 | $valueObject = new Text('Lorem ipsum'); 131 | $valueObject->__construct(' Lorem ipsum '); 132 | }); 133 | 134 | test('can extend protected methods in text', function () { 135 | $text = new TestText('Lorem ipsum'); 136 | $text->validate(); 137 | $this->assertSame('Lorem ipsum', $text->value()); 138 | }); 139 | 140 | class TestText extends Text 141 | { 142 | public function validate(): void 143 | { 144 | parent::validate(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/Unit/ValueObjectCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('make:value-object', [ 13 | 'name' => 'TestValueObject', 14 | ]); 15 | 16 | $this->assertFileExists($pathToGeneratedFile); 17 | 18 | $fileString = File::get($pathToGeneratedFile); 19 | 20 | $this->assertStringContainsString('declare(strict_types=1);', $fileString); 21 | $this->assertStringContainsString('use MichaelRubel\ValueObjects\ValueObject;', $fileString); 22 | $this->assertStringContainsString('@method static static make(mixed ...$values)', $fileString); 23 | $this->assertStringContainsString('class TestValueObject extends ValueObject', $fileString); 24 | $this->assertStringContainsString('public function value(): string', $fileString); 25 | $this->assertStringContainsString('public function toArray(): array', $fileString); 26 | $this->assertStringContainsString('public function __toString(): string', $fileString); 27 | $this->assertStringContainsString('Value of TestValueObject cannot be empty.', $fileString); 28 | }); 29 | 30 | test('value object command option', function () { 31 | $option = app(ValueObjectMakeCommand::class) 32 | ->getNativeDefinition() 33 | ->getOption('value-object'); 34 | 35 | $fakeOption = new InputOption('value-object', null, InputOption::VALUE_NONE, 'Create a value object'); 36 | 37 | $this->assertEquals($option, $fakeOption); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/Unit/ValueObjectEqualityTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($vo1->equals($vo2)); 12 | $this->assertTrue($vo2->equals($vo1)); 13 | 14 | $this->assertFalse($vo1->equals($vo3)); 15 | $this->assertFalse($vo2->equals($vo3)); 16 | $this->assertFalse($vo3->equals($vo1)); 17 | $this->assertFalse($vo3->equals($vo2)); 18 | }); 19 | 20 | test('value objects are not equal', function () { 21 | $vo1 = new ClassString('Exception'); 22 | $vo2 = new ClassString('Exception'); 23 | $vo3 = new ClassString('InvalidArgumentException'); 24 | 25 | $this->assertFalse($vo1->notEquals($vo2)); 26 | $this->assertFalse($vo2->notEquals($vo1)); 27 | 28 | $this->assertTrue($vo1->notEquals($vo3)); 29 | $this->assertTrue($vo2->notEquals($vo3)); 30 | $this->assertTrue($vo3->notEquals($vo1)); 31 | $this->assertTrue($vo3->notEquals($vo2)); 32 | }); 33 | 34 | test('decimal object equality works as expected', function () { 35 | $vo1 = new Number(123456789.1234, scale: 2); 36 | $vo2 = new Number('123456789.1234', scale: 3); 37 | $vo3 = new Number('123456789.1234', scale: 2); 38 | $vo4 = clone $vo1; 39 | 40 | $this->assertFalse($vo1->equals($vo2)); 41 | $this->assertTrue($vo1->notEquals($vo2)); 42 | $this->assertTrue($vo3->equals($vo1)); 43 | $this->assertTrue($vo4->equals($vo1)); 44 | $this->assertFalse($vo2->equals($vo3)); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/Unit/ValueObjectTest.php: -------------------------------------------------------------------------------- 1 | value()); 9 | }); 10 | $valueObject = new Text('Lorem ipsum'); 11 | $this->assertSame(['Lorem ipsum'], $valueObject->collect()->toArray()); 12 | }); 13 | 14 | test('can use makeOrNull', function () { 15 | $this->assertNull(Text::makeOrNull(null)); 16 | $this->assertNull(Text::makeOrNull('')); 17 | $this->assertNull(Text::makeOrNull('')?->value()); 18 | 19 | $this->assertEquals(Text::make('Lorem ipsum'), Text::makeOrNull('Lorem ipsum')); 20 | $this->assertSame('Lorem ipsum', Text::makeOrNull('Lorem ipsum')->value()); 21 | }); 22 | 23 | test('toString casts value to string', function () { 24 | $vo = new TestVO; 25 | $this->assertSame('100', $vo->toString()); 26 | }); 27 | 28 | class TestVO extends ValueObject 29 | { 30 | public function value(): int 31 | { 32 | return 100; 33 | } 34 | } 35 | --------------------------------------------------------------------------------