O que é Symfony?
O Symfony é duas coisas ao mesmo tempo. Ele é um conjunto de componentes PHP reutilizáveis — HttpFoundation, Routing, Console, Mailer, Serializer e dúzias de outros — e também é um framework web completo montado a partir desses componentes. Essa natureza dual explica quase tudo sobre ele: o framework é estável porque suas partes são estáveis, e suas partes são úteis por si só.
Criado por Fabien Potencier em 2005 e mantido pela SensioLabs, o Symfony tornou-se a “sala de máquinas” do PHP moderno. O Laravel é construído sobre componentes do Symfony, o Drupal e o phpBB dependem deles, e milhares de bibliotecas os utilizam sem nunca tocar no framework completo.
O Symfony exige mais de você do que um framework “batteries-included”. Em troca, ele entrega um sistema sobre o qual você pode raciocinar, estender e atualizar por anos.
Componentes: o produto real
Os componentes são o coração do projeto. Cada um resolve um único problema e não possui dependência do framework.
- HttpFoundation — classes
RequesteResponseorientadas a objetos. - Routing — mapeia URLs para controllers usando atributos, YAML ou PHP.
- Console — constrói ferramentas de linha de comando com argumentos e opções.
- Mailer — envia e-mails através de transports e mensagens com templates.
- Serializer — converte objetos para JSON, XML e vice-versa.
- EventDispatcher — desacopla o código com eventos e listeners.
Você pode instalar qualquer um deles diretamente:
composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
$page = $request->query->getInt('page', 1);
Essa portabilidade é o motivo pelo qual os componentes do Symfony aparecem em todos os lugares, até mesmo em projetos que jamais adotariam o framework completo.
O kernel HTTP e controllers
Em uma aplicação completa, cada requisição flui através do HttpKernel. Ele dispara um evento, faz a correspondência de uma rota, chama um controller e transforma o Response retornado em saída. Os controllers estendem AbstractController para obter helpers convenientes.
class BlogController extends AbstractController
{
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug, PostRepository $posts): Response
{
$post = $posts->findOneBy(['slug' => $slug]);
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('blog/show.html.twig', ['post' => $post]);
}
}
Um controller deve retornar um Response. $this->render() constrói um a partir de um template Twig, $this->json() retorna JSON e $this->redirectToRoute() retorna um redirecionamento. Como o objeto de resposta é explícito, os testes e o middleware são simples.
Templates Twig estendem um layout base e preenchem blocos, e a saída é escapada, a menos que você opte por desativar isso:
{# templates/blog/show.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.publishedAt|date('F j, Y') }}</p>
{{ post.body|raw }}
</article>
{% endblock %}
Roteamento por atributos
As rotas são declaradas ao lado da action que as manipula, utilizando atributos do PHP 8.
#[Route('/blog', name: 'blog_index', methods: ['GET'])]
#[Route('/blog/{slug}', name: 'blog_show', methods: ['GET'])]
O roteamento também pode ficar em config/routes.yaml quando você prefere mantê-lo separado. Atributos são o padrão no Symfony moderno porque a rota e o método permanecem juntos. Verifique o que está registrado a qualquer momento com:
php bin/console debug:router
O service container e o autowiring
Quase tudo no Symfony é um service, e o container os constrói e conecta. Com o autowiring habilitado, você só precisa adicionar o type-hint no construtor.
class PostPublisher
{
public function __construct(
private EntityManagerInterface $em,
private MailerInterface $mailer,
) {}
}
config/services.yaml instrui o container a realizar o autowire de suas classes:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/Entity/'
- '../src/Kernel.php'
Quando existirem duas implementações de uma interface, vincule a correta explicitamente. O container é compilado em PHP puro para produção, e é por isso que ele é rápido, apesar de ser tão dinâmico para escrever.
Bundles e receitas do Flex
Um bundle empacota funcionalidades em uma unidade distribuível. Funcionalidades de terceiros — Doctrine, segurança, mailer, painéis administrativos — chegam como um bundle, e o próprio framework é composto por bundles principais.
Instalar um deles requer apenas um comando:
composer require symfony/orm-pack
O Symfony Flex é o plugin do Composer que torna esse processo agradável. Quando um pacote possui uma receita, o Flex cria os arquivos de configuração, registra o bundle e, frequentemente, adiciona variáveis de ambiente ou um serviço docker-compose. É por isso que as aplicações Symfony modernas possuem configurações pequenas e explícitas, em vez de páginas de boilerplate.
Doctrine ORM
O Doctrine é a camada de persistência padrão. As entidades são classes PHP simples anotadas com atributos de mapeamento, e os repositórios encapsulam as queries.
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $body;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $publishedAt = null;
}
As queries geralmente são expressas através da API do repositório ou DQL, em vez de strings SQL:
$latest = $posts->findBy([], ['publishedAt' => 'DESC'], limit: 10);
As alterações de schema são gerenciadas com o Doctrine Migrations, que compara o seu mapeamento com o banco de dados e gera arquivos de migração versionados que você pode revisar antes de executar.
Configuração e ambientes
O Symfony separa a configuração do código e a aplica por ambiente. APP_ENV seleciona o ambiente, e os arquivos sob config/ são sobrepostos uns aos outros.
.envcontém os padrões locais e variáveis de ambiente.config/packages/configura os bundles para todos os ambientes.config/packages/dev/econfig/packages/prod/fazem a sobrescrita por ambiente.config/routes.yamleconfig/services.yamlsão as configurações próprias do app.
O ambiente dev habilita o profiler e ferramentas de debug, test utiliza um banco de dados separado, e prod compila tudo para maior performance. Limpar o cache após alterações de configuração é uma parte normal do fluxo de trabalho:
php bin/console cache:clear
Messenger e processamento assíncrono
O componente Messenger retira o processamento de dentro da requisição. Você despacha uma mensagem para um bus e um handler a processa, seja imediatamente ou em um worker.
final class UserRegistered
{
public function __construct(public readonly int $userId) {}
}
#[AsMessageHandler]
final class SendWelcomeEmail
{
public function __invoke(UserRegistered $message): void
{
// send the email here
}
}
Despache de qualquer lugar e consuma a fila em um processo separado:
$bus->dispatch(new UserRegistered($user->getId()));
php bin/console messenger:consume async
Os transports podem ser Doctrine, Redis, AMQP ou um serviço de terceiros, e mensagens que falharam podem ser repetidas ou armazenadas, tornando o processamento em background uma prioridade nativa do sistema.
Testando com PHPUnit
O Symfony já vem com o PHPUnit e uma classe base WebTestCase que inicializa o kernel e realiza requisições HTTP reais.
class BlogControllerTest extends WebTestCase
{
public function testShowPost(): void
{
$client = static::createClient();
$client->request('GET', '/blog/hello-world');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello world');
}
}
Como os serviços são injetados, os testes unitários podem substituir uma dependência por um stub ou um mock sem precisar mexer no container. Deixe os testes de integração para a camada HTTP e os testes unitários para os serviços que contêm as regras de negócio.
Melhores práticas
- Prefira a injeção via construtor em vez de buscar serviços diretamente do container.
- Mantenha os controllers enxutos; mova a lógica para os services.
- Use repositories para consultas e mantenha o SQL fora dos controllers.
- Declare as rotas como atributos ao lado da action que elas chamam.
- Deixe o Flex gerenciar a configuração do bundle em vez de escrever boilerplate.
- Separe a configuração por ambiente e mantenha segredos em variáveis de ambiente.
- Coloque tarefas demoradas no Messenger em vez de processá-las durante a requisição.
- Analise o profiler quando algo estiver lento ou inesperado.
Erros comuns
- Chamar
$container->get()em vez de injetar dependências. - Colocar lógica de negócio em controllers e entities.
- Esquecer de registrar ou vincular uma interface e depois questionar o que o container resolveu.
- Commitar segredos do
.envem vez de mantê-los fora do controle de versão. - Editar migrations geradas após elas terem sido executadas.
- Misturar estilos de configuração aleatoriamente entre YAML, XML e PHP.
- Ignorar avisos de depreciação até a próxima atualização major.
Próximos passos
O Symfony oferece os componentes e a disciplina necessários para construir sistemas duradouros, e suas ideias estão presentes em diversos frameworks do ecossistema. Leia o guia do Laravel para ver esses componentes sendo utilizados para o desenvolvimento rápido de aplicações, ou compare essa filosofia com a abordagem minimalista do Express. Para projetar e evoluir as APIs que você expõe, continue com REST e versionamento de API.