PHPackages                             grzegorzdrozd/ext-apa - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. grzegorzdrozd/ext-apa

ActivePhp-ext[Utility &amp; Helpers](/categories/utility)

grzegorzdrozd/ext-apa
=====================

AfterParseAction - automatically call PHP code when attributes are detected during class loading

v1.0.0(2mo ago)42AGPL-3.0-onlyPHPPHP ^8.2

Since May 15Pushed 2mo agoCompare

[ Source](https://github.com/GrzegorzDrozd/ext-apa)[ Packagist](https://packagist.org/packages/grzegorzdrozd/ext-apa)[ RSS](/packages/grzegorzdrozd-ext-apa/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependenciesVersions (2)Used By (0)

AfterParseAction
================

[](#afterparseaction)

A PHP extension that calls your code when `#[\AfterParseAction]` appears on a class, method, or function. The call happens automatically during class loading. No reflection needed. No container compilation. No cache warmup.

Why?
----

[](#why)

Attributes in PHP are passive. You annotate a method with `#[Route('/users')]` but nothing happens until some framework code scans every class with reflection and acts on what it finds. That scanning is a compiler pass, a container build step, or a cache warmup command.

This extension removes that step. Put `#[\AfterParseAction]` on a method, tell it what to call, and the extension does the rest when the class loads.

Quick Example
-------------

[](#quick-example)

```
class Router {
    public static array $routes = [];

    public static function add(string $class, string $method, string $path, string $httpMethod, ...$args): void {
        self::$routes[$path][$httpMethod] = [$class, $method, $args];
    }
}

class UserController {
    #[\AfterParseAction([Router::class, 'add'], path: '/users', httpMethod: 'GET')]
    public function list() {}

    #[\AfterParseAction([Router::class, 'add'], path: '/users/{id}', httpMethod: 'DELETE')]
    public function delete(int $id) {}
}
```

When `UserController` loads (via `require`, autoload, whatever), the extension calls:

```
Router::add('UserController', 'list', path: '/users', httpMethod: 'GET');
Router::add('UserController', 'delete', path: '/users/{id}', httpMethod: 'DELETE');
```

That's it. Routes are on a list. No scanning needed.

More Examples
-------------

[](#more-examples)

### Event Listeners

[](#event-listeners)

```
class EventDispatcher {
    public static array $listeners = [];

    public static function addListener(string $class, string $method, string $event, int $priority = 0): void {
        self::$listeners[$event][] = ['handler' => [$class, $method], 'priority' => $priority];
    }
}

class OrderSubscriber {
    #[\AfterParseAction([EventDispatcher::class, 'addListener'], event: 'order.created', priority: 10)]
    public function onOrderCreated(OrderEvent $event) {}

    #[\AfterParseAction([EventDispatcher::class, 'addListener'], event: 'order.shipped')]
    public function onOrderShipped(OrderEvent $event) {}
}
```

### Console Commands

[](#console-commands)

```
#[\AfterParseAction([CommandRegistry::class, 'add'], name: 'app:import', description: 'Import data from CSV')]
class ImportCommand {
    public function execute(): int { /* ... */ }
}
```

### Middleware

[](#middleware)

```
class SecurityMiddleware {
    #[\AfterParseAction([MiddlewareStack::class, 'add'], name: 'csrf', priority: 10)]
    public function csrf(Request $request): Response {}

    #[\AfterParseAction([MiddlewareStack::class, 'add'], name: 'auth', priority: 20)]
    public function auth(Request $request): Response {}
}
```

### The Bootstrap

[](#the-bootstrap)

```
// Your entire "compiler pass":
foreach (glob('src/Controller/*.php') as $file) require $file;
foreach (glob('src/Listener/*.php') as $file) require $file;

// Done. Everything is registered.
$routes = Router::$routes;
$listeners = EventDispatcher::$listeners;
```

You still need to load every class. Autoloading won't help here since it only triggers when a class is first referenced. A glob loop, a class map, or `composer`'s `classmap` autoload directive all work fine.

Symfony Standalone Components
-----------------------------

[](#symfony-standalone-components)

Symfony's routing, event-dispatcher and console components work without the full framework. But wiring them requires manual `addRoute()`, `addListener()`, `addCommand()` calls. Symfony's own attributes (`#[AsCommand]`, `#[AsEventListener]`) only work with the DI container.

APA gives you attribute-based registration with standalone components. No container, no compiler passes, no `cache:clear`.

### Routing

[](#routing)

```
// bootstrap.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

class Routes {
    public static RouteCollection $collection;

    public static function add(string $class, string $method, string $path, string $httpMethod): void {
        self::$collection->add(
            "$class::$method",
            new Route($path, ['_controller' => [$class, $method], '_method' => $httpMethod])
        );
    }
}
Routes::$collection = new RouteCollection();

foreach (glob('src/Controller/*.php') as $file) require $file;
// Routes::$collection is now populated. Pass it to UrlMatcher.
```

```
// src/Controller/UserController.php
class UserController {
    #[\AfterParseAction([Routes::class, 'add'], path: '/users', httpMethod: 'GET')]
    public function list(): Response { /* ... */ }

    #[\AfterParseAction([Routes::class, 'add'], path: '/users/{id}', httpMethod: 'GET')]
    public function show(int $id): Response { /* ... */ }
}
```

Without APA you'd write this by hand:

```
$routes->add('user_list', new Route('/users', ['_controller' => [UserController::class, 'list']]));
$routes->add('user_show', new Route('/users/{id}', ['_controller' => [UserController::class, 'show']]));
// ... for every route
```

### EventDispatcher

[](#eventdispatcher)

```
// bootstrap.php
use Symfony\Component\EventDispatcher\EventDispatcher;

class Events {
    public static EventDispatcher $dispatcher;

    public static function listen(string $class, string $method, string $event, int $priority = 0): void {
        self::$dispatcher->addListener($event, [new $class(), $method], $priority);
    }
}
Events::$dispatcher = new EventDispatcher();

foreach (glob('src/Listener/*.php') as $file) require $file;
```

```
// src/Listener/OrderListener.php
class OrderListener {
    #[\AfterParseAction([Events::class, 'listen'], event: 'order.created', priority: 10)]
    public function onCreated(OrderEvent $event): void { /* ... */ }

    #[\AfterParseAction([Events::class, 'listen'], event: 'order.shipped')]
    public function onShipped(OrderEvent $event): void { /* ... */ }
}
```

### Console

[](#console)

```
// bin/console
use Symfony\Component\Console\Application;

class Commands {
    public static Application $app;

    public static function register(string $class, ?string $method, string $name, string $description = ''): void {
        $command = new $class();
        $command->setName($name);
        $command->setDescription($description);
        self::$app->add($command);
    }
}
Commands::$app = new Application();

foreach (glob('src/Command/*.php') as $file) require $file;
Commands::$app->run();
```

```
// src/Command/ImportCommand.php
use Symfony\Component\Console\Command\Command;

#[\AfterParseAction([Commands::class, 'register'], name: 'app:import', description: 'Import data')]
class ImportCommand extends Command {
    protected function execute(InputInterface $input, OutputInterface $output): int {
        $output->writeln('Importing...');
        return Command::SUCCESS;
    }
}
```

Without APA (or with Symfony's `#[AsCommand]` which needs the DI container):

```
$app->add(new ImportCommand());
// For every command in your app
```

### Use as a build step

[](#use-as-a-build-step)

These examples collect data into static arrays at runtime, but you can also use them as a compiler pass. Load all classes, collect the routes/listeners/commands, then dump the result to a PHP file:

```
// Commands.php
class Commands
{
    public static array $commands = [];

    public static function register(string $class, ?string $method, string $name, string $description = ''): void {
        self::$commands[] = [
            'class'=>$class,
            'method'=>$method,
            'name'=>$name,
            'description'=>$description
        ];
    }
};

// TestCommand.php
use Symfony\Component\Console\Command\Command;

#[\AfterParseAction([Commands::class, 'register'], name: 'app:test')]
class TestCommand extends Command {

}

// build.php
use Symfony\Component\Console\Command\Command;

require_once '../vendor/autoload.php';

foreach (glob('Commands/*.php', GLOB_BRACE) as $file) {
    require_once $file;
}

$file = "
