定制 componenta/interceptor 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

componenta/interceptor

Composer 安装命令:

composer require componenta/interceptor

包简介

Attribute-driven method interception for Componenta

README 文档

README

PHP 8.4+ License MIT Tests MSI

Middleware-style interceptor pipeline for PHP callables. Wrap any function, method or closure with cross-cutting logic (logging, caching, transactions, authorization, serialization) declared either via pipe() or method-level attributes.

Русская документация

Installation

composer require componenta/interceptor

Requirements

  • PHP 8.4+
  • psr/container
  • componenta/di (CallableExecutorInterface, FactoryInterface, ParametersResolver)

Related Packages

Package Why it matters here
componenta/di Invokes callables and resolves missing parameters before interceptors run.
componenta/reflection Reads callable reflection and method attributes lazily.
componenta/config Registers context factories and attribute interceptors.
componenta/interceptor-app Compiles interceptor attributes into application cache.
componenta/pipeline Similar chain idea for PSR-15 HTTP middleware; this package wraps arbitrary PHP callables.
  • componenta/reflection (lazy reflector resolution)
  • componenta/config (optional — ConfigProvider integration)

Quick Start

use Componenta\DI\CallableExecutorInterface;
use Componenta\Interceptor\CallbackInterceptorFactory;
use Componenta\Interceptor\InterceptingExecutor;

$executor = new InterceptingExecutor(
    $container->get(CallableExecutorInterface::class),
    CallbackInterceptorFactory::around(
        before: fn ($ctx) => $ctx->withAttribute('started', microtime(true)),
        after:  fn ($result, $ctx) => ['result' => $result, 'ms' => (microtime(true) - $ctx->getAttribute('started')) * 1000],
    ),
);

$result = $executor->call([$controller, 'handle'], ['id' => 42]);

Core Concepts

Interceptor

A class implementing InterceptorInterface. Receives the execution context and a continuation handler; may act before/after, short-circuit, or transform the result.

interface InterceptorInterface
{
    public function intercept(
        CallableContextInterface $context,
        ContextHandlerInterface $handler,
    ): mixed;
}

Context

Immutable object carrying the callable, its parameters, arbitrary attributes, and a lazily-resolved reflector. Mutators return new instances:

$context = $context
    ->withParameter('userId', 42)
    ->withAttribute('trace.id', $traceId);

Pipeline

InterceptingExecutor composes interceptors into a pre-built chain on first use. Execution order is FIFO — the first registered interceptor is outermost (runs first in the call direction, last on unwind):

$executor = new InterceptingExecutor($base, $auth, $logger, $cache);

// Single invocation, one pass through the chain:
//
//   auth.before → logger.before → cache.before → callable
//                                              → cache.after → logger.after → auth.after
//
// Each interceptor's intercept() is called exactly once. Work placed before
// $handler->handle() runs on the way in; work after it runs on unwind.

pipe() returns a new immutable pipeline:

$withTx = $executor->pipe($transactionInterceptor);

Short-circuit

An interceptor may return without invoking the handler (auth rejections, cache hits, maintenance screens). The pipeline stops, and the value bubbles back through outer interceptors.

Attributes

Declare interceptors on methods via #[Intercept]:

use Componenta\Interceptor\Attribute\Intercept;

final class UserController
{
    #[Intercept(CacheInterceptor::class, ['ttl' => 300])]
    #[Intercept(AuthInterceptor::class)]
    public function show(int $id): User { /* ... */ }
}

Resolution is driven by AttributeInterceptor (register it once in your pipeline). The interceptor instance is built via FactoryInterface with the declared params. Attributes are read as layers wrapped around the method, from outside in — the topmost attribute is the outermost layer (enters first, returns last), the bottommost attribute is closest to the method body.

Put entry-side interceptors (authorization, rate limits, caching gates) above result-side ones (response formatting, serialization, pagination). The method's return value flows outward through the inner layers first, so a serializer placed below a response wrapper gets the raw value and passes the serialized string up to the wrapper:

#[Respond(200, 'application/json')]      // outermost — wraps the final string in a PSR-7 response
#[Serialize(context: [...])]             // innermost — receives the raw return value first
public function show(int $id): User { /* ... */ }

Attribute classes can also implement InterceptorInterface directly — they are instantiated through native PHP attribute construction:

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final readonly class WrapJson implements InterceptorInterface
{
    public function intercept($ctx, $handler): mixed
    {
        return json_encode($handler->handle($ctx));
    }
}

Scopes

Restrict where an interceptor runs by implementing ScopedInterface on the attribute or on the interceptor instance:

use Componenta\Interceptor\{Scope, ScopedInterface};

final class RespondInterceptor implements InterceptorInterface, ScopedInterface
{
    public function getScopes(): array { return [Scope::HTTP]; }
    // ...
}

The integrator signals the current scope by setting a context attribute before the pipeline runs:

$context = $context->withAttribute(ScopedInterface::SCOPE_ATTRIBUTE, Scope::HTTP);

Attribute-level scope takes priority over instance-level scope. Interceptors without either ScopedInterface always match.

Built-in scopes: HTTP, CONSOLE, GRPC, QUEUE, WEBSOCKET. Custom scopes are arbitrary strings.

Callback Interceptors

Build interceptors from closures without dedicated classes:

use Componenta\Interceptor\CallbackInterceptorFactory as F;

$logger   = F::before(fn ($ctx) => $log->info('calling ' . $ctx->reflector->name));
$envelope = F::after(fn ($result) => ['data' => $result]);
$recover  = F::catch(fn (\Throwable $e) => ['error' => $e->getMessage()]);
$cleanup  = F::finally(fn () => $this->releaseLock());

$tracer = F::around(
    before: fn ($ctx) => $ctx->withAttribute('t0', microtime(true)),
    after:  function ($result, $ctx) use ($log) {
        $log->info(sprintf('%.2fms', (microtime(true) - $ctx->getAttribute('t0')) * 1000));
        return $result;
    },
);

Parameter Resolution

Register ParameterResolvingInterceptor to enrich the callable's parameters through DI before downstream interceptors see them:

new InterceptingExecutor(
    $container->get(CallableExecutorInterface::class),
    new ParameterResolvingInterceptor($parametersResolver), // outermost — runs first
    $container->get(AttributeInterceptor::class),
);

This lets attribute interceptors read resolved arguments (e.g., $ctx->parameters for cache keys).

Caching

AttributeInterceptor caches attribute resolution on two levels:

  1. Candidates per signature#[Intercept] instances are created once per method and reused.
  2. Composed chains per terminal — stored in a WeakMap keyed by the terminal handler; innermost link holds the terminal weakly, so GC reclaims entries when the terminal goes out of scope (e.g., when pipe() discards an old pipeline).

No configuration required — caching is always on. Closures bypass the cache (no stable signature).

Container Wiring

Register the module's ConfigProvider in your application:

new \Componenta\Interceptor\ConfigProvider();

It binds CallableContextFactory and AttributeInterceptor via invokable factories.

License

MIT

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 3
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-14

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固