Skip to content

Package-Level Middleware

Several internal Laravel packages provide middleware functionality for caching, APM tracing, error tracking, and AOP-style method decorators. These packages are located in packages/libs/.

PackageLocationPurpose
laravel-http-cache-controlpackages/libs/laravel-http-cache-control/HTTP caching headers
laravel-apmpackages/libs/laravel-apm/OpenTelemetry tracing
laravel-sentrypackages/libs/laravel-sentry/Error tracking & sanitization
laravel-aoppackages/libs/laravel-aop/Aspect-oriented programming
method-delegatepackages/libs/method-delegate/Base delegate for AOP attributes

Location: packages/libs/laravel-http-cache-control/

Adds HTTP cache control headers and supports ETag-based caching with 304 responses.

  • Adds Cache-Control header based on config
  • Computes weak ETags using base64-encoded MD5
  • Returns 304 Not Modified for matching ETags
  • Handles public/private cache directives
  • Adds Vary header management
  • Skips OPTIONS requests
class SetCacheHeaders
{
private const string CACHE_CONTROL_PRIVATE = 'private';
private const string CACHE_CONTROL_PUBLIC = 'public';
private const string CACHE_CONTROL_NO_CACHE = 'no-cache';
public function handle($request, Closure $next)
{
$response = $next($request);
if (!$request->isMethod(Request::METHOD_OPTIONS)) {
if ($request->isMethodCacheable()) {
if ($response instanceof Response || $response instanceof JsonResponse) {
// Compute weak ETag using base64-encoded MD5
$etag = rtrim(base64_encode(md5($response->getContent() === false ? '' : trim($response->getContent()), true)), '=');
$response->setEtag($etag, true);
$cache_control = config('http-cache-control.cache-control');
$is_private = in_array(self::CACHE_CONTROL_PRIVATE, $cache_control, true);
$is_public = in_array(self::CACHE_CONTROL_PUBLIC, $cache_control, true);
if ($is_private && $is_public) {
throw new LogicException('Cache-control cannot have both private and public');
}
// Handle cache-control directives...
if ($cache_control !== []) {
$response->headers->set('Cache-Control', $cache_control);
}
// Check for conditional request
if ($response->isCacheable()) {
$request_etags = $request->getETags();
if (in_array('W/"' . $etag . '"', $request_etags, true)) {
$response->setNotModified();
}
}
} else {
$response->headers->set('Cache-Control', self::CACHE_CONTROL_NO_CACHE);
}
// Set Vary header from config
$vary = config('http-cache-control.vary');
if ($vary !== []) {
$response->setVary(implode(', ', $vary));
}
}
} else {
$response->headers->remove('Cache-Control');
}
return $response;
}
}
  • Uses $request->isMethodCacheable() to check if method supports caching
  • Computes ETag: rtrim(base64_encode(md5($content, true)), '=')
  • Sets weak ETag via $response->setEtag($etag, true) (second param = weak)
  • Uses Symfony’s $response->setNotModified() for 304 responses
  • Removes Cache-Control for OPTIONS requests
  • Throws LogicException if both private and public are configured
config/http-cache-control.php
return [
/**
* Vary header values
* e.g. ['Accept', 'Authorization', 'Cookie']
*/
'vary' => [],
/**
* Response Cache-Control Directives
* e.g. ['public', 'max-age=0', 'must-revalidate']
*/
'cache-control' => [],
];

Location: packages/libs/laravel-apm/

OpenTelemetry-based application performance monitoring. Uses event listeners, NOT middleware.

  • Request tracing via RequestHandled event listener
  • Database query span tracking
  • Cache operation span tracking
  • Redis operation span tracking
  • Configurable URI prefix exclusion
  • ApmTracer facade for creating spans

Implementation (Event-Based, NOT Middleware)

Section titled “Implementation (Event-Based, NOT Middleware)”

The package uses TracksRequestTrait which listens to Laravel events:

