Production-ready Symfony Cache adapter for Google Cloud Firestore. Perfect for Cloud Run, App Engine, and other stateless GCP environments where filesystem cache is not persistent.
- PSR-6 compliant - Full implementation of
Psr\Cache\CacheItemPoolInterface - Tag-aware caching - Invalidate groups of cache entries with tags
- Automatic pruning - Clean up expired cache entries
- Data compression - Optional gzip compression to reduce Firestore storage costs
- Versioning support - Automatic cache invalidation on schema changes
- Namespace isolation - Multi-tenant support with cache namespacing
- High performance - Batch operations and optimized Firestore queries
- Cloud Run optimized - Works seamlessly with ephemeral containers
composer require vasco-fund/symfony-firestore-cache-adapter- PHP 8.2 or higher
- Symfony 6.4 or 7.x
- Google Cloud Firestore credentials
# config/packages/cache.yaml
framework:
cache:
prefix_seed: your_app_name
pools:
app:
adapter: cache.adapter.firestore
services:
cache.adapter.firestore:
class: Vasco\FirestoreCache\FirestoreAdapter
arguments:
$projectId: '%env(GOOGLE_CLOUD_PROJECT)%'
$namespace: '%kernel.environment%'
$defaultLifetime: 3600# config/packages/cache.yaml
services:
cache.adapter.firestore:
class: Vasco\FirestoreCache\FirestoreAdapter
arguments:
$projectId: '%env(GOOGLE_CLOUD_PROJECT)%'
$namespace: '%kernel.environment%'
$defaultLifetime: 3600
$options:
# Collection name in Firestore
collection: 'symfony_cache'
# Enable gzip compression
compression: true
# Cache version for automatic invalidation
version: 1
# Maximum items per batch operation
max_batch_size: 500
# Firestore credentials path (optional if using default credentials)
keyFilePath: '%env(GOOGLE_APPLICATION_CREDENTIALS)%'# config/packages/cache.yaml
framework:
cache:
pools:
app.cache:
adapter: cache.adapter.firestore
tags: trueuse Psr\Cache\CacheItemPoolInterface;
class YourService
{
public function __construct(
private CacheItemPoolInterface $cache
) {}
public function getData(string $id): array
{
$item = $this->cache->getItem('data_' . $id);
if (!$item->isHit()) {
$data = $this->fetchDataFromDatabase($id);
$item->set($data);
$item->expiresAfter(3600); // 1 hour
$this->cache->save($item);
}
return $item->get();
}
}use Symfony\Contracts\Cache\TagAwareCacheInterface;
class ProductService
{
public function __construct(
private TagAwareCacheInterface $cache
) {}
public function getProduct(int $id): Product
{
return $this->cache->get(
'product_' . $id,
function (ItemInterface $item) use ($id) {
$item->tag(['products', 'product_' . $id]);
$item->expiresAfter(86400); // 24 hours
return $this->repository->find($id);
}
);
}
public function invalidateProduct(int $id): void
{
// Invalidate specific product
$this->cache->invalidateTags(['product_' . $id]);
}
public function invalidateAllProducts(): void
{
// Invalidate all products
$this->cache->invalidateTags(['products']);
}
}use Vasco\FirestoreCache\FirestoreAdapter;
class CachePruneCommand extends Command
{
public function __construct(
private FirestoreAdapter $cache
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Remove all expired cache entries
$this->cache->prune();
return Command::SUCCESS;
}
}# Required
GOOGLE_CLOUD_PROJECT=your-project-id
# Optional - if not using default GCP credentials
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json# cloudrun.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: your-app
spec:
template:
metadata:
annotations:
run.googleapis.com/service-account: your-sa@project.iam.gserviceaccount.com
spec:
containers:
- image: gcr.io/project/image
env:
- name: GOOGLE_CLOUD_PROJECT
value: "your-project-id"# Grant Firestore User role to Cloud Run service account
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/datastore.user"- Firestore charges per document read/write operation
- Enable compression to reduce storage size (typically 60-80% reduction)
- Use appropriate TTL values to minimize storage
- Consider namespacing for cache isolation
- Firestore adds network latency (~10-50ms) compared to filesystem
- Much faster than filesystem on Cloud Run (no persistent disk)
- Use batch operations when possible (implemented internally)
- Consider longer TTL for frequently accessed data
- Enable compression for large cache values
- Use tags sparingly - Each tag creates an additional document
- Set appropriate TTL - Don't cache forever
- Monitor Firestore usage in GCP Console
- Use namespaces for multi-environment setups
composer testcomposer test:coveragecomposer phpstancomposer cs:check
composer cs:fixcomposer qasymfony_cache (collection)
├── {namespace}:{key} (document)
│ ├── value: string (compressed or raw)
│ ├── expiresAt: timestamp
│ ├── version: int
│ └── compressed: bool
symfony_cache_tags (collection)
├── {namespace}:{tag} (document)
│ └── items: array<string> (cache keys)
Please see CONTRIBUTING.md for details on how to contribute to this project.
If you discover any security-related issues, please email remi@vasco.fund instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
- Support for Firestore in Datastore mode
- Metrics and monitoring integration
- Async operations support
- Redis-like atomic operations
- Multi-region replication support
- Memcached/Redis on GCP - Better performance but requires managed service
- Symfony's ChainAdapter - Combine Firestore with APCu for local cache
- Google Cloud Storage - Cheaper but slower than Firestore
Q: Why Firestore instead of Redis/Memcached? A: Firestore is serverless, scales automatically, and works perfectly with Cloud Run's ephemeral containers. No need to manage a separate cache cluster.
Q: What about costs? A: Firestore pricing is per operation. With compression and proper TTL, costs are typically $5-20/month for small to medium applications.
Q: Can I use this in production? A: Yes! This adapter is production-ready with comprehensive tests, strict type checking (PHPStan level 9), and follows Symfony best practices.
Q: Does it work with Symfony 6? A: Yes, it supports Symfony 6.4 LTS and Symfony 7.x.