Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ A powerful CakePHP plugin for discovering, caching, and querying PHP 8 attribute
- [Find by Class Name](#find-by-class-name)
- [Find by Target Type](#find-by-target-type)
- [Cache Management](#cache-management)
- [Events](#events)
- [Working with AttributeInfo](#working-with-attributeinfo)
- [Example: Building a Route Registry](#example-building-a-route-registry)
- [Console Commands](#console-commands)
Expand Down Expand Up @@ -470,6 +471,76 @@ if ($registry->isCacheEnabled()) {

The cache records file modification timestamps (`filemtime`) for discovered attributes, which you can use to detect when source files have changed and decide when to rebuild or clear the cache.

### Events

The AttributeRegistry dispatches events at key points in the discovery and caching lifecycle. You can listen to these events to implement custom functionality like performance monitoring, logging, or integration with other systems.

**Available Events:**

| Event Name | When Fired | Event Data | Description |
|------------|------------|------------|-------------|
| `AttributeRegistry.beforeDiscover` | Start of `discover()` | None | Fired before attribute discovery begins (every call) |
| `AttributeRegistry.afterDiscover` | End of `discover()` | `attributes` (AttributeCollection) | Fired after discovery completes with the result collection |
| `AttributeRegistry.beforeScan` | Before file scanning | None | Fired only when scanning files (not when using cached data) |
| `AttributeRegistry.afterScan` | After file scanning | `attributes` (AttributeCollection) | Fired after scanning completes with discovered attributes |
| `AttributeRegistry.beforeCacheClear` | Start of `clearCache()` | None | Fired before clearing the cache |
| `AttributeRegistry.afterCacheClear` | End of `clearCache()` | `success` (bool) | Fired after cache clearing with success status |

**Event Listener Example:**

```php
use Cake\Event\EventInterface;
use Cake\Event\EventManager;
use AttributeRegistry\Event\AttributeRegistryEvents;

// Listen to the afterDiscover event
EventManager::instance()->on(
AttributeRegistryEvents::AFTER_DISCOVER,
function (EventInterface $event) {
$attributes = $event->getData('attributes');
$count = $attributes->count();

Log::info("Discovered {$count} attributes");
}
);

// Track scanning performance
$scanTimer = null;

EventManager::instance()->on(
AttributeRegistryEvents::BEFORE_SCAN,
function (EventInterface $event) use (&$scanTimer) {
$scanTimer = microtime(true);
}
);

EventManager::instance()->on(
AttributeRegistryEvents::AFTER_SCAN,
function (EventInterface $event) use (&$scanTimer) {
$duration = microtime(true) - $scanTimer;
$attributes = $event->getData('attributes');

Log::info(sprintf(
'Scanned %d attributes in %.2fms',
$attributes->count(),
$duration * 1000
));
}
);

// Clear related caches when AttributeRegistry cache is cleared
EventManager::instance()->on(
AttributeRegistryEvents::AFTER_CACHE_CLEAR,
function (EventInterface $event) {
if ($event->getData('success')) {
// Clear your custom route cache, API documentation cache, etc.
Cache::delete('my_route_cache');
Cache::delete('api_docs');
}
}
);
```

### Working with AttributeInfo

Each discovered attribute is returned as an `AttributeInfo` value object:
Expand Down
57 changes: 52 additions & 5 deletions src/AttributeRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

use AttributeRegistry\Collection\AttributeCollection;
use AttributeRegistry\Enum\AttributeTargetType;
use AttributeRegistry\Event\AttributeRegistryEvents;
use AttributeRegistry\Service\AttributeParser;
use AttributeRegistry\Service\AttributeScanner;
use AttributeRegistry\Service\CompiledCache;
use AttributeRegistry\Service\PathResolver;
use AttributeRegistry\Service\PluginLocator;
use Cake\Core\Configure;
use Cake\Event\EventDispatcherInterface;
use Cake\Event\EventDispatcherTrait;

/**
* Main registry for discovering and querying PHP attributes.
Expand All @@ -27,9 +30,14 @@
* // Via dependency injection (in controllers, commands, etc.)
* public function index(AttributeRegistry $registry): Response
* ```
*
* @implements \Cake\Event\EventDispatcherInterface<\AttributeRegistry\AttributeRegistry>
*/
class AttributeRegistry
class AttributeRegistry implements EventDispatcherInterface
{
/** @use \Cake\Event\EventDispatcherTrait<\AttributeRegistry\AttributeRegistry> */
use EventDispatcherTrait;

private const CACHE_KEY = 'attribute_registry';

private static ?self $instance = null;
Expand Down Expand Up @@ -140,18 +148,29 @@ private static function createFromConfig(): self
*/
public function discover(): AttributeCollection
{
// Dispatch before discover event
$this->dispatchEvent(AttributeRegistryEvents::BEFORE_DISCOVER);

if ($this->discoveredAttributes !== null) {
return new AttributeCollection($this->discoveredAttributes);
$collection = new AttributeCollection($this->discoveredAttributes);
$this->dispatchAfterDiscover($collection);

return $collection;
}

/** @var array<\AttributeRegistry\ValueObject\AttributeInfo>|null $cached */
$cached = $this->cache->get(self::CACHE_KEY);
if ($cached !== null) {
$this->discoveredAttributes = $cached;
$collection = new AttributeCollection($this->discoveredAttributes);
$this->dispatchAfterDiscover($collection);

return new AttributeCollection($this->discoveredAttributes);
return $collection;
}

// Dispatch before scan event
$this->dispatchEvent(AttributeRegistryEvents::BEFORE_SCAN);

$attributes = [];
foreach ($this->scanner->scanAll() as $attribute) {
$attributes[] = $attribute;
Expand All @@ -160,7 +179,16 @@ public function discover(): AttributeCollection
$this->cache->set(self::CACHE_KEY, $attributes);
$this->discoveredAttributes = $attributes;

return new AttributeCollection($attributes);
$collection = new AttributeCollection($attributes);

// Dispatch after scan event
$this->dispatchEvent(AttributeRegistryEvents::AFTER_SCAN, [
'attributes' => $collection,
]);

$this->dispatchAfterDiscover($collection);

return $collection;
}

/**
Expand Down Expand Up @@ -211,9 +239,16 @@ public function findByTargetType(AttributeTargetType $type): array
*/
public function clearCache(): bool
{
$this->dispatchEvent(AttributeRegistryEvents::BEFORE_CACHE_CLEAR);

$this->discoveredAttributes = null;
$success = $this->cache->delete(self::CACHE_KEY);

$this->dispatchEvent(AttributeRegistryEvents::AFTER_CACHE_CLEAR, [
'success' => $success,
]);

return $this->cache->delete(self::CACHE_KEY);
return $success;
}

/**
Expand All @@ -238,4 +273,16 @@ public function warmCache(): bool

return true;
}

/**
* Dispatch the after discover event with the collection.
*
* @param \AttributeRegistry\Collection\AttributeCollection $collection The discovered attributes collection
*/
private function dispatchAfterDiscover(AttributeCollection $collection): void
{
$this->dispatchEvent(AttributeRegistryEvents::AFTER_DISCOVER, [
'attributes' => $collection,
]);
}
}
155 changes: 155 additions & 0 deletions src/Event/AttributeRegistryEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);

namespace AttributeRegistry\Event;

/**
* Event names dispatched by AttributeRegistry.
*
* The AttributeRegistry dispatches events at key points in the discovery and caching lifecycle.
* All events use the subject object (AttributeRegistry instance) and can access it via
* $event->getSubject().
*/
final class AttributeRegistryEvents
{
/**
* Fired before attribute discovery starts.
*
* This event is dispatched at the very start of every discover() call, regardless of
* whether the result will be served from memory cache, disk cache, or freshly scanned.
*
* Event Data: None
*
* Typical Use Cases:
* - Start performance timers for monitoring discovery duration
* - Log discovery requests for debugging
* - Initialize contextual data for subsequent events
* - Increment metrics counters
*
* Note: This event fires even when results are already cached in memory, making it
* suitable for monitoring all discovery requests.
*/
public const BEFORE_DISCOVER = 'AttributeRegistry.beforeDiscover';

/**
* Fired after attribute discovery completes.
*
* This event is dispatched at the end of every discover() call after the AttributeCollection
* has been created, regardless of the source (memory cache, disk cache, or fresh scan).
*
* Event Data:
* - 'attributes' (AttributeCollection): The discovered attributes collection with all
* filtering methods available. Use $collection->count() to get the total number.
*
* Typical Use Cases:
* - Complete performance timers started in beforeDiscover
* - Log discovery results and statistics
* - Post-process or validate discovered attributes
* - Trigger dependent cache warming operations
* - Update metrics dashboards with attribute counts
*
* Note: This event provides the same collection regardless of cache source, making it
* ideal for consistent post-processing logic.
*/
public const AFTER_DISCOVER = 'AttributeRegistry.afterDiscover';

/**
* Fired before scanning files for attributes.
*
* This event is dispatched only when attributes need to be scanned from source files,
* which occurs when:
* - No memory cache exists (first discovery in the request)
* - No disk cache exists OR cache is disabled
*
* This event does NOT fire when results are served from cache.
*
* Event Data: None
*
* Typical Use Cases:
* - Start scan-specific performance timers
* - Log when actual file scanning occurs (vs cache hits)
* - Monitor scan frequency to optimize cache strategy
* - Prepare resources needed for file scanning
*
* Relationship: Always followed by afterScan when fired. Always occurs between
* beforeDiscover and afterDiscover events.
*/
public const BEFORE_SCAN = 'AttributeRegistry.beforeScan';

/**
* Fired after scanning files for attributes completes.
*
* This event is dispatched only after successfully scanning source files for attributes.
* It fires in the same conditions as beforeScan (no cache or cache disabled).
*
* Event Data:
* - 'attributes' (AttributeCollection): The freshly scanned attributes collection before
* being cached. This is the same collection that will be passed to afterDiscover.
*
* Typical Use Cases:
* - Complete scan-specific performance timers
* - Log scan duration and file counts
* - Monitor scanning performance degradation
* - Trigger cache optimization based on scan metrics
* - Alert on unexpectedly slow scans
*
* Note: This event provides insight into scan performance separate from cache operations.
* Use this rather than afterDiscover when you specifically want to monitor file scanning.
*/
public const AFTER_SCAN = 'AttributeRegistry.afterScan';

/**
* Fired before clearing the attribute cache.
*
* This event is dispatched at the start of clearCache() before any cache files are deleted.
*
* Event Data: None
*
* Typical Use Cases:
* - Trigger clearing of related/dependent caches
* - Log cache clearing operations for audit trails
* - Backup current cache before clearing (if needed)
* - Send notifications about cache invalidation
*
* Relationship: Always followed by afterCacheClear when clearCache() is called.
*/
public const BEFORE_CACHE_CLEAR = 'AttributeRegistry.beforeCacheClear';

/**
* Fired after the attribute cache has been cleared.
*
* This event is dispatched at the end of clearCache() after attempting to delete cache files.
*
* Event Data:
* - 'success' (bool): Whether the cache was successfully cleared. True indicates all cache
* files were deleted. False indicates a failure occurred during deletion.
*
* Typical Use Cases:
* - Verify cache clearing completed successfully
* - Log cache clearing results
* - Clear related caches only if AttributeRegistry cache cleared successfully
* - Trigger cache warming operations after successful clear
* - Alert on cache clearing failures
*
* Example:
* ```php
* EventManager::instance()->on(
* AttributeRegistryEvents::AFTER_CACHE_CLEAR,
* function (EventInterface $event) {
* if ($event->getData('success')) {
* Cache::delete('my_route_cache');
* Cache::delete('api_documentation');
* }
* }
* );
* ```
*/
public const AFTER_CACHE_CLEAR = 'AttributeRegistry.afterCacheClear';

/**
* Prevent instantiation
*/
private function __construct()
{
}
}
Loading
Loading