DEV_MODE is a PHP constant defined in config/constants.php that controls a significant portion of framework behaviour. It's the single most important constant to get right — wrong in development means stale cache frustration, wrong in production means serious performance degradation.
// config/constants.php
const DEV_MODE = true; // development
const DEV_MODE = false; // production
DEV_MODE and Symfony's APP_ENV are independent. You can mix them:
DEV_MODE |
APP_ENV |
Use case |
|---|---|---|
true |
dev |
Normal local development |
true |
prod |
Debug production config locally |
false |
dev |
Test production cache behaviour |
false |
prod |
Production |
Never deploy with DEV_MODE = true. The performance difference is significant — route parsing, template compilation, and translation scanning all happen on every request in dev mode.
// In PHP_SF\System\Kernel constructor
if ( DEV_MODE === true ) {
if ( function_exists( 'apcu_clear_cache' ) )
apcu_clear_cache();
}
APCu is cleared at the start of every request in dev mode. This ensures code changes are always reflected immediately — no stale in-process cache hiding your updates.
In production, APCu persists between requests and is only cleared by app:cache:clear or a PHP-FPM restart.
if ( DEV_MODE )
Debug::enable();
Enables Symfony's error handler which converts PHP errors, warnings, and notices into exceptions with full stack traces and context. Makes silent failures loud during development.
In production this is not enabled — unhandled exceptions go through Symfony's error page system configured in config/routes/framework.yaml.
In DEV_MODE = true, the router re-scans all registered controller directories on every request and never writes to the route cache:
// In Router::parseRoutes()
if ( DEV_MODE === false )
ca()->set( 'cache:routes_list', j_encode( static::$routesList ), null );
New routes, renamed routes, changed middleware — all picked up immediately without a cache clear. In production routes are cached in Redis indefinitely and require app:cache:clear after deployment.
TEMPLATES_CACHE_ENABLED = true has no effect when DEV_MODE = true. Templates always load from source files:
// In AbstractController::render() and AbstractView::import()
if ( TEMPLATES_CACHE_ENABLED ) {
$result = TemplatesCache::getInstance()->getCachedTemplateClass( $view );
// ...
}
Since TEMPLATES_CACHE_ENABLED is ignored in dev mode, template changes are always reflected without a cache clear.
In DEV_MODE, the Translator scans all entity classes for #[TranslatablePropertyName] attributes and adds missing keys to locale files on every request:
// In Translator::loadTranslation()
if ( DEV_MODE )
$this->saveTranslatablePropertyNames();
Add a new entity property with #[TranslatablePropertyName('My Property')] and the key appears in all locale files on the next request automatically.
In production, translations are loaded once from Redis cache and files are never touched.
// In Kernel::addRoutesToSymfony()
if ( DEV_MODE || ( $OAResponseAttrs = mca()->get( "cache:oa_response_attrs:$routeName" ) ) === null ) {
$OAResponseAttrs = ( new ReflectionClass( $route['class'] ) )
->getMethod( $route['method'] )
->getAttributes( Response::class );
In dev mode the OpenAPI attribute reflection runs on every request — newly added #[Response] attributes on framework routes are picked up in the API documentation immediately.
AbstractEntity::clearQueryBuilderCache() deletes Redis keys matching *doctrine_result_cache:* on every entity write. In production, query results are cached between writes. In dev mode the cache is never populated for doctrine result cache keys — changes to entity data always reflect immediately in queries.
public/testing.php is a designated scratchpad file included at the end of public/index.php before routing runs:
// public/index.php
require_once __DIR__ . '/testing.php';
// public/testing.php
<?php /** @noinspection ForgottenDebugOutputInspection */
declare( strict_types=1 );
//exit( die( dd() ) );
Use it for quick one-off debugging — dump a variable, test a function, exit early to inspect state — without touching any application code. The file is committed but its contents are intentionally throwaway. The @noinspection ForgottenDebugOutputInspection suppresses PhpStorm warnings about debug output in the file.
Clean it up after debugging. It runs on every request so leaving dd() calls in it will break the application for everyone.
A debug endpoint is available that dumps the full merged route list from both the framework router and Symfony:
GET /api/routes_list
// In DefaultController
#[NoReturn]
#[Route( url: 'api/routes_list', httpMethod: 'GET' )]
final public function api_routes_list(): void
{
dd(
array_merge(
Router::getRoutesList(),
Kernel::getInstance()
->getContainer()
->get( 'router' )
?->getRouteCollection()
?->all() ?? []
)
);
}
This endpoint is restricted to DEV_MODE=true only. Use it to:
routeLink()Two separate cache systems need clearing independently:
# Framework cache — Redis (routes, templates, translations)
php bin/console app:cache:clear
# Symfony cache — var/cache/ (container, router, Twig)
php bin/console symfony:cache:clear
app:cache:clear clears:
rca()->clear()aca()->clear() (if available)mca()->clear() (if available)s()->clear()symfony:cache:clear runs Symfony's standard cache:clear, cache:pool:clear, and cache:warmup commands in sequence.
In development you rarely need symfony:cache:clear unless you change a Symfony bundle configuration or service definition. You frequently need app:cache:clear after route changes or template changes when testing with DEV_MODE = false.
There is also an API endpoint for clearing application cache from a running server:
GET /api/cache_clear/all
This clears all cache adapters and is restricted to AVAILABLE_HOSTS. Useful in deployment scripts that need to clear cache without SSH access to the console.
The Symfony web profiler is available in dev environment and works normally — the toolbar appears at the bottom of every page response, showing request time, database queries, cache hits, events, and more.
Since framework routes are dispatched before Symfony boots, the profiler has limited visibility into framework-specific operations. It will show:
It will not show:
rc() / rca() / rp() directlyaca()Both are available everywhere via Symfony's VarDumper:
// Dump and continue
dump( $player );
dump( $request->request->all() );
dump( Router::$currentRoute );
// Dump and die
dd( $player, $guild, $allies );
dd() in a controller stops execution before Response::send() is called — the output buffer is never flushed and no header/footer renders. You see the raw dump output with no page layout around it, which is usually what you want when debugging controller logic.
In views, dump() output appears inline in the page at the point it's called. The VarDumper handler is reset to null at the start of Response::send() to prevent dump output leaking into buffered responses in unexpected ways:
// In Router::sendRouteMethodResponse()
if ( self::$routeMethodResponse instanceof Response ) {
VarDumper::setHandler( null );
ob_start( ... );
Leaving DEV_MODE = true in production — routes won't cache, APCu is wiped on every boot, translations are scanned on every request, and template compilation never runs. Performance will be noticeably worse. Always verify DEV_MODE = false before deploying.
Leaving debug code in testing.php — testing.php runs before every request. An exit() or dd() left in it breaks the entire application for all users. Make it a habit to clean the file immediately after using it.
Expecting the profiler to show Redis operations — direct Redis operations via rc(), rca(), and rp() are not instrumented by Symfony's profiler. Use redis-cli MONITOR for real-time Redis command tracing during development.
Only clearing one cache system after config changes — Symfony container changes need symfony:cache:clear. Framework route or template changes need app:cache:clear. Changing .env values needs both since the Symfony container caches compiled env vars. When in doubt, run both.
Using the routes list endpoint in production — GET /api/routes_list dumps the full route list including middleware configuration and controller class names. This is information disclosure you probably don't want publicly accessible. Remove or protect the endpoint before deploying.