A Beginner's Guide to Symfony: Build a Task Manager CRUD App
Welcome to my blog! If you're new to PHP web development or have dabbled with frameworks like Laravel, this guide is your gateway to Symfony—a powerful, flexible PHP framework for building modern web applications. In this tutorial, we'll create a Task Manager, a simple CRUD (Create, Read, Update, Delete) app, with a focus on crafting a clean front-end using HTML, CSS, and Symfony's Twig templating engine. We'll also dive deep into connecting Symfony to a database using Doctrine, ensuring you understand every step. By the end, you'll have a fully functional app and the confidence to explore Symfony further.
This guide is tailored for beginners. I assume you know basic PHP, HTML, CSS, and a bit of SQL. If you're familiar with Laravel, I'll draw comparisons to make the transition smoother. Let's dive in!
What is Symfony?
Symfony is an open-source PHP framework launched in 2005 by Fabien Potencier. It's maintained by SensioLabs and a global community. Unlike Laravel, which prioritizes rapid development with conventions, Symfony offers flexibility through its modular, component-based architecture. You can use it for full-stack apps or just specific components (e.g., Laravel uses Symfony's HttpFoundation).
Why Choose Symfony?
- Modular Design: Over 50 reusable components for flexibility.
- Scalability: Powers high-traffic sites like Spotify and BlaBlaCar.
- Explicit Control: More configuration than Laravel's "magic," ideal for complex projects.
- Community & Docs: Excellent documentation at symfony.com/doc.
- Long-Term Support: Stable versions like Symfony 7.1 (as of September 2025).
Laravel Users: Symfony's bin/console is like artisan, and both use MVC. However, Symfony requires more explicit setup, which can feel verbose but ensures clarity.
Setting Up Your Environment
Before we code, let's set up your system. You'll need:
- PHP 8.1+ (with PDO for databases).
- Composer (PHP dependency manager).
- MySQL (or another database like PostgreSQL).
- Symfony CLI (recommended for ease).
Step 1: Install Composer
Download Composer from getcomposer.org:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/local/bin/composer
Step 2: Install Symfony CLI
The Symfony CLI simplifies project creation and server management:
curl -sS https://get.symfony.com/cli/installer | bash
Follow OS-specific instructions to add it to your PATH.
Step 3: Create a Symfony Project
We'll use the full-stack skeleton for a complete setup with Twig and Doctrine:
composer create-project symfony/website-skeleton my-task-app
cd my-task-app
Step 4: Start the Server
Run the built-in development server:
symfony server:start
Visit http://localhost:8000 to see the Symfony welcome page.
Troubleshooting: Run symfony check:requirements to verify your setup. Missing PHP extensions? Install them (e.g., sudo apt-get install php8.1-mysql on Ubuntu).
Understanding Symfony's Structure
Your project folder contains:
bin/: CLI tools (console).config/: Configuration files (YAML, PHP).public/: Web root (index.php, assets).src/: Your PHP code (controllers, entities).templates/: Twig templates.var/: Cache, logs.vendor/: Dependencies.
Bundles: These are like Laravel packages, extending functionality. Your app is a bundle, and you'll add others like DoctrineBundle.
Core Concepts
1. Routing
Routes map URLs to controllers. We'll use PHP attributes (modern, like Laravel's route definitions).
Example:
// src/Controller/WelcomeController.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class WelcomeController extends AbstractController
{
#[Route('/welcome', name: 'welcome')]
public function index(): Response
{
return $this->render('welcome/index.html.twig', ['message' => 'Hello, Symfony!']);
}
}
2. Controllers
Controllers process requests and return responses. Use AbstractController for helpers like rendering templates.
3. Twig Templates
Twig is Symfony's templating engine, similar to Laravel's Blade but stricter (auto-escapes output). Templates use .html.twig files and support inheritance.
Example (templates/welcome/index.html.twig):
{% extends 'base.html.twig' %}
{% block title %}Welcome{% endblock %}
{% block body %}
{{ message }}
{% endblock %}
4. Database with Doctrine
Doctrine is Symfony's ORM, like Laravel's Eloquent, mapping PHP classes to database tables. Here's a beginner-friendly guide to setting it up:
Step-by-Step Database Setup
- Install Doctrine:
Ensure Doctrine is included:
composer require symfony/orm-pack symfony/maker-bundle --dev - Configure the Database:
Edit
.envto set your database connection. For MySQL:DATABASE_URL="mysql://root:password@127.0.0.1:3306/task_manager?serverVersion=8&charset=utf8mb4"Replace
root,password, andtask_managerwith your MySQL credentials and database name. - Create the Database:
Run:
php bin/console doctrine:database:createThis creates an empty database called
task_manager. - Create an Entity:
Entities are PHP classes representing tables. Create a
Taskentity:php bin/console make:entity TaskAdd fields:
name(string, length 255)description(text)dueDate(datetime)completed(boolean)
This generates
src/Entity/Task.php. - Migrate the Schema:
Create a migration file:
php bin/console make:migrationApply it to create the
tasktable:php bin/console doctrine:migrations:migrate - Interact with Data:
Use the
EntityManagerto perform CRUD operations. Example:// Create a task $task = new Task(); $task->setName('Learn Symfony'); $task->setDueDate(new \DateTime('tomorrow')); $entityManager->persist($task); $entityManager->flush(); // Read all tasks $tasks = $entityManager->getRepository(Task::class)->findAll();
Common Pitfalls:
- Wrong
DATABASE_URL? Double-check credentials and MySQL port. - Table not created? Ensure migrations ran successfully.
- Use the Symfony Profiler (bottom bar in dev mode) to debug queries.
Why Doctrine? It abstracts SQL, ensures type safety, and supports multiple databases (MySQL, PostgreSQL, etc.).
Building the Task Manager CRUD App
Let's create a Task Manager where users can create, view, edit, and delete tasks. We'll use clean, responsive front-end templates and connect to the database with Doctrine.
Step 1: Create the Entity
Already done above (Task entity with name, description, dueDate, completed). Ensure the table exists via migrations.
Step 2: Create the Form
Generate a form for tasks:
php bin/console make:form TaskType Task
Edit src/Form/TaskType.php for labels and validation:
// src/Form/TaskType.php
namespace App\Form;
use App\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', null, [
'label' => 'Task Name',
'constraints' => [new NotBlank(['message' => 'Task name is required'])]
])
->add('description', null, ['label' => 'Description'])
->add('dueDate', DateTimeType::class, [
'label' => 'Due Date',
'widget' => 'single_text'
])
->add('completed', null, ['label' => 'Completed?']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
Step 3: Create the Controller
Generate a controller:
php bin/console make:controller TaskController
Update src/Controller/TaskController.php:
// src/Controller/TaskController.php
namespace App\Controller;
use App\Entity\Task;
use App\Form\TaskType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class TaskController extends AbstractController
{
#[Route('/tasks', name: 'task_index')]
public function index(EntityManagerInterface $em): Response
{
$tasks = $em->getRepository(Task::class)->findAll();
return $this->render('task/index.html.twig', ['tasks' => $tasks]);
}
#[Route('/tasks/new', name: 'task_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
$task = new Task();
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($task);
$em->flush();
$this->addFlash('success', 'Task created!');
return $this->redirectToRoute('task_index');
}
return $this->render('task/new.html.twig', ['form' => $form->createView()]);
}
#[Route('/tasks/{id}/edit', name: 'task_edit')]
public function edit(Request $request, Task $task, EntityManagerInterface $em): Response
{
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->flush();
$this->addFlash('success', 'Task updated!');
return $this->redirectToRoute('task_index');
}
return $this->render('task/edit.html.twig', ['form' => $form->createView(), 'task' => $task]);
}
#[Route('/tasks/{id}/delete', name: 'task_delete', methods: ['POST'])]
public function delete(Request $request, Task $task, EntityManagerInterface $em): Response
{
if ($this->isCsrfTokenValid('delete'.$task->getId(), $request->get('_token'))) {
$em->remove($task);
$em->flush();
$this->addFlash('success', 'Task deleted!');
}
return $this->redirectToRoute('task_index');
}
}
Explanation:
index: Fetches all tasks from the database.new: Creates a form, saves a new task.edit: Updates an existing task.delete: Removes a task with CSRF protection.addFlash: Shows success messages.
Step 4: Create Front-End Templates
We'll use Twig for templates, styled with CSS for a clean, responsive UI. Readers will copy these into their Symfony project.
Base Template
Create templates/base.html.twig in your Symfony project:
{% block title %}Task Manager{% endblock %}
{% block stylesheets %}{% endblock %}
Task Manager
{% for message in app.flashes('success') %}
{{ message }}
{% endfor %}
{% block body %}{% endblock %}
CSS
Create public/css/style.css in your Symfony project:
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background: #f4f4f4;
}
header {
background: #2c3e50;
color: #fff;
padding: 10px 20px;
text-align: center;
}
header h1 {
margin: 0;
}
nav a {
color: #fff;
margin: 0 15px;
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
main {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.alert-success {
background: #dff0d8;
color: #3c763d;
padding: 10px;
margin-bottom: 15px;
border-radius: 5px;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.task-list li:last-child {
border-bottom: none;
}
.task-list a, .task-list button {
margin-left: 10px;
padding: 5px 10px;
border-radius: 3px;
}
.task-list a {
background: #3498db;
color: #fff;
text-decoration: none;
}
.task-list button {
background: #e74c3c;
color: #fff;
border: none;
cursor: pointer;
}
.task-list a:hover, .task-list button:hover {
opacity: 0.8;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
}
form label {
font-weight: bold;
}
form input, form textarea {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
form button {
background: #2ecc71;
color: #fff;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
form button:hover {
background: #27ae60;
}
footer {
text-align: center;
padding: 10px;
background: #2c3e50;
color: #fff;
}
Note: Place this CSS file in your Symfony project's public/css/ folder.
Index Template
Create templates/task/index.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Task List{% endblock %}
{% block body %}
Tasks
Add New Task
{% if tasks|length > 0 %}
{% for task in tasks %}
-
{{ task.name }} - Due: {{ task.dueDate|date('Y-m-d') }}
{% if task.completed %} (Completed) {% endif %}
Edit
{% endfor %}
{% else %}
No tasks found.
{% endif %}
{% endblock %}
New/Edit Template
Create templates/task/new.html.twig (copy for edit.html.twig, change title to "Edit Task"):
{% extends 'base.html.twig' %}
{% block title %}New Task{% endblock %}
{% block body %}
Create New Task
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
{% endblock %}
Step 5: Test the App
- Run
symfony server:start. - Visit
/tasksto see the task list. - Click "Add New Task" to create a task.
- Edit or delete tasks from the list.
- Success messages appear after actions.
Troubleshooting:
- Check
var/log/dev.logfor errors. - Use the Symfony Profiler (bottom bar in dev mode) to inspect queries.
- Ensure
style.cssis inpublic/css/.
Step 6: Enhancing the Database
To make your app more robust:
- Validation: Add constraints in
TaskType.php(e.g., max length forname). - Custom Queries: Create a repository method for overdue tasks:
// src/Repository/TaskRepository.php public function findOverdue(): array { return $this->createQueryBuilder('t') ->andWhere('t.dueDate < :now') ->setParameter('now', new \DateTime()) ->getQuery() ->getResult(); } - Fixtures: Use
doctrine/doctrine-fixtures-bundleto seed test data.
Best Practices
- Use
APP_ENV=devfor development,prodfor production. - Clear cache:
php bin/console cache:clear. - Secure forms with CSRF tokens (included above).
- Test with PHPUnit (
composer require phpunit). - Explore Symfony Docs and SymfonyCasts.
Conclusion
Congratulations! You've built a Task Manager with Symfony, featuring a responsive front-end and robust database integration. You’ve learned:
- Setting up Symfony and Doctrine.
- Creating controllers, forms, and Twig templates.
- Styling with HTML and CSS.
- Managing a database with Doctrine.
Next steps: Add user authentication, task categories, or an API. Share your app in the comments, and happy coding!
Comments
Post a Comment