Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
"require": {
"php": ">=7.2",
"nikic/php-parser": "^4",
"symfony/finder": "^4|^5"
"symfony/finder": "^4|^5|^6"
}
}
6 changes: 6 additions & 0 deletions src/Iterator/ClassIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use hanneskod\classtools\Iterator\Filter\NotFilter;
use hanneskod\classtools\Iterator\Filter\TypeFilter;
use hanneskod\classtools\Iterator\Filter\WhereFilter;
use hanneskod\classtools\Iterator\Filter\AttributeFilter;
use hanneskod\classtools\Exception\LogicException;
use hanneskod\classtools\Loader\ClassLoader;
use hanneskod\classtools\Exception\ReaderException;
Expand Down Expand Up @@ -144,6 +145,11 @@ public function cache(): Filter
return $this->filter(new CacheFilter);
}

public function attribute(string $attribute_class_name): Filter
{
return $this->filter(new AttributeFilter($attribute_class_name));
}

public function transform(Writer $writer): string
{
$code = '';
Expand Down
5 changes: 5 additions & 0 deletions src/Iterator/ClassIteratorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public function not(Filter $filter): Filter;
*/
public function cache(): Filter;

/**
* Attribute iterator
*/
public function attribute(string $attribute_class_name): Filter;

/**
* Transform found classes
*/
Expand Down
68 changes: 68 additions & 0 deletions src/Iterator/Filter/AttributeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://www.wtfpl.net/ for more details.
*/

declare(strict_types = 1);

namespace hanneskod\classtools\Iterator\Filter;

use hanneskod\classtools\Iterator\ClassIterator;
use hanneskod\classtools\Iterator\Filter;

/**
* Filter classes based on presence of a PHP 8 attribute on a class level
* Note this is a basic implementation that simply looks for at least one declaration
* of a given attribute class and considers this a match
*
* Attribute class name must be passed, not an instance of the attribute
*
* #[Attribute]
* class CoolStuff { ... }
*
* #[CoolStuff]
* class BeCool { ... }
*
* // Print all classes tagged with a CoolStuff attribute
* foreach ($iter->attribute(CoolStuff::class) as $class) {
* echo $class->getName();
* }
*
* Note this works only in PHP 8.0.1 or higher, but syntax is PHP 7 compaptible
* so whilst this cannot be used on earlier versions it also won't break them
*
* @author Mark Hewitt <mark.hewitt@centurionsolutions.com.au>
*/
final class AttributeFilter extends ClassIterator implements Filter
{
use FilterTrait;

/**
* @var string
*/
private $attribute_class_name;

/**
* Register matching attribute name
*/
public function __construct(string $attribute_class_name)
{
parent::__construct();
$this->attribute_class_name = $attribute_class_name;
}

public function getIterator(): iterable
{
foreach ($this->getBoundIterator() as $className => $reflectedClass) {
// if the class implements the given attribute (one or more times)
// then this is a match, and we yield with the found class
if ( !empty($reflectedClass->getAttributes($this->attribute_class_name, \ReflectionAttribute::IS_INSTANCEOF)) ) {
yield $className => $reflectedClass;
}
}
}
}
73 changes: 73 additions & 0 deletions tests/Iterator/ClassIteratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,79 @@ public function testCacheFilter()
);
}

public function testAttributeFilter()
{
if ( version_compare(PHP_VERSION, '8.0.1') >= 0) {

# define our own system under test, adding attributes etc to standard test will break
# tests for minimize etc
MockFinder::setIterator(
new \ArrayIterator([
new MockSplFileInfo(''),
new MockSplFileInfo('<?php #[Attribute] class NotUsed { }'),
new MockSplFileInfo('<?php #[Attribute] class CoolStuff { }'),
new MockSplFileInfo('<?php #[Attribute] class EvenCoolerStuff { }'),
new MockSplFileInfo('<?php #[Attribute] class WayCool extends CoolStuff { }'),
new MockSplFileInfo('<?php #[CoolStuff] class BeCool { }'),
new MockSplFileInfo("<?php #[EvenCoolerStuff]\n#[CoolStuff]\nclass TheCoolest { }"),
new MockSplFileInfo('<?php #[WayCool] class InheritedCoolness {}'),
new MockSplFileInfo('<?php class NotCool {}')
])
);

$classIterator = new ClassIterator(new MockFinder);
$classIterator->enableAutoloading();

// look for class tagged with 'CoolStuff'
// we should find both BeCool, TheCoolest and WayCool because filter uses instance inheritance
// by default, thus WayCool extends CoolStuff and is detect by this filter
$result = iterator_to_array(
$classIterator->attribute('CoolStuff')
);
$this->assertArrayHasKey(
'BeCool',
$result
);
$this->assertArrayHasKey(
'TheCoolest',
$result
);
$this->assertArrayHasKey(
'InheritedCoolness',
$result
);
$this->assertCount(
3,
$result
);

// look for class tagged with 'EvenCoolerStuff'
// we should find only TheCoolest
$result = iterator_to_array(
$classIterator->attribute('EvenCoolerStuff')
);
$this->assertArrayHasKey(
'TheCoolest',
$result
);
$this->assertCount(
1,
$result
);

// look for an attribute not tagged onto anything, should return empty
$result = iterator_to_array(
$classIterator->attribute('NotUsed')
);
$this->assertEmpty(
$result
);

} else {
$this->markTestSkipped('This test requires PHP 8.0.1 or higher');
}
}

public function testMinimize()
{

Expand Down