Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cache Bundle Laravel Package

emag-tech-labs/cache-bundle

Annotation Cache Bundle lets you cache Symfony service method responses using a simple annotation or PHP 8 attribute. It proxies annotated services and stores results in any PSR-6 cache (array, Redis, Memcache), returning cached data automatically on repeat calls.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package (it targets Symfony, not Laravel — see Gotchas):
composer require emag-tech-labs/cache-bundle
  1. Register the bundle in config/bundles.php unless a Flex recipe did it for you:
return [
    // ...
    EMAGTechLabs\AnnotationCacheBundle\AnnotationCacheBundle::class => ['all' => true],
];
  1. Configure two things — a PSR-6 cache pool service id and the list of service ids the compiler pass should scan:
# config/packages/annotation_cache.yaml  (verify the exact root key below)
emag_tech_labs_cache:
    cache_pool: 'cache.app'          # any service implementing Psr\Cache\CacheItemPoolInterface
    services:
        - App\Service\CatalogQuery
        - App\Service\CurrencyRates

Run bin/console config:dump-reference <bundle-alias> first — the root key and option names are the one thing you should confirm from DependencyInjection/Configuration.php before writing config, because a typo is silent (nothing gets proxied).

  1. Annotate your first method. The README's "How it works" says the bundle scans methods, generates a ocramius/proxy-manager proxy that extends your service, and overrides the annotated method so the call goes through getCached:
namespace App\Service;

use EMAGTechLabs\AnnotationCacheBundle\Annotation\Cache;   // docblock style
// or: use EMAGTechLabs\AnnotationCacheBundle\Attribute\Cache; // PHP 8+ attribute

class CatalogQuery
{
    public function __construct(private ProductRepository $repo) {}

    /**
     * @Cache(ttl=600)
     */
    public function findFeaturedProducts(int $categoryId, string $locale): array
    {
        return $this->repo->findFeatured($categoryId, $locale);
    }
}
  1. Clear the container and warm the cache, then smoke-test the hit path:
bin/console cache:clear
bin/console cache:pool:clear cache.app

Then call the method twice from a controller or bin/console command and confirm the second call is measurably faster / the DB query doesn't reappear in the profiler.

Where to look first: the Annotation/Cache.php (or Attribute/Cache.php) class to learn the real option names (key, ttl, pool overrides), CacheableClassTrait::getCached() to understand key construction, and the compiler pass that does the service replacement. bin/console debug:container App\Service\CatalogQuery after a cache clear should show the proxy class name instead of your class — that single check tells you whether the bundle is active for that service.

First real use case: a "read-mostly, expensive, argument-deterministic" service — catalog/category lookups, exchange-rate snapshots, CMS menu trees, static reference-data lists. Do not start with anything user-specific or write-heavy.

Implementation Patterns

Annotate a thin query service, not a fat domain service. The proxy overrides public methods on the injected service; keeping the annotated surface small (2–5 methods, scalar args, array/scalar returns) keeps key generation predictable and makes cache invalidation tractable.

Key strategy. Let the bundle derive the key from method name + arguments for simple scalar signatures, and supply an explicit key when the arguments are noisy:

/** @Cache(key="catalog.featured.{categoryId}.{locale}", ttl=900) */
public function findFeaturedProducts(int $categoryId, string $locale): array

Anything not part of the key is a correctness bug: if a method reads from $this->request or a static flag, the cached result will leak across requests. Push all variable input into the parameter list.

Per-method TTL tiers. Volatile data (rates, stock) gets 60–300s; reference data (categories, countries, config strings) gets hours. Don't set one global TTL; the annotation supports per-method values, so use them.

Multiple pools. Point a shared, long-lived pool at reference data and keep cache.app/a request pool for short-lived stuff, so a cache:pool:clear in deploy doesn't nuke everything, or so a Redis eviction policy doesn't take out your static data.

Decorator-style integration. If you can't annotate an existing class (third-party, final, or you don't want proxies in the hot path), create your own adapter service that delegates and annotate the adapter, then inject the adapter wherever the original was used. Same result, no proxy on the original.

Invalidation helper. Write one small CatalogCacheKeys class that produces keys, and have both the annotation and your invalidation code use it. Without this, "clear the cache after a product import" degrades into cache:pool:clear — a thundering-herd event:

final class CatalogCacheKeys
{
    public static function featured(int $categoryId, string $locale): string
    {
        return sprintf('catalog.featured.%d.%s', $categoryId, $locale);
    }
}

Then $pool->deleteItem(CatalogCacheKeys::featured($id, $locale)) in the import command.

Environment split. Dev: filesystem/array adapter so you can inspect keys. Test: array adapter, and assert hit/miss counts on the pool rather than timing. Prod: Redis or Memcached with an eviction policy that matches your data tiers.

Testing strategy.

  • Unit-test your service class directly — the proxy is bypassed, so new CatalogQuery($repoStub) tests the real logic.
  • Integration-test the container: boot the kernel, fetch the service, call twice, assert the repository stub was called once. This is the test that actually proves the annotation works.
  • Add one test that calls debug:container output or asserts get_class($service) !== CatalogQuery::class so a silently-disabled bundle fails CI.

Workflow for a new annotation: annotate → cache:clear → debug:container to confirm the proxy → profiler cache panel to confirm hits → only then measure the performance win.

Gotchas and Tips

This is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no Illuminate\Contracts\Cache adapter, and no Laravel bridge. In a Laravel app you would reimplement the same idea (a decorator or a Cache::remember wrapper around a repository) rather than install this. Evaluate it strictly as a Symfony-container feature.

Container replacement is compile-time. The proxy is generated when the container is compiled, so adding/changing an annotation requires bin/console cache:clear — a stale proxy after a deploy is the most common "why isn't my cache working" cause. In dev this bites constantly; hook cache:clear into your deploy script after composer install.

Ordering vs. other compiler passes. Any bundle or pass that re-registers/replaces the service definition (decorates=, setAlias, test doubles, autowire overrides) can undo the proxy. If debug:container shows your class instead of the proxy, look at pass priority and whether something else owns that definition.

Proxy constraints. Because the proxy extends your class, these break or behave oddly:

  • final classes and final public methods — cannot be extended/overridden.
  • private/protected methods — not intercepted, so annotating them is a silent no-op.
  • Methods returning self/static — the proxy breaks the type contract.
  • Constructors that are private or do heavy side effects — the proxy instantiates lazily, so side effects can move to first method call.
  • Strict identity checks like $service::class === CatalogQuery::class or instanceof against a final sibling class — the runtime class is the generated proxy.

Key generation traps.

  • Objects as arguments: two logically-equal DateTime/entity instances can serialize differently and produce different keys → permanent misses.
  • Doctrine entities as arguments: pass the ID, not the entity. Even if it works, the key becomes unreadable and fragile.
  • Closures/resources/functions as arguments will fail to serialize or produce unstable keys.
  • Very long argument strings (base64, JSON blobs) can blow past Redis/PSR-6 key length limits; hash them yourself before the call.

Serialization of return values matters. Redis/Memcached pools serialize the response. Arrays, scalars, and simple DTOs are fine; Doctrine entities with lazy proxies, Closures, resources, and objects holding open handles are not. Prefer primitives + arrays from annotated methods, or use a serializer-aware pool.

null and empty results get cached too. A query that returns null because a row didn't exist yet will keep returning null for the full TTL after the row appears. Either keep TTLs short on lookups that can be "not created yet", or bypass the annotation and call Cache::get style logic for those paths. Decide your negative-caching policy explicitly.

Side effects are cached away. An annotated method that also does logging, event dispatch, counters, or writes will skip all of that on a hit. Annotate pure reads only.

Annotation vs. attribute. On PHP 8+, prefer the attribute class — docblock annotations depend on doctrine/annotations, which is being removed from the Doctrine ecosystem. If you're on PHP 8 and the docblock silently isn't detected, check which class the scanner is looking for.

Debugging a miss. In dev, wire the annotated methods to a traceable filesystem/array pool and inspect keys with cache:pool:clear + the profiler's cache panel. Print the generated key by looking at getCached in the trait — if the key differs between two "identical" calls, it's almost always an object argument or an auto-generated key including positional data you didn't expect.

Dependency risk. Small community (19 stars, no known dependents), last release March 2023. The proxy layer historically pulls ocramius/proxy-manager, which is abandoned in favour of friendsofphp/proxy-manager-lts — verify your lockfile resolves to the LTS fork on modern PHP, and pin the bundle. Budget an escape hatch: because the pattern is just "cache around a public method", you can replace annotations with a hand-written decorator per service in an afternoon if the bundle stops being maintained.

Extension points. Beyond the annotation options, your levers are (a) the injected PSR-6 pool (custom adapter, namespacing, tagging via a tag-aware pool), and (b) wrapping the annotated service in your own decorator if you need key derivation the bundle doesn't offer. There's no documented key-generator SPI, so don't plan on one — plan on explicit key= templates instead.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect