Skip to content
Some core and sharing classes for Windwalker components.

Attributes ​

PHP attributes attach metadata to classes, properties, methods, and constants. Reading them with raw Reflection takes several steps: fetch the list, filter by name, walk up the parent classes, and instantiate each match. AttributesAccessor performs these steps in one call, and it accepts a class name, an object, or a reflector. Routing tables, validation rules, and framework components all use this pattern.

AttributesAccessor ​

Minimal Setup ​

Define an attribute class, apply it, and read it back. No configuration is needed:

php
use Windwalker\Utilities\Attributes\AttributesAccessor;

#[\Attribute(\Attribute::TARGET_CLASS)]
class Route
{
    public function __construct(public string $path) {}
}

#[Route('/users')]
class UserController {}

$route = AttributesAccessor::getFirstAttributeInstance(
    UserController::class,
    Route::class,
);

echo $route->path; // /users

getFirstAttributeInstance() returns the attribute as an object, or null when the class has no matching attribute.

Showcase: Building a Route Table ​

The following example stores a route on each controller method, then builds a table from the methods and calls one of them:

php
use Windwalker\Utilities\Attributes\AttributesAccessor;

#[\Attribute(\Attribute::TARGET_METHOD)]
class Get
{
    public function __construct(public string $path) {}
}

class PostController
{
    #[Get('/posts')]
    public function index(): string
    {
        return 'list';
    }

    #[Get('/posts/show')]
    public function show(): string
    {
        return 'one post';
    }
}

$table = [];

foreach (
    AttributesAccessor::getMembersWithAttribute(
        PostController::class,
        Get::class,
    ) as [$method, $attr]
) {
    $table[$attr->newInstance()->path] = $method->getName();
}

$controller = new PostController();

echo $controller->{$table['/posts']}(); // list

getMembersWithAttribute() yields each member that carries the attribute, as a reflector and its ReflectionAttribute. Call newInstance() on the ReflectionAttribute to read the attribute's arguments.

Reading Class Attributes ​

The lookup functions accept the same three inputs:

  • A class name, such as UserController::class.
  • An object, such as new UserController().
  • A reflector, such as new ReflectionProperty(...).

getFirstAttribute() returns a ReflectionAttribute, and getFirstAttributeInstance() returns the attribute object. Both return null when nothing matches.

Class lookups include attributes from parent classes. Pass ReflectionAttribute::IS_INSTANCEOF as the third argument to also match subclasses of the attribute:

php
AttributesAccessor::getAttributesFromAny(
    ChildController::class,
    Route::class,
    \ReflectionAttribute::IS_INSTANCEOF,
);

Attribute names are compared case-insensitively when you use runAttributeIfExists().

Members of a Class ​

getMembersWithAttribute() scans constants, properties, and methods of a class, and the cases of an enum. The generator yields an array, keyed by the member's name:

php
foreach (
    AttributesAccessor::getMembersWithAttribute(
        UserController::class,
        Route::class,
        0,
        \ReflectionMethod::class,
    ) as $name => [$method, $attr]
) {
    // Only methods are considered.
}

The fourth argument filters the members. It accepts a reflector class name, such as \ReflectionMethod::class, or a closure that receives the member and the attribute and returns true to keep the match.

getFirstMemberWithAttribute() takes the same arguments and returns the first match as [$member, $attribute], or null.

Running a Handler ​

runAttributeIfExists() calls a handler for each matching attribute. The handler receives the attribute object and the source value. The method returns how many attributes matched:

php
$count = AttributesAccessor::runAttributeIfExists(
    UserController::class,
    Route::class,
    function (Route $route, string $source): void {
        // Register $route for $source.
    },
);

This is useful when you want to act on every match, not only the first.

Scanning a Directory ​

scanDirAndRunAttributes() reads all PHP files in a directory, loads each class, and runs a handler for the classes that carry the attribute. It returns the match count for each class name:

php
$results = AttributesAccessor::scanDirAndRunAttributes(
    Route::class,
    __DIR__ . '/Controller',
    'App\\Controller',
    fn (Route $route, string $class, $file) => register($route, $class),
);

INFO

This method needs the windwalker/filesystem package. Without it, the method throws a DomainException.

Trait Options ​

A trait often needs settings that differ for each class that uses it. TraitOptions is a class-level attribute that stores those settings, keyed by trait name. A trait reads them with TraitOptions::getOption():

