diff --git a/.travis.yml b/.travis.yml index f2bdd83..a9422ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ before_script: - mkdir -p build/logs script: - - vendor/bin/phpstan analyse -l 7 src/ tests/ fixtures/ + - vendor/bin/phpstan analyse -l7 -cphpstan.neon src/ tests/ fixtures/ - vendor/bin/phpunit --coverage-clover build/logs/clover.xml after_script: diff --git a/README.md b/README.md index 426a3dd..a80339d 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ function deep_copy($var) static $copier = null; if (null === $copier) { - $copier = new DeepCopy(true); + $copier = new DeepCopy(); } return $copier->copy($var); @@ -124,8 +124,11 @@ function deep_copy($var) You can add filters to customize the copy process by adding filters: ```php -$copier = new DeepCopy(); -$copier->addFilter($filter, $matcher); +$copier = new DeepCopy( + false, + false, + [[$filter, $matcher]] +); ``` During the copy process, when a property is matched by a [matcher][matcher], then the [filter][filter] associated to @@ -203,8 +206,11 @@ use DeepCopy\Filter\SetNullFilter; $object = MyClass::load(123); echo $object->id; // 123 -$copier = new DeepCopy(); -$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id')); +$copier = new DeepCopy( + false, + false, + [[new SetNullFilter(), new PropertyNameMatcher('id')]] +); $copy = $copier->copy($object); @@ -221,10 +227,10 @@ use DeepCopy\DeepCopy; use DeepCopy\Filter\KeepFilter; use DeepCopy\Matcher\PropertyMatcher; -$copier = new DeepCopy(); -$copier->addFilter( - new KeepFilter(), - new PropertyMatcher(MyClass::class, 'category') +$copier = new DeepCopy( + false, + false, + [[new KeepFilter(), new PropertyMatcher(MyClass::class, 'category')]] ); $copy = $copier->copy($object); // $object is an instance of MyClass @@ -242,10 +248,10 @@ use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter; use DeepCopy\Matcher\PropertyTypeMatcher; use Doctrine\Common\Collections\Collection; -$copier = new DeepCopy(); -$copier->addFilter( - new DoctrineCollectionFilter(), - new PropertyTypeMatcher(Collection::class) +$copier = new DeepCopy( + false, + false, + [[new DoctrineCollectionFilter(), new PropertyTypeMatcher(Collection::class)]] ); $copy = $copier->copy($object); @@ -262,10 +268,10 @@ use DeepCopy\DeepCopy; use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter; use DeepCopy\Matcher\PropertyMatcher; -$copier = new DeepCopy(); -$copier->addFilter( - new DoctrineEmptyCollectionFilter(), - new PropertyMatcher(MyClass::class, 'myProperty') +$copier = new DeepCopy( + false, + false, + [[new DoctrineEmptyCollectionFilter(), new PropertyMatcher(MyClass::class, 'myProperty')]] ); $copy = $copier->copy($object); @@ -292,9 +298,14 @@ use DeepCopy\Filter\SetNullFilter; use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher; use DeepCopy\Matcher\PropertyNameMatcher; -$copier = new DeepCopy(); -$copier->addFilter(new DoctrineProxyFilter(), new DoctrineProxyMatcher()); -$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id')); +$copier = new DeepCopy( + false, + false, + [ + [new DoctrineProxyFilter(), new DoctrineProxyMatcher()], + [new SetNullFilter(), new PropertyNameMatcher('id')], + ] +); $copy = $copier->copy($object); @@ -311,14 +322,19 @@ use DeepCopy\DeepCopy; use DeepCopy\Filter\ReplaceFilter; use DeepCopy\Matcher\PropertyMatcher; -$copier = new DeepCopy(); -$copier->addFilter( - new ReplaceFilter( - function ($currentValue): string { - return $currentValue . ' (copy)' - } - ), - new PropertyMatcher(MyClass::class, 'title') +$copier = new DeepCopy( + false, + false, + [ + [ + new ReplaceFilter( + function ($currentValue): string { + return $currentValue . ' (copy)' + } + ), + new PropertyMatcher(MyClass::class, 'title'), + ], + ] ); $copy = $copier->copy($object); // $object is an instance of MyClass @@ -333,14 +349,19 @@ use DeepCopy\DeepCopy; use DeepCopy\TypeFilter\ReplaceFilter; use DeepCopy\TypeMatcher\TypeMatcher; -$copier = new DeepCopy(); -$copier->addFilter( - new ReplaceFilter( - function (MyClass $myClass): string { - return get_class($myClass) - } - ), - new TypeMatcher(MyClass::class) +$copier = new DeepCopy( + false, + false, + [ + [ + new ReplaceFilter( + function (MyClass $myClass): string { + return get_class($myClass) + } + ), + new TypeMatcher(MyClass::class), + ], + ] ); $copy = $copier->copy([new MyClass, 'some string', new MyClass]); @@ -359,10 +380,11 @@ use DeepCopy\TypeFilter\ShallowCopyFilter; use DeepCopy\TypeMatcher\TypeMatcher; use Mockery as m; -$copier = new DeepCopy(); -$copier->addTypeFilter( - new ShallowCopyFilter, - new TypeMatcher(m\MockInterface::class) +$copier = new DeepCopy( + false, + false, + [], + [[new ShallowCopyFilter, new TypeMatcher(m\MockInterface::class)]] ); $myServiceWithMocks = new MyService( diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..ef31a6e --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + ignoreErrors: + - '#PHPDoc tag \@param has invalid value \(Array\#' + - '#PHPDoc tag \@param has invalid value \(Array\#' diff --git a/src/DeepCopy/DeepCopy.php b/src/DeepCopy/DeepCopy.php index b9c2075..d9fcd4c 100644 --- a/src/DeepCopy/DeepCopy.php +++ b/src/DeepCopy/DeepCopy.php @@ -21,7 +21,6 @@ use function is_object; use function is_resource; use function spl_object_id; -use function sprintf; final class DeepCopy { @@ -40,35 +39,43 @@ final class DeepCopy */ private $typeFilters = []; - private $skipUncloneable = false; + private $skipUncloneable; private $useCloneMethod; /** - * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used - * instead of the regular deep cloning. + * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will + * be used instead of the regular deep cloning. + * @param bool $skipUncloneable If enabled, will not throw an exception when coming across an uncloneable + * property. + * @param Array List of filter-matcher pairs + * @param Array List of type filter-matcher pairs */ - public function __construct(bool $useCloneMethod = false) - { + public function __construct( + bool $useCloneMethod = false, + bool $skipUncloneable = false, + array $filters = [], + array $typeFilters = [] + ) { $this->useCloneMethod = $useCloneMethod; - $this->addTypeFilter( + foreach ($filters as [$filter, $matcher]) { + $this->addFilter($filter, $matcher); + } + + $typeFilters[] = [ new DateIntervalFilter(), new TypeMatcher(DateInterval::class) - ); - $this->addTypeFilter( + ]; + $typeFilters[] = [ new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class) - ); - } + ]; - /** - * If enabled, will not throw an exception when coming across an uncloneable property. - */ - public function skipUncloneable(bool $skipUncloneable = true): self - { - $this->skipUncloneable = $skipUncloneable; + foreach ($typeFilters as [$filter, $matcher]) { + $this->addTypeFilter($filter, $matcher); + } - return $this; + $this->skipUncloneable = $skipUncloneable; } /** @@ -85,16 +92,6 @@ public function copy($value) return $this->recursiveCopy($value); } - public function addFilter(Filter $filter, Matcher $matcher): void - { - $this->filters[] = [$matcher, $filter]; - } - - public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher): void - { - $this->typeFilters[] = [$matcher, $filter]; - } - /** * @return mixed */ @@ -146,12 +143,7 @@ private function copyObject(object $object): object return $object; } - throw new CloneException( - sprintf( - 'The class "%s" is not cloneable.', - $reflectedObject->getName() - ) - ); + throw CloneException::unclonableClass($reflectedObject->getName()); } $newObject = clone $object; @@ -217,6 +209,16 @@ function ($object) { $property->setValue($object, $this->recursiveCopy($propertyValue)); } + private function addFilter(Filter $filter, Matcher $matcher): void + { + $this->filters[] = [$matcher, $filter]; + } + + private function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher): void + { + $this->typeFilters[] = [$matcher, $filter]; + } + /** * @return TypeFilter|null The first filter that matches variable or `null` if no such filter found */ diff --git a/src/DeepCopy/Exception/CloneException.php b/src/DeepCopy/Exception/CloneException.php index d7a2a27..7d2c80c 100644 --- a/src/DeepCopy/Exception/CloneException.php +++ b/src/DeepCopy/Exception/CloneException.php @@ -6,4 +6,13 @@ class CloneException extends UnexpectedValueException { + final public static function unclonableClass(string $class): self + { + return new self( + sprintf( + 'The class "%s" is not cloneable.', + $class + ) + ); + } } diff --git a/src/DeepCopy/deep_copy.php b/src/DeepCopy/deep_copy.php index 030a54f..45b40f6 100644 --- a/src/DeepCopy/deep_copy.php +++ b/src/DeepCopy/deep_copy.php @@ -6,11 +6,20 @@ * Deep copies the given value. * * @param mixed $value - * @param bool $useCloneMethod - * - * @return mixed + * @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will + * be used instead of the regular deep cloning. + * @param bool $skipUncloneable If enabled, will not throw an exception when coming across an uncloneable + * property. + * @param Array List of filter-matcher pairs + * @param Array List of type filter-matcher pairs */ -function deep_copy($value, $useCloneMethod = false) +function deep_copy( + $value, + bool $useCloneMethod = false, + bool $skipUncloneable = false, + array $filters = [], + array $typeFilters = [] +) { - return (new DeepCopy($useCloneMethod))->copy($value); + return (new DeepCopy($useCloneMethod, $skipUncloneable, $filters, $typeFilters))->copy($value); } diff --git a/tests/DeepCopyTest/DeepCopyTest.php b/tests/DeepCopyTest/DeepCopyTest.php index 8bff7de..7e3892e 100644 --- a/tests/DeepCopyTest/DeepCopyTest.php +++ b/tests/DeepCopyTest/DeepCopyTest.php @@ -46,18 +46,6 @@ public function test_it_can_copy_scalar_values($value) $this->assertSame($value, $copy); } - public function provideScalarValues() - { - return [ - [true], - ['string'], - [null], - [10], - [-1], - [.5], - ]; - } - public function test_it_can_copy_an_array_of_scalar_values() { $copy = deep_copy([10, 20]); @@ -74,6 +62,12 @@ public function test_it_can_copy_an_object() $this->assertEqualButNotSame($object, $copy); } + private function assertEqualButNotSame($expected, $val) + { + $this->assertEquals($expected, $val); + $this->assertNotSame($expected, $val); + } + public function test_it_can_copy_an_array_of_objects() { $object = [new stdClass()]; @@ -113,6 +107,18 @@ function (array $vals) use ($createObject) { ); } + public function provideScalarValues() + { + return [ + [true], + ['string'], + [null], + [10], + [-1], + [.5], + ]; + } + public function test_it_can_copy_an_object_with_an_object_property() { $foo = new stdClass(); @@ -166,10 +172,15 @@ public function test_it_can_copy_an_object_with_a_date_object_property() */ public function test_it_skips_the_copy_for_userland_datetimezone() { - $deepCopy = new DeepCopy(); - $deepCopy->addFilter( - new SetNullFilter(), - new PropertyNameMatcher('cloned') + $deepCopy = new DeepCopy( + false, + true, + [ + [ + new SetNullFilter(), + new PropertyNameMatcher('cloned') + ] + ] ); $object = new stdClass(); @@ -186,10 +197,15 @@ public function test_it_skips_the_copy_for_userland_datetimezone() */ public function test_it_skips_the_copy_for_userland_dateinterval() { - $deepCopy = new DeepCopy(); - $deepCopy->addFilter( - new SetNullFilter(), - new PropertyNameMatcher('cloned') + $deepCopy = new DeepCopy( + false, + true, + [ + [ + new SetNullFilter(), + new PropertyNameMatcher('cloned') + ] + ] ); $object = new stdClass(); @@ -295,8 +311,7 @@ public function test_it_can_skip_uncloneable_objects() { $object = new f004\UnclonableItem(); - $deepCopy = new DeepCopy(); - $deepCopy->skipUncloneable(true); + $deepCopy = new DeepCopy(false, true); $copy = $deepCopy->copy($object); @@ -325,10 +340,16 @@ public function test_it_only_uses_the_userland_defined_cloned_method_when_config public function test_it_uses_type_filter_to_copy_objects_if_matcher_matches() { - $deepCopy = new DeepCopy(); - $deepCopy->addTypeFilter( - new ShallowCopyFilter(), - new TypeMatcher(f006\A::class) + $deepCopy = new DeepCopy( + false, + false, + [], + [ + [ + new ShallowCopyFilter(), + new TypeMatcher(f006\A::class), + ] + ] ); $a = new f006\A; @@ -346,10 +367,15 @@ public function test_it_uses_type_filter_to_copy_objects_if_matcher_matches() public function test_it_uses_filters_to_copy_object_properties_if_matcher_matches() { - $deepCopy = new DeepCopy(); - $deepCopy->addFilter( - new SetNullFilter(), - new PropertyNameMatcher('cloned') + $deepCopy = new DeepCopy( + false, + false, + [ + [ + new SetNullFilter(), + new PropertyNameMatcher('cloned') + ] + ] ); $a = new f006\A; @@ -366,14 +392,19 @@ public function test_it_uses_filters_to_copy_object_properties_if_matcher_matche public function test_it_uses_the_first_filter_matching_for_copying_object_properties() { - $deepCopy = new DeepCopy(); - $deepCopy->addFilter( - new SetNullFilter(), - new PropertyNameMatcher('cloned') - ); - $deepCopy->addFilter( - new KeepFilter(), - new PropertyNameMatcher('cloned') + $deepCopy = new DeepCopy( + false, + false, + [ + [ + new SetNullFilter(), + new PropertyNameMatcher('cloned') + ], + [ + new KeepFilter(), + new PropertyNameMatcher('cloned') + ] + ] ); $a = new f006\A; @@ -417,8 +448,16 @@ public function test_it_can_copy_a_SplDoublyLinkedList() */ public function test_matchers_can_access_to_parent_private_properties() { - $deepCopy = new DeepCopy(); - $deepCopy->addFilter(new SetNullFilter(), new PropertyTypeMatcher(stdClass::class)); + $deepCopy = new DeepCopy( + false, + false, + [ + [ + new SetNullFilter(), + new PropertyTypeMatcher(stdClass::class), + ] + ] + ); $object = new f008\B(new stdClass()); @@ -434,8 +473,18 @@ public function test_private_property_of_parent_object_copy_with_filters_and_mat $object->setAProp(new stdClass()); $object->setBProp(new stdClass()); - $deepCopy = new DeepCopy(); - $deepCopy->addFilter(new ReplaceFilter(function() {return 'foo';}), new PropertyTypeMatcher(stdClass::class)); + $deepCopy = new DeepCopy( + false, + false, + [ + [ + new ReplaceFilter(function () { + return 'foo'; + }), + new PropertyTypeMatcher(stdClass::class), + ] + ] + ); $new = $deepCopy->copy($object); @@ -450,18 +499,23 @@ public function test_it_can_apply_two_filters() { $object = new f009\A(); - $deepCopy = new DeepCopy(); - $deepCopy->addFilter(new DoctrineProxyFilter(), new DoctrineProxyMatcher()); - $deepCopy->addFilter(new SetNullFilter(), new PropertyNameMatcher('foo')); + $deepCopy = new DeepCopy( + false, + false, + [ + [ + new DoctrineProxyFilter(), + new DoctrineProxyMatcher(), + ], + [ + new SetNullFilter(), + new PropertyNameMatcher('foo'), + ] + ] + ); $copy = $deepCopy->copy($object); $this->assertNull($copy->foo); } - - private function assertEqualButNotSame($expected, $val) - { - $this->assertEquals($expected, $val); - $this->assertNotSame($expected, $val); - } } diff --git a/tests/DeepCopyTest/Filter/Doctrine/DoctrineCollectionFilterTest.php b/tests/DeepCopyTest/Filter/Doctrine/DoctrineCollectionFilterTest.php index b8ef634..bbd6fbe 100644 --- a/tests/DeepCopyTest/Filter/Doctrine/DoctrineCollectionFilterTest.php +++ b/tests/DeepCopyTest/Filter/Doctrine/DoctrineCollectionFilterTest.php @@ -16,7 +16,8 @@ class DoctrineCollectionFilterTest extends TestCase { public function test_it_copies_the_object_property_array_collection() { - $object = new class { + $object = new class + { public $foo; }; $oldCollection = new ArrayCollection(); @@ -28,7 +29,7 @@ public function test_it_copies_the_object_property_array_collection() $filter->apply( $object, new ReflectionProperty($object, 'foo'), - function($item) { + function ($item) { return null; } ); diff --git a/tests/DeepCopyTest/Filter/Doctrine/DoctrineEmptyCollectionFilterTest.php b/tests/DeepCopyTest/Filter/Doctrine/DoctrineEmptyCollectionFilterTest.php index f95128c..a77e6bf 100644 --- a/tests/DeepCopyTest/Filter/Doctrine/DoctrineEmptyCollectionFilterTest.php +++ b/tests/DeepCopyTest/Filter/Doctrine/DoctrineEmptyCollectionFilterTest.php @@ -16,7 +16,8 @@ class DoctrineEmptyCollectionFilterTest extends TestCase { public function test_it_sets_the_object_property_to_an_empty_doctrine_collection() { - $object = new class { + $object = new class + { public $foo; }; $collection = new ArrayCollection(); @@ -29,7 +30,7 @@ public function test_it_sets_the_object_property_to_an_empty_doctrine_collection $filter->apply( $object, new ReflectionProperty($object, 'foo'), - function($item) { + function ($item) { return null; } ); diff --git a/tests/DeepCopyTest/Filter/Doctrine/DoctrineProxyFilterTest.php b/tests/DeepCopyTest/Filter/Doctrine/DoctrineProxyFilterTest.php index 7c75ac6..a2d5871 100644 --- a/tests/DeepCopyTest/Filter/Doctrine/DoctrineProxyFilterTest.php +++ b/tests/DeepCopyTest/Filter/Doctrine/DoctrineProxyFilterTest.php @@ -22,7 +22,7 @@ public function test_it_loads_the_doctrine_proxy() $filter->apply( $foo, new ReflectionProperty($foo, 'bar'), - function($item) { + function ($item) { throw new BadMethodCallException('Did not expect to be called.'); } ); diff --git a/tests/DeepCopyTest/Filter/KeepFilterTest.php b/tests/DeepCopyTest/Filter/KeepFilterTest.php index 89fce27..9dc132b 100644 --- a/tests/DeepCopyTest/Filter/KeepFilterTest.php +++ b/tests/DeepCopyTest/Filter/KeepFilterTest.php @@ -14,7 +14,8 @@ class KeepFilterTest extends TestCase { public function test_it_does_not_change_the_filtered_object_property() { - $object = new class { + $object = new class + { public $foo; }; $keepObject = new stdClass(); @@ -22,7 +23,8 @@ public function test_it_does_not_change_the_filtered_object_property() $filter = new KeepFilter(); - $filter->apply($object, new ReflectionProperty($object, 'foo'), function () {}); + $filter->apply($object, new ReflectionProperty($object, 'foo'), function () { + }); $this->assertSame($keepObject, $object->foo); } diff --git a/tests/DeepCopyTest/Filter/ReplaceFilterTest.php b/tests/DeepCopyTest/Filter/ReplaceFilterTest.php index fe8e0e9..08fc872 100644 --- a/tests/DeepCopyTest/Filter/ReplaceFilterTest.php +++ b/tests/DeepCopyTest/Filter/ReplaceFilterTest.php @@ -16,7 +16,8 @@ class ReplaceFilterTest extends TestCase */ public function test_it_applies_the_callback_to_the_specified_property(callable $callback, array $expected) { - $object = new class { + $object = new class + { public $data; }; $object->data = [ @@ -32,7 +33,7 @@ public function test_it_applies_the_callback_to_the_specified_property(callable function () { return null; } - ); + ); $this->assertEquals($expected, $object->data); } diff --git a/tests/DeepCopyTest/Filter/SetNullFilterTest.php b/tests/DeepCopyTest/Filter/SetNullFilterTest.php index 37981ba..0486520 100644 --- a/tests/DeepCopyTest/Filter/SetNullFilterTest.php +++ b/tests/DeepCopyTest/Filter/SetNullFilterTest.php @@ -15,14 +15,16 @@ public function test_it_sets_the_given_property_to_null() { $filter = new SetNullFilter(); - $object = new class { + $object = new class + { public $foo; public $bim; }; $object->foo = 'bar'; $object->bim = 'bam'; - $filter->apply($object, new ReflectionProperty($object, 'foo'), function () {}); + $filter->apply($object, new ReflectionProperty($object, 'foo'), function () { + }); $this->assertNull($object->foo); $this->assertEquals('bam', $object->bim); diff --git a/tests/DeepCopyTest/Matcher/Doctrine/DoctrineProxyMatcherTest.php b/tests/DeepCopyTest/Matcher/Doctrine/DoctrineProxyMatcherTest.php index 0df5b2d..e3e35b6 100644 --- a/tests/DeepCopyTest/Matcher/Doctrine/DoctrineProxyMatcherTest.php +++ b/tests/DeepCopyTest/Matcher/Doctrine/DoctrineProxyMatcherTest.php @@ -30,7 +30,8 @@ public function providePairs() return [ [new FooProxy(), true], [ - new class { + new class + { public $foo; }, false diff --git a/tests/DeepCopyTest/Matcher/PropertyNameMatcherTest.php b/tests/DeepCopyTest/Matcher/PropertyNameMatcherTest.php index 6760cc7..12f1018 100644 --- a/tests/DeepCopyTest/Matcher/PropertyNameMatcherTest.php +++ b/tests/DeepCopyTest/Matcher/PropertyNameMatcherTest.php @@ -16,7 +16,8 @@ class PropertyNameMatcherTest extends TestCase */ public function test_it_matches_the_given_property($prop, $expected) { - $object = new class { + $object = new class + { public $foo; public $bar; }; diff --git a/tests/DeepCopyTest/Matcher/PropertyTypeMatcherTest.php b/tests/DeepCopyTest/Matcher/PropertyTypeMatcherTest.php index ad3e5fa..a7278e2 100644 --- a/tests/DeepCopyTest/Matcher/PropertyTypeMatcherTest.php +++ b/tests/DeepCopyTest/Matcher/PropertyTypeMatcherTest.php @@ -41,7 +41,8 @@ public function providePairs() [$object2, false], [$object3, false], [ - new class { + new class + { public $foo; }, false diff --git a/tests/DeepCopyTest/TypeMatcher/TypeMatcherTest.php b/tests/DeepCopyTest/TypeMatcher/TypeMatcherTest.php index cfb061b..33d32e6 100644 --- a/tests/DeepCopyTest/TypeMatcher/TypeMatcherTest.php +++ b/tests/DeepCopyTest/TypeMatcher/TypeMatcherTest.php @@ -45,10 +45,10 @@ class Bar extends Foo { } -interface IA +class A implements IA { } -class A implements IA +interface IA { }