Skip to content

API Allow to filter for classes which are annoted by an attribute - #11997

Open
beerbohmdo wants to merge 1 commit into
silverstripe:6from
beerbohmdo:feat/attribute-support
Open

beerbohmdo wants to merge 1 commit into
silverstripe:6from
beerbohmdo:feat/attribute-support

Conversation

@beerbohmdo

@beerbohmdo beerbohmdo commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Implements #11996

Description

Add support for attributes to the class manifest. So that you get all classes which are annotated by a given attribute.

I also added something which bothers me for sometime: You currently can't use implementorsOf for interfaces which extends another interface :/. You have to know the child interfaces and call implementsOf manually for all child interfaces ...

Manual testing steps

#[Attribute(Attribute::TARGET_CLASS)]
class CustomAttribute {}

#[CustomAttribute]
class MyClass {}

var_dump(ClassInfo::getAttributesForClass(CustomAttribute::class));

Issues

Pull request checklist

  • The target branch is correct
  • All commits are relevant to the purpose of the PR (e.g. no debug statements, unrelated refactoring, or arbitrary linting)
    • Small amounts of additional linting are usually okay, but if it makes it hard to concentrate on the relevant changes, ask for the unrelated changes to be reverted, and submitted as a separate PR.
  • The commit messages follow our commit message guidelines
  • The PR follows our contribution guidelines
  • Code changes follow our coding conventions
  • This change is covered with tests (or tests aren't necessary for this change)
  • Any relevant User Help/Developer documentation is updated; for impactful changes, information is added to the changelog for the intended release
  • CI is green

@beerbohmdo

beerbohmdo commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I forgot to add tests, I will provide them.

/edit: Done

@beerbohmdo
beerbohmdo force-pushed the feat/attribute-support branch from f448972 to e43af19 Compare July 17, 2026 10:41
@beerbohmdo beerbohmdo mentioned this pull request Jul 17, 2026
4 tasks done
@beerbohmdo
beerbohmdo force-pushed the feat/attribute-support branch 3 times, most recently from e873239 to a187d79 Compare July 17, 2026 13:18
@beerbohmdo

Copy link
Copy Markdown
Contributor Author

The CI fails but I am not sure if that is my fault.

@beerbohmdo
beerbohmdo force-pushed the feat/attribute-support branch from a187d79 to af94d16 Compare September 11, 2026 10:04

@ChloeSartorelli ChloeSartorelli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added something which bothers me for sometime: You currently can't use implementorsOf for interfaces which extends another interface :/. You have to know the child interfaces and call implementsOf manually for all child interfaces ...

Please split this into at least a separate commit if not a separate PR. It's a completely different concern that should ideally be reviewed on its own merits.

Comment thread src/Core/ClassInfo.php Outdated
/**
* Get the list of classes which are annonated by a given attribute
*
* @param class-string $attribute Name of a attribute class or an interface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "or an interface"? Isn't this about attributes specifically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attributes are classes and these can have interfaces.

When you define custom attributes you normally use a composision pattern instead of a subclass pattern.

For example if you want to create a Validator attribute, you would create a Validator interface and than Attributes for Max, Min, MaxLength, MinLength, etc.

interface Validator
{
    public function validate(ValidationResult $result, string $name, mixed $value): void;
}

#[Attribute(Attribute::TARGET_PARAMETER|Attribute::TARGET_PROPERTY)]
final class Max implements Validator
{
    public function __construct(
        public readonly int $max,
        public readonly ?string $message = null,
    ) {
    }

    public function validate(ValidationResult $result, string $name, mixed $value): void
    {
        if ($value > $this->max) {
            $result->addFieldError(
                $name,
                $this->message ?? $name . ' must be less than or equal to ' . $this->max
            );
        }
    }
}

#[Attribute(Attribute::TARGET_PARAMETER|Attribute::TARGET_PROPERTY)]
final class MaxLength implements Validator
{
    public function __construct(
        public readonly int $maxLength,
        public readonly ?string $message = null
    ) {
    }

    public function validate(ValidationResult $result, string $name, mixed $value): void
    {
        if (strlen($value) > $this->maxLength) {
            $result->addFieldError(
                $name,
                $this->message ?? $name . ' must not exceed ' . $this->maxLength . ' characters'
            );
        }
    }
}

Comment thread src/Core/ClassInfo.php Outdated
* Get the list of classes which are annonated by a given attribute
*
* @param class-string $attribute Name of a attribute class or an interface
* @param bool $instanceOf Should subclasses of the Attribute be included?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$allowSubclasses seems like a more directly applicable name for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instanceOf is the name php uses in Reflection 🤷, so I kept it.

Comment thread src/Core/ClassInfo.php Outdated
* @param "classes"|"interfaces"|"traits"|"enums" $type Which type of structure should returned, defaults to "classes"
* @return array
*/
public static function annotatedBy(string $attribute, bool $instanceOf = true, string $type = 'classes'): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static function annotatedBy(string $attribute, bool $instanceOf = true, string $type = 'classes'): array
public static function classesWithAttribute(string $attribute, bool $instanceOf = true, string $type = 'classes'): array

I think this method name is more immediately applicable - if I don't remember the method name but know I want to get classes with this attribute, I'll likely type ClassInfo::attribute and let my IDE find the method for me.

Comment thread src/Core/ClassInfo.php Outdated
*
* @param class-string $attribute Name of a attribute class or an interface
* @param bool $instanceOf Should subclasses of the Attribute be included?
* @param "classes"|"interfaces"|"traits"|"enums" $type Which type of structure should returned, defaults to "classes"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a concept we have elsewhere in ClassInfo - can you please explain why we're including it here? Seems a bit outside the existing patterns.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically all these types can have Attributes ... but normally you would only need it for classes.

The alternative would be a method for every type which only differs in one keyword and that for a use case which may never be needed.

Comment thread src/Core/ClassInfo.php Outdated
* @param class-string $attribute Name of a attribute class or an interface
* @param bool $instanceOf Should subclasses of the Attribute be included?
* @param "classes"|"interfaces"|"traits"|"enums" $type Which type of structure should returned, defaults to "classes"
* @return array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return array

We don't need PHPDocs that just repeat exactly what the method signature says.

Comment thread src/Core/ClassInfo.php Outdated
Comment on lines +222 to +225
* @param string $className
* @param string $attributeName
* @param bool $instanceOf
* @return array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param string $className
* @param string $attributeName
* @param bool $instanceOf
* @return array

Please give this method a PHPDoc description so the API docs explain what this method does.

@beerbohmdo

Copy link
Copy Markdown
Contributor Author

I also added something which bothers me for sometime: You currently can't use implementorsOf for interfaces which extends another interface :/. You have to know the child interfaces and call implementsOf manually for all child interfaces ...

Please split this into at least a separate commit if not a separate PR. It's a completely different concern that should ideally be reviewed on its own merits.

I use this internally in the getAnnotatedBy method. I can split that, but than this PR would rely on the other.

@beerbohmdo
beerbohmdo force-pushed the feat/attribute-support branch 17 times, most recently from 4cb28ea to f5d1099 Compare September 29, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants