Anti-Patterns & Common Mistakes
When to Use
When you need to identify and fix common config management mistakes — patterns that break deployment, cause data loss, or create security issues.
Critical Anti-Patterns
Making config changes in production UI
BAD WORKFLOW:
1. Edit view in production UI
2. Save changes
3. Changes live but not in Git
4. Next deploy: drush cim overwrites with Git version
5. Production changes lost
WHY THIS IS WRONG: Production changes aren't version-controlled, can't rollback, lost on next import from Git. Always develop locally, export, commit, import.
FIX: Develop locally -> Export -> Commit -> Import to production. Use drush config:status to detect drift.
No config schema defined
// Config saved but no schema defined
$config = \Drupal::service('config.factory')->getEditable('mymodule.settings');
$config->set('api_key', 'value')->save();
// On import: "Missing schema for mymodule.settings"
// On validation: "Config cannot be validated"
WHY THIS IS WRONG: Without schema, Drupal can't validate, type-cast, or constrain config values. Leads to type coercion failures, validation errors, broken imports.
FIX: Define schema in config/schema/mymodule.schema.yml with proper types and constraints.
Committing credentials to Git
# BAD — config/sync/mymodule.settings.yml committed to Git
api_key: 'secret-key-abc-123'
api_secret: 'super-secret-xyz'
WHY THIS IS WRONG: Git history is permanent and public (or accessible to unauthorized users). Credentials in repo are a security breach.
FIX: Use settings.php overrides (gitignored):
// settings.local.php
$config['mymodule.settings']['api_key'] = getenv('API_KEY');
Static Drupal calls in classes
// BAD
class MyService {
public function doSomething() {
$config = \Drupal::config('mymodule.settings');
}
}
WHY THIS IS WRONG: Tight coupling, not testable, violates Drupal coding standards. Static calls can't be mocked in unit tests.
FIX: Inject ConfigFactoryInterface:
// GOOD
class MyService {
protected ConfigFactoryInterface $configFactory;
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
}
Modifying config with overrides active
// Production has override in settings.php:
// $config['system.site']['name'] = 'Production Site';
// Admin edits site name via UI, saves
$config = \Drupal::service('config.factory')->getEditable('system.site');
$config->set('name', 'New Name')->save();
// Override still active — 'New Name' never displayed
WHY THIS IS WRONG: Overrides applied at runtime after database read. Edits to database value are overridden immediately. User sees no effect of their change.
FIX: Remove override from settings.php if config should be editable. Use Config Split for environment-specific values instead.
Not clearing cache after config import
drush cim -y
# Config imported, but site still uses old cached config
WHY THIS IS WRONG: Config values are cached (render cache, dynamic page cache, etc.). Imported changes not reflected until cache cleared.
FIX: Always clear cache after import:
drush cim -y && drush cr
Hardcoding config names
// BAD
$config = \Drupal::config('mymodule.settings');
$view = \Drupal::config('views.view.my_view');
WHY THIS IS WRONG: Fragile, breaks on renames, no IDE autocomplete, hard to refactor.
FIX: Use constants or entity type manager:
// GOOD
const SETTINGS_CONFIG = 'mymodule.settings';
$config = $this->configFactory->get(self::SETTINGS_CONFIG);
// BETTER for entities
$view = $this->entityTypeManager->getStorage('view')->load('my_view');
Importing without checking status first
# BAD
drush cim -y
# GOOD
drush config:status
drush config:diff system.site
drush cim -y
WHY THIS IS WRONG: Blind import can overwrite production-only changes, delete unexpected config, break site. Always review what's changing.
FIX: Check drush config:status and drush config:diff before import. Create database backup before import.
No FullyValidatable constraint
# BAD — Lenient validation
mymodule.settings:
type: config_object
mapping:
api_key:
type: string
# GOOD — Strict validation
mymodule.settings:
type: config_object
constraints:
FullyValidatable: ~
mapping:
api_key:
type: string
constraints:
NotBlank: {}
WHY THIS IS WRONG: Without FullyValidatable, Drupal allows missing keys, extra keys, invalid types. Validation is too lenient, invalid config slips through.
FIX: Add FullyValidatable constraint and proper field-level constraints.
Using config for content
# BAD — User data in config
mymodule.users:
users:
- name: 'John'
email: 'john@example.com'
- name: 'Jane'
email: 'jane@example.com'
WHY THIS IS WRONG: Config is for structure and settings, not user-generated content. Content belongs in nodes, users, custom entities. Config is deployed, content is site-specific.
FIX: Use content entities (nodes, custom entities) or State API for runtime data.
Heavy processing in config events
// BAD — Long-running task in SAVE event
public function onConfigSave(ConfigCrudEvent $event) {
// API call that takes 5 seconds
$this->apiClient->syncData();
}
WHY THIS IS WRONG: Config save events block the config save operation. Heavy processing makes all config saves slow, affects admin UI, import, etc.
FIX: Queue heavy tasks:
public function onConfigSave(ConfigCrudEvent $event) {
$queue = \Drupal::queue('mymodule_sync');
$queue->createItem(['config_name' => $event->getConfig()->getName()]);
}
See Also
- Best Practices & Patterns — correct patterns
- Security & Performance — security and performance issues
- Config Factory & Reading Config — proper config access