From 5138f3719fb0c3cc82b9b582fd74a627ef4ccc0d Mon Sep 17 00:00:00 2001 From: Gustavo Freze de Araujo Santos Date: Mon, 25 Jul 2022 23:33:38 -0300 Subject: [PATCH] feat: Adds value-object implementation. --- .github/FUNDING.yml | 1 + .github/workflows/ci.yml | 45 ++++++ .gitignore | 5 + LICENSE | 21 +++ Makefile | 22 +++ README.md | 84 +++++++++++- composer.json | 68 +++++++++ infection.json.dist | 23 ++++ phpmd.xml | 57 ++++++++ phpunit.xml | 25 ++++ src/Internal/Exceptions/InvalidProperty.php | 14 ++ .../Exceptions/PropertyCannotBeChanged.php | 14 ++ .../PropertyCannotBeDeactivated.php | 14 ++ src/ValueObject.php | 55 ++++++++ src/ValueObjectAdapter.php | 37 +++++ tests/ComplexValueTest.php | 129 ++++++++++++++++++ tests/Mock/AmountMock.php | 10 ++ tests/Mock/ComplexValueMock.php | 15 ++ tests/Mock/MultipleValueMock.php | 15 ++ tests/Mock/SingleValueMock.php | 15 ++ tests/Mock/TransactionMock.php | 10 ++ tests/MultipleValueTest.php | 108 +++++++++++++++ tests/SingleValueTest.php | 65 +++++++++ 23 files changed, 850 insertions(+), 2 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 composer.json create mode 100644 infection.json.dist create mode 100644 phpmd.xml create mode 100644 phpunit.xml create mode 100644 src/Internal/Exceptions/InvalidProperty.php create mode 100644 src/Internal/Exceptions/PropertyCannotBeChanged.php create mode 100644 src/Internal/Exceptions/PropertyCannotBeDeactivated.php create mode 100644 src/ValueObject.php create mode 100644 src/ValueObjectAdapter.php create mode 100644 tests/ComplexValueTest.php create mode 100644 tests/Mock/AmountMock.php create mode 100644 tests/Mock/ComplexValueMock.php create mode 100644 tests/Mock/MultipleValueMock.php create mode 100644 tests/Mock/SingleValueMock.php create mode 100644 tests/Mock/TransactionMock.php create mode 100644 tests/MultipleValueTest.php create mode 100644 tests/SingleValueTest.php diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3865a3c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: gustavofreze \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..32850ac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + auto-review: + name: Auto review + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install dependencies + run: composer update --no-progress --optimize-autoloader + + - name: Run phpcs + run: composer phpcs + + - name: Run phpmd + run: composer phpmd + + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install dependencies + run: composer update --no-progress --optimize-autoloader + + - name: Run unit tests + env: + XDEBUG_MODE: coverage + run: composer test + + - name: Run mutation tests + run: composer test-mutation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd34b23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +vendor +report +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31d63cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Tiny Blocks + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00764c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.1.7 + +.PHONY: configure test test-no-coverage review show-reports clean + +configure: + @${DOCKER_RUN} composer update --optimize-autoloader + +test: review + @${DOCKER_RUN} composer tests + +test-no-coverage: review + @${DOCKER_RUN} composer tests-no-coverage + +review: + @${DOCKER_RUN} composer review + +show-reports: + @sensible-browser report/coverage/coverage-html/index.html report/coverage/mutation-report.html + +clean: + @sudo chown -R ${USER}:${USER} ${PWD} + @rm -rf report vendor diff --git a/README.md b/README.md index 211dce0..8f678af 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# value-object -Delimits default behaviors for Value Objects. +# Value Object + +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) + +* [Overview](#overview) +* [Installation](#installation) +* [How to use](#how-to-use) +* [License](#license) +* [Contributing](#contributing) + +
+ +## Overview + +A **V**alue **O**bject (**VO**) is an immutable type that is only distinguishable by the state of its properties, that +is, unlike an entity, which has a unique identifier and remains distinct even if its properties are identical, VOs with +the same properties can be considered the same. + +Because they are immutable, VOs cannot be changed once created. Modifying one is conceptually the same as discard the +old one and create a new one. + +More details about [VOs](https://martinfowler.com/bliki/ValueObject.html). + +
+ +## Installation + +```bash +composer require tiny-blocks/value-object +``` + +
+ +## How to use + +The library exposes available behaviors through the `ValueObject` interface, and the implementation of these behaviors +through the `ValueObjectAdapter` trait. + +### Concrete implementation + +With the implementation of the `ValueObject` interface, and the `ValueObjectAdapter` trait, the use of +`__get`, `__set` and `__unset` methods is suppressed, making the object immutable. + +```php +equals(other: $otherTransactionId); # 1 (true) +``` + +## License + +Math is licensed under [MIT](/LICENSE). + +
+ +## Contributing + +Please follow the [contributing guidelines](https://github.com/tiny-blocks/tiny-blocks/blob/main/CONTRIBUTING.md) to +contribute to the project. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..19f707c --- /dev/null +++ b/composer.json @@ -0,0 +1,68 @@ +{ + "name": "tiny-blocks/value-object", + "type": "library", + "license": "MIT", + "homepage": "https://github.com/tiny-blocks/value-object", + "description": "Delimits default behaviors for Value Objects.", + "prefer-stable": true, + "minimum-stability": "stable", + "keywords": [ + "vo", + "psr", + "psr-4", + "psr-12", + "tiny-blocks", + "value-object" + ], + "authors": [ + { + "name": "Gustavo Freze de Araujo Santos", + "homepage": "https://github.com/gustavofreze" + } + ], + "config": { + "sort-packages": true, + "allow-plugins": { + "infection/extension-installer": true + } + }, + "autoload": { + "psr-4": { + "TinyBlocks\\Vo\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "TinyBlocks\\Vo\\": "tests/" + } + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "infection/infection": "^0.26", + "phpmd/phpmd": "^2.12", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" + }, + "scripts": { + "phpcs": "phpcs --standard=PSR12 --extensions=php ./src", + "phpmd": "phpmd ./src text phpmd.xml --suffixes php --ignore-violations-on-exit", + "test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests", + "test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4", + "test-no-coverage": "phpunit --no-coverage", + "test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4", + "review": [ + "@phpcs", + "@phpmd" + ], + "tests": [ + "@test", + "@test-mutation" + ], + "tests-no-coverage": [ + "@test-no-coverage", + "@test-mutation-no-coverage" + ] + } +} diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..e9bacf3 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,23 @@ +{ + "timeout": 10, + "testFramework": "phpunit", + "tmpDir": "report/", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "report/logs/infection-text.log", + "summary": "report/logs/infection-summary.log" + }, + "mutators": { + "@default": true, + "PublicVisibility": false, + "ProtectedVisibility": false + }, + "phpUnit": { + "configDir": "", + "customPath": "./vendor/bin/phpunit" + } +} \ No newline at end of file diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..cd9072e --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,57 @@ + + + PHPMD Custom rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3d05fc8 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,25 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Internal/Exceptions/InvalidProperty.php b/src/Internal/Exceptions/InvalidProperty.php new file mode 100644 index 0000000..cfe4d7c --- /dev/null +++ b/src/Internal/Exceptions/InvalidProperty.php @@ -0,0 +1,14 @@ + for class <%s>.'; + parent::__construct(sprintf($template, $key, $class)); + } +} diff --git a/src/Internal/Exceptions/PropertyCannotBeChanged.php b/src/Internal/Exceptions/PropertyCannotBeChanged.php new file mode 100644 index 0000000..860d1fd --- /dev/null +++ b/src/Internal/Exceptions/PropertyCannotBeChanged.php @@ -0,0 +1,14 @@ + cannot be changed in class <%s>.'; + parent::__construct(sprintf($template, $key, $class)); + } +} diff --git a/src/Internal/Exceptions/PropertyCannotBeDeactivated.php b/src/Internal/Exceptions/PropertyCannotBeDeactivated.php new file mode 100644 index 0000000..7d90457 --- /dev/null +++ b/src/Internal/Exceptions/PropertyCannotBeDeactivated.php @@ -0,0 +1,14 @@ + cannot be deactivated in class <%s>.'; + parent::__construct(sprintf($template, $key, $class)); + } +} diff --git a/src/ValueObject.php b/src/ValueObject.php new file mode 100644 index 0000000..d4f8141 --- /dev/null +++ b/src/ValueObject.php @@ -0,0 +1,55 @@ +values() == $other->values(); + } + + public function __get(mixed $key): void + { + if (!property_exists($this, $key)) { + throw new InvalidProperty(key: $key, class: get_called_class()); + } + } + + public function __set(mixed $key, mixed $value): void + { + throw new PropertyCannotBeChanged(key: $key, class: get_called_class()); + } + + public function __unset(mixed $key): void + { + throw new PropertyCannotBeDeactivated(key: $key, class: get_called_class()); + } +} diff --git a/tests/ComplexValueTest.php b/tests/ComplexValueTest.php new file mode 100644 index 0000000..5d1f1ce --- /dev/null +++ b/tests/ComplexValueTest.php @@ -0,0 +1,129 @@ +equals(other: $other); + + self::assertTrue($actual); + } + + public function testWhenEqualIsFalse(): void + { + $complex = new ComplexValueMock( + single: new SingleValueMock(id: 1000), + multiple: new MultipleValueMock( + id: 999, + transactions: [ + new TransactionMock(id: 1, amount: new AmountMock(value: 0.99, currency: 'USD')), + new TransactionMock(id: 2, amount: new AmountMock(value: 10.55, currency: 'USD')) + ] + ) + ); + $other = new ComplexValueMock( + single: new SingleValueMock(id: 1000), + multiple: new MultipleValueMock( + id: 999, + transactions: [ + new TransactionMock(id: 1, amount: new AmountMock(value: 0.99, currency: 'USD')), + new TransactionMock(id: 2, amount: new AmountMock(value: 10.56, currency: 'USD')) + ] + ) + ); + + $actual = $complex->equals(other: $other); + + self::assertFalse($actual); + } + + public function testInvalidProperty(): void + { + $this->expectException(InvalidProperty::class); + $this->expectErrorMessage('Invalid property for class .'); + + $complex = new ComplexValueMock( + single: new SingleValueMock(id: 1000), + multiple: new MultipleValueMock( + id: 999, + transactions: [ + new TransactionMock(id: 1, amount: new AmountMock(value: 0.99, currency: 'USD')), + new TransactionMock(id: 2, amount: new AmountMock(value: 10.55, currency: 'USD')) + ] + ) + ); + $complex->__get('other'); + } + + public function testPropertyCannotBeChanged(): void + { + $this->expectException(PropertyCannotBeChanged::class); + $this->expectErrorMessage('Property cannot be changed in class .'); + + $complex = new ComplexValueMock( + single: new SingleValueMock(id: 1000), + multiple: new MultipleValueMock( + id: 999, + transactions: [ + new TransactionMock(id: 1, amount: new AmountMock(value: 0.99, currency: 'USD')), + new TransactionMock(id: 2, amount: new AmountMock(value: 10.55, currency: 'USD')) + ] + ) + ); + $complex->__set('other', new StdClass()); + } + + public function testPropertyCannotBeDeactivated(): void + { + $this->expectException(PropertyCannotBeDeactivated::class); + $this->expectErrorMessage( + 'Property cannot be deactivated in class .' + ); + + $complex = new ComplexValueMock( + single: new SingleValueMock(id: 1000), + multiple: new MultipleValueMock( + id: 999, + transactions: [ + new TransactionMock(id: 1, amount: new AmountMock(value: 0.99, currency: 'USD')), + new TransactionMock(id: 2, amount: new AmountMock(value: 10.55, currency: 'USD')) + ] + ) + ); + $complex->__unset('other'); + } +} diff --git a/tests/Mock/AmountMock.php b/tests/Mock/AmountMock.php new file mode 100644 index 0000000..a959b4d --- /dev/null +++ b/tests/Mock/AmountMock.php @@ -0,0 +1,10 @@ +equals(other: $other); + + self::assertTrue($actual); + } + + public function testWhenEqualIsFalse(): void + { + $multiple = new MultipleValueMock( + id: 123, + transactions: [ + new TransactionMock(id: 100, amount: new AmountMock(value: 10.0, currency: 'BRL')), + new TransactionMock(id: 200, amount: new AmountMock(value: 11.01, currency: 'BRL')) + ] + ); + $other = new MultipleValueMock( + id: 123, + transactions: [ + new TransactionMock(id: 100, amount: new AmountMock(value: 10.0, currency: 'USD')), + new TransactionMock(id: 200, amount: new AmountMock(value: 11.01, currency: 'USD')) + ] + ); + + $actual = $multiple->equals(other: $other); + + self::assertFalse($actual); + } + + public function testInvalidProperty(): void + { + $this->expectException(InvalidProperty::class); + $this->expectErrorMessage('Invalid property for class .'); + + $multiple = new MultipleValueMock( + id: 123, + transactions: [ + new TransactionMock(id: 100, amount: new AmountMock(value: 10.0, currency: 'BRL')), + new TransactionMock(id: 200, amount: new AmountMock(value: 11.01, currency: 'BRL')) + ] + ); + $multiple->__get('other'); + } + + public function testPropertyCannotBeChanged(): void + { + $this->expectException(PropertyCannotBeChanged::class); + $this->expectErrorMessage( + 'Property cannot be changed in class .' + ); + + $multiple = new MultipleValueMock( + id: 123, + transactions: [ + new TransactionMock(id: 100, amount: new AmountMock(value: 10.0, currency: 'BRL')), + new TransactionMock(id: 200, amount: new AmountMock(value: 11.01, currency: 'BRL')) + ] + ); + $multiple->__set('other', new StdClass()); + } + + public function testPropertyCannotBeDeactivated(): void + { + $this->expectException(PropertyCannotBeDeactivated::class); + $this->expectErrorMessage( + 'Property cannot be deactivated in class .' + ); + + $multiple = new MultipleValueMock( + id: 123, + transactions: [ + new TransactionMock(id: 100, amount: new AmountMock(value: 10.0, currency: 'BRL')), + new TransactionMock(id: 200, amount: new AmountMock(value: 11.01, currency: 'BRL')) + ] + ); + $multiple->__unset('other'); + } +} diff --git a/tests/SingleValueTest.php b/tests/SingleValueTest.php new file mode 100644 index 0000000..64722e1 --- /dev/null +++ b/tests/SingleValueTest.php @@ -0,0 +1,65 @@ +equals(other: $other); + + self::assertTrue($actual); + } + + public function testWhenEqualIsFalse(): void + { + $single = new SingleValueMock(id: 1); + $other = new SingleValueMock(id: 2); + + $actual = $single->equals(other: $other); + + self::assertFalse($actual); + } + + public function testInvalidProperty(): void + { + $this->expectException(InvalidProperty::class); + $this->expectErrorMessage('Invalid property for class .'); + + $single = new SingleValueMock(id: 1); + + $single->__get('other'); + } + + public function testPropertyCannotBeChanged(): void + { + $this->expectException(PropertyCannotBeChanged::class); + $this->expectErrorMessage('Property cannot be changed in class .'); + + $single = new SingleValueMock(id: 1); + + $single->__set('other', new StdClass()); + } + + public function testPropertyCannotBeDeactivated(): void + { + $this->expectException(PropertyCannotBeDeactivated::class); + $this->expectErrorMessage( + 'Property cannot be deactivated in class .' + ); + + $single = new SingleValueMock(id: 1); + + $single->__unset('other'); + } +}