data that provides informationWikipedia
about other data
Information about code that is not executable
/**
* @Command(name="product:create")
*/
class CreateProductCommand implements Command {
// Externally mutable, which is not good.
public static string $name = 'product:create';
// More verbose, but more flexible if some logic is needed.
public static function name(): string {
return "product:create";
}
public function run(): void {
// ...
}
}
// Could be far from the actual code
// Possibly YAML/XML file that translates to this.
function register_commands(CommandList $commands) {
$commands->add('product:create', CreateProductCommand::class);
}
@-tags
to class propertiescf: https://www.doctrine-project.org/2022/11/04/annotations-to-attributes.html
/**
* @Annotation
*/
final class Command {
public $name;
}
/**
* @Command(name="product:create")
*/
class CreateProductCommand implements Command {
public function run(): void {
// ...
}
}
// Load through reflection
$reflectionClass = new ReflectionClass(CreateProductCommand::class);
$reader = new AnnotationReader();
$annotation = $reader->getClassAnnotation(
$reflectionClass,
Command::class
);
print $annotation->name; // "product:create"
#[\Attribute]
final class Command {
public function __construct(public string $name) {}
}
#[Command(name: 'product:create')]
class CreateProductCommand implements Command {
public function run(): void {
// ...
}
}
// Load through reflection
$rClass = new ReflectionClass(CreateProductCommand::class);
$attribs = $rClass
->getAttributes(Command::class, \ReflectionAttribute::IS_INSTANCEOF);
$cmd = $attribs[0]->getInstance();
print $cmd->name; // "product:create"
$rClass = new ReflectionClass(CreateProductCommand::class);
// Get all attributes as raw data.
/** @var \ReflectionAttribute[] */
$attribs = $rClass->getAttributes(Command::class);
var_dump($attribs[0]->getName());
var_dump($attribs[0]->getArguments());
var_dump($attribs[0]->getTarget());
var_dump($attribs[0]->isRepeated());
var_dump($attribs[0]->getInstance());
string(7) "Command"
array(1) {
["name"]=>
string(14) "product:create"
}
int(1)
bool(false)
object(Command)#3 (1) {
["name"]=>
string(14) "product:create"
}
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
class OnClass {}
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class OnClassOrMethod {}
#[Foo]
#[Bar('baz')]
class Demo {
#[Beep]
private Foo $foo;
public function __construct(
#[Load(context: 'foo', bar: true)]
private readonly FooService $fooService,
#[LoadProxy(context: 'bar')]
private readonly BarService $barService,
) {}
/**
* Sets the foo.
*/
#[Poink('narf'), Narf('poink')]
public function setFoo(#[Beep] Foo $new): void
{
// ...
}
}
#[SensitiveParameter]
- Hides parameter from backtrace (Eg, passwords.)#[ReturnTypeWillChange]
- Ignore return type mismatch. (Mostly for internal.)#[AllowDynamicProperties]
- What it says. (8.2+)#[Override]
- Error if there is no parent method. (8.3+)Don't solve a problem, build a tool to solve the problem, then use it.
FromReflection*
#[\Attribute(\Attribute::TARGET_CLASS)]
class Widget implements FromReflectionClass {
public readonly string $name;
public function fromReflection(\ReflectionClass $subject): void {
$this->name ??= $subject->getShortName();
}
}
#[Widget]
class Dohicky { ... }
$analyzer = new \Crell\AttributeUtils\ClassAnalyzer();
// Analyze Dohicky with respect to Widget
$def = $analyzer->analyze(Dohicky::class, Widget::class);
$def->name; // "Dohicky"
Also FromReflectionMethod
, FromReflectionFunction
, FromReflectionProperty
, FromReflectionParameter
, FromReflectionClassConstant
ParseProperties
#[\Attribute(\Attribute::TARGET_CLASS)]
class Data implements ParseProperties {
public readonly array $props;
public function propertyAttribute(): string { return MyProperty::class; }
public function includePropertiesByDefault(): bool { return true; }
public function setProperties(array $ps): void { $this->props = $ps; }
}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MyProperty {
public function __construct(
public readonly string $column = '',
) {}
}
#[Data]
class Record {
#[MyProperty(column: 'beep')]
protected property $foo;
private property $bar;
}
$dataAttrib = $analyzer->analyze(Record::class, Data::class);
// $dataAttrib instanceof Data
// $dataAttrib->props is ['foo' => MyProperty('beep'), 'bar' => MyProperty('')]
Parse*
#[\Attribute(\Attribute::TARGET_CLASS)]
class MyClass implements ParseProperties, ParseStaticProperites,
ParseMethods, ParseStaticMethods, ParseClassConstants {}
#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)]
class MyConstant {}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MyProperty {}
#[\Attribute(\Attribute::TARGET_METHOD)]
class MyMethod implements ParseParameters {}
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MyParam {}
#[MyClass]
class Record {
#[MyConstant]
public const AConstant = 'value';
#[MyProperty]
protected string $foo;
#[MyMethod]
public function run(#[MyParam] string $key): string { ... }
}
ReadsClass
For handling defaults
#[\Attribute(\Attribute::TARGET_CLASS)]
class MyClass implements ParseProperties {
public function __construct(public readonly ?string $color = 'red') {}
// ...
}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MyProperty implements ReadsClass {
public readonly string $color;
public function __construct(?string $color = null) {
if ($color) $this->color = $color;
}
public function fromClassAttribute(MyClass $classDef): void {
$this->color ??= $classDef->color;
}
}
#[MyClass(color: 'green')]
class Record {
#[MyProperty] // Will be green.
protected string $first;
#[MyProperty(color: 'blue')] // Will be blue.
protected string $first;
}
#[\Attribute(\Attribute::TARGET_CLASS)]
class Person implements Inheritable {
public function __construct(public string $name = '') {}
}
#[Person(name: 'Jorge')]
class A {}
class B extends A {}
$attrib = $analyzer->analyze(B::class, Person::class);
print $attrib->name . PHP_EOL; // prints Jorge
#[\Attribute(\Attribute::TARGET_CLASS)]
class Manager extends Person {
public function __construct(string $name = '', public string $dept) {
parent::_construct($name);
}
}
#[Manager(name: 'Jorge', dept: 'Accounting')]
class A {}
$attrib = $analyzer->analyze(A::class, Person::class);
print $attrib->name . PHP_EOL; // prints Jorge
print $attrib->dept . PHP_EOL; // prints Accounting
Analyzer does instanceof
, so child attributes work, too
#[\Attribute(\Attribute::TARGET_CLASS)]
readonly class Person implements HasSubAttributes {
public int $age;
public function __construct(public string name = 'none') {}
public function subAttributes(): array {
return [Age::class => 'fromAge'];
}
public function fromAge(?Age $sub): void {
// Can also store the sub-attribute if you prefer.
$this->age = $sub?->age ?? 0;
}
}
#[\Attribute(\Attribute::TARGET_CLASS)]
readonly class Age {
public function __construct(public int $age = 0) {}
}
#[Person(name: 'Larry'), Age(21)]
class A {}
$attribA = $analyzer->analyze(A::class, Person::class);
print "$attribA->name, $attribA->age\n"; // prints "Larry, 21"
class B {}
$attribB = $analyzer->analyze(B::class, Person::class);
print "$attribB->name, $attribB->age\n"; // prints "none, 0"
#[Person(name: 'Larry')]
class C {}
$attribC = $analyzer->analyze(C::class, Person::class);
print "$attribC->name, $attribC->age\n"; // prints "Larry, 0"
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Field implements Excludable {
public function __construct(
public readonly bool $exclude = false,
) {}
public function exclude(): bool {
return $this->exclude;
}
}
#[ClassSettings(includeByDefault: true)]
class User {
public function __construct(
private string $loginName,
private string $displayName,
#[Field(exclude: true), SensitiveParameter]
private string $password,
) {}
}
#[\Attribute(\Attribute::TARGET_CLASS)]
readonly class Person implements HasSubAttributes {
public array $knows;
public function __construct(public string name = 'none') {}
public function subAttributes(): array {
return [Knows::class => 'fromKnows'];
}
public function fromKnows(array $knows): void { $this->knows = $knows; }
}
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
readonly class Knows implements Multivalue {
public function __construct(public string $name) {}
}
#[Person(name: 'Larry')]
#[Knows('Kai')]
#[Knows('Molly')]
class A {}
class B {}
#[\Attribute(\Attribute::TARGET_CLASS)]
readonly class Labeled implements ParseProperties {
public array $properties;
public function setProperties(array $ps): void { $this->properties ??= $ps; }
public function includePropertiesByDefault(): bool { return true; }
public function propertyAttribute(): string { return Label::class; }
}
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
readonly class Label implements SupportsScopes, Excludable {
public function __construct(
public string $name = 'None',
public ?string $language = null,
public bool $exclude = false,
) {}
public function scopes(): array {
return [$this->language];
}
public function exclude(): bool { return $this->exclude; }
}
class App {
#[Label(name: 'Installation')]
#[Label(name: 'InstalaciĆ³n', language: 'es')]
public string $install;
#[Label(name: 'Setup')]
#[Label(name: 'Configurar', language: 'es')]
#[Label(name: 'Einrichten', language: 'de')]
public string $setup;
#[Label(name: 'Einloggen', language: 'de')]
#[Label(language: 'fr', exclude: true)]
public string $login;
public string $custom;
}
$labels = $analyzer->analyze(App::class, Labeled::class);
// install = "Installation", setup = "Setup", login = "None", custom = "None"
$labels = $analyzer->analyze(App::class, Labeled::class, scopes: ['es']);
// install = "InstalaciĆ³n", setup = "Configurar", login = "None", custom = "None"
$labels = $analyzer->analyze(App::class, Labeled::class, scopes: ['fr']);
// install = "Installation", setup = "Setup", custom = "None"
#[\Attribute(\Attribute::TARGET_CLASS)]
readonly class Names implements ParseProperties {
// ...
public function propertyAttribute(): string { return FancyName::class; }
}
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)]
readonly class FancyName implements TransitiveProperty {
public function __construct(public string $name = '') {}
}
class Stuff {
protected string $bare;
#[FancyName('A happy little integer')]
protected int $namedNumber;
#[FancyName('Her Majesty Queen Elizabeth II')]
protected Person $namedPerson;
protected Person $anonymousPerson;
}
#[FancyName('I am not an object, I am a free man!')]
class Person {}
$p = $analyzer->analyze(Stuff::class, Names::class)->properties;
print $p['bare']->name; // ""
print $p['namedNumber']->name; // "A happy little integer"
print $p['namedPerson']->name; // "Her Majesty Queen Elizabeth II"
print $p['anonymousPerson']->name; // "I am not an object, I am a free man!"
Finalizable
class Info implements Finalizable, FromReflectionClass {
public readonly string $name;
public function __construct(?string $name = null) {
if ($name) $this->name = $name;
}
public function fromReflection(\ReflectionClass $r) {
if ($r->getNamespace() == '\\My\\Space') $this->name ??= $r->getShortName();
}
public function finalize() {
$this->name ??= 'Untitled';
}
}
use Crell\AttributeUtils\FuncAnalyzer;
#[MyFunc]
function beep(int $a) {}
$closure = #[MyClosure] fn(int $a) => $a + 1;
// For functions...
$analyzer = new FuncAnalyzer();
$funcDef = $analyzer->analyze('beep', MyFunc::class);
// For closures
$analyzer = new FuncAnalyzer();
$funcDef = $analyzer->analyze($closure, MyFunc::class);
ParseParameters
, Finalizable
, FromReflectionFunction
sequenceDiagram actor user as User code participant Analyzer participant RDB as ReflectionDefinitionBuilder participant AttributeParser user->>Analyzer: analyze() activate Analyzer Analyzer->>AttributeParser: construct(scopes) activate AttributeParser Analyzer->>RDB: construct(parser, self) activate RDB Analyzer->>AttributeParser: Get attribute Analyzer->>RDB: Get Properties Analyzer->>RDB: Get Static Properties Analyzer->>RDB: Get Methods Analyzer->>RDB: Get Methods Analyzer->>RDB: Get Static Methods Analyzer->>RDB: Get Enum cases Analyzer->>RDB: Get Constants deactivate AttributeParser deactivate RDB Analyzer-->>user: return deactivate Analyzer
readonly class PublicFacing {
public function __construct(private DepA $depA, private DepB $debB) {}
public function run(A $paramA, B $paramB) {
$runner = new Executor($this->depA, $this->depB, $paramA, $paramB);
return $runner->run();
}
}
readonly class Executor {
public function __construct(
public DepA $depA, public DepB $debB, private,
public A $a, public B $b) {}
public function run(): Result {
$this->foo(); // Can use all the passed dependencies
$this->a->bar($this); // Pass all the deps to A, so it can use them.
}
}
$this
around dependencies
#[ClassSettings(renameWith: new Prefix('mail_')]
class MailConfig {
protected string $host = 'smtp.example.com';
protected int $port = 25;
protected string $user = 'me';
#[Field(renameWith: Case::CAPITALIZE)]
protected string $password = 'sssh';
#[PostLoad]
private function validate(): void {
if ($port > 1024) throw new \InvalidArgumentException();
}
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class Field implements
FromReflectionProperty,
HasSubAttributes,
Excludable,
SupportsScopes,
ReadsClass,
Finalizable
use Crell\Serde\Attributes\StaticTypeMap;
#[StaticTypeMap(key: 'type', map: [
'paper' => PaperBook::class,
'ebook' => DigitalBook::class,
])]
interface Book {}
class PaperBook implements Book {}
class DigitalBook implements Book {}
class Sale {
protected Book $book;
protected float $discountRate;
}
class ListenerOne {
public function __construct(
private readonly DependencyA $depA,
) {}
public function __invoke(MyEvent $event): void { ... }
}
#[ListenerBefore(ListenerOne::class)]
class ListenerTwo {
public function __invoke(MyEvent $event): void { ... }
}
$provider->listenerService(ListenerOne::class);
$provider->listenerService(ListenerTwo::class);
class MyListeners {
#[Listener(id: 'a')]
public function onA(CollectingEvent $event): void {
$event->add('A');
}
#[ListenerPriority(priority: 5)]
public function onB(CollectingEvent $event): void {
$event->add('B');
}
#[ListenerAfter(after: 'a')]
public function onD(CollectingEvent $event): void {
$event->add('D');
}
#[ListenerPriority(priority: -5)]
public function notNormalName(CollectingEvent $event): void {
$event->add('F');
}
public function ignoredMethodThatDoesNothing(): void {
throw new \Exception('What are you doing here?');
}
}
#[Routes(path: '/app/v1', perm: 'auth user')]
class ProductController {
#[Route]
public function index() { ... }
#[Route(path: '/{$id}')]
public function get(string $id) { ... }
#[Route(method: Http::Post, perm: 'create post')]
public function post(Product $p) { ... }
#[Route(path: '/{$id}', method: Http::Delete, perm: 'delete')]
public function delete(string $id) { ... }
}
class SomeUserService {
public function __construct(
private readonly ADependency $dep,
#[Channel('user')]
private readonly LoggerInterface $logger,
) {}
}