SOLID Best Practices Checklist
When to Use
Use this checklist during code review to verify SOLID compliance.
Checklist
Single Responsibility Principle
- [ ] Each service has one clear responsibility
- [ ] Controllers are thin (< 50 lines), delegate to services
- [ ] Forms only handle structure/validation, delegate processing to services
- [ ] Hook classes organized by domain (EntityHooks, FormHooks), not all in one
- [ ] No "Manager" or "Helper" services doing 10 unrelated things
Open/Closed Principle
- [ ] Using plugins to extend systems (blocks, fields, views)
- [ ] Using hooks/events to extend behavior, not modifying core
- [ ] Config overrides in settings.php, not editing config files
- [ ] Service decorators for extending existing services
- [ ] No patches to core unless absolutely necessary (and documented)
Liskov Substitution Principle
- [ ] Custom entities extend correct base (ContentEntityBase vs ConfigEntityBase)
- [ ] Forms extend correct base (FormBase vs ConfigFormBase vs SettingsForm)
- [ ] Calling parent methods in overrides (parent::submitForm())
- [ ] Access hooks return AccessResult, not boolean
- [ ] Not breaking behavioral contracts (e.g., save() must persist)
Interface Segregation Principle
- [ ] Entities implement only needed interfaces (EntityOwnerInterface if has owner)
- [ ] Not implementing interfaces with fake/stub methods
- [ ] Using focused injection interfaces (ContainerInjectionInterface, not fat interface)
- [ ] Role-specific interfaces, not one giant interface
- [ ] Using traits when implementing interfaces (EntityOwnerTrait)
Dependency Inversion Principle
- [ ] All dependencies constructor-injected, no
\Drupal::service()in classes - [ ] Type-hinting interfaces, not concrete classes
- [ ] Services defined in *.services.yml with
autowire: true - [ ] Service aliases for interfaces (Interface: '@service_id')
- [ ] No
new ClassName()in business logic (use factories or injection) - [ ] No global variables ($user), use injected services
Security Best Practices
- [ ] Access results include cache contexts (cachePerUser, cachePerPermissions)
- [ ] Using entity API, not direct database queries (access control built in)
- [ ] Input validation in forms, not just in services
- [ ] Not trusting user input (use $form_state->getValue(), validate/sanitize)
- [ ] Checking access in controllers (access callbacks in routing.yml)
Performance Best Practices
- [ ] Using OOP hooks (PSR-4 autoload) instead of procedural (always load)
- [ ] Autowiring services (reduces boilerplate)
- [ ] Caching expensive operations (entity queries, API calls)
- [ ] Lazy-loading services (
lazy: truein services.yml for expensive services) - [ ] Not doing heavy processing in hooks that run on every request
Testability
- [ ] Unit tests for services (mocked dependencies)
- [ ] Kernel tests for entity/form behavior
- [ ] All business logic in services, not controllers (easier to test)
- [ ] No static calls (mockable dependencies only)
See Also
- Common Mistakes for anti-patterns