PHP Symfony Basics — Complete Beginner's Guide to the Symfony Framework
In this tutorial, you will learn about PHP Symfony Basics. We cover key concepts, practical examples, and best practices to help you master this topic.
Symfony is a high-performance PHP framework built from reusable components that powers enterprise applications, APIs, and content management systems with a modular architecture and strict coding standards. This tutorial covers installing Symfony, understanding bundles and services, defining routes with annotations and attributes, building Twig templates, modeling data with Doctrine ORM and migrations, building forms with validation, and using the Symfony Console for CLI commands.
What You'll Learn
- Installing Symfony via Composer and understanding the project structure
- Creating bundles and registering services in the service container
- Defining routes using PHP 8 attributes and YAML configuration
- Building templates with Twig including inheritance and filters
- Using Doctrine ORM for entities, repositories, migrations, and relationships
- Creating and validating forms with Symfony Form component
- Writing custom console commands
Why It Matters
Symfony is the foundation of Drupal, eZ Platform, and many enterprise PHP applications. Its component-based design means you can use its libraries (HttpKernel, Routing, Console, Form) in any PHP project, even outside Symfony itself. The framework enforces high code quality standards, has a strong commitment to backward compatibility, and provides tools like the Symfony Profiler that make development and debugging transparent.
Real-World Use
The backend of Durga Antivirus Pro's cloud scanning service is built with Symfony. Its console commands process uploaded files in a job queue, the Doctrine ORM manages scan result storage, and the Symfony Mailer component sends alert notifications to customers. The framework's security component handles API token authentication and role-based access control.
Learning Path
flowchart LR A[Laravel Basics] --> B[Symfony Basics\nYou are here] B --> C[Security] style B fill:#f90,color:#fff
Installing Symfony
Create a new Symfony project:
composer create-project symfony/skeleton my-app
cd my-app
Start the development server:
symfony server:start
Output:
OK Web server is using PHP 8.3.0
OK Web server is listening on http://127.0.0.1:8000
Or use the PHP built-in server:
php -S 127.0.0.1:8000 -t public/
Understanding Bundles and Services
In Symfony, everything is a service. Services are registered in the service container and wired together using dependency injection. Bundles package related functionality (routing, templating, database).
Open config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
Create a simple service at src/Service/Greeting.php:
<?php
namespace App\Service;
class Greeting
{
public function greet(string $name): string
{
return "Hello, $name! Welcome to Symfony services.";
}
}
Use the service in a controller:
<?php
namespace App\Controller;
use App\Service\Greeting;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class GreetingController extends AbstractController
{
public function __construct(
private Greeting $greeting
) {}
#[Route('/hello/{name}', name: 'hello')]
public function hello(string $name): Response
{
$message = $this->greeting->greet($name);
return new Response($message);
}
}
Test with curl:
curl http://127.0.0.1:8000/hello/Alice
Output:
Hello, Alice! Welcome to Symfony services.
Routing with Attributes
Symfony supports PHP 8 attributes for routes. The #[Route] attribute can be applied to controllers and actions:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/products')]
class ProductController extends AbstractController
{
#[Route('/', name: 'product_index', methods: ['GET'])]
public function index(): JsonResponse
{
return $this->json([
['id' => 1, 'name' => 'Laptop'],
['id' => 2, 'name' => 'Phone'],
]);
}
#[Route('/{id}', name: 'product_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): JsonResponse
{
return $this->json(['id' => $id, 'name' => "Product $id"]);
}
}
Twig Templates
Twig is Symfony's templating engine. Create templates/base.html.twig:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Symfony App{% endblock %}</title>
</head>
<body>
<header>
<h1>Symfony Demo</h1>
</header>
<main>
{% block body %}{% endblock %}
</main>
</body>
</html>
Create templates/product/list.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Product List{% endblock %}
{% block body %}
<h2>Products</h2>
<ul>
{% for product in products %}
<li>{{ product.name }} - ${{ product.price|number_format(2) }}</li>
{% endfor %}
</ul>
{% endblock %}
Update the controller to render the template:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products', name: 'product_list')]
public function list(): Response
{
$products = [
['name' => 'Laptop', 'price' => 999.50],
['name' => 'Phone', 'price' => 699.99],
];
return $this->render('product/list.html.twig', ['products' => $products]);
}
}
Doctrine ORM and Migrations
Doctrine is Symfony's default ORM. Create an entity:
php bin/console make:entity Product
Follow the interactive prompts to add fields. The generated entity at src/Entity/Product.php:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProductRepository::class)]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: 'decimal', precision: 8, scale: 2)]
private ?float $price = null;
public function getId(): ?int { return $this->id; }
public function getName(): ?string { return $this->name; }
public function setName(string $name): static { $this->name = $name; return $this; }
public function getPrice(): ?float { return $this->price; }
public function setPrice(float $price): static { $this->price = $price; return $this; }
}
Configure the database in .env:
DATABASE_URL="mysql://root:password@127.0.0.1:3306/myapp"
Create the database and run migrations:
php bin/console doctrine:database:create
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Use the Repository in a controller:
<?php
namespace App\Controller;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/products/from-db', name: 'product_db')]
public function fromDatabase(ProductRepository $repository): Response
{
$products = $repository->findAll();
return $this->render('product/list.html.twig', ['products' => $products]);
}
}
Common Mistakes
- Not using autowiring: If a service constructor changes, autowiring automatically updates the container. Manually wiring services in services.yaml is error-prone.
- Putting business logic in controllers: Controllers should only handle HTTP concerns. Move logic to services or entities for testability.
- Forgetting to run migrations after entity changes: Doctrine checks the schema against the database. If you add a field but do not migrate, you get a database error.
- Using $this->get() instead of constructor injection: The old Controller::get() method is deprecated. Use constructor injection or action-level dependency injection for services.
- Not clearing the cache after configuration changes: Symfony caches configuration. Run
php bin/console cache:clearif your changes do not take effect.
Practice Questions
What is the purpose of autowiring in Symfony?
- Autowiring automatically resolves constructor dependencies from the service container, eliminating the need for manual service configuration in most cases.
How do you define a route with a numeric parameter in Symfony?
- Use the requirements parameter in the Route attribute:
#[Route('/product/{id}', requirements: ['id' => '\d+'])].
- Use the requirements parameter in the Route attribute:
What is Twig's
extendstag used for?- It tells Twig that the current template inherits from a parent template, filling in blocks defined by the parent.
How do you create a database table in Symfony?
- Create an entity with
php bin/console make:entity, generate a Migration withmake:migration, and run it withdoctrine:migrations:migrate.
- Create an entity with
Challenge: Build a contact management system with Symfony that has CRUD operations, form validation, and a Twig-based UI. Use Doctrine relationships to link contacts to categories.
Mini Project
Create a job board application with Symfony:
- Define a
Jobentity with fields: title, company, description, location, salary, and created_at. - Create a form type for job submissions with validation rules.
- Build routes and controllers for listing jobs, viewing a single job, and posting a new job.
- Use Twig templates with a base layout and form theming.
- Add a search feature that filters jobs by title or location using a Doctrine query.
- Protect the job posting route with Symfony's security component (form login).
FAQ
What's Next
Now that you know both Laravel and Symfony, learn how to secure your PHP applications in the Security lesson.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro