Skip to content

Caching

Gacela caches at three different levels. Each solves a different problem. They compose, they don't replace one another.

LayerWhat it cachesWhereTypical use
Framework resolutionResolved facades, factories, configs, merged configMemory or diskAlways on, pick the mode per environment
Cacheable methodsReturn values of facade methodsMemory (pluggable)Expensive, deterministic reads
Value primitivesArbitrary key → value data, optionally with a dependency graphDiskYour code needs its own cache (compilers, pipelines, parsed artefacts)

Layer 1: Framework resolution cache

Gacela resolves classes by convention: FacadeFactoryProviderConfig. Those lookups walk namespaces and files, and the merged configuration is reassembled from every config/*.php file. All of it is memoised once per process, and can additionally be persisted to disk between runs.

  • In-memory (default): InMemoryCache holds resolved class names for the life of the process.
  • On-disk: ClassNamePhpCache and CustomServicesPhpCache persist the same data to gacela-class-names.php / gacela-custom-services.php; MergedConfigCache persists the merged configuration to gacela-merged-config[{env}].php keyed per APP_ENV.

Configure at bootstrap:

php
use Gacela\Framework\Bootstrap\GacelaConfig;
use Gacela\Framework\Gacela;

Gacela::bootstrap(__DIR__, static function (GacelaConfig $config): void {
    $config->enableFileCache();                  // use the default cache dir
    // $config->enableFileCache('/custom/dir');  // or pick one
    // $config->setFileCache(false);             // explicitly off
    // $config->resetInMemoryCache();            // wipe static caches (tests)
});

The directory can also be overridden at runtime with the GACELA_CACHE_DIR environment variable. Handy when the same image is reused across environments.

Typical wiring:

  • Development: file cache off. Edits take effect immediately.
  • Production: file cache on, pre-populated with vendor/bin/gacela cache:warm, directory baked into the image. Re-deploy (or cache:clear) to refresh.
  • Tests: call resetInMemoryCache() between suites so resolution state doesn't bleed.

See also: Opcache preload for getting PHP itself to cache Gacela's own source files.

Layer 2: Cacheable facade methods

Cache the result of a facade method with the #[Cacheable] attribute. CacheableTrait is built into AbstractFacade, no extra use needed. Full reference: Cacheable methods.

php
use Gacela\Framework\AbstractFacade;
use Gacela\Framework\Attribute\Cacheable;

final class CatalogFacade extends AbstractFacade
{
    #[Cacheable(ttl: 3600)]
    public function getPopularProducts(): array
    {
        return $this->cached(fn (): array =>
            $this->getFactory()->createRepository()->fetchPopular(),
        );
    }
}

Storage is InMemoryCacheStorage by default, which means entries die with the request on PHP-FPM. For cross-request caching swap in a shared backend (APCu, Redis, PSR-16) via CacheableConfig::setStorage().

php
CatalogFacade::clearMethodCache();                        // all of this facade
CatalogFacade::clearMethodCacheFor('getPopularProducts'); // one method, any args

Layer 3: Value primitives

When your code needs a cache (compiled artefacts, parsed data, a build pipeline) use Gacela\Framework\Cache\FileCache:

php
use Gacela\Framework\Cache\FileCache;

$cache = new FileCache('/var/cache/myapp');

$cache->put('user:42', $user, ttl: 600);
$cache->get('user:42');     // $user, or null after TTL expiry
$cache->forget('user:42');
$cache->clear();
  • One .php file per key (SHA1-hashed), written atomically via staged .tmp + rename.
  • TTL per entry; ttl: 0 means forever.
  • beginBatch() / commitBatch() defer writes behind a single index-locked flush. Useful for warming many entries at once.
  • stats() returns entry count, total bytes, and oldest/newest timestamps.
  • Safe against torn reads: concurrent readers see either the previous file or the new one, never a half-written one.

ScopedCache: dependency-aware decorator

When invalidating one entry should cascade to every downstream entry that derived from it, wrap FileCache in ScopedCache:

php
use Gacela\Framework\Cache\FileCache;
use Gacela\Framework\Cache\ScopedCache;

$cache = new ScopedCache(new FileCache('/var/cache/myapp'));

$cache->put('ns:core', $envCore);
$cache->put('file:a.php', $compiledA);
$cache->put('fragment:a#1', $fragment);

$cache->dependsOn('file:a.php', 'ns:core');
$cache->dependsOn('fragment:a#1', 'file:a.php');

$cache->invalidate('ns:core');          // cascades: file:a.php and fragment:a#1 also go
$cache->invalidateLeaf('file:a.php');   // only this key; dependents stay valid
  • get / put / has delegate straight to the underlying FileCache. Zero overhead on the hot path.
  • The dependency graph is persisted alongside the values (.gacela-scoped-cache-graph.php) and survives process restarts.
  • Cycles are rejected eagerly at dependsOn(): self, two-node, and transitive.
  • Single-writer concurrency: multiple processes racing on dependsOn() may lose edges added between load and persist.

Picking a layer

  • Make Gacela's own resolution faster → Layer 1, enableFileCache() + cache:warm.
  • Memoise a specific facade method → Layer 2, #[Cacheable].
  • Cache arbitrary application data → Layer 3, FileCache.
  • Same, but invalidation must cascade → Layer 3, ScopedCache.