A Beginner's Guide to Symfony: Build a Task Manager CRUD App

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

  1. Install Doctrine:

    Ensure Doctrine is included:

    
    composer require symfony/orm-pack symfony/maker-bundle --dev
                    
  2. Configure the Database:

    Edit .env to 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, and task_manager with your MySQL credentials and database name.

  3. Create the Database:

    Run:

    
    php bin/console doctrine:database:create
                    

    This creates an empty database called task_manager.

  4. Create an Entity:

    Entities are PHP classes representing tables. Create a Task entity:

    
    php bin/console make:entity Task
                    

    Add fields:

    • name (string, length 255)
    • description (text)
    • dueDate (datetime)
    • completed (boolean)

    This generates src/Entity/Task.php.

  5. Migrate the Schema:

    Create a migration file:

    
    php bin/console make:migration
                    

    Apply it to create the task table:

    
    php bin/console doctrine:migrations:migrate
                    
  6. Interact with Data:

    Use the EntityManager to 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 %}

© 2025 Task Manager

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

  1. Run symfony server:start.
  2. Visit /tasks to see the task list.
  3. Click "Add New Task" to create a task.
  4. Edit or delete tasks from the list.
  5. Success messages appear after actions.

Troubleshooting:

  • Check var/log/dev.log for errors.
  • Use the Symfony Profiler (bottom bar in dev mode) to inspect queries.
  • Ensure style.css is in public/css/.

Step 6: Enhancing the Database

To make your app more robust:

  • Validation: Add constraints in TaskType.php (e.g., max length for name).
  • 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-bundle to seed test data.

Best Practices

  • Use APP_ENV=dev for development, prod for 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

Popular posts from this blog

Automate Blog Content Creation with n8n and Grok 3 API

Neuro-Symbolic Integration: Enhancing LLMs with Knowledge Graphs

Understanding and Using the Generalized Pareto Distribution (GPD)