From 118c927a5ac235b62c9c7b5eb40ee312f640cba1 Mon Sep 17 00:00:00 2001 From: Laurens Hellemons Date: Mon, 4 Feb 2019 20:00:46 +0100 Subject: [PATCH] Added WeakRef support Added resource value support Moved SingleValueObjectTrait validation / normalization logic to factory method (to avoid unnecessary instantiation) Improved documentation Fixed money example --- README.md | 12 ++- docs/examples/money.md | 10 +- docs/single-value-objects.md | 29 ++++-- docs/value-objects.md | 24 ++++- src/Value/Ref/Ref.php | 46 +++++++++ src/Value/Ref/StrongRef.php | 34 +++++++ src/Value/Ref/WeakRef.php | 36 +++++++ src/Value/SingleValueObjectTrait.php | 14 ++- src/Value/ValueObjectTrait.php | 35 +++++-- test/Value/SingleValueObjectTraitTest.php | 40 ++++++++ test/Value/ValueObjectTraitTest.php | 117 ++++++++++++++++++++++ test/Value/test_resource | 1 + 12 files changed, 366 insertions(+), 32 deletions(-) create mode 100644 src/Value/Ref/Ref.php create mode 100644 src/Value/Ref/StrongRef.php create mode 100644 src/Value/Ref/WeakRef.php create mode 100644 test/Value/test_resource diff --git a/README.md b/README.md index f64c5ea..6c5cc83 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Read the full documentation [here](docs/index.md). Usage ----- +Install the package using composer. + ``` composer require lhellemons/php-value-objects ``` @@ -36,9 +38,11 @@ final class Weekday return self::define('TUESDAY'); } - ... + // ... } -... + +// ... + $monday = Weekday::MONDAY(); $tuesday = Weekday::TUESDAY(); $deliveryDay = WeekDay::MONDAY(); @@ -74,7 +78,9 @@ final class EmailAddress return $this->emailAddressString; } } -... + +// ... + $emailAddress = EmailAddress::of("annie@email.com"); $sameEmailAddress = EmailAddress::of(" ANNIE@EMAIL.COM"); diff --git a/docs/examples/money.md b/docs/examples/money.md index ba82aa3..5927fc5 100644 --- a/docs/examples/money.md +++ b/docs/examples/money.md @@ -6,6 +6,10 @@ Martin Fowler (https://martinfowler.com/eaaCatalog/money.html). Here is a sample implementation using a ValueObject: ```php +use SolidPhp\ValueObjects\Enum\EnumInterface; +use SolidPhp\ValueObjects\Enum\EnumTrait; +use SolidPhp\ValueObjects\Value\ValueObjectTrait; + class Currency implements EnumInterface { use EnumTrait; @@ -83,10 +87,10 @@ final class Money $share = $this->multiply(1 / $count); $shares = array_fill(0, $count, $share); - $remainder = $this->subtract($share->multiply($count), $result); - $results[$count-1] = $results[$count-1]->add($remainder); + $remainder = $this->subtract($share->multiply($count)); + $shares[$count-1] = $shares[$count-1]->add($remainder); - return $results; + return $shares; } private function assertSameCurrency(Money $money): void diff --git a/docs/single-value-objects.md b/docs/single-value-objects.md index 1802632..0371678 100644 --- a/docs/single-value-objects.md +++ b/docs/single-value-objects.md @@ -15,17 +15,22 @@ The `SingleValueObjectTrait` implements these methods for you, so in most cases you can just define your class like this and it will work: ```php -class MySimpleValueObject implements SingleValueObjectInterface +use SolidPhp\ValueObjects\Value\SingleValueObjectInterface; +use SolidPhp\ValueObjects\Value\SingleValueObjectTrait; + +class EmailAddress implements SingleValueObjectInterface { use SingleValueObjectTrait; } ``` -or extend the `SimpleValueObject` abstract class, which is provided for +or extend the `SingleValueObject` abstract class, which is provided for convenience and is basically just a shorthand for the above code block: ```php -class MySimpleValueObject extends SimpleValueObject +use SolidPhp\ValueObjects\Value\SingleValueObject; + +class EmailAddress extends SingleValueObject { } ``` @@ -38,13 +43,16 @@ case, you can override the `validateRawValue` method from `SingleValueObjectTrai add any validation you might need: ```php -class Email implements SingleValueObjectInterface +use SolidPhp\ValueObjects\Value\SingleValueObjectInterface; +use SolidPhp\ValueObjects\Value\SingleValueObjectTrait; + +class EmailAddress implements SingleValueObjectInterface { use SingleValueObjectTrait; protected static function validateRawValue($rawValue): void { - if (0 === preg_match('/\w+\@\w.com/', $rawValue) { + if (0 === preg_match('/\w+\@\w.com/', $rawValue)) { throw new DomainException('Not a valid e-mail address'); } } @@ -65,18 +73,22 @@ object, for example to trim any whitespace. For this, you can override the `normalizeValidRawValue` method from `SingleValueObjectTrait`: ```php -class LastName implements SingleValueObjectInterface +use SolidPhp\ValueObjects\Value\SingleValueObjectInterface; +use SolidPhp\ValueObjects\Value\SingleValueObjectTrait; + +class EmailAddress implements SingleValueObjectInterface { use SingleValueObjectTrait; - protected static function normalizeValidRawValue($validRawValue) + protected static function normalizeValidRawValue($validRawValue): string { return trim($validRawValue); } } ``` -`normalizeValidRawValue` is called after validation, so +`normalizeValidRawValue` is called after validation, so you can assume +the parameter is valid according to the rules of your class. Considerations -------------- @@ -86,4 +98,3 @@ value object also apply here. - Don't mutate instance properties - Don't deserialize directly; always use the `of` factory method -- Use only scalar values; no objects or arrays. diff --git a/docs/value-objects.md b/docs/value-objects.md index 14d69bb..b451dfb 100644 --- a/docs/value-objects.md +++ b/docs/value-objects.md @@ -26,6 +26,8 @@ their parameters, and then call `return static::getInstance()` with the paramete Example: ```php +use SolidPhp\ValueObjects\Value\ValueObjectTrait; + class Point { // use ValueObjectTrait to get access to the getInstance class method to use in your factory methods @@ -86,12 +88,14 @@ $myPoint === $myOtherPoint; // false Important things to remember ---------------------------- -Value objects add many advantages to your codebase, but these come with the following caveats: +Value objects add many advantages to your codebase. These flow directly +from the following usage rules: - Never mutate the value of your instance properties! You can add non-static factory methods that use `static::getInstance` to construct a new instance based on the current instance. - This also means no setter methods! Check out the [money example](examples/money.md). -- Don't directly `unserialize` a value object; this will always create a new instance. Value objects + This also means no setter methods! Check out the [money example](examples/money.md) for an example of how to work with immutable + value object instances. +- Value objects cannot directly be `unserialize`d; this would always create a new instance. Value objects usually contain only scalars as properties, so serialization / unserialization should be a matter of getting those scalars for serialization and using the unserialized scalars to produce the correct call to a factory method for deserialization. In the future this library may add a trait that will @@ -108,3 +112,17 @@ $entity->property = 2; $valueObjectB = ValueClass::fromEntity($entity); $valueObjectA === $valueObjectB; // true ``` + +WeakRef +------- + +Value objects support the PHP [WeakRef extension](http://php.net/manual/en/book.weakref.php). +If your PHP runtime has the WeakRef extension installed, value object + instances will be kept as weak references, and will be + garbage-collected + when they are no longer referenced in code. This means it becomes + possible to instantiate enormous numbers of value objects for + quick tasks without consuming excessive memory. + +If the WeakRef extension is not installed, value object instances will +be kept in memory until the script finishes. diff --git a/src/Value/Ref/Ref.php b/src/Value/Ref/Ref.php new file mode 100644 index 0000000..dc48cec --- /dev/null +++ b/src/Value/Ref/Ref.php @@ -0,0 +1,46 @@ +object; + } + + /** + * @param object|null $object + */ + public function set($object): void + { + $this->object = $object; + } + + public function has(): bool + { + return $this->object !== null; + } +} diff --git a/src/Value/Ref/WeakRef.php b/src/Value/Ref/WeakRef.php new file mode 100644 index 0000000..5847750 --- /dev/null +++ b/src/Value/Ref/WeakRef.php @@ -0,0 +1,36 @@ +weakRef ? $this->weakRef->get() : null; + } + + /** + * @param object|null $object + */ + public function set($object): void + { + $this->weakRef = new PhpWeakRef($object); + } + + public function has(): bool + { + return $this->weakRef && $this->weakRef->valid(); + } +} diff --git a/src/Value/SingleValueObjectTrait.php b/src/Value/SingleValueObjectTrait.php index 8495398..ab5345f 100644 --- a/src/Value/SingleValueObjectTrait.php +++ b/src/Value/SingleValueObjectTrait.php @@ -14,20 +14,24 @@ trait SingleValueObjectTrait /* implements SingleValueObjectInterface */ { use ValueObjectTrait; - /** @var string */ + /** @var string|int|float|bool */ private $value; - final protected function __construct($rawValue) + final protected function __construct($value) { - static::validateRawValue($rawValue); - $this->value = static::normalizeValidRawValue($rawValue); + $this->value = $value; } final public static function of($rawValue): self { - return self::getInstance($rawValue); + static::validateRawValue($rawValue); + + return self::getInstance(static::normalizeValidRawValue($rawValue)); } + /** + * @return bool|float|int|string + */ final public function getValue() { return $this->value; diff --git a/src/Value/ValueObjectTrait.php b/src/Value/ValueObjectTrait.php index 5e9491b..7145ecb 100644 --- a/src/Value/ValueObjectTrait.php +++ b/src/Value/ValueObjectTrait.php @@ -1,9 +1,9 @@ has()) { + return $ref->get(); + } + + $instance = new static(...$values); + $ref->set($instance); + + return $instance; } public function __get($name) { - throw new ValueObjectException(sprintf('%s is a value object class, its properties cannot be gotten directly', static::class)); + throw new ValueObjectException( + sprintf('%s is a value object class, its properties cannot be gotten directly', static::class) + ); } public function __isset($name) { - throw new ValueObjectException(sprintf('%s is a value object class, its properties cannot be inspected.', static::class)); + throw new ValueObjectException( + sprintf('%s is a value object class, its properties cannot be inspected.', static::class) + ); } final public function __set($name, $value) @@ -67,7 +84,6 @@ final public function __clone() } } - function calculateKey(...$values): string { if (\count($values) === 0) { @@ -75,7 +91,7 @@ function calculateKey(...$values): string } if (\count($values) > 1) { - return implode('|', array_map('SolidPhp\ValueObjects\Value\calculateKey',$values)); + return implode('|', array_map('SolidPhp\ValueObjects\Value\calculateKey', $values)); } [$value] = $values; @@ -93,7 +109,8 @@ function calculateKey(...$values): string } if (\is_resource($value)) { - throw new \InvalidArgumentException('Value object cannot be constructed based on a resource'); + // note: this works because PHP stringifies resources as 'resource id #x' and never reuses x during a script run + return 'resource:' . $value; } return sprintf('scalar<%s>:%s', \gettype($value), $value); diff --git a/test/Value/SingleValueObjectTraitTest.php b/test/Value/SingleValueObjectTraitTest.php index d6859a0..f1a8112 100644 --- a/test/Value/SingleValueObjectTraitTest.php +++ b/test/Value/SingleValueObjectTraitTest.php @@ -48,6 +48,46 @@ public function getCasesForOf(): array ]; } + /** + * @dataProvider getCasesForSame + * @param $valueObjectA + * @param $valueObjectB + * @param bool $expectedSame + */ + public function testSame($valueObjectA, $valueObjectB, bool $expectedSame): void + { + if ($expectedSame) { + $this->assertSame($valueObjectA, $valueObjectB); + } else { + $this->assertNotSame($valueObjectA, $valueObjectB); + } + } + + public function getCasesForSame(): array + { + return [ + 'simple (string) - equal' => [SimpleSingleValueObject::of('foo'), SimpleSingleValueObject::of('foo'), true], + 'simple (string) - not equal' => [SimpleSingleValueObject::of('foo'), SimpleSingleValueObject::of('bar'), false], + + 'simple (int) - equal' => [SimpleSingleValueObject::of(1), SimpleSingleValueObject::of(1), true], + 'simple (int) - not equal' => [SimpleSingleValueObject::of(1), SimpleSingleValueObject::of(2), false], + + 'simple (float) - equal' => [SimpleSingleValueObject::of(1.0), SimpleSingleValueObject::of(1.0), true], + 'simple (float) - not equal' => [SimpleSingleValueObject::of(1), SimpleSingleValueObject::of(1.1), false], + + 'simple (bool) - equal' => [SimpleSingleValueObject::of(true), SimpleSingleValueObject::of(true), true], + 'simple (bool) - not equal' => [SimpleSingleValueObject::of(true), SimpleSingleValueObject::of(false), false], + + 'array - equal' => [SimpleSingleValueObject::of(['foo']), SimpleSingleValueObject::of(['foo']), true], + 'array - different values' => [SimpleSingleValueObject::of(['foo']), SimpleSingleValueObject::of(['bar']), false], + 'array - different keys' => [SimpleSingleValueObject::of([1 => 'foo']), SimpleSingleValueObject::of([2 => 'foo']), false], + 'array - different length' => [SimpleSingleValueObject::of(['foo']), SimpleSingleValueObject::of(['foo', 'bar']), false], + + 'normalized - equal' => [NormalizationSingleValueObject::of('foo'), NormalizationSingleValueObject::of(' FOO '), true], + 'normalized - not equal' => [NormalizationSingleValueObject::of('foo'), NormalizationSingleValueObject::of('bar'), false], + ]; + } + /** * @dataProvider getCasesForGetValue * diff --git a/test/Value/ValueObjectTraitTest.php b/test/Value/ValueObjectTraitTest.php index 2490e44..c3f0bbd 100644 --- a/test/Value/ValueObjectTraitTest.php +++ b/test/Value/ValueObjectTraitTest.php @@ -116,8 +116,125 @@ public function testNoMutation(): void $this->expectNotToPerformAssertions(); } + + /** + * @dataProvider getCasesForValueTypes + * @param $firstValue + * @param $secondValue + * @param bool $expectedSame + */ + public function testValueTypes($firstValue, $secondValue, bool $expectedSame): void + { + $instanceA = ValueTypesType::of($firstValue); + $instanceB = ValueTypesType::of($secondValue); + + if ($expectedSame) { + $this->assertSame($instanceA, $instanceB); + } else { + $this->assertNotSame($instanceA, $instanceB); + } + } + + public function getCasesForValueTypes(): array + { + $stdClassA = new \stdClass(); + $stdClassA->foo = 'foo'; + $stdClassB = new \stdClass(); + $stdClassB->foo = 'bar'; + + $objectA = new Object(); + $objectB = new Object(); + + $resourceA = fopen(__DIR__ . '/test_resource', 'r'); + $resourceB = fopen(__DIR__ . '/test_resource', 'r'); + + return [ + 'boolean - equal' => [true, true, true], + 'boolean - not equal' => [true, false, false], + 'int - equal' => [1,1,true], + 'int - not equal' => [1,2,false], + 'float - equal' => [1.0, 1.0, true], + 'float - not equal' => [1.0, 1.1, false], + 'string - equal' => ['foo', 'foo', true], + 'string - not equal' => ['foo', 'bar', false], + 'array - empty equal' => [[], [], true], + 'array - equal values' => [['a'],['a'], true], + 'array - different values' => [['a'],['b'], false], + 'array - different keys' => [[1 => 'a'],[2 => 'a'], false], + + 'stdClass - same instance' => [$stdClassA, $stdClassA, true], + 'stdClass - different instances' => [$stdClassA, $stdClassB, false], + 'stdClass - different literals' => [new \stdClass(), new \stdClass(), true], + + 'object - same instance' => [$objectA, $objectA, true], + 'object - different instance' => [$objectA, $objectB, false], + + 'resource - same instance' => [$resourceA, $resourceA, true], + 'resource - different instance' => [$resourceA, $resourceB, false], + + 'boolean / int' => [true, 1, false], + 'boolean / float' => [true, 1.0, false], + 'boolean / string' => [true, '1', false], + 'boolean / array' => [true, [1], false], + 'boolean / resource' => [true, $resourceA, false], + 'boolean / object' => [true, $objectA, false], + 'boolean / stdClass' => [true, new \stdClass(), false], + + 'int / float' => [1, 1.0, false], + 'int / string' => [1, '1', false], + 'int / array' => [1, [1], false], + 'int / resource' => [1, $resourceA, false], + 'int / object' => [1, $objectA, false], + 'int / stdClass' => [1, new \stdClass(), false], + + 'float / string' => [1.0, '1.0', false], + 'float / array' => [1.0, [1.0], false], + 'float / resource' => [1.0, $resourceA, false], + 'float / object' => [1.0, $objectA, false], + 'float / stdClass' => [1.0, new \stdClass(), false], + + 'string / array' => ['foo', ['foo'], false], + 'string / resource' => ['foo', $resourceA, false], + 'string / object' => ['foo', $objectA, false], + 'string / stdClass' => ['foo', new \stdClass(), false], + + 'array / resource' => [[1], $resourceA, false], + 'array / object' => [[1], $objectA, false], + + 'resource / object' => [$resourceA, $objectA, false], + 'resource / stdClass' => [$resourceA, $stdClassA, false], + + 'array / empty stdClass' => [[], new \stdClass(), true], + 'array / non-empty stdClass' => [['foo'], (object)['foo'], true], + 'array / different non-empty stdClass' => [['foo'], (object)['bar'], false], + ]; + } } +class ValueTypesType +{ + use ValueObjectTrait; + + private $value; + + private function __construct($value) + { + $this->value = $value; + } + + public static function of($value): self + { + return self::getInstance($value); + } + + public function getValue() + { + return $this->value; + } +} + +class Object {} + class FromValuesType { use ValueObjectTrait; diff --git a/test/Value/test_resource b/test/Value/test_resource new file mode 100644 index 0000000..84102df --- /dev/null +++ b/test/Value/test_resource @@ -0,0 +1 @@ +The quick brown fox jumps over the lazy dog