Best Practices

This page is a capstone checklist for the whole Symfony track — the habits worth carrying into every real project you build with it. Each item links back to the page that covers it in full, so treat this as a quick-reference summary rather than a first read.

1) Prefer PHP 8 Attributes Over YAML/XML

Symfony still supports YAML and XML configuration, but attributes keep the rule and the code it affects in the same file — a route stays next to the action it maps to, a validation constraint stays next to the property it protects. See Routing and Validation.

#[Route('/articles/{id}', name: 'article_show')]
public function show(int $id) { /* ... */ }
  • Default to attributes for routing and validation on new code.
  • Reach for YAML only when a value needs to change per-environment without touching PHP.

2) Keep Real Credentials Out of Version Control

Database usernames and passwords belong in .env.local, which is git-ignored by default, never in the committed .env file. Anyone who clones the repository gets working placeholder values from .env and supplies their own real credentials locally. See Database Configuration.

# .env.local — real credentials, never committed
DATABASE_URL="mysql://real_user:real_password@127.0.0.1:3306/app_db"
  • Confirm .env.local is listed in .gitignore on every project.
  • Never paste a real password directly into .env, even temporarily.

3) Keep Controllers Thin

A controller action's job is to receive the request, ask a service to do the real work, and return a response — not to contain business logic itself. Push data rules and processing into focused service classes, where they're easier to test and reuse from more than one action. See Services & Dependency Injection.

// Thin: the controller just delegates
public function publish(Article $article)
{
    $this->notifier->notifyPublished($article);
}
  • If an action is doing calculations or complex conditionals, move that logic into a service.
  • A controller action that's mostly delegation is usually a sign you're on the right track.

4) Let Form + Validation Handle Input

Hand-rolled $_POST checks mean hand-rolled CSRF handling and hand-rolled error messages — both easy to get wrong. The Form component paired with Assert constraints gives you both for free, consistently, on every form. See Forms and Validation.

if ($form->isSubmitted() && $form->isValid()) {
    // $article already satisfies its Assert constraints
}
  • Default to the Form component for anything that writes to an Entity.
  • Never trust $_POST or $request->request data as already valid.

5) Use IsGranted Consistently, Not Ad-Hoc Role Checks

Scattering manual if ($user->getRoles()) checks through controllers is easy to get inconsistent and easy to forget on a new action. Standardizing on #[IsGranted] or denyAccessUnlessGranted() makes every protected action enforce its role the same way, in the same place. See Security.

#[IsGranted('ROLE_ADMIN')]
public function delete(Article $article) { /* ... */ }
  • Guard every sensitive action with #[IsGranted] or denyAccessUnlessGranted().
  • Avoid writing your own role-comparison logic when the Security component already does it.

6) Favor Constructor Injection and Small Services

A handful of focused service classes, each type-hinted into the constructors that need them, is easier to test and reason about than one large "God" class or a pile of static helper methods. Let the container do the wiring instead of reaching for new or static calls.

public function __construct(private ArticleNotifier $notifier)
{
}
  • Split a growing service into smaller ones once it starts doing more than one job.
  • Type-hint dependencies in the constructor; avoid static factories and global state.

7) Use bin/console for Anything Outside an HTTP Request

Cron jobs, batch imports, and scheduled cleanup tasks don't have a browser waiting for a response, so they don't belong in a controller. A custom bin/console command is the natural home for that kind of work, and it can inject the exact same services a controller would. See Console Commands.

bin/console app:send-newsletter
  • Write a custom command for any job meant to run on a schedule rather than per-request.
  • Reuse existing services inside commands instead of duplicating their logic.
Quick recap:
PracticeCovered in
Prefer attributes over YAML/XMLRouting & Validation
Keep credentials in .env.localDatabase Configuration
Keep controllers thinServices & Dependency Injection
Let Form + Validation handle inputForms
Use IsGranted consistentlySecurity
Favor constructor injectionThis page
Use console commands for non-HTTP workConsole Commands
You've completed the Symfony track! Congratulations on working through everything from installation and directory structure to routing, controllers, Twig, Doctrine ORM, forms, validation, the service container, console commands, and security. The core ideas here — constructor injection, an ORM mapping objects to tables, attribute-based configuration, and a clean separation between thin controllers and focused services — aren't unique to Symfony. They carry straight over into Laravel, CakePHP, and most other modern PHP frameworks, so everything you've practiced here keeps paying off. Click Next below to head back to the PHP hub and pick another track to continue learning.