trait TracksRequestTrait
{
abstract protected function getCurrentSpan(): SpanInterface;
abstract protected function shouldSampled(SpanInterface $span): bool;
abstract protected function getRouteName(Request $request): string;
abstract protected function additionRequestAttributes(SpanInterface $span): void;
/**
* HTTP request trace - listens to RequestHandled event
*/
protected function bootRequestTrace(): void
{
Event::listen(RequestHandled::class, function (RequestHandled $event): void {
$span = $this->getCurrentSpan();
if (!$this->shouldSampled($span)) {
return;
}
$route_name = $this->getRouteName($event->request);
$span->updateName($route_name);
$span->setAttributes([
HttpAttributes::HTTP_REQUEST_METHOD => $event->request->method(),
HttpAttributes::HTTP_ROUTE => $route_name,
UrlAttributes::URL_PATH => $event->request->getPathInfo(),
UrlAttributes::URL_QUERY => $event->request->getQueryString() ?? '',
ServerAttributes::SERVER_ADDRESS => $event->request->getHost(),
UserAgentAttributes::USER_AGENT_ORIGINAL => $event->request->userAgent() ?? '',
UrlAttributes::URL_SCHEME => $event->request->getScheme(),
HttpAttributes::HTTP_RESPONSE_STATUS_CODE => $event->response->getStatusCode(),
ClientAttributes::CLIENT_ADDRESS => $event->request->ip() ?? '',
// Non-standard attributes
'http.referer' => $event->request->headers->get('referer'),
]);
$this->additionRequestAttributes($span);
});
}
}
class ApmTracer extends Facade
{
/**
* Run a callable within a span
*/
public static function runInSpan(callable $fn, string $name, array $attributes, int $kind): mixed
{
$span = static::spanBuilder($name)->setSpanKind($kind)->startSpan();
$scope = $span->setAttributes($attributes)->activate();
try {
return $fn($span);
} finally {
$span->end();
$scope->detach();
}
}
}
// config/apm.php (published from package)
return [
'enabled' => env('OPEN_TELEMETRY_ENABLED', true),
'endpoint' => env('OPEN_TELEMETRY_OTLP_ENDPOINT', 'opentelemetry-collector.opentelemetry-collector.svc:4317'),
/**
* URIs starting with these prefixes will not be traced
*/
'exclude_uri_prefixes' => [],
'preference_handler' => \RightCapital\LaravelApm\DefaultPreference::class,
];
  • NOT a middleware - uses event listeners via TracksRequestTrait
  • Config key is exclude_uri_prefixes (plural), not exclude_uri_prefix
  • Uses RequestHandled event to capture request/response data

Location: packages/libs/laravel-sentry/

Sentry error tracking with security-focused data sanitization.

  • Sanitizes exception messages (masks secrets/tokens/passwords)
  • Masks request body data using configurable patterns
  • Masks headers (Authorization, CSRF tokens, Cookies)
  • Injects IP into user context

Static method, NOT invocable class:

class BeforeSendingInterceptor
{
public static function handle(Event $event): Event
{
self::sanitizeExceptionMessage($event);
self::sanitizeRequest($event);
self::setIpAddressToUser($event);
return $event;
}
private static function sanitizeExceptionMessage(Event $event): void
{
if ($event->getExceptions() !== []) {
$mask_pattern = '/(secret|token|password)=([^&]*)?/';
foreach ($event->getExceptions() as $bag) {
$message = \Safe\preg_replace($mask_pattern, '$1=' . Utils::STRING_MASK, $bag->getValue());
$bag->setValue($message);
}
}
}
private static function sanitizeRequest(Event $event): void
{
$request = $event->getRequest();
$request_body = $request['data'] ?? null;
$request_headers = $request['headers'] ?? null;
$request_cookies = $request['cookies'] ?? null;
if ($request_body !== null) {
$patterns = array_merge(
['/(\b|_)(authorization|auth|password|passwd|secret|card_number)(\b|_)/'],
config('sentry-laravel.sanitization.request_body_patterns', [])
);
$request['data'] = Utils::maskDataByPatterns($request_body, $patterns);
$event->setRequest($request);
}
if ($request_headers !== null) {
$patterns = [
'Authorization',
'Proxy-Authorization',
'X-Csrf-Token',
'X-CSRFToken',
'X-XSRF-TOKEN',
'Cookie',
...config('sentry-laravel.sanitization.header_patterns', []),
];
$request['headers'] = Utils::maskDataByPatterns($request_headers, $patterns);
$event->setRequest($request);
}
if ($request_cookies !== null) {
$patterns = config('sentry-laravel.sanitization.cookie_patterns', []);
if (is_string(config('session.cookie'))) {
$patterns[] = config('session.cookie');
}
$request['cookies'] = Utils::maskDataByPatterns($request_cookies, $patterns);
$event->setRequest($request);
}
}
private static function setIpAddressToUser(Event $event): void
{
$user_context = $event->getUser();
if ($user_context === null) {
$user_context = new UserDataBag();
}
if ($user_context->getIpAddress() === null) {
$user_context->setIpAddress(app('request')->getClientIp());
}
$event->setUser($user_context);
}
}
final class Utils
{
public const string STRING_MASK = '***';
/**
* All patterns are case-insensitive
* Patterns starting with '/' are treated as regex
* Other patterns are compared with strcasecmp
*/
public static function maskDataByPatterns(array $data, array $patterns): array
{
array_walk($data, function (mixed &$value, int|string $key) use ($patterns): void {
if (is_string($key)) {
foreach ($patterns as $pattern) {
// Regex pattern (starts with /)
if ($pattern[0] === '/' && \Safe\preg_match($pattern . 'i', $key) === 1) {
$value = self::STRING_MASK;
}
// Exact match (case-insensitive)
elseif ($pattern[0] !== '/' && strcasecmp($pattern, $key) === 0) {
$value = self::STRING_MASK;
}
}
}
// Recursively process arrays
if (is_array($value)) {
$value = self::maskDataByPatterns($value, $patterns);
}
});
return $data;
}
}
  • handle() is a static method, not __invoke()
  • Uses Utils::maskDataByPatterns() for sanitization, not inline logic
  • Configuration keys:
    • sentry-laravel.sanitization.request_body_patterns
    • sentry-laravel.sanitization.header_patterns
    • sentry-laravel.sanitization.cookie_patterns
  • Default body patterns include regex: /(\b|_)(authorization|auth|password|passwd|secret|card_number)(\b|_)/
  • Automatically includes session cookie name in cookie patterns

Location: packages/libs/laravel-aop/

Aspect-oriented programming with PHP 8 attributes for method decoration.

All AOP attributes extend RightCapital\MethodDelegate\Delegate from the method-delegate package:

packages/libs/method-delegate/src/Delegate.php
abstract class Delegate implements DelegateInterface
{
public readonly Closure $next;
public readonly string $method_name;
public readonly object|string|null $bind;
public function compose(callable $next, string $method_name, object|string|null $bind = null): callable
{
$this->next = $next instanceof Closure ? $next : Closure::fromCallable($next);
$this->method_name = $method_name;
$this->bind = $bind;
return $this;
}
/**
* Check if array is a callable array [ClassName::class, 'methodName']
*/
protected static function isCallableArray(array $maybe_callable): bool
{
return array_is_list($maybe_callable)
&& count($maybe_callable) === 2
&& is_string($maybe_callable[0])
&& is_string($maybe_callable[1])
&& method_exists($maybe_callable[0], $maybe_callable[1]);
}
/**
* Convert callable array to closure bound to current instance
*/
protected function useAsClosure(array $callable): Closure
{
return (fn (...$args) => call_user_func_array($callable, $args))->bindTo(
$this->bind instanceof $callable[0] ? $this->bind : null,
$callable[0],
);
}
}
AttributePurpose
#[Cache]Cache method results
#[Retry]Retry on failure
#[Transactional]Database transaction wrapper
#[RescueFrom]Exception handling
#[Once]Memoize method results

Caches method results with support for dynamic keys, TTL, and tags via callable arrays.

#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
final class Cache extends Delegate
{
/**
* @param CallableArray|string $key Callable returns string key
* @param CallableArray|int|null $ttl Callable returns TTL from result
* @param string|null $store Cache store name
* @param CallableArray|array|null $tags Callable returns array of tags
*/
public function __construct(
public readonly array|string $key,
public readonly array|int|null $ttl = null,
public readonly string|null $store = null,
public readonly array|null $tags = null,
) {
if (is_array($this->key) && !self::isCallableArray($this->key)) {
throw new TypeError('Invalid callable array $key.');
}
if (is_array($this->ttl) && !self::isCallableArray($this->ttl)) {
throw new TypeError('Invalid callable array $ttl.');
}
}
public function __invoke(mixed ...$args): mixed
{
$cache = app('cache')->store($this->store);
$tags = $this->getTags($args);
if ($tags !== null) {
$cache = $cache->tags($tags);
}
return $cache->remember(
key : $this->getKey($args),
ttl : $this->getTtl(),
callback: fn () => ($this->next)(...$args),
);
}
}
class OrderService
{
// Static string key
#[Cache(key: 'orders:list', ttl: 600)]
public function listOrders(): array
{
return Order::all()->toArray();
}
// Dynamic key via callable array
#[Cache(key: [self::class, 'orderCacheKey'], ttl: 600, tags: ['orders'])]
public function getOrder(int $id): Order
{
return Order::findOrFail($id);
}
public function orderCacheKey(Cache $delegate, array $args): string
{
return "order:{$args[0]}";
}
}

Retries method execution on failure with configurable backoff.

#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
final class Retry extends Delegate
{
/**
* @param int|list<int> $times Retry count OR backoff sequence [100, 200, 400]
* @param int|CallableArray $sleep_ms Sleep between retries (or callable)
* @param CallableArray|class-string $when Exception class or callable filter
*/
public function __construct(
public readonly int|array $times,
public readonly int|array $sleep_ms = 0,
public readonly array|string|null $when = null,
) {}
public function __invoke(mixed ...$args): mixed
{
$attempts = 0;
$backoff = [];
$times = $this->times;
// If times is array, use it as backoff sequence
if (is_array($times)) {
$backoff = $times;
$times = count($times) + 1;
}
while (true) {
$attempts++;
$times--;
try {
return ($this->next)(...$args);
} catch (Throwable $e) {
if ($times < 1) {
throw $e;
}
// Check if exception matches filter
if (is_string($this->when) && !is_a($e, $this->when)) {
throw $e;
}
if (is_array($this->when) && !$this->useAsClosure($this->when)($e, $times, $this)) {
throw $e;
}
// Calculate sleep time
$sleep_ms = $backoff[$attempts - 1] ?? (
is_array($this->sleep_ms)
? $this->useAsClosure($this->sleep_ms)($attempts, $e)
: $this->sleep_ms
);
Sleep::usleep($sleep_ms * 1_000);
}
}
}
}
class ExternalApiService
{
// Simple retry 3 times with 200ms sleep
#[Retry(times: 3, sleep_ms: 200)]
public function fetchData(string $endpoint): array
{
return Http::get($endpoint)->json();
}
// Exponential backoff using array
#[Retry(times: [100, 200, 400, 800])]
public function fetchWithBackoff(string $endpoint): array
{
return Http::get($endpoint)->json();
}
// Only retry on specific exception
#[Retry(times: 3, sleep_ms: 100, when: ConnectionException::class)]
public function fetchWithFilter(string $endpoint): array
{
return Http::get($endpoint)->json();
}
}

Wraps method in database transaction.

#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
final class Transactional extends Delegate
{
/**
* @param int $attempts Retry count for deadlocks
* @param string|null $connection Database connection name
*/
public function __construct(
public readonly int $attempts = 1,
public readonly string|null $connection = null,
) {}
public function __invoke(mixed ...$args): mixed
{
return app('db')
->connection($this->connection)
->transaction(
callback: fn () => ($this->next)(...$args),
attempts: $this->attempts,
);
}
}
class OrderService
{
#[Transactional]
public function createOrder(array $data): Order
{
$order = Order::create($data);
$order->items()->createMany($data['items']);
return $order;
}
#[Transactional(connection: 'mysql_secondary', attempts: 3)]
public function syncToSecondary(Order $order): void
{
// Sync with retry on deadlock
}
}

Catches exceptions and returns a fallback value.

#[Attribute(Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)]
final class RescueFrom extends Delegate
{
/**
* @param class-string|list|CallableArray $exception Exception to catch
* @param mixed|CallableArray $as Return value on exception
* @param bool|class-string|list|CallableArray $report Whether to report exception
*/
public function __construct(
public readonly string|array $exception,
public readonly mixed $as = null,
public readonly bool|string|array $report = true,
) {}
public function __invoke(mixed ...$args): mixed
{
try {
return ($this->next)(...$args);
} catch (Throwable $e) {
if ($this->shouldReport($e, $args)) {
report($e);
}
if ($this->shouldRescue($e, $args)) {
return is_array($this->as) && self::isCallableArray($this->as)
? $this->useAsClosure($this->as)($e, ...$args)
: $this->as;
}
throw $e;
}
}
}
class NotificationService
{
// Return false on exception, report it
#[RescueFrom(exception: GuzzleException::class, as: false, report: true)]
public function sendPushNotification(User $user, string $message): bool
{
return $this->pushService->send($user->push_token, $message);
}
// Multiple exception types
#[RescueFrom(exception: [ConnectionException::class, TimeoutException::class], as: null)]
public function fetchOptionalData(): ?array
{
return Http::get('https://api.example.com/data')->json();
}
}

Memoizes method results per instance (like Laravel’s once() helper).

#[Attribute(Attribute::TARGET_METHOD)]
final class Once extends Delegate
{
public function __invoke(mixed ...$args): mixed
{
$bind = $this->bind;
$hash = $args === [] ? '' : hash('xxh128', implode(',', array_map(self::hash(...), $args)));
return LaravelOnce::instance()->value(new Onceable(
hash : "$this->method_name($hash)",
object : $bind,
callable: fn () => ($this->next)(...$args),
));
}
// Can only apply to instance methods
public function compose(callable $next, string $method_name, object|string|null $bind = null): callable
{
if (!is_object($bind)) {
throw new InvalidArgumentException($this::class . ' can only apply to instance methods');
}
return parent::compose($next, $method_name, $bind);
}
public static function flush(): void
{
LaravelOnce::flush();
}
}
class ExpensiveService
{
#[Once]
public function calculateComplexResult(int $id): array
{
// Only calculated once per instance + arguments
return DB::table('data')->where('id', $id)->get()->toArray();
}
}

Attributes are applied in order (top to bottom = outer to inner):

class OrderService
{
#[RescueFrom(exception: ApiException::class, report: true, as: null)]
#[Retry(times: 3, sleep_ms: 100)]
#[Transactional]
#[Cache(key: [self::class, 'orderKey'], ttl: 600)]
public function processOrder(int $id): ?Order
{
// 1. RescueFrom: catches ApiException, returns null
// 2. Retry: retries up to 3 times on failure
// 3. Transactional: wraps in DB transaction
// 4. Cache: caches the result
return $this->doProcess($id);
}
}

PackageComponentPurposeType
laravel-http-cache-controlSetCacheHeadersETag and cache headersMiddleware
laravel-apmTracksRequestTraitOpenTelemetry tracingEvent Listener
laravel-sentryBeforeSendingInterceptorError sanitizationStatic callback
laravel-aopCache, Retry, etc.Method decorationAttributes extending Delegate
method-delegateDelegateBase class for AOPAbstract class