php
use Windwalker\Utilities\Attributes\TraitOptions;

#[TraitOptions(['Cacheable' => ['ttl' => 300]])]
class ProductService
{
    // use Cacheable;
}

TraitOptions::getOption(ProductService::class, 'Cacheable', 'ttl');
// 300

getOptions() returns all options of one trait, and getAllOptions() returns the options of every trait. Missing values return null or an empty array.

INFO

The results are cached for each class in the current process, so the attribute is read only once per class.

The Prop Attribute ​

Prop is a marker for properties. It has no methods and no arguments. The Edge component reads it to recognize the properties of a component class. Apply it to a property, and find marked properties with getMembersWithAttribute():

php
#[Prop]
public string $title = '';

Dump Attributes ​

Debug output, logs, and arrays built for display often include whole objects. Those objects may hold credentials, caches, or internal state that should not appear in the output. The reverse also happens: a protected property may be exactly what you need to see while debugging. The Expose, Invisible, and Transient attributes let a class decide which of its properties appear in these dumps, without changing the code that creates the dump.

Dump Minimal Setup ​

Mark a property with Invisible, then pass the object to one of the dump helper functions in the Windwalker namespace:

php
use Windwalker\Utilities\Attributes\Invisible;

class User
{
    public string $name = 'alice';

    #[Invisible]
    public string $cache = 'warm';
}

\Windwalker\get_object_dump_values(new User());
// ['name' => 'alice']

The helpers are plain functions, so there is no container or service to set up.

Showcase: Dumping an Account ​

The following class exposes its role in dumps, hides its password, and keeps a token out of every dump:

php
use ReflectionProperty;
use Windwalker\Utilities\Attributes\Expose;
use Windwalker\Utilities\Attributes\Invisible;
use Windwalker\Utilities\Attributes\Transient;
use Windwalker\Utilities\TypeCast;

final class Account
{
    public string $email = '[email protected]';

    #[Expose]
    protected string $role = 'editor';

    #[Invisible]
    public string $password = 'secret';

    #[Transient]
    public string $sessionToken = 'abc123';
}

$account = new Account();

$dump = \Windwalker\get_object_dump_values($account);
// ['email' => '[email protected]', 'role' => 'editor']

$public = TypeCast::toArray(
    $account,
    false,
    false,
    ReflectionProperty::IS_PUBLIC,
);
// ['email' => '[email protected]']

The protected role appears only because of Expose. The password and the token are absent from both results.

The Three Attributes ​

Each attribute applies to a property. They differ in what they change:

  • Expose: includes a protected or private property in the dump.
  • Invisible: excludes a property from the dump, even when it is public.
  • Transient: extends Invisible, so it excludes the property the same way. Use it for state that should never be dumped, such as a cache.

Subclasses of these attributes work the same way, because the check includes subclasses of Expose and Invisible.

TIP

Use Invisible or Transient for secrets and caches. Using Expose on a private property is a deliberate choice, so keep it for values that are safe to show.

Which Properties Are Included ​

Without Expose, the helpers skip protected and private properties. Public properties are included unless Invisible is present.

An uninitialized typed property is skipped until it is assigned. After assignment, it appears in the next dump:

php
class Profile
{
    public ?string $bio;
}

$profile = new Profile();

\Windwalker\get_object_dump_values($profile); // []

$profile->bio = 'Hello';

\Windwalker\get_object_dump_values($profile); // ['bio' => 'Hello']

Filtering the Dump ​

The second argument of both helpers is a filter. It accepts ReflectionProperty flags, such as ReflectionProperty::IS_PUBLIC. null, the default, applies no filter:

php
\Windwalker\get_object_dump_values(
    $account,
    \ReflectionProperty::IS_PUBLIC,
);

get_object_dump_props() takes the same arguments, but returns the ReflectionProperty objects instead of the values.

Where the Attributes Apply ​

The attributes are read only by the dump helpers. Two paths use them:

  • The helper functions get_object_dump_values() and get_object_dump_props().
  • TypeCast::toArray(), when you pass a $filter as its fourth argument.

Without a filter, TypeCast::toArray() returns the result of get_object_vars(), which ignores these attributes.

INFO

Native functions, such as json_encode() and serialize(), do not read these attributes. Use a dedicated serializer or a DTO when a value must be excluded from output of that kind.

  • Enum Metadata: titles, icons, colors, and other metadata on enum cases.

Released under the MIT License.