Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attribute utils #26

Merged
merged 12 commits into from
Feb 24, 2024
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
],
"require": {
"php": "~8.1",
"crell/attributeutils": "^1.1",
"crell/ordered-collection": "v2.x-dev",
"fig/event-dispatcher-util": "^1.3",
"psr/container": "^1.0 || ^2.0",
Expand Down
137 changes: 129 additions & 8 deletions src/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,48 @@
namespace Crell\Tukio;

use Attribute;
use Crell\AttributeUtils\Finalizable;
use Crell\AttributeUtils\FromReflectionMethod;
use Crell\AttributeUtils\HasSubAttributes;
use Crell\AttributeUtils\ParseMethods;
use Crell\AttributeUtils\ParseStaticMethods;
use Crell\AttributeUtils\ReadsClass;

/**
* The main attribute to customize a listener.
*/
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Listener implements ListenerAttribute
class Listener implements ListenerAttribute, HasSubAttributes, ParseMethods, ReadsClass, Finalizable, FromReflectionMethod, ParseStaticMethods
{
/**
* @var Listener[]
*
* This is only used by the class-level attribute. When used on a method level it is ignored.
*/
public readonly array $methods;

/**
* @var Listener[]
*
* This is only used by the class-level attribute. When used on a method level it is ignored.
*/
public readonly array $staticMethods;


/** @var string[] */
public array $before = [];

/** @var string[] */
public array $after = [];
public ?int $priority = null;

public readonly bool $hasDefinition;

/**
* This is only meaningful on the method attribute.
*/
public readonly int $paramCount;

/**
* @param ?string $id
* The identifier by which this listener should be known. If not specified one will be generated.
Expand All @@ -29,13 +57,93 @@ class Listener implements ListenerAttribute
public function __construct(
public ?string $id = null,
public ?string $type = null,
) {}
) {
if ($id || $this->type) {
$this->hasDefinition = true;
}
}

public function fromReflection(\ReflectionMethod $subject): void
{
$this->paramCount = $subject->getNumberOfRequiredParameters();
if ($this->paramCount === 1) {
$params = $subject->getParameters();
// getName() isn't part of the interface, but is present. PHP bug.
// @phpstan-ignore-next-line
$this->type ??= $params[0]->getType()?->getName();
}
}

/**
* This will only get called when this attribute is on a class.
*
* @param Listener[] $methods
*/
public function setMethods(array $methods): void
{
$this->methods = $methods;
}

public function includeMethodsByDefault(): bool
{
return true;
}

public function methodAttribute(): string
{
return __CLASS__;
}

/**
* @param array<string, Listener> $methods
*/
public function setStaticMethods(array $methods): void
{
$this->staticMethods = $methods;
}

public function includeStaticMethodsByDefault(): bool
{
return true;
}

public function staticMethodAttribute(): string
{
return __CLASS__;
}


/**
* This will only get called when this attribute is used on a method.
*
* @param Listener $class
*/
public function fromClassAttribute(object $class): void
{
$this->id ??= $class->id;
$this->type ??= $class->type;
$this->priority ??= $class->priority;
$this->before ??= $class->before;
$this->after ??= $class->after;
}

public function subAttributes(): array
{
return [
ListenerBefore::class => 'fromBefore',
ListenerAfter::class => 'fromAfter',
ListenerPriority::class => 'fromPriority',
];
}

/**
* @param array<ListenerBefore> $attribs
*/
public function absorbBefore(array $attribs): void
public function fromBefore(array $attribs): void
{
if ($attribs) {
$this->hasDefinition ??= true;
}
foreach ($attribs as $attrib) {
$this->id ??= $attrib->id;
$this->type ??= $attrib->type;
Expand All @@ -46,19 +154,32 @@ public function absorbBefore(array $attribs): void
/**
* @param array<ListenerAfter> $attribs
*/
public function absorbAfter(array $attribs): void
public function fromAfter(array $attribs): void
{
if ($attribs) {
$this->hasDefinition ??= true;
}
foreach ($attribs as $attrib) {
$this->id ??= $attrib->id;
$this->type ??= $attrib->type;
$this->after = [...$this->after, ...$attrib->after];
}
}

public function absorbPriority(ListenerPriority $attrib): void
public function fromPriority(?ListenerPriority $attrib): void
{
if ($attrib) {
$this->hasDefinition ??= true;
}
$this->id ??= $attrib?->id;
$this->type ??= $attrib?->type;
$this->priority = $attrib?->priority;
}

public function finalize(): void
{
$this->id ??= $attrib->id;
$this->type ??= $attrib->type;
$this->priority = $attrib->priority;
$this->methods ??= [];
$this->hasDefinition ??= false;
}

}
5 changes: 3 additions & 2 deletions src/ListenerAfter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
namespace Crell\Tukio;

use Attribute;
use Crell\AttributeUtils\Multivalue;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class ListenerAfter implements ListenerAttribute
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ListenerAfter implements ListenerAttribute, Multivalue
{
/** @var string[] */
public array $after = [];
Expand Down
5 changes: 3 additions & 2 deletions src/ListenerBefore.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
namespace Crell\Tukio;

use Attribute;
use Crell\AttributeUtils\Multivalue;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class ListenerBefore implements ListenerAttribute
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class ListenerBefore implements ListenerAttribute, Multivalue
{
/** @var string[] */
public array $before = [];
Expand Down
2 changes: 1 addition & 1 deletion src/ListenerPriority.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Attribute;

#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class ListenerPriority implements ListenerAttribute
{
public function __construct(
Expand Down
2 changes: 1 addition & 1 deletion src/ListenerProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ protected function getServiceMethodType(string $methodName): string
{
try {
// We don't have a real object here, so we cannot use first-class-closures.
// PHPStan complains that an aray is not a callable, even though it is, because PHP.
// PHPStan complains that an array is not a callable, even though it is, because PHP.
// @phpstan-ignore-next-line
$type = $this->getParameterType([$this->serviceClass, $methodName]);
} catch (\InvalidArgumentException $exception) {
Expand Down
33 changes: 20 additions & 13 deletions src/OrderedListenerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,34 @@ public function listenerService(
$type = $this->getParameterType([$service, $method]);
}

$orderSpecified = !is_null($priority) || !empty($before) || !empty($after);

// In the special case that the service is the class name, we can
// leverage attributes.
if (class_exists($service)) {
if (!$orderSpecified && class_exists($service)) {
$listener = [$service, $method];
/** @var Listener $def */
$def = $this->getAttributeDefinition($listener);
$def = $this->classAnalyzer->analyze($service, Listener::class);
$def = $def->methods[$method];
$id ??= $def?->id ?? $this->getListenerId($listener);

// If any ordering is specified explicitly, that completely overrules any
// attributes.
if (!is_null($priority) || $before || $after) {
$def->priority = $priority;
$def->before = $before;
$def->after = $after;
}
return $this->listener($this->makeListenerForService($service, $method), priority: $def->priority, before: $def->before, after: $def->after, id: $id, type: $type);
return $this->listeners->add(
item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type),
id: $id,
priority: $def->priority,
before: $def->before,
after: $def->after
);
}


$id ??= $service . '-' . $method;
return $this->listener($this->makeListenerForService($service, $method), priority: $priority, before: $before, after: $after, id: $id, type: $type);
$id ??= $service . '::' . $method;
return $this->listeners->add(
item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type),
id: $id,
priority: $priority,
before: $before,
after: $after,
);
}

/**
Expand Down
14 changes: 7 additions & 7 deletions src/OrderedProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,17 @@ public function addListenerServiceAfter(string $after, string $service, string $
/**
* Registers all listener methods on a service as listeners.
*
* A method on the specified class is a listener if:
* - It is public.
* A public method on the specified class is a listener if either of these is true:
* - It's name is in the form on*. onUpdate(), onUserLogin(), onHammerTime() will all be registered.
* - It has a Listener/ListenerBefore/ListenerAfter attribute.
* - It has a Listener/ListenerBefore/ListenerAfter/ListenerPriority attribute.
*
* The event type the listener is for will be derived from the type declaration in the method signature.
* The event type the listener is for will be derived from the type declaration in the method signature,
* unless overriden by an attribute..
*
* @param class-string $class
* The class name to be registered as a subscriber.
* @param string $service
* The name of a service in the container.
* @param null|string $service
* The name of a service in the container. If not specified, it's assumed to be the same as the class.
*/
public function addSubscriber(string $class, string $service): void;
public function addSubscriber(string $class, ?string $service = null): void;
}
15 changes: 5 additions & 10 deletions src/ProviderBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,17 @@ public function listenerService(
$type = $this->getParameterType([$service, $method]);
}

$orderSpecified = !is_null($priority) || !empty($before) || !empty($after);

// In the special case that the service is the class name, we can
// leverage attributes.
if (class_exists($service)) {
if (!$orderSpecified && class_exists($service)) {
$listener = [$service, $method];
/** @var Listener $def */
$def = $this->getAttributeDefinition($listener);
$def = $this->classAnalyzer->analyze($service, Listener::class);
$def = $def->methods[$method];
$id ??= $def?->id ?? $this->getListenerId($listener);

// If any ordering is specified explicitly, that completely overrules any
// attributes.
if (!is_null($priority) || $before || $after) {
$def->priority = $priority;
$def->before = $before;
$def->after = $after;
}

$entry = new ListenerServiceEntry($service, $method, $type);
return $this->listeners->add($entry, $id, priority: $def->priority, before: $def->before, after: $def->after);
}
Expand Down
Loading
Loading