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:
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; // /usersgetFirstAttributeInstance() 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:
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']}(); // listgetMembersWithAttribute() 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:
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:
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:
$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:
$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():
use Windwalker\Utilities\Attributes\TraitOptions;
#[TraitOptions(['Cacheable' => ['ttl' => 300]])]
class ProductService
{
// use Cacheable;
}
TraitOptions::getOption(ProductService::class, 'Cacheable', 'ttl');
// 300getOptions() 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():
#[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:
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:
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: extendsInvisible, 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:
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:
\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()andget_object_dump_props(). TypeCast::toArray(), when you pass a$filteras 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.
Related
- Enum Metadata: titles, icons, colors, and other metadata on enum cases.