Symfony is a powerful and flexible PHP framework designed for building robust web applications. It follows the Model-View-Controller (MVC) architectural pattern, which helps separate the application logic from the user interface, promoting organized and maintainable code. One of Symfony's standout features is its modularity; it is built on a collection of reusable components that can be used independently or together, allowing developers to tailor their applications to specific needs. I will install Symfony 6.4 version and this is Linux 🐧 tutorial!
Alright let's enter into "skeleton/public" directory and start PHP server there:
The Symfony Maker Bundle is a development tool that helps Symfony developers quickly generate code and boilerplate for common tasks. It streamlines the process of creating various components of a Symfony application, allowing developers to focus more on the business logic rather than repetitive coding tasks.
. Read about
format before you edit these type of files.
In Symfony, a service is a reusable piece of code that performs a specific task or provides a particular functionality within an application. Services are typically defined as classes that encapsulate business logic, data processing, or any other operations that can be reused across different parts of the application. Sadly we can't use maker to create service.
With the TemplateService, you can load an HTML file and pass it as a string into the parse method. The method will search through the loaded string for tokens like {{test}} and replace them with the corresponding values from the associative array you provide.
PHP Twig is a popular templating engine for PHP. It is designed to be simple, flexible, and powerful, making it easier to create and manage dynamic web pages and applications. Twig provides a syntax that is easy to read and write, and it allows developers to separate the presentation logic from the application logic. This separation of concerns makes the code more maintainable and easier to update.
directory. This is where your twig html files will be. Create file
The rootUrl (obtained using $request->getSchemeAndHttpHost()) contains the base URL of the application, including the scheme (HTTP/HTTPS) and the host (domain name or IP address). This can be very useful in templates where you need to generate absolute URLs for assets, links, or API endpoints.
For instance, if you need to link to a static asset (like an image or a stylesheet) or create an absolute link in your HTML, you can prepend rootUrl to relative paths.
What is an Entity?
In Symfony, an
Entity is a PHP class that maps to a database table. Each property in the entity class corresponds to a column in the table. Entities are the building blocks of your application's data model.
Symfony uses Doctrine ORM (Object-Relational Mapping) to manage entities. Doctrine allows you to define your data structure in PHP code, and it handles the underlying SQL database interactions.
So we will need to install
doctrine:
composer require doctrine/orm
😋 What's the point of ORM when you can just ask AI to create an SQL database for you and provide more secure PDO functions?
Creating an Entity
php bin/console make:entity
This command will guide you through creating a new entity class. You'll be asked to define the class name and its properties. Each property corresponds to a column in the database table.
Remember you are doing this so that you don't need to write SQL code. Entities are in
/src/Entity directory.
For this tutorial, create "title" string [255] and "content" string [5000] chars long.
Mapping Entities to the Database
After defining your entity, Symfony will use Doctrine's
annotations to map the class properties to the database columns. These annotations specify the data type, constraints, and relationships between entities.
Example of an entity class:
namespace App\Entity;
use App\Repository\PostRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 5000)]
private ?string $content = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): static
{
$this->content = $content;
return $this;
}
}
In Symfony, a Repository is a class that provides a way to interact with the database for a specific entity. It allows you to encapsulate database queries, offering methods to find, save, or retrieve data from the entity's table, making data access more organized and reusable in your application. PostRepository.php is generated in
/src/Repository:
namespace App\Repository;
use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
// /**
// * @return Post[] Returns an array of Post objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Post
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
ENV
The
.env file in Symfony is a key component used to manage environment-specific configurations in your Symfony application. It allows you to define environment variables that can be used throughout your application, helping to keep sensitive information, such as database credentials or API keys, out of your source code.
First let's create database in phpMyAdmin:
DROP DATABASE IF EXISTS symfony_test;
CREATE DATABASE symfony_test;
DROP USER IF EXISTS 'symfony_test'@'localhost';
CREATE USER 'symfony_test'@'localhost' IDENTIFIED BY 'symfony_test';
GRANT ALL ON symfony_test.* TO 'symfony_test'@'localhost';
USE symfony_test;
We want to add is mariadb (MYSQL) server to env.
🚀 mariadb --version
mariadb Ver 15.1 Distrib 10.6.18-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
Version number is important. So at bottom of
.env we add:
DATABASE_URL="mysql://symfony_test:symfony_test@127.0.0.1:3306/symfony_test?serverVersion=mariadb-10.6.18"
Make sure you have only one DATABASE_URL uncommented!
Database Migrations
Once your entity is defined, you need to create a corresponding database table. Symfony uses
migrations to handle this. A migration is a PHP file that contains SQL statements to update your database schema.
Install:
🚀 composer require doctrine/doctrine-migrations-bundle
You can generate a migration file using the following command:
🚀 php bin/console make:migration
To execute the migration and apply the changes to your database, run:
🚀 php bin/console doctrine:migrations:migrate
...and it's in our database:
How to add data to database?
Before we do anything, we could generate some fake data for our database. So I'll ask
AI to do it:
i have sql tables "title" 225 long and "content" 255 long, write me sql insert script with data about sea animals. 20 entries.
INSERT INTO post(title, content) VALUES
('Dolphin', 'Dolphins are highly intelligent marine mammals known for their playful behavior and social nature.'),
('Shark', 'Sharks are a group of elasmobranch fish characterized by a cartilaginous skeleton and a powerful sense of smell.'),
('Octopus', 'Octopuses are soft-bodied, eight-limbed mollusks known for their intelligence and ability to camouflage.'),
('Sea Turtle', 'Sea turtles are reptiles that spend most of their lives in the ocean and are known for their long migrations.'),
('Clownfish', 'Clownfish are small, brightly colored fish that live in anemones and are known for their symbiotic relationship with them.'),
('Blue Whale', 'The blue whale is the largest animal known to have ever existed, reaching lengths of up to 100 feet.'),
('Starfish', 'Starfish, or sea stars, are echinoderms that typically have five arms and can regenerate lost limbs.'),
('Jellyfish', 'Jellyfish are gelatinous creatures that drift through the ocean and have tentacles that can sting.'),
('Seahorse', 'Seahorses are unique fish known for their horse-like appearance and the male’s role in pregnancy.'),
('Manta Ray', 'Manta rays are large, gentle creatures that glide through the ocean and are known for their acrobatic jumps.'),
('Anglerfish', 'Anglerfish are deep-sea fish known for their bioluminescent lure used to attract prey in dark waters.'),
('Narwhal', 'Narwhals are medium-sized whales known for their long, spiral tusks, often referred to as the "unicorn of the sea."'),
('Sea Lion', 'Sea lions are marine mammals known for their playful behavior and are often seen basking on rocks.'),
('Coral', 'Coral is a marine invertebrate that forms large colonies and is vital for marine ecosystems as a habitat.'),
('Manatee', 'Manatees, also known as sea cows, are large, gentle herbivorous mammals that inhabit warm coastal waters.'),
('Barracuda', 'Barracudas are large, predatory fish known for their speed and sharp teeth, often found in tropical oceans.'),
('Pufferfish', 'Pufferfish are known for their ability to inflate themselves as a defense mechanism against predators.'),
('Sea Urchin', 'Sea urchins are spiny, globular animals that play a crucial role in marine ecosystems as grazers.'),
('Kraken', 'The kraken is a legendary sea monster said to dwell off the coasts of Norway and Greenland, often depicted as a giant octopus.'),
('Swordfish', 'Swordfish are large, predatory fish known for their elongated, flat bills and are popular in sport fishing.');
Open
phpMyAdmin and paste this code into SQL tab.
Back to our
MyController.php file:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Service\TemplateService;
// 👇 ADD THIS:
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\Post;
use App\Repository\PostRepository;
class MyController extends AbstractController
{
#[Route('/my', name: 'app_my')]
public function index(): JsonResponse
{
return $this->json([
'message' => 'Welcome to your new controller!',
'path' => 'src/Controller/MyController.php',
]);
}
#[Route('/my2', name: 'app_my_2')]
public function hello(): Response
{
return new Response("HELLO WORLD!");
}
#[Route('/template', name: 'app_template')]
public function template(TemplateService $templateService): Response
{
$output = $templateService->parse("{{test}}
", ["test" => "Hellooo!"]);
return new Response($output);
}
#[Route('/twig', name: 'app_twig')]
public function twig(Request $request): Response
{
$mainTemplate = $this->renderView('test.html.twig', [
'rootUrl' => $request->getSchemeAndHttpHost(),
'title' => "My title",
'body' => "Hello body!",
]);
return new Response($mainTemplate);
}
// 👇 NEW METHOD IS HERE:
#[Route('/add-data', name: 'app_addData')]
public function addData(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$post = new Post();
$post->setTitle("Hello");
$post->setContent("Void!");
$entityManager->persist($post);
$entityManager->flush();
return new Response('ok');
}
}
This will add data to database if we go to
/add-data route:
Get data
Get data by id:
#[Route('/get-data/{id}', name: 'app_getDataById')]
public function getDataById(ManagerRegistry $doctrine, int $id): Response
{
$entityManager = $doctrine->getManager();
$postRepository = $entityManager->getRepository(Post::class);
// Find the post by its ID
$post = $postRepository->find($id);
if (!$post) {
return new Response('Post not found', 404);
}
// Prepare the response
$responseContent = 'Title: ' . $post->getTitle() . ', Content: ' . $post->getContent();
return new Response($responseContent);
}
Get data with pagination:
http://localhost:9876/get-all-data?page=1&per_page=10
We will use "page" and "per_page" queryParams to get pagination data.
#[Route('/get-all-data', name: 'app_getAllData')]
public function getAllData(ManagerRegistry $doctrine, Request $request): JsonResponse
{
$entityManager = $doctrine->getManager();
$postRepository = $entityManager->getRepository(Post::class);
// Get query parameters
$page = max(1, $request->query->getInt('page', 1)); // default to 1
$perPage = max(1, $request->query->getInt('per_page', 10)); // default to 10
// Calculate offset
$offset = ($page - 1) * $perPage;
// Get the total count of posts
$totalPosts = $postRepository->count([]);
// Fetch paginated results
$posts = $postRepository->findBy([], null, $perPage, $offset);
// Prepare the data
$data = [];
foreach ($posts as $post) {
$data[] = [
'id' => $post->getId(),
'title' => $post->getTitle(),
'content' => $post->getContent(),
];
}
// Prepare the response with pagination metadata
$response = [
'data' => $data,
'meta' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $totalPosts,
'total_pages' => ceil($totalPosts / $perPage),
],
];
return new JsonResponse($response);
}
Search in database:
#[Route('/search', name: 'app_search')]
public function search(ManagerRegistry $doctrine, Request $request): JsonResponse
{
$entityManager = $doctrine->getManager();
$postRepository = $entityManager->getRepository(Post::class);
// Get the search query parameter
$query = $request->query->get('q', '');
if (empty($query)) {
return new JsonResponse(['error' => 'Query parameter "q" is required'], 400);
}
// Create a QueryBuilder instance
$qb = $postRepository->createQueryBuilder('p');
// Add conditions to search in both title and content columns
$qb->where(
$qb->expr()->orX(
$qb->expr()->like('p.title', ':query'),
$qb->expr()->like('p.content', ':query')
)
)
->setParameter('query', '%' . $query . '%');
// Execute the query
$posts = $qb->getQuery()->getResult();
// Prepare the data
$data = [];
foreach ($posts as $post) {
$data[] = [
'id' => $post->getId(),
'title' => $post->getTitle(),
'content' => $post->getContent(),
];
}
return new JsonResponse(['data' => $data]);
}
We will again use queryparams to search database:
http://localhost:9876/search?q=whale
User Authentication
To add login and logout functionality we will do following:
Install Security Bundle
Install Symfony Security bundle using Composer:
🚀 composer require symfony/security-bundle
Okay, this is where things get a little messy in Symfony, especially when handling registration and plain passwords. There's nothing in the documentation about registration, and not even AI was able to help me with that.
So I have done what I could with the documentation and with a little help from Mr. AI.
We will create "Dashboard" page that will be not accessible unless you have logged on!
🚀 php bin/console make:controller Dashboard
Create "User" entity
/src/Entity/User.php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true, unique: true)]
private ?string $nickname = null;
#[ORM\Column(length: 320, unique: true)]
private ?string $email = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $modifiedAt = null;
#[ORM\Column(length: 320)]
private ?string $password = null;
#[ORM\Column]
private ?bool $active = null;
#[ORM\Column(type: 'json')]
private array $roles = [];
public function getId(): ?int
{
return $this->id;
}
public function getNickname(): ?string
{
return $this->nickname;
}
public function setNickname(?string $nickname): static
{
$this->nickname = $nickname;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getModifiedAt(): ?\DateTimeImmutable
{
return $this->modifiedAt;
}
public function setModifiedAt(?\DateTimeImmutable $modifiedAt): static
{
$this->modifiedAt = $modifiedAt;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function isActive(): ?bool
{
return $this->active;
}
public function setActive(bool $active): static
{
$this->active = $active;
return $this;
}
public function getRoles(): array
{
$roles = $this->roles;
// Ensure every user has at least ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getUserIdentifier(): string
{
// Return the email or username as the unique identifier
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you had any temporary sensitive data, clear it here
// For example, $this->plainPassword = null;
}
}
And
/usr/Repository/UserRepository.php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
// /**
// * @return User[] Returns an array of User objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?User
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
UserController
/src/Controller/UserController.php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Bundle\SecurityBundle\Security;
class UserController extends AbstractController
{
#[Route('/user/login', name: 'user_login_get', methods: ['GET'])]
public function loginGet(): Response
{
return $this->render('/user/login.html.twig', [
'controller_name' => 'DashboardController',
]);
}
#[Route('/user/login', name: 'user_login_post', methods: ['POST'])]
public function loginPost(Request $request, Security $security, EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher): Response
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$userRepository = $entityManager->getRepository(User::class);
$user = $userRepository->findOneBy(['email' => $email]);
if (!$user) {
return $this->json(['message' => 'User not found!'], Response::HTTP_NOT_FOUND);
}
if (!$passwordHasher->isPasswordValid($user, $password)) {
return $this->json(['message' => 'Password is invalid!'], Response::HTTP_NOT_FOUND);
}
$security->login($user);
return $this->json(['message' => 'User logged on.']);
}
#[Route('/user/register', name: 'user_register_get', methods: ['GET'])]
public function registerGet(): Response
{
return $this->render('/user/register.html.twig', [
'controller_name' => 'DashboardController',
]);
}
#[Route('/user/register', name: 'user_register_post', methods: ['POST'])]
public function registerPost(Request $request, EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher): Response
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$userRepository = $entityManager->getRepository(User::class);
$user = $userRepository->findOneBy(['email' => $email]);
if ($user) {
return $this->json(['message' => 'User with this email already exists!'], Response::HTTP_NOT_FOUND);
}
$hashedPassword = $passwordHasher->hashPassword(new User(), $password);
$user = new User();
$user->setEmail($email)
->setPassword($hashedPassword)
->setRoles(['ROLE_USER'])
->setCreatedAt(new \DateTimeImmutable())
->setModifiedAt(new \DateTimeImmutable())
->setActive(1);
$entityManager->persist($user);
$entityManager->flush();
return $this->json(['message' => 'User registrated!']);
}
// The logout is automatically handled by Symfony at route: /user/logout
};
And finally
/config/packages/security.yaml. It's yaml, be careful with spacing and tabs!
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
users_in_memory: { memory: null }
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
json_login:
check_path: user_login_post
username_path: email
password_path: password
logout:
path: /user/logout
invalidate_session: true
target: /
access_control:
- { path: ^/dashboard, roles: IS_AUTHENTICATED_FULLY }
Run
reset.sh to setup database with new structure.
I will stop here. You have the rest of the code here:
Download Project
Unpack and run:
🚀 composer install