sympress/framework-bundle is a SymPress bundle that brings Symfony-style framework services into WordPress projects.
It now uses Symfony's real symfony/framework-bundle as the compatibility layer and keeps the SymPress
WordPress object-cache integration on top.
The core surfaces are:
- Symfony FrameworkBundle web/kernel services such as request stack, HTTP kernel, controllers, error rendering, routing infrastructure and console integration.
- Symfony Cache 8.2 based PSR-6, PSR-16 and Symfony Contracts cache services.
- Framework-Bundle-style cache pool services such as
cache.app,cache.system,cache.validator,cache.serializer, adapter prototypes, clearers, pruners, tag-aware pools, Redis tag-aware pools, named custom pools andcache:pool:*console commands. - WordPress
object-cache.phpdrop-in support with a Symfony-oriented operational surface: APCu, Redis, Memcached, filesystem, SQLite/PDO, request-memory caching, multisite global groups, non-persistent groups, group flushing, runtime flushing, admin-bar flush, WP-CLI flush/purge and scheduled purge.
Optional FrameworkBundle integrations such as Form, Validator, Serializer, Messenger, Mailer, Notifier, HttpClient, Workflow, Lock, RateLimiter, UID, WebLink and Webhook are enabled through Symfony's upstream configuration whenever the matching Symfony component is installed.
Require the package in a SymPress project:
composer require sympress/framework-bundleProjects must provide a randomly generated APP_SECRET of at least 32 bytes for production.
An existing missing or shorter value remains usable during an upgrade: WordPress object caching,
cache.app and custom application pools use request memory until a strong secret is supplied.
This also works when updating the bundle with an existing Kernel 1.1.3 installation.
Runtime Doctor checks this production requirement before deployment. The bundle passes APP_SECRET
directly to Symfony's FrameworkBundle and does not derive a secret from project paths or AUTH_KEY.
Application pools authenticate serialized payloads before restoring values, including filesystem,
PDO/SQLite, APCu, Redis and Memcached adapters and configured custom marshallers. Existing unsigned
entries become cache misses and refill normally after the upgrade. Application pools configured with
PHP file or system adapters use signed filesystem storage instead. Symfony's own cache.system
retains its trusted compiled-code storage and remains available without an application secret.
The bundle is discovered through Composer metadata:
{
"extra": {
"kernel": {
"bundle": "SymPress\\Framework\\SymPressFrameworkBundle",
"entry": "framework-bundle"
}
}
}Run the package checks before opening a pull request:
composer qaThe QA script runs PHPCS, PHPStan and PHPUnit. The GitHub workflow uses the
shared SymPress workflow set and the organization .github repository provides
issue and community templates.
The separate cache-backend smoke runs the native adapters against real Redis and Memcached services in CI. Run it locally with both PHP extensions and services available:
SYMPRESS_LIVE_CACHE_TESTS=1 composer tests:backendsThe bundle provides defaults that work without project config. Projects can override the framework.cache parameter in a SymPress config file:
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $container): void {
$container->parameters()->set('framework.cache', [
'app' => 'cache.adapter.redis',
'default_redis_provider' => 'redis://redis:6379',
'pools' => [
'cache.marketing' => [
'adapter' => 'cache.adapter.filesystem',
'default_lifetime' => 600,
'tags' => true,
'public' => true,
],
'cache.remote' => [
'adapters' => [
['name' => 'cache.adapter.redis', 'provider' => 'redis://redis:6379'],
'cache.adapter.array',
],
],
],
]);
};Symfony-style extension configuration is also supported:
framework:
secret: '%env(APP_SECRET)%'
router:
resource: '%kernel.project_dir%/config/routes.yaml'
cache:
app: cache.adapter.redis
default_redis_provider: 'redis://redis:6379'Pool configuration accepts Symfony-style adapter or adapters keys, provider-specific adapter entries, tags, default_lifetime, marshaller, clearer and early_expiration_message_bus tag attributes. cache.adapter.redis_tag_aware and cache.adapter.valkey_tag_aware are treated as native tag-aware pools instead of being wrapped in a generic TagAwareAdapter.
The WordPress drop-in can be configured with constants or environment variables.
Constants take precedence; native Runtime/Symfony Dotenv values in $_ENV and
$_SERVER are consumed before the process environment. Credentials from .env
need no putenv() export to child processes:
define('SYMPRESS_CACHE_DRIVER', 'redis');
define('SYMPRESS_CACHE_DSN', 'redis://redis:6379');
define('SYMPRESS_CACHE_IN_MEMORY', true);
define('SYMPRESS_CACHE_PURGE_INTERVAL', 43200);
define('SYMPRESS_CACHE_SECRET', getenv('APP_SECRET'));Supported drivers are array, filesystem, apcu, redis, memcached, pdo, sqlite and null. SYMPRESS_CACHE_DRIVER_ARGS accepts JSON or base64-encoded JSON for driver-specific settings.
SYMPRESS_CACHE_BYPASS can be set as a constant or environment variable to disable the drop-in for emergency operations.
The canonical drop-in source is vendor/sympress/framework-bundle/dropin/object-cache.php.
Projects managed by sympress/runtime should publish that file into
WP_CONTENT_DIR/object-cache.php through the SymPress Runtime dropins step:
{
"dropins": {
"object-cache.php": "vendor/sympress/framework-bundle/dropin/object-cache.php"
}
}Run only the drop-in publish step when the source changes:
composer sympress-runtime dropins --no-interactionProjects without SymPress Runtime can explicitly call DropInInstaller::install() during setup, or dispatch sympress_setup_object_cache. The bundle never scans or publishes the drop-in during ordinary requests. Installation respects wp_is_file_mod_allowed() and DISALLOW_FILE_MODS.
Managed SymPress drop-ins are only rewritten when their contents change and third-party drop-ins without the sympress-framework-object-cache marker are left untouched by the runtime installer.
SymPress Runtime remains the preferred owner in SymPress Runtime projects because it publishes the drop-in during Composer/project setup instead of an explicit application setup step.
The delegator resolves the Composer autoloader from APP_PROJECT_DIR, WP_CONTENT_DIR, ABSPATH, or nearby active release directories, then uses SYMPRESS_PROJECT_DIR as a compatibility fallback; SYMPRESS_COMPOSER_AUTOLOAD and SYMPRESS_OBJECT_CACHE_FUNCTIONS can override those paths for custom layouts.
This makes the same file work when copied by SymPress Runtime, symlinked from content-dev, or installed by the runtime fallback.
If a persistent backend cannot be initialized, the drop-in logs or warns about the backend failure before falling back to request-local array cache.
WP-CLI cache flushes run directly in the current CLI process and never create temporary PHP endpoints in the web root.
Redis and Memcached use native object-cache backends instead of Symfony internals for WordPress counter semantics. add, replace, incr and decr are mapped to backend-native atomic operations where the backend supports them; Redis counters use a Lua script so missing keys are not created and decrements clamp to zero like WordPress expects. Existing non-numeric counter values follow WordPress core semantics and are treated as zero. Flushes use versioned namespaces, so group/runtime invalidation does not depend on scanning or reflecting backend internals. The other Symfony-backed drivers keep best-effort semantics because PSR-6 does not expose cross-process compare-and-swap primitives.
All persistent WordPress drivers require SYMPRESS_CACHE_SECRET or APP_SECRET of at least 32 bytes;
AUTH_KEY is not a signing-secret fallback. Missing or short object-cache secrets use request-local
cache without repeating a warning on every request; production validation reports the missing
requirement. Redis and Memcached retain
native counter operations; APCu, filesystem and SQLite/PDO authenticate serialized values with the
same signed codec before reconstructing objects. Treat persistent cache backends and the kernel
cache directory as trusted infrastructure; do not expose them to untrusted writers.
WordPress cache prefixes include a hash of the project identity and environment, including when
SYMPRESS_CACHE_PREFIX is set. Identity resolves from the literal SYMPRESS_PROJECT_DIR, then
APP_PROJECT_DIR, ABSPATH, WP_CONTENT_DIR, then the working directory. Set SYMPRESS_PROJECT_DIR
to a persistent deployment base or project identifier before the kernel/drop-in boots and retain it
across release directories. Symfony's default framework.cache.prefix_seed uses the same identity,
falling back to kernel.project_dir when it is absent. Package/configuration discovery and the
kernel's actual project directory still use the active release. Environment resolves from APP_ENV, APP_RUNTIME_ENV,
WP_ENVIRONMENT_TYPE, then production. The signing key is also bound to this scoped prefix.
This changes the cache identity on upgrade; existing cache records remain unused and refill normally.
Operational notes:
composer sympress-runtime dropinspreserves unmanaged targets in native mode and respectsprevent-overwrite. Do not enable this drop-in alongside another object-cache drop-in.- The portable delegator performs a few early
is_file()checks to resolve the Composer autoloader. For standard SymPress Runtime layouts this avoids absolute build paths while keeping request overhead small. - Existing deployments with an older generated drop-in keep using it until SymPress Runtime republishes the file or an explicit setup installer rewrites a managed SymPress drop-in.
When a secret is configured, unsigned sympress-cache-v1 and unframed string
payloads are misses; legacy decoding remains available only to the explicitly
secretless codec. Counter integers remain native numeric values. Signed payloads
validate the HMAC before deserializing. The codec permits only stdClass,
WP_Post, WP_Term, WP_Comment, WP_User, WP_Error, WP_Site, WP_Network, DateTime,
DateTimeImmutable, and DateTimeZone. Unsupported objects (including nested
objects) are rejected rather than invoking application magic methods. Custom
application cache objects can be admitted with SYMPRESS_CACHE_ALLOWED_CLASSES, a comma-separated
list of exact class names such as WC_Product,Vendor\\Plugin\\CacheValue. Wildcards and permissive
boolean values are rejected. Additional classes apply only to authenticated signed payloads;
review their deserialization/magic methods before adding them. Classes may load later during
WordPress/plugin bootstrap. Prefer plain data arrays when object restoration is unnecessary.
Filesystem and default SQLite cache paths use a private per-user temporary directory with a
separate project/environment namespace and mode 0700. APCu still requires trusted PHP-FPM pools:
the extension can deserialize PHP objects written directly through its API before userland code runs.
The SymPress marshaller writes signed strings only. Directories beneath
WP_CONTENT_DIR or the declared HTTP document root are rejected; configure a
private path outside every location served by your web server. Initialization
failure diagnostics include the exception class and never the exception text,
which can contain a backend DSN or credentials.
wordpress.content_url is an environment placeholder resolved through
WordPressContentUrlProcessor when the container runs, so container compilation
does not capture a build host URL. Its runtime value comes from content_url('/').