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

App Cache Bundle Laravel Package

php-solution/app-cache-bundle

php-solution/app-cache-bundle provides application-level caching for PHP projects, with configurable cache storage and simple integration to speed up repeated work and reduce load.

View on GitHub
Deep Wiki
Context7

Getting Started

The framework reality check comes first. The package is php-solution/app-cache-bundle, but the repository is sf-app-cache-bundle. The sf- prefix and the -bundle suffix are Symfony conventions — this is a Symfony HttpKernel bundle, not a Laravel package. Laravel has no bundle loader. composer require php-solution/app-cache-bundle will place code in vendor/, but nothing in bootstrap/app.php, config/app.php, or the container will ever boot the Bundle class, register the DI extension, or load the service definitions. There is no extra.laravel.providers auto-discovery entry to bind to. For a Laravel codebase the honest verdict is not applicable as a dependency — read it as a reference implementation and use Laravel's native cache layer.

If you are porting the idea (recommended path), read the source in this order:

  1. composer.json — look at type (expect symfony-bundle), autoload.psr-4 (the namespace almost certainly does not match PhpSolution\AppCacheBundle — do not assume), and the Symfony version constraints (a 2-star bundle is usually pinned to an old minor).
  2. The Bundle class — usually ~20 lines, tells you nothing except the DI extension alias.
  3. DependencyInjection/*Extension.php — this is the single most valuable file. The config key it registers (app_cache or similar) and the array shape it accepts tells you exactly what knobs the author thought mattered: pool name, TTL default, key prefix, maybe a debug toggle.
  4. Resources/config/ (or config/) — service definitions. Note which services are public: true, which are aliased to PSR-6 / PSR-16 interfaces, and whether a compiler pass or tagged iterator is used.
  5. Any src/Attribute/ or src/Annotation/ classes — the attribute name and its argument list is the public API most readers care about.

First use case. Based on the name and typical shape, this is application-level result/response caching: an attribute placed on a controller action, resolved by a kernel.controller/kernel.response event subscriber, which derives a cache key and short-circuits the controller by returning a stored Response on a hit. In Laravel, that maps to a route middleware or a controller-level attribute resolved in middleware — both of which are 30–60 lines and something you should own.

Minimum viable port — a day-one Laravel equivalent:

// app/Cache/AppCache.php
namespace App\Cache;

use Illuminate\Support\Facades\Cache;

class AppCache
{
    public function remember(string $key, int $ttl, callable $callback, ?string $tag = null)
    {
        $key = $this->key($key);
        if ($tag && config('cache.default') === 'redis') {
            return Cache::tags([$tag])->remember($key, $ttl, $callback);
        }
        return Cache::remember($key, $ttl, $callback);
    }

    public function forget(string $key): void
    {
        Cache::forget($this->key($key));
    }

    public function flushTag(string $tag): void
    {
        Cache::tags([$tag])->flush();
    }

    protected function key(string $key): string
    {
        return config('cache.prefix').':app:'.$key;
    }
}

That reproduces — in about 40 lines you can read, test, and debug — the entire value proposition the bundle advertises. Bind it as a singleton in AppServiceProvider and inject it into services. Do not add the Composer dependency.

Implementation Patterns

Pattern 1 — Attribute-based response caching in Laravel. The most direct analogue to a Symfony controller attribute is a PHP 8 attribute plus a route middleware. Define the attribute:

#[Attribute(Attribute::TARGET_METHOD)]
class CacheResponse
{
    public function __construct(
        public int $ttl = 3600,
        public ?string $key = null,
        public array $vary = [], // e.g. ['user_id', 'locale']
    ) {}
}

Resolve it in middleware via reflection:

class CacheResponseMiddleware
{
    public function handle($request, Closure $next, int $ttl = 3600): mixed
    {
        $route = $request->route();
        $action = $route->getAction('uses');
        $ref = new ReflectionMethod($action[0], $action[1]);
        $attr = $ref->getAttributes(CacheResponse::class)[0] ?? null;

        if (! $attr || ! in_array($request->method(), ['GET', 'HEAD'])) {
            return $next($request);
        }

        $cfg = $attr->newInstance();
        $key = $cfg->key ?? $route->getName().'.'.sha1($request->fullUrl());

        $cached = Cache::get($key);
        if ($cached !== null) {
            return response($cached['body'], $cached['status'])
                ->withHeaders($cached['headers'] ?? []);
        }

        $response = $next($request);
        if ($response->isSuccessful()) {
            Cache::put($key, [
                'body'   => $response->getContent(),
                'status' => $response->getStatusCode(),
                'headers'=> $response->headers->all(),
            ], $cfg->ttl);
        }
        return $response;
    }
}

This is exactly what a Symfony bundle of this shape does — you have simply moved it from kernel.response to Laravel's middleware pipeline. That is the whole point: the semantics are identical; only the extension point moves.

Pattern 2 — Service-level caching, injected. Prefer injection over the cache() helper inside anything you want to unit test. Bind AppCache as a singleton, inject it, and mock it in tests. This is what the bundle's "service-friendly API" description is getting at: the Symfony version exposes a cache service by id so services can request it via constructor injection; the Laravel equivalent is a typed constructor dependency on AppCache.

Pattern 3 — Config profiles. The bundle almost certainly defines a config key with named profiles (e.g. short, medium, long TTLS, or one profile per backend pool). Mirror that in config/cache.php-adjacent config or a dedicated config/app_cache.php:

return [
    'profiles' => [
        'short'  => ['ttl' => 60,    'prefix' => 's'],
        'medium' => ['ttl' => 3600,  'prefix' => 'm'],
        'long'   => ['ttl' => 86400, 'prefix' => 'l'],
    ],
    'enabled' => env('APP_CACHE_ENABLED', true),
];

Resolve the profile name from the attribute: #[CacheResponse(profile: 'long')]. This is how you make caching configurable per environment without editing call sites — the reason a bundle of this kind exists at all.

Pattern 4 — Explicit invalidation at the write site. Do not try to infer invalidation. On Product::updateOrCreate(...), explicitly call $appCache->flushTag('product:'.$product->id) and $appCache->forget('product_list'). The bundle, having no model knowledge, cannot do this for you either; a bundle that only stores and retrieves (as the description suggests) will leave invalidation entirely to the caller. Design for this from the start.

Pattern 5 — Versioned keys. If you are porting the TTL/key strategy, add a v prefix bumped on deploy (app:cache:v3:...). This is a cheap way to invalidate an entire generation of keys without knowing every key. Symfony cache pools often use a namespace that increments; Laravel users often forget this and end up running php artisan cache:clear on deploy, which is a blunt instrument.

Pattern 6 — Read-through in a repository or query layer. For data-level caching (not response caching), wrap repository find/all calls with $this->cache->remember(...) inside the repository class, with the model class as the tag. This keeps caching out of controllers and makes it greppable.

Gotchas and Tips

Bundle classes will never boot in Laravel. If you composer require this and add it to config/app.php or bootstrap/providers.php, nothing happens. Laravel's service provider discovery reads extra.laravel.providers from composer.json; a Symfony bundle exposes Symfony\Component\HttpKernel\Bundle\Bundle subclasses and uses DependencyInjection\Extension for config. These are not interchangeable. Registering the Bundle in a Laravel provider and calling ->boot() is not supported and will fail on missing ContainerBuilder.

Do not assume the PSR interface. Symfony's cache contract supports both PSR-6 (pools, CacheItemPoolInterface) and PSR-16 (simple, CacheInterface). If the bundle injects a PSR-6 pool, its API (getItem/save/getItems) is not the same as Laravel's Cache::get/put. If you are translating code, check the interface before copying method names — a PSR-6 getItem() returns a hit/miss object, not the value.

TTL of 0 or null means "forever" in some backends and "use default" in others. Laravel's Cache::put($key, $value, 0) on Redis means no expiry; on file it also means no expiry; but on some drivers a null TTL falls back to the config default. When porting, be explicit and test the driver you actually run. Symfony's CacheItem::expiresAfter(null) also means "use pool default". The semantics diverge; do not assume parity.

Key collisions. With CACHE_PREFIX set per environment, you are usually safe, but a key like product_list plus a vary on locale will silently return the wrong locale if you forget the locale in the key. Always fold every input that changes the response into the key — the attribute's vary list exists precisely for this. Forgetting it is the #1 stale-read bug in this class of code.

Cache stampede is not solved for you. Neither the bundle's description nor the assessment material mentions stampede protection (per-key locks). If a hot key expires and 200 concurrent requests miss simultaneously, you get 200 DB hits. Add a Cache::lock('stampede:'.$key, 10)->block(5, fn () => ...) guard around expensive misses if p95 matters.

Debugging stale responses in Laravel specifically. cache()->forget() on a tagged store flushes the tag, not the key — if you mixed tagged and untagged writes to the same key, behaviour is undefined. For Redis, redis-cli --scan --pattern 'prefix:app:*' | head is faster than clicking through the app. If you suspect stale reads in a queued context, remember Cache is shared between web and queue workers; a worker can repopulate a key the web process just invalidated.

The enabled toggle is not free. If you add an APP_CACHE_ENABLED=false flag, every read path must check it — and if the write path still populates, you will get a very confusing "cache is off but Redis is full" situation. Either short-circuit both reads and writes on the flag or don't have one; having half of it is worse than none.

Extension points you would get from a bundle, and their Laravel equivalents. A Symfony bundle of this shape typically offers: (a) a custom attribute/annotation to mark cacheable actions — Laravel equivalent: your own attribute + middleware; (b) a cache pool registered by name — Laravel equivalent: a named Cache::store('name') entry in config/cache.php; (c) a compiler pass for tagged cache-aware services — Laravel equivalent: Container::tag() + Container::tagged() in a service provider; (d) an event subscriber on the kernel — Laravel equivalent: middleware. Every extension point has a native Laravel counterpart with the same semantics. That symmetry is why the bundle adds nothing to a Laravel app.

If you genuinely need Symfony interop (mixed stack, e.g. a Laravel app fronting a Symfony service), do not put a Symfony bundle in the Laravel container. Run the Symfony app as a separate service, expose an HTTP/queue boundary, and let each side use its own native cache layer. The bridge is the network boundary, not DependencyInjection.

Licence and maintenance check before any vendoring. If you do decide to copy code in rather than depend on the package, check the licence in composer.json (MIT is common; GPL is a distribution problem) and note the pinned commit in a comment so the provenance is auditable. With 2 stars and 0 dependents, this package is effectively unmaintained — assuming you will own it is the correct assumption.

Testing discipline. Whatever you port, write tests for the invalidating path, not the storing path. A cache that stores correctly and invalidates incorrectly is a bug factory that passes all your "does it cache" tests. Test: write, read hits; update, read misses; tag flush clears everything under the tag; TTL expires when it should (use Cache::set and Carbon::setTestNow).

When to stop reading and start writing. If the source review takes more than 30 minutes, stop — you have already spent more on an unmaintained 2-star package than on writing the replacement. The value in this repository is the idea, not the code.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle