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/.
Package Locations
Section titled “Package Locations”| Package | Location | Purpose |
|---|---|---|
| laravel-http-cache-control | packages/libs/laravel-http-cache-control/ | HTTP caching headers |
| laravel-apm | packages/libs/laravel-apm/ | OpenTelemetry tracing |
| laravel-sentry | packages/libs/laravel-sentry/ | Error tracking & sanitization |
| laravel-aop | packages/libs/laravel-aop/ | Aspect-oriented programming |
| method-delegate | packages/libs/method-delegate/ | Base delegate for AOP attributes |
1. laravel-http-cache-control
Section titled “1. laravel-http-cache-control”Location: packages/libs/laravel-http-cache-control/
Adds HTTP cache control headers and supports ETag-based caching with 304 responses.
Features
Section titled “Features”- Adds
Cache-Controlheader based on config - Computes weak ETags using base64-encoded MD5
- Returns
304 Not Modifiedfor matching ETags - Handles public/private cache directives
- Adds
Varyheader management - Skips OPTIONS requests
Middleware Implementation
Section titled “Middleware Implementation”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; }}Key Behavior
Section titled “Key Behavior”- 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
LogicExceptionif both private and public are configured
Configuration
Section titled “Configuration”return [ /** * Vary header values * e.g. ['Accept', 'Authorization', 'Cookie'] */ 'vary' => [],
/** * Response Cache-Control Directives * e.g. ['public', 'max-age=0', 'must-revalidate'] */ 'cache-control' => [],];2. laravel-apm
Section titled “2. laravel-apm”Location: packages/libs/laravel-apm/
OpenTelemetry-based application performance monitoring. Uses event listeners, NOT middleware.
Features
Section titled “Features”- Request tracing via
RequestHandledevent listener - Database query span tracking
- Cache operation span tracking
- Redis operation span tracking
- Configurable URI prefix exclusion
ApmTracerfacade 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); }); }}ApmTracer Facade
Section titled “ApmTracer Facade”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(); } }}Configuration
Section titled “Configuration”// 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,];Key Differences from Documentation
Section titled “Key Differences from Documentation”- NOT a middleware - uses event listeners via
TracksRequestTrait - Config key is
exclude_uri_prefixes(plural), notexclude_uri_prefix - Uses
RequestHandledevent to capture request/response data
3. laravel-sentry
Section titled “3. laravel-sentry”Location: packages/libs/laravel-sentry/
Sentry error tracking with security-focused data sanitization.
Features
Section titled “Features”- 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
BeforeSendingInterceptor
Section titled “BeforeSendingInterceptor”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); }}Utils::maskDataByPatterns
Section titled “Utils::maskDataByPatterns”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; }}Key Differences from Documentation
Section titled “Key Differences from Documentation”handle()is a static method, not__invoke()- Uses
Utils::maskDataByPatterns()for sanitization, not inline logic - Configuration keys:
sentry-laravel.sanitization.request_body_patternssentry-laravel.sanitization.header_patternssentry-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
4. laravel-aop
Section titled “4. laravel-aop”Location: packages/libs/laravel-aop/
Aspect-oriented programming with PHP 8 attributes for method decoration.
Architecture
Section titled “Architecture”All AOP attributes extend RightCapital\MethodDelegate\Delegate from the method-delegate package:
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], ); }}Available Attributes
Section titled “Available Attributes”| Attribute | Purpose |
|---|---|
#[Cache] | Cache method results |
#[Retry] | Retry on failure |
#[Transactional] | Database transaction wrapper |
#[RescueFrom] | Exception handling |
#[Once] | Memoize method results |
Cache Attribute
Section titled “Cache Attribute”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]}"; }}Retry Attribute
Section titled “Retry Attribute”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(); }}Transactional Attribute
Section titled “Transactional Attribute”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 }}RescueFrom Attribute
Section titled “RescueFrom Attribute”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(); }}Once Attribute
Section titled “Once Attribute”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(); }}Combining Attributes
Section titled “Combining Attributes”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); }}Summary
Section titled “Summary”| Package | Component | Purpose | Type |
|---|---|---|---|
| laravel-http-cache-control | SetCacheHeaders | ETag and cache headers | Middleware |
| laravel-apm | TracksRequestTrait | OpenTelemetry tracing | Event Listener |
| laravel-sentry | BeforeSendingInterceptor | Error sanitization | Static callback |
| laravel-aop | Cache, Retry, etc. | Method decoration | Attributes extending Delegate |
| method-delegate | Delegate | Base class for AOP | Abstract class |