Skip to content

Commit

Permalink
Merge pull request #26 from Crell/attribute-utils
Browse files Browse the repository at this point in the history
Attribute utils
  • Loading branch information
Crell authored Feb 24, 2024
2 parents 3821d1d + 2b14d49 commit 02ae16a
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 201 deletions.
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

0 comments on commit 02ae16a

Please sign in to comment.