diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22aac70 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/tests export-ignore +/vendor export-ignore + +/LICENSE export-ignore +/Makefile export-ignore +/README.md export-ignore +/phpmd.xml export-ignore +/phpunit.xml export-ignore +/phpstan.neon.dist export-ignore +/infection.json.dist export-ignore + +/.github export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32850ac..105bc90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,11 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Use PHP 8.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Install dependencies run: composer update --no-progress --optimize-autoloader @@ -33,6 +38,11 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Use PHP 8.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Install dependencies run: composer update --no-progress --optimize-autoloader diff --git a/Makefile b/Makefile index 9bf35fe..1b3026e 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,17 @@ DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.2 -.PHONY: configure test test-no-coverage review show-reports clean +.PHONY: configure test test-file test-no-coverage review show-reports clean configure: @${DOCKER_RUN} composer update --optimize-autoloader -test: review +test: @${DOCKER_RUN} composer tests -test-no-coverage: review +test-file: + @${DOCKER_RUN} composer tests-file-no-coverage ${FILE} + +test-no-coverage: @${DOCKER_RUN} composer tests-no-coverage review: @@ -19,4 +22,4 @@ show-reports: clean: @sudo chown -R ${USER}:${USER} ${PWD} - @rm -rf report vendor + @rm -rf report vendor .phpunit.cache diff --git a/composer.json b/composer.json index 56c4aa9..867bde8 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,6 @@ "keywords": [ "vo", "psr", - "psr-4", - "psr-12", "tiny-blocks", "value-object" ], @@ -20,6 +18,10 @@ "homepage": "https://github.com/gustavofreze" } ], + "support": { + "issues": "https://github.com/tiny-blocks/value-object/issues", + "source": "https://github.com/tiny-blocks/value-object" + }, "config": { "sort-packages": true, "allow-plugins": { @@ -37,24 +39,27 @@ } }, "require": { - "php": "^8.1||^8.2" + "php": "^8.2" }, "require-dev": { - "infection/infection": "^0.26", - "phpmd/phpmd": "^2.13", - "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "^3.7" + "phpmd/phpmd": "^2.15", + "phpunit/phpunit": "^11", + "phpstan/phpstan": "^1", + "infection/infection": "^0.29", + "squizlabs/php_codesniffer": "^3.10" }, "scripts": { "phpcs": "phpcs --standard=PSR12 --extensions=php ./src", "phpmd": "phpmd ./src text phpmd.xml --suffixes php --ignore-violations-on-exit", + "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress", "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" + "@phpmd", + "@phpstan" ], "tests": [ "@test", diff --git a/infection.json.dist b/infection.json.dist index e9bacf3..351e7f9 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -1,23 +1,21 @@ { "timeout": 10, "testFramework": "phpunit", - "tmpDir": "report/", + "tmpDir": "report/infection/", "source": { "directories": [ "src" ] }, "logs": { - "text": "report/logs/infection-text.log", - "summary": "report/logs/infection-summary.log" + "text": "report/infection/logs/infection-text.log", + "summary": "report/infection/logs/infection-summary.log" }, "mutators": { - "@default": true, - "PublicVisibility": false, - "ProtectedVisibility": false + "@default": true }, "phpUnit": { "configDir": "", "customPath": "./vendor/bin/phpunit" } -} \ No newline at end of file +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b31dbc6 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + paths: + - src + level: 9 + tmpDir: report/phpstan + ignoreErrors: + - '#return type has no value type specified in iterable type array#' + reportUnmatchedIgnoredErrors: false diff --git a/phpunit.xml b/phpunit.xml index 3d05fc8..7f080dd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,25 +1,35 @@ + bootstrap="vendor/autoload.php" + failOnRisky="true" + failOnWarning="true" + cacheDirectory=".phpunit.cache" + beStrictAboutOutputDuringTests="true"> + + + + src + + + - tests + tests - - src - + + + + + + + + + + diff --git a/src/Immutable.php b/src/Immutable.php new file mode 100644 index 0000000..c87ad11 --- /dev/null +++ b/src/Immutable.php @@ -0,0 +1,40 @@ +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->expectExceptionMessage('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(key: 'other'); - } - - public function testPropertyCannotBeChanged(): void - { - $this->expectException(PropertyCannotBeChanged::class); - $this->expectExceptionMessage( - '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(key: 'other', value: new StdClass()); - } - - public function testPropertyCannotBeDeactivated(): void - { - $this->expectException(PropertyCannotBeDeactivated::class); - $this->expectExceptionMessage( - '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(key: 'other'); - } -} diff --git a/tests/Mock/AmountMock.php b/tests/Mock/AmountMock.php deleted file mode 100644 index a959b4d..0000000 --- a/tests/Mock/AmountMock.php +++ /dev/null @@ -1,10 +0,0 @@ -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->expectExceptionMessage('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(key: 'other'); - } - - public function testPropertyCannotBeChanged(): void - { - $this->expectException(PropertyCannotBeChanged::class); - $this->expectExceptionMessage( - '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(key: 'other', value: new StdClass()); - } - - public function testPropertyCannotBeDeactivated(): void - { - $this->expectException(PropertyCannotBeDeactivated::class); - $this->expectExceptionMessage( - '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(key: 'other'); - } -} diff --git a/tests/SingleValueTest.php b/tests/SingleValueTest.php deleted file mode 100644 index 17175e7..0000000 --- a/tests/SingleValueTest.php +++ /dev/null @@ -1,67 +0,0 @@ -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->expectExceptionMessage('Invalid property for class .'); - - $single = new SingleValueMock(id: 1); - - $single->__get(key: 'other'); - } - - public function testPropertyCannotBeChanged(): void - { - $this->expectException(PropertyCannotBeChanged::class); - $this->expectExceptionMessage( - 'Property cannot be changed in class .' - ); - - $single = new SingleValueMock(id: 1); - - $single->__set(key: 'other', value: new StdClass()); - } - - public function testPropertyCannotBeDeactivated(): void - { - $this->expectException(PropertyCannotBeDeactivated::class); - $this->expectExceptionMessage( - 'Property cannot be deactivated in class .' - ); - - $single = new SingleValueMock(id: 1); - - $single->__unset(key: 'other'); - } -} diff --git a/tests/ValueObjectTest.php b/tests/ValueObjectTest.php new file mode 100644 index 0000000..c771ee8 --- /dev/null +++ b/tests/ValueObjectTest.php @@ -0,0 +1,88 @@ +equals(other: $orderTwo); + + /** @Then the orders should be considered equal as all attributes match */ + self::assertTrue($actual); + } + + public function testValueObjectsAreNotEqual(): void + { + /** @Given two orders with different products */ + $productOne = new Product(name: 'Laptop', amount: new Amount(value: 100.0, currency: 'USD')); + $productTwo = new Product(name: 'Mouse', amount: new Amount(value: 50.0, currency: 'USD')); + $productThree = new Product(name: 'Keyboard', amount: new Amount(value: 75.0, currency: 'USD')); + + $orderOne = new Order(id: 1, products: [$productOne, $productTwo]); + $orderTwo = new Order(id: 1, products: [$productOne, $productThree]); + + /** @When checking if both orders, with different products, are not equal */ + $actual = $orderOne->equals(other: $orderTwo); + + /** @Then the orders should not be considered equal as products differ */ + self::assertFalse($actual); + } + + public function testWhenInvalidProperty(): void + { + /** @Given an Order object */ + $order = new Order(id: 1); + + /** @When trying to access a non-existing property */ + /** @Then it should throw InvalidProperty exception */ + $template = 'Invalid property <%s> for class <%s>.'; + $this->expectException(InvalidProperty::class); + $this->expectExceptionMessage(sprintf($template, 'nonExistentProperty', Order::class)); + $order->__get(key: 'nonExistentProperty'); + } + + public function testWhenPropertyCannotBeChanged(): void + { + /** @Given an Order object */ + $order = new Order(id: 1); + + /** @When trying to set a property */ + /** @Then it should throw PropertyCannotBeChanged exception */ + $template = 'Property <%s> cannot be changed in class <%s>.'; + $this->expectException(PropertyCannotBeChanged::class); + $this->expectExceptionMessage(sprintf($template, 'id', Order::class)); + $order->__set(key: 'id', value: 2); + } + + public function testWhenPropertyCannotBeDeactivated(): void + { + /** @Given an Order object */ + $order = new Order(id: 1); + + /** @When trying to unset a property */ + /** @Then it should throw PropertyCannotBeDeactivated exception */ + $template = 'Property <%s> cannot be deactivated in class <%s>.'; + $this->expectException(PropertyCannotBeDeactivated::class); + $this->expectExceptionMessage(sprintf($template, 'id', Order::class)); + $order->__unset(key: 'id'); + } +}