diff --git a/.travis.yml b/.travis.yml index 5575707..06d2664 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ php: - 5.5 - 5.6 - 7.0 + - 7.1 install: - alias composer=composer\ -n && composer selfupdate diff --git a/composer.json b/composer.json index cc0fb25..c271665 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "license": "LGPL-3.0", "require": { "php": ">=5.5", - "scriptfusion/mapper": "^1|^0", + "scriptfusion/mapper": "^1", "scriptfusion/static-class": "^1", "scriptfusion/retry-error-handlers": "^1", "psr/cache": "^1", diff --git a/src/Options/EncapsulatedOptions.php b/src/Options/EncapsulatedOptions.php index 03dc621..d91a44c 100644 --- a/src/Options/EncapsulatedOptions.php +++ b/src/Options/EncapsulatedOptions.php @@ -20,6 +20,36 @@ final public function copy() return $this->options + $this->defaults; } + /** + * Merges the specified overriding options into this instance giving precedence to the overriding options. Only + * option values that have been explicitly set are merged; that is, defaults are not merged. + * + * @param EncapsulatedOptions $overridingOptions Overriding options. + */ + final public function merge(EncapsulatedOptions $overridingOptions) + { + if (!$overridingOptions instanceof static) { + throw new MergeException('Cannot merge: options must be an instance of "' . get_class($this) . '".'); + } + + $this->options = $this->mergeOptions($this->options, $overridingOptions->options); + } + + /** + * Merges the specified options with the specified overrides giving precedence to the overriding options. + * + * Overriding this method in a derived class allows it to implement a custom merging strategy. + * + * @param array $options Options of this instance. + * @param array $overrides Overriding options. + * + * @return array Merged options. + */ + protected function mergeOptions(array $options, array $overrides) + { + return $overrides + $options; + } + /** * Gets the value for the specified option name. Returns the specified * default value if option name is not set. @@ -61,6 +91,10 @@ final protected function &getReference($option) */ final protected function set($option, $value) { + if (is_object($value) || is_resource($value)) { + throw new \InvalidArgumentException('Value must not be an object or resource.'); + } + $this->options["$option"] = $value; return $this; diff --git a/src/Options/MergeException.php b/src/Options/MergeException.php new file mode 100644 index 0000000..e8ba07a --- /dev/null +++ b/src/Options/MergeException.php @@ -0,0 +1,10 @@ +options->getFoo()); } + public function testSetObject() + { + $this->setExpectedException(\InvalidArgumentException::class); + + $this->options->setFoo($this->options); + } + + public function testSetResource() + { + $this->setExpectedException(\InvalidArgumentException::class); + + $this->options->setFoo(STDIN); + } + public function testSetNullOverridesDefault() { $this->options->setFoo(null); @@ -41,6 +57,53 @@ public function testCopy() self::assertSame(['foo' => 'bar'], $this->options->copy()); } + public function testMerge() + { + $a = $this->options; + $b = (new TestOptions)->setFoo('bar'); + $c = clone $a; + + self::assertSame('foo', $a->getFoo()); + self::assertSame('bar', $b->getFoo()); + self::assertSame('foo', $c->getFoo()); + + // Merging in b sets c to 'bar'. + $c->merge($b); + + self::assertSame('foo', $a->getFoo()); + self::assertSame('bar', $b->getFoo()); + self::assertSame('bar', $c->getFoo()); + + // Merging in a does not change the value of c because no options have been set explicitly for a. + $c->merge($a); + + self::assertSame('foo', $a->getFoo()); + self::assertSame('bar', $b->getFoo()); + self::assertSame('bar', $c->getFoo()); + + // Merging in a sets c to 'foo' after it has been explicitly set for a. + $a->setFoo('foo'); + $c->merge($a); + + self::assertSame('foo', $a->getFoo()); + self::assertSame('bar', $b->getFoo()); + self::assertSame('foo', $c->getFoo()); + } + + public function testMergeDerivedClass() + { + $this->options->merge(\Mockery::mock(TestOptions::class)); + + // PHPUnit asserts no exception is thrown. + } + + public function testMergeNonDerivedClass() + { + $this->setExpectedException(MergeException::class, TestOptions::class); + + $this->options->merge(\Mockery::mock(EncapsulatedOptions::class)); + } + public function testGetReference() { $this->options->setFoo(['bar' => 'bar', 'baz' => 'baz']);