diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2b59743..3d05f2f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - php: [7.4, 8.0, 8.1] + php: [8.0, 8.1] dependency-version: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.dependency-version }} diff --git a/README.md b/README.md index 9403e0c..d55d204 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Use composer to add DMS\Filter to your app ## Usage +### Annotation way + Your Entity: ```php @@ -81,6 +83,60 @@ Filtering: Full example: https://gist.github.com/1098352 +### Attribute way + +Your Entity: + +```php + +``` + +Filtering: +```php +loader = $loader; + + //Get a MetadataFactory + $metadataFactory = new Mapping\ClassMetadataFactory($loader); + + //Get a Filter + $filter = new DMS\Filter\Filter($metadataFactory); + + + //Get your Entity + $user = new App\Entity\User(); + $user->name = "My name"; + $user->email = " email@mail.com"; + + //Filter you entity + $filter->filter($user); + + echo $user->name; //"My name" + echo $user->email; //"email@mail.com" +?> +``` + ## Dependencies This package relies on these external libraries: diff --git a/composer.json b/composer.json index 4546489..2ee8bf0 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "require": { - "php": "^7.4 || ~8.0 || ~8.1", + "php": "~8.0 || ~8.1", "doctrine/annotations": "^1.13", "laminas/laminas-zendframework-bridge": "^1.0" }, diff --git a/src/DMS/Filter/Mapping/Loader/AttributeLoader.php b/src/DMS/Filter/Mapping/Loader/AttributeLoader.php new file mode 100644 index 0000000..5abaf90 --- /dev/null +++ b/src/DMS/Filter/Mapping/Loader/AttributeLoader.php @@ -0,0 +1,32 @@ +getReflectionClass()->getProperties() as $property) { + $this->readProperty($property, $metadata); + } + + return true; + } + + private function readProperty(ReflectionProperty $property, ClassMetadataInterface $metadata): void + { + if ($property->getDeclaringClass()->getName() !== $metadata->getClassName()) { + return; + } + + foreach ($property->getAttributes(Rule::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $metadata->addPropertyRule($property->getName(), $attribute->newInstance()); + } + } +} diff --git a/src/DMS/Filter/Rules/Alnum.php b/src/DMS/Filter/Rules/Alnum.php index 8b8d273..daf1d39 100644 --- a/src/DMS/Filter/Rules/Alnum.php +++ b/src/DMS/Filter/Rules/Alnum.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class Alnum extends RegExp { /** diff --git a/src/DMS/Filter/Rules/Alpha.php b/src/DMS/Filter/Rules/Alpha.php index 3867515..27fd21e 100644 --- a/src/DMS/Filter/Rules/Alpha.php +++ b/src/DMS/Filter/Rules/Alpha.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class Alpha extends RegExp { /** diff --git a/src/DMS/Filter/Rules/BooleanScalar.php b/src/DMS/Filter/Rules/BooleanScalar.php index 2c76f19..e42b473 100644 --- a/src/DMS/Filter/Rules/BooleanScalar.php +++ b/src/DMS/Filter/Rules/BooleanScalar.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class BooleanScalar extends Rule { } diff --git a/src/DMS/Filter/Rules/Callback.php b/src/DMS/Filter/Rules/Callback.php index 1342550..42dcab0 100644 --- a/src/DMS/Filter/Rules/Callback.php +++ b/src/DMS/Filter/Rules/Callback.php @@ -14,6 +14,7 @@ * * @Annotation */ +#[\Attribute] class Callback extends Rule { public const SELF_METHOD_TYPE = 'self_method'; diff --git a/src/DMS/Filter/Rules/Digits.php b/src/DMS/Filter/Rules/Digits.php index 9500d3a..0a59a27 100644 --- a/src/DMS/Filter/Rules/Digits.php +++ b/src/DMS/Filter/Rules/Digits.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class Digits extends RegExp { /** diff --git a/src/DMS/Filter/Rules/FloatScalar.php b/src/DMS/Filter/Rules/FloatScalar.php index b3ca0e0..fccd589 100644 --- a/src/DMS/Filter/Rules/FloatScalar.php +++ b/src/DMS/Filter/Rules/FloatScalar.php @@ -9,6 +9,7 @@ * * @Annotation */ +#[\Attribute] class FloatScalar extends Rule { } diff --git a/src/DMS/Filter/Rules/HtmlEntities.php b/src/DMS/Filter/Rules/HtmlEntities.php index bd381e6..a7b0586 100644 --- a/src/DMS/Filter/Rules/HtmlEntities.php +++ b/src/DMS/Filter/Rules/HtmlEntities.php @@ -10,6 +10,7 @@ * * @Annotation */ +#[\Attribute] class HtmlEntities extends Rule { /** diff --git a/src/DMS/Filter/Rules/IntScalar.php b/src/DMS/Filter/Rules/IntScalar.php index 20a7a01..b43ede5 100644 --- a/src/DMS/Filter/Rules/IntScalar.php +++ b/src/DMS/Filter/Rules/IntScalar.php @@ -9,6 +9,7 @@ * * @Annotation */ +#[\Attribute] class IntScalar extends Rule { } diff --git a/src/DMS/Filter/Rules/Laminas.php b/src/DMS/Filter/Rules/Laminas.php index f8c1a31..e0efa98 100644 --- a/src/DMS/Filter/Rules/Laminas.php +++ b/src/DMS/Filter/Rules/Laminas.php @@ -10,6 +10,7 @@ * * @Annotation */ +#[\Attribute] class Laminas extends Rule { /** diff --git a/src/DMS/Filter/Rules/PregReplace.php b/src/DMS/Filter/Rules/PregReplace.php index 3a890b1..6941ffe 100644 --- a/src/DMS/Filter/Rules/PregReplace.php +++ b/src/DMS/Filter/Rules/PregReplace.php @@ -10,6 +10,7 @@ * * @Annotation */ +#[\Attribute] class PregReplace extends Rule { /** diff --git a/src/DMS/Filter/Rules/RegExp.php b/src/DMS/Filter/Rules/RegExp.php index eedc7dc..67e3f0f 100644 --- a/src/DMS/Filter/Rules/RegExp.php +++ b/src/DMS/Filter/Rules/RegExp.php @@ -10,6 +10,7 @@ * * @Annotation */ +#[\Attribute] class RegExp extends Rule { /** diff --git a/src/DMS/Filter/Rules/StripNewlines.php b/src/DMS/Filter/Rules/StripNewlines.php index 52eb6ce..eb4b864 100644 --- a/src/DMS/Filter/Rules/StripNewlines.php +++ b/src/DMS/Filter/Rules/StripNewlines.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class StripNewlines extends Rule { } diff --git a/src/DMS/Filter/Rules/StripTags.php b/src/DMS/Filter/Rules/StripTags.php index 82e509b..8c9c9ba 100644 --- a/src/DMS/Filter/Rules/StripTags.php +++ b/src/DMS/Filter/Rules/StripTags.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class StripTags extends Rule { /** diff --git a/src/DMS/Filter/Rules/ToLower.php b/src/DMS/Filter/Rules/ToLower.php index 9fc4c0d..643fd66 100644 --- a/src/DMS/Filter/Rules/ToLower.php +++ b/src/DMS/Filter/Rules/ToLower.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class ToLower extends Rule { /** diff --git a/src/DMS/Filter/Rules/ToUpper.php b/src/DMS/Filter/Rules/ToUpper.php index 5628e1f..d0f186f 100644 --- a/src/DMS/Filter/Rules/ToUpper.php +++ b/src/DMS/Filter/Rules/ToUpper.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class ToUpper extends Rule { /** diff --git a/src/DMS/Filter/Rules/Trim.php b/src/DMS/Filter/Rules/Trim.php index fde362a..1880bf7 100644 --- a/src/DMS/Filter/Rules/Trim.php +++ b/src/DMS/Filter/Rules/Trim.php @@ -8,6 +8,7 @@ * * @Annotation */ +#[\Attribute] class Trim extends Rule { /** diff --git a/src/DMS/Filter/Rules/Zend.php b/src/DMS/Filter/Rules/Zend.php index 2c05171..30e0ff7 100644 --- a/src/DMS/Filter/Rules/Zend.php +++ b/src/DMS/Filter/Rules/Zend.php @@ -12,6 +12,7 @@ * * @Annotation */ +#[\Attribute] class Zend extends Rule { /** diff --git a/tests/DMS/Filter/FilterTest.php b/tests/DMS/Filter/FilterTest.php index 3c1349c..f8c09c5 100644 --- a/tests/DMS/Filter/FilterTest.php +++ b/tests/DMS/Filter/FilterTest.php @@ -6,28 +6,22 @@ use DMS\Tests\FilterTestCase; use DMS\Tests\Dummy; use DMS\Filter\Mapping\ClassMetadataFactory; +use Generator; class FilterTest extends FilterTestCase { - protected Filter $filter; - - public function setUp(): void + /** + * @dataProvider filterClassDataProvider + */ + public function testFilter(Filter $filter, $class): void { - parent::setUp(); - - $this->filter = new Filter($this->buildMetadataFactory(), new FilterLoader()); - } - - public function testFilter(): void - { - $class = new Dummy\Classes\AnnotatedClass(); $class->name = "Sir Isaac Newton"; $class->nickname = "justaname"; $class->description = "This is an apple.
Isn't it?
"; $classClone = clone $class; - $this->filter->filterEntity($class); + $filter->filterEntity($class); $this->assertNotEquals($classClone->name, $class->name); $this->assertEquals($classClone->nickname, $class->nickname); @@ -37,9 +31,11 @@ public function testFilter(): void $this->assertStringNotContainsString("", $class->description); } - public function testFilterWithParent(): void + /** + * @dataProvider filterChildClassDataProvider + */ + public function testFilterWithParent(Filter $filter, $class): void { - $class = new Dummy\Classes\ChildAnnotatedClass(); $class->name = "Sir Isaac Newton"; $class->nickname = "justaname"; $class->description = "This is an apple.
Isn't it?
"; @@ -47,7 +43,7 @@ public function testFilterWithParent(): void $classClone = clone $class; - $this->filter->filterEntity($class); + $filter->filterEntity($class); $this->assertNotEquals($classClone->name, $class->name); $this->assertEquals($classClone->nickname, $class->nickname); @@ -59,15 +55,17 @@ public function testFilterWithParent(): void $this->assertStringNotContainsString(" ", $class->surname); } - public function testFilterProperty(): void + /** + * @dataProvider filterClassDataProvider + */ + public function testFilterProperty(Filter $filter, $class): void { - $class = new Dummy\Classes\AnnotatedClass(); $class->name = "Sir Isaac Newton"; $class->description = "This is an apple.Isn't it?
"; $classClone = clone $class; - $this->filter->filterProperty($class, 'description'); + $filter->filterProperty($class, 'description'); $this->assertEquals($classClone->name, $class->name); $this->assertNotEquals($classClone->description, $class->description); @@ -76,11 +74,14 @@ public function testFilterProperty(): void $this->assertStringNotContainsString("", $class->description); } - public function testFilterValue(): void + /** + * @dataProvider filterDataProvider + */ + public function testFilterValue(Filter $filter): void { $value = "this is a string
with tags
and malformed"; - $filtered = $this->filter->filterValue($value, new Rules\StripTags()); + $filtered = $filter->filterValue($value, new Rules\StripTags()); $this->assertNotEquals($value, $filtered); @@ -88,12 +89,15 @@ public function testFilterValue(): void $this->assertStringNotContainsString('', $filtered); } - public function testFilterValueWithArray(): void + /** + * @dataProvider filterDataProvider + */ + public function testFilterValueWithArray(Filter $filter): void { $value = "this is a string
with tags
and\n malformed"; $filters = [new Rules\StripTags(), new Rules\StripNewlines()]; - $filtered = $this->filter->filterValue($value, $filters); + $filtered = $filter->filterValue($value, $filters); $this->assertNotEquals($value, $filtered); @@ -102,14 +106,57 @@ public function testFilterValueWithArray(): void $this->assertStringNotContainsString('\n', $filtered); } - public function testNotFailOnNull(): void + /** + * @dataProvider filterDataProvider + */ + public function testNotFailOnNull(Filter $filter): void { $this->expectNotToPerformAssertions(); - $this->filter->filterEntity(null); + $filter->filterEntity(null); } - public function testGetMetadataFactory(): void + /** + * @dataProvider filterDataProvider + */ + public function testGetMetadataFactory(Filter $filter): void + { + $this->assertInstanceOf(ClassMetadataFactory::class, $filter->getMetadataFactory()); + } + + public function filterClassDataProvider(): Generator { - $this->assertInstanceOf(ClassMetadataFactory::class, $this->filter->getMetadataFactory()); + yield 'Annotation' => [ + new Filter($this->buildMetadataFactoryWithAnnotationLoader(), new FilterLoader()), + new Dummy\Classes\AnnotatedClass(), + ]; + + yield 'Attribute' => [ + new Filter($this->buildMetadataFactoryWithAttributeLoader(), new FilterLoader()), + new Dummy\Classes\AttributedClass(), + ]; + } + + public function filterChildClassDataProvider(): Generator + { + yield 'Annotation' => [ + new Filter($this->buildMetadataFactoryWithAnnotationLoader(), new FilterLoader()), + new Dummy\Classes\ChildAnnotatedClass(), + ]; + + yield 'Attribute' => [ + new Filter($this->buildMetadataFactoryWithAttributeLoader(), new FilterLoader()), + new Dummy\Classes\ChildAttributedClass(), + ]; + } + + public function filterDataProvider(): Generator + { + yield 'Annotation' => [ + new Filter($this->buildMetadataFactoryWithAnnotationLoader(), new FilterLoader()), + ]; + + yield 'Attribute' => [ + new Filter($this->buildMetadataFactoryWithAttributeLoader(), new FilterLoader()), + ]; } } diff --git a/tests/DMS/Filter/Mapping/ClassMetadataFactoryTest.php b/tests/DMS/Filter/Mapping/ClassMetadataFactoryTest.php index faeb2aa..60d316e 100644 --- a/tests/DMS/Filter/Mapping/ClassMetadataFactoryTest.php +++ b/tests/DMS/Filter/Mapping/ClassMetadataFactoryTest.php @@ -3,58 +3,81 @@ namespace DMS\Filter\Mapping; use DMS\Filter\Mapping\Loader\AnnotationLoader; +use DMS\Filter\Mapping\Loader\AttributeLoader; +use DMS\Filter\Mapping\Loader\LoaderInterface; +use DMS\Tests\Dummy\Classes\AttributedClass; use DMS\Tests\FilterTestCase; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Cache\ArrayCache; use DMS\Tests\Dummy\Classes\AnnotatedClass; -use DMS\Filter\Mapping\ClassMetadataInterface; +use Generator; class ClassMetadataFactoryTest extends FilterTestCase { - /** - * @var ClassMetadataFactory + * @dataProvider factoryDataProvider */ - protected ClassMetadataFactory $factory; - - public function setUp(): void -{ - parent::setUp(); - - $this->factory = $this->buildMetadataFactory(); - } - - public function testGetClassMetadata(): void + public function testGetClassMetadata(ClassMetadataFactory $factory, $class): void { - $metadata = $this->factory->getClassMetadata(AnnotatedClass::class); + $metadata = $factory->getClassMetadata($class); $this->assertInstanceOf(ClassMetadataInterface::class, $metadata); } - public function testParsedMetadataFromFactory(): void + /** + * @dataProvider factoryDataProvider + */ + public function testParsedMetadataFromFactory(ClassMetadataFactory $factory, $class): void { - $metadata = $this->factory->getClassMetadata(AnnotatedClass::class); + $metadata = $factory->getClassMetadata($class); - $metadataReparsed = $this->factory->getClassMetadata(AnnotatedClass::class); + $metadataReparsed = $factory->getClassMetadata($class); $this->assertSame($metadata, $metadataReparsed); } - public function testCachedMetadataFromFactory(): void + /** + * @dataProvider loaderDataProvider + */ + public function testCachedMetadataFromFactory(LoaderInterface $loader, $class): void { $cache = new ArrayCache(); - $reader = new AnnotationReader(); - $loader = new AnnotationLoader($reader); - $this->factory = new ClassMetadataFactory($loader, $cache); + $factory = new ClassMetadataFactory($loader, $cache); - $metadata = $this->factory->getClassMetadata(AnnotatedClass::class); + $metadata = $factory->getClassMetadata($class); - $this->assertTrue($cache->contains(ltrim(AnnotatedClass::class, '\\'))); + $this->assertTrue($cache->contains(ltrim($class, '\\'))); //Get new Factory to retrieve from cache - $this->factory = new ClassMetadataFactory($loader, $cache); - $metadataCached = $this->factory->getClassMetadata(AnnotatedClass::class); + $factory = new ClassMetadataFactory($loader, $cache); + $metadataCached = $factory->getClassMetadata($class); $this->assertEquals($metadata, $metadataCached); } + + public function factoryDataProvider(): Generator + { + yield 'Annotation' => [ + $this->buildMetadataFactoryWithAnnotationLoader(), + AnnotatedClass::class, + ]; + + yield 'Attribute' => [ + $this->buildMetadataFactoryWithAttributeLoader(), + AttributedClass::class, + ]; + } + + public function loaderDataProvider(): Generator + { + yield 'Annotation' => [ + new AnnotationLoader(new AnnotationReader()), + AnnotatedClass::class, + ]; + + yield 'Attribute' => [ + new AttributeLoader(), + AttributedClass::class, + ]; + } } diff --git a/tests/DMS/Filter/Mapping/Loader/AttributeLoaderTest.php b/tests/DMS/Filter/Mapping/Loader/AttributeLoaderTest.php new file mode 100644 index 0000000..aa84277 --- /dev/null +++ b/tests/DMS/Filter/Mapping/Loader/AttributeLoaderTest.php @@ -0,0 +1,30 @@ +loadClassMetadata($metadata); + + $this->assertTrue($loadMetadataResult); + + $classProperties = array_map( + fn (ReflectionProperty $property) => $property->getName(), + $metadata->getReflectionClass()->getProperties() + ); + + $this->assertSame($classProperties, $metadata->getFilteredProperties()); + } +} diff --git a/tests/DMS/Filter/Rules/CallbackTest.php b/tests/DMS/Filter/Rules/CallbackTest.php index 07cbc35..7fcc80c 100644 --- a/tests/DMS/Filter/Rules/CallbackTest.php +++ b/tests/DMS/Filter/Rules/CallbackTest.php @@ -4,6 +4,7 @@ namespace DMS\Filter\Rules; use DMS\Tests\Dummy\Classes\AnnotatedClass; +use DMS\Tests\Dummy\Classes\AttributedClass; use DMS\Tests\FilterTestCase; use DMS\Filter\Exception\InvalidCallbackException; use stdClass; @@ -38,6 +39,9 @@ public function provideInputs(): array [[AnnotatedClass::class, 'anotherCallback'], Callback::CALLABLE_TYPE, false], [[AnnotatedClass::class, 'missingCallback'], null, true], [[new AnnotatedClass(), 'callbackMethod'], Callback::CALLABLE_TYPE, false], + [[AttributedClass::class, 'anotherCallback'], Callback::CALLABLE_TYPE, false], + [[AttributedClass::class, 'missingCallback'], null, true], + [[new AttributedClass(), 'callbackMethod'], Callback::CALLABLE_TYPE, false], ['strlen', Callback::CALLABLE_TYPE, false], [$closure, Callback::CLOSURE_TYPE, false], [1, null, true], diff --git a/tests/DMS/Tests/Dummy/Classes/AnnotatedInterface.php b/tests/DMS/Tests/Dummy/Classes/AnnotatedInterface.php deleted file mode 100644 index 84304da..0000000 --- a/tests/DMS/Tests/Dummy/Classes/AnnotatedInterface.php +++ /dev/null @@ -1,7 +0,0 @@ -")] + public string $description; + + #[Filter\Callback("callbackMethod")] + public ?string $callback = null; + + #[Filter\Callback(["DMS\Tests\Dummy\Classes\AnnotatedClass", "anotherCallback"])] + public ?string $callback2 = null; + + public function callbackMethod($value): string + { + return 'called_back'; + } + + public static function anotherCallback($value): string + { + return 'called_back'; + } +} diff --git a/tests/DMS/Tests/Dummy/Classes/ChildAnnotatedClass.php b/tests/DMS/Tests/Dummy/Classes/ChildAnnotatedClass.php index 9a005c2..4f5ba4f 100644 --- a/tests/DMS/Tests/Dummy/Classes/ChildAnnotatedClass.php +++ b/tests/DMS/Tests/Dummy/Classes/ChildAnnotatedClass.php @@ -4,7 +4,7 @@ use DMS\Filter\Rules as Filter; -class ChildAnnotatedClass extends AnnotatedClass implements AnnotatedInterface +class ChildAnnotatedClass extends AnnotatedClass { /** * @Filter\Trim() diff --git a/tests/DMS/Tests/Dummy/Classes/ChildAttributedClass.php b/tests/DMS/Tests/Dummy/Classes/ChildAttributedClass.php new file mode 100644 index 0000000..7687cce --- /dev/null +++ b/tests/DMS/Tests/Dummy/Classes/ChildAttributedClass.php @@ -0,0 +1,11 @@ +buildMetadataFactory(); - } - public function setUp(): void { parent::setUp(); @@ -25,7 +19,7 @@ public function tearDown(): void parent::tearDown(); } - protected function buildMetadataFactory(): ClassMetadataFactory + protected function buildMetadataFactoryWithAnnotationLoader(): ClassMetadataFactory { $reader = new Annotations\AnnotationReader(); @@ -33,4 +27,11 @@ protected function buildMetadataFactory(): ClassMetadataFactory return new ClassMetadataFactory($loader); } + + protected function buildMetadataFactoryWithAttributeLoader(): ClassMetadataFactory + { + $loader = new Mapping\Loader\AttributeLoader(); + + return new ClassMetadataFactory($loader); + } }