ZetCode

Symfony Entity

last modified March 4, 2025

Symfony Entity tutorial shows how to create entities in Symfony application.

Symfony

Symfony is one of the leading PHP frameworks. It is a set of reusable PHP components and a PHP framework for web projects. Symfony was published as free software in 2005. Symfony was inspired by Django, RoR, and Spring frameworks.

Entity

An entity is a lightweight domain object which is to be persisted. Typically, an entity represents a table in a relational database, and each entity instance corresponds to a row in the table.

A repository is an abstraction of the persistence functionality. It allows to store, retrieve and search for entity objects. In essence, a repository is a collection of entity objects.

Symfony entity example

In the following example, we work with the City entity.

$ symfony new syment --version=7.2 --webapp
$ cd syment

We create a new Symfony 7.2 project with the --webapp option, which includes common bundles for web applications, and navigate to the project directory.

$ php bin/console --version
Symfony 7.2.0 (env: dev, debug: true)

We work with Symfony 7.2.0 version.

$ composer require symfony/orm-pack

We install the Doctrine ORM pack, which includes annotations and other necessary dependencies. The annot package is no longer needed separately as it's bundled with symfony/orm-pack.

$ composer require --dev orm-fixtures maker

We install the fixtures and maker bundles for development purposes. Fixtures load fake data into the database, and the maker bundle assists with scaffolding.

.env
DATABASE_URL="sqlite:///%kernel.project_dir%/var/ydb.db"

In the .env file, we define the database URL. We use SQLite for simplicity in this example.

$ php bin/console doctrine:database:create

The doctrine:database:create command creates a new database based on the provided URL.

$ php bin/console make:entity

Using the make:entity command, we create a new City entity. This generates src/Entity/City.php and src/Repository/CityRepository.php. We add two properties: name (string, 255 characters) and population (integer).

src/Entity/City.php
<?php

namespace App\Entity;

use App\Repository\CityRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CityRepository::class)]
#[ORM\Table(name: 'cities')]
class City
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column]
    private ?int $population = 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 getPopulation(): ?int
    {
        return $this->population;
    }

    public function setPopulation(int $population): static
    {
        $this->population = $population;
        return $this;
    }
}

This is the City entity updated for Symfony 7.2. We use PHP 8 attribute syntax (e.g., #[ORM\Entity]) instead of PHPDoc annotations. The entity maps to the cities table.

src/Repository/CityRepository.php
<?php

namespace App\Repository;

use App\Entity\City;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<City>
 *
 * @method City|null find($id, $lockMode = null, $lockVersion = null)
 * @method City|null findOneBy(array $criteria, array $orderBy = null)
 * @method City[]    findAll()
 * @method City[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class CityRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, City::class);
    }
}

The CityRepository remains largely the same but uses modern PHP type hints and docblock notation.

$ php bin/console make:migration

We generate a migration with make:migration to version the database schema changes.

$ php bin/console doctrine:migrations:migrate

This applies the migration, creating a file like Version20250304120000.php (timestamp will vary).

src/Migrations/Version20250304120000.php
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250304120000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Create cities table';
    }

    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE cities (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
          name VARCHAR(255) NOT NULL, population INTEGER NOT NULL)');
    }

    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE cities');
    }
}

The migration file now includes a description and simplified SQLite-specific SQL.

$ php bin/console make:fixtures

We create a CityFixtures class with make:fixtures to load sample data.

src/DataFixtures/CityFixtures.php
<?php

namespace App\DataFixtures;

use App\Entity\City;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class CityFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $cities = [
            ['name' => 'Bratislava', 'population' => 432000],
            ['name' => 'Budapest', 'population' => 1759000],
            ['name' => 'Prague', 'population' => 1280000],
            ['name' => 'Warsaw', 'population' => 1748000],
            ['name' => 'Los Angeles', 'population' => 3971000],
            ['name' => 'New York', 'population' => 8550000],
            ['name' => 'Edinburgh', 'population' => 464000],
            ['name' => 'Berlin', 'population' => 3671000],
        ];

        foreach ($cities as $data) {
            $city = new City();
            $city->setName($data['name']);
            $city->setPopulation($data['population']);
            $manager->persist($city);
        }

        $manager->flush();
    }
}

The CityFixtures class is updated with a more concise structure using an array and a loop.

$ php bin/console doctrine:fixtures:load

This command loads the fixture data into the cities table.

src/Controller/CityController.php
<?php

namespace App\Controller;

use App\Repository\CityRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class CityController extends AbstractController
{
    #[Route('/cities', name: 'cities')]
    public function index(CityRepository $cityRepository): JsonResponse
    {
        $cities = $cityRepository->findAll();

        if (empty($cities)) {
            return new JsonResponse(['message' => 'No data found'], Response::HTTP_NOT_FOUND);
        }

        return $this->json($cities);
    }
}

The CityController uses the new #[Route] attribute syntax and returns a JsonResponse directly. The response handling is streamlined.

$ symfony serve

We start the Symfony development server.

$ curl localhost:8000/cities
[{"id":1,"name":"Bratislava","population":432000},
{"id":2,"name":"Budapest","population":1759000},
{"id":3,"name":"Prague","population":1280000},
{"id":4,"name":"Warsaw","population":1748000},
{"id":5,"name":"Los Angeles","population":3971000},
{"id":6,"name":"New York","population":8550000},
{"id":7,"name":"Edinburgh","population":464000},
{"id":8,"name":"Berlin","population":3671000}]

We test the endpoint with curl and retrieve the JSON data.

In this tutorial, we worked with an entity in a Symfony 7.2 application using modern PHP and Symfony features.

List all Symfony tutorials.