Caching
Gacela caches at three different levels. Each solves a different problem. They compose, they don't replace one another.
| Layer | What it caches | Where | Typical use |
|---|---|---|---|
| Framework resolution | Resolved facades, factories, configs, merged config | Memory or disk | Always on, pick the mode per environment |
| Cacheable methods | Return values of facade methods | Memory (pluggable) | Expensive, deterministic reads |
| Value primitives | Arbitrary key → value data, optionally with a dependency graph | Disk | Your code needs its own cache (compilers, pipelines, parsed artifacts) |
Layer 1: Framework resolution cache
Gacela resolves classes by convention: Facade → Factory → Provider → Config. 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):
InMemoryCacheholds resolved class names for the life of the process. - On-disk:
ClassNamePhpCache,CustomServicesPhpCache, andMergedConfigCachepersist the same data in project-scoped PHP files. Filenames include an application-root hash, preventing applications that share a cache directory from serving each other's data; merged config files are also scoped byAPP_ENV.
Module containers are scopes of one application container in 2.0. They also share an in-process constructor-plan cache, so classes used by several modules are reflected once while bindings, tags, instances, and Provider registrations remain scoped. Persisting constructor plans was measured as a net loss and is not enabled automatically.
Configure at bootstrap:
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('var/cache'); // relative to the app root
// $config->setFileCache(false); // explicitly off
// $config->resetInMemoryCache(); // wipe static caches (tests)
});A configured path is resolved relative to the application root—even a leading slash does not escape it. Use the GACELA_CACHE_DIR environment variable for a genuinely absolute directory outside the project; it takes precedence and is used verbatim.
With the file cache enabled, the merged configuration auto-warms on the first miss: the first bootstrap persists the app- and environment-scoped merged-config file, so later bootstraps skip globbing and parsing config files—no manual cache:warm is required for that layer.
In a read-only environment (e.g. a read-only project root inside a build sandbox) the file caches degrade gracefully to in-memory instead of failing the bootstrap: writes become no-ops, no raw PHP warnings are emitted, and any pre-warmed cache files already on disk stay readable. Warm-at-build / run-read-only deployments keep their cache hits.
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 (orcache: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.
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().
CatalogFacade::clearMethodCacheFor('getPopularProducts'); // one method, any args
CatalogFacade::clearMethodCache(); // the whole shared storeclearMethodCache() is not facade-scoped: it calls clear() on the shared storage backend. Gacela::resetCache() only clears Gacela's default in-memory method cache and deliberately leaves a custom APCu/Redis backend registered through CacheableConfig::setStorage() alone.
Layer 3: Value primitives
When your code needs a cache (compiled artifacts, parsed data, or a build pipeline), use Gacela\Framework\Cache\FileCache:
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
.phpfile per key (SHA1-hashed), written atomically via staged.tmp+rename. writeContentsAtomically(string $file, string $content): bool— atomically writes already-rendered content to a path, with the same staged-.tmp+renameguarantees asput(). The higher-levelwriteAtomically()wraps it.- TTL per entry;
ttl: 0means 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:
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 validget/put/hasdelegate straight to the underlyingFileCache. 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.