Olá, pessoal. Boas-vindas à Alura! Meu nome é Vinicius Dias e vou guiá-los nesse treinamento de Symfony.
Vinicius Dias é uma pessoa de pele clara, olhos escuros e cabelos pretos. Usa bigode e cavanhaque, e tem cabelo curto. Veste camiseta azul, tem um microfone de lapela na gola da camiseta, e está sentado em uma cadeira preta. Ao fundo, há uma parede lisa com iluminação lilás gradiente.
Ao longo do treinamento, aprenderemos muita coisa legal, continuando a partir de onde paramos no treinamento anterior. Estamos desenvolvendo um sistema para controle das séries que assistimos ou queremos assistir.
Na minha tela ainda faltam algumas funcionalidades, por exemplo, não tem o botão de adicionar.
Nesse treinamento falaremos um pouco sobre segurança. Embora não seja a primeira coisa que veremos, vamos implementar um formulário de login. Neste formulário poderemos, obviamente, realizar o login e ter acesso às funcionalidades completas.
Repare que essas funcionalidades envolvem bastante coisa, por exemplo, ter uma lista de todas as temporadas de uma série, dentro de cada uma das séries poderemos marcar episódios como assistidos ou não, poderemos adicionar novas séries somente se estivermos logados.
Vamos implementar muitas funcionalidades interessantes neste treinamento.
Além de novas funcionalidades, também aprenderemos sobre coisas que funcionam "por debaixo dos panos". Por exemplo, aprenderemos o conceito de cache e como implementar manualmente uma busca por cache. Depois, veremos como configurar o Doctrine para utilizar o cache no Symfony. Vamos até configurar o second-level cache, que é algo complexo de configurar, mas como estamos usando um framework essa complexidade será um pouco abstraída.
Em relação à segurança vamos fazer a autenticação dos usuários e a autorização também, escondendo algumas telas, não permitindo acesso a algumas páginas, etc.
Se você ficar com dúvida durante o curso, compartilhe conosco lá no fórum ou no Discord da Alura, com certeza alguém vai conseguir te ajudar.
Espero você no próximo vídeo para começarmos a modelar o relacionamento entre nossas séries e temporadas, e as temporadas e os episódios assistidos!
Agora vamos adicionar funcionalidades ao nosso sistema. Praticaremos um pouco mais o uso do framework para colocar as funcionalidades.
Começaremos exatamente de onde paramos no segundo treinamento de Symfony. O código é o mesmo, mas removi as séries que estavam cadastradas porque vamos modificar a modelagem do sistema para que cada série tenha temporadas e essas temporadas tenham episódios.
Vamos para o código, que está com o servidor rodando, vamos interromper o servidor e limpar a tela.
No terminal, usaremos o comando make:entity
para criar uma nova entidade de episódio.
php bin/console make:entity Episode
A entidade episódio vai ter um número, number
, que será tipo inteiro, smallint
.
Se quisermos ver uma lista dos tipos basta escrever um sinal de interrogação
?
e pressionar "Enter".
Main types
string
text
boolean
integer (or smallint,bigint)
float
Ele vai perguntar se o campo pode ser nulo (Can this field be null in the database?). Não, não pode ser nulo.
Por enquanto, não adicionaremos nenhuma outra propriedade. Então a entidade Episode
está pronta, podemos pressionar "Enter". Agora, em vez de rodar a migration, já vamos criar a entidade de temporada, em inglês "season".
php bin/console make:entity Season
Ela vai ter uma propriedade number
, que será smallint
e também não pode ser nulo.
Além disso, teremos mais uma propriedade, a de episódios: episodes
, que terá um tipo relacionamento em que uma temporada tem muitos episódios. Podemos digitar OneToMany
. Mas se você ainda não conseguir raciocinar com esses tipos de relacionamentos, pode digitar "relation". Em seguida, ele vai perguntar com qual classe esse campo deve se relacionar? Deve se relacionar com Episode
. Ele vai exibir uma descrição do que vai acontecer conforme o tipo de relação.
Queremos que cada temporada possa ter vários episódios, então vamos escolher utilizar o OneToMany
.
Além de adicionar esse campo de episódios, lá no Episode
ele vai adicionar uma propriedade que vai permitir o acesso à temporada dele.
Vamos manter o nome por padrão desse campo, que é season
e esse campo não pode ser nulo.
Ele avisa que criou algumas coisas, criou um método para remover o episódio de uma temporada, agora ele vai perguntar se queremos remover os episódios que ficarem órfãos, ou seja, se tiver algum episódio que não está relacionado a alguma temporada vamos querer removê-los?
Sim, porque senão teremos alguma inconsistência, já informamos que o campo de temporada não pode ser nulo.
Nossas entidades foram atualizadas. Podemos pressionar "Enter" para encerrar.
Agora vamos ver como ficou o código da nossa modelagem.
Vamos ver como ficou o código do arquivo Episode.php
. Nele temos o id, que pode ser um inteiro, e o número do episódio que também é inteiro; e a season
que é do tipo season
.
//cód. omitido
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'smallint')]
private int $number
#[ORM\ManyToOne(targetEntity: Season::class, inversedBy: 'episodes')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
//cód. omitido
Aqui poderíamos, na minha opinião, criar o construtor para receber o número e a temporada, mas o Symfony criou getters e setters para nós. Então, não vamos mexer nisso, deixaremos como está. E na hora de utilizar essas classes analisaremos o que será necessário alterar.
Em Season.php
temos o id inteiro, o número inteiro e os episódios. E no construtor ele já inicializou como uma ArrayCollection
.
Temos também os getters e setters, o getter de episódios já está definido como Collection.
Ele tem um método para adicionar um episódio, addEpisode
, ele verifica e se o episódio não existir ainda será adicionado.
E na hora de remover, vamos tirar esse episódio da coleção de episódios e definir a season como null, ou seja, remover o relacionamento dos dois lados.
//cód. omitido
public function getEpisodes(): Collection
{
return $this->episodes;
}
public function addEpisode(Episode $episode): self
{
if (!$this->episodes->contains($episode)) {
$this->episodes[] = $episode;
$episode->setSeason($this);
}
//cód. omitido
Poderíamos remover boa parte desse código, mas por enquanto vou deixar como o Symfony gerou. E pretendo limpar esse código não utilizado mais adiante.
Agora falta inserir as temporadas em Serie.php
. Vamos voltar para a linha de comando.
Podemos também utilizar o make:Entity
para alterar uma entidade, basta digitar uma entidade que já existe. No caso, editaremos a Series
.
$php bin/console make:entity Series
Quando fizermos esse comando, ele vai avisar que essa entidade já existe. Então, vamos adicionar novos campos. Adicionaremos as seassons
(temporadas). Uma série pode ter várias temporadas, então o tipo dela será OnetoMany
, a classe será Season
e o nome desse atributo será "series", o que está padrão. Não pode ser nulo, e vamos responder "sim" para a pergunta se queremos remover objetos órfãos.
Vamos adicionar e verificar o que foi feito no código. Vou corrigir a indentação que foi alterada porque ele precisou adicionar a linha $this->seassons = new ArrayCollection();
no nosso construtor.
//cód. omitido
public function __construct(
#[ORM\Column]
#[Assert\NotBlank]
#[Assert\Length(min: 5)]
private string $name = ''
) {
$this->seasons = new ArrayCollection();
}
//cód. omitido
Agora temos as nossas seasons
que são uma Collection
e vão se relacionar com a classe season.
Além disso, ele adicionou os métodos de adicionar e remover uma temporada, e lá em Season.php
adicionou um campo de series
.
#[ORM\ManyToOne(targetEntity: Series::class, inversedBy: 'seasons')]
#[ORM\JoinColumn(nullable: false)]
private Series $series;
Lembrando que devemos importar do namespace correto, as coleções vêm desse namespace:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Agora já podemos voltar ao terminal e criar as migrations com o comando php bin/console make:migration
.
Ele criou as nossas migrations, criando a tabela de episódio e definindo índice para a chave estrangeira season_id
; e criando a tabela de temporada e também sua chave estrangeira com o series_id
.
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE episode (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, season_id INTEGER NOT NULL, number SMALLINT NOT NULL)');
$this->addSql('CREATE INDEX IDX_DDAA1CDA4EC001D1 ON episode (season_id)');
$this->addSql('CREATE TABLE season (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, series_id INTEGER NOT NULL, number SMALLINT NOT NULL)');
$this->addSql('CREATE INDEX IDX_F0E45BA95278319C ON season (series_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE episode');
$this->addSql('DROP TABLE season');
}
A princípio está tudo correto, vamos executar essa migration com o comando php bin/console doctrine:migrations:migrate
. E vamos confirmar a execução desse comando.
Agora, podemos atualizar o cadastro.
Para isso, vamos subir novamente o servidor com o comando php -$ 0.0.0.0:8123 -t public/
.
Agora vamos para o nosso sistema no navegador e clicar no botão "Adicionar". Agora queremos adicionar mais informações além do nome dela para que possamos definir o nome da série, a quantidade de temporadas e o número de episódios que cada temporada tem.
Imagine que vamos cadastrar a série "Grey's Anatomy", não quero ter que digitar primeiro o nome da série para depois preencher episódio por episódio e temporada por temporada, porque essa série tem cerca de 18 temporadas com 20 episódios em cada.
Então, vamos fazer essa inserção em "batch", em lotes. Claro que isso pode gerar alguns problemas, tem séries que não têm o mesmo número de episódios em todas as temporadas, por exemplo. Mas vamos fazer dessa forma para simplificar.
Então, nesta tela "Nova Série" teremos um campo para nome, número de temporadas e para a quantidade de episódios por temporada. A partir disso faremos uma inserção mais complexa.
No próximo vídeo começaremos a atualizar nosso cadastro.
Agora vamos atualizar o formulário.
Vamos para SeriesController.php
, onde criamos o formulário.
Criaremos uma nova pasta, para separarmos bem e você entender que o que vamos criar agora não é um conceito do framework Symfony, é um conceito que existe em código em geral, um conceito de orientação a objetos.
Selecionaremos "New > PHP Class" e criaremos um DTO (Data Transfer Object), que é um objeto que existe somente para segurar alguns dados que vamos transferir de um lado para outro. O nome dessa classe será "SeriesCreateFromInput" e o namespace será "App\DTO".
Então, dentro do namespace DTO, que será uma nova pasta "DTO", teremos as entradas de um formulário para a criação de uma nova série.
Em SeriesCreateFromInput
teremos tudo no construtor e tudo como "read only".
namespace App\DTO;
class SeriesCreateFromInput
{
public function __construct(
public readonly string $seriesName,
public readonly int $seasonsQuantity,
public readonly int $episodesPerSeason,
) {
}
}
Por enquanto é isso que precisamos: o nome da série, a quantidade de temporadas e os episódios por temporada.
Agora que temos esse novo dado para ser criado, poderemos alterar o formulário. No SeriesType.php
teremos os campos seriesName
, seasonsQuantity
e episodesPerSeason
:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('seriesName', type:_, options: ['label' => 'Nome:'])
->add('seasonsQuantity', options: ['label' => 'Qtd Temporadas:'])
->add('episodesPerSeason', options: ['label' => 'Ep por Temporada:'])
->add('save', SubmitType::class, ['label' => $options['is_edit'] ? 'Editar' : 'Adicionar'])
->setMethod($options['is_edit'] ? 'PATCH' : 'POST')
;
}
Poderíamos informar qual é o tipo do dado, mas vamos esperar para checar se baseado no tipo do dado que receberemos ele consegue inferir que vai ser um número e não um texto. Vamos alterar Series
para SeriesCreateFromInput
:
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => SeriesCreateFromInput::class,
'is_edit' => false,
]);
Além disso, no topo do código podemos remover a linha use App\Entity\Series;
.
Agora, no nosso form em SeriesController.php
vamos criar esse novo tipo de dados: new SeriesCreateFromInput()
.
public function addSeriesForm(): Response
{
$seriesForm = $this->createForm(SeriesType::class, new SeriesCreateFromInput());
return $this->renderForm('series/form.html.twig', compact('seriesForm'));
}
Esperamos receber esse tipo de dados, não mais uma nova série. Então, na hora de adicionar uma série, não teremos mais uma série preenchida e sim um SeriesCreateFromInput()
. Atualizaremos o nome da variável series
para input
.
$input = new SeriesCreateFromInput();
$seriesForm = $this->createForm(SeriesType::class, $input)
->handleRequest($request);
if (!$seriesForm->isValid()) {
return $this->renderForm('series/form.html.twig', compact('seriesForm'));
}
Vamos adicionar uma linha com o código dd($input);
para ver se esse input está chegando corretamente.
Feito o nosso formulário, vamos dar uma olhada em como está a nossa view em "templates > series > form.html.twig".
Estamos utilizando um formato um pouco mais controlado. Vamos atualizar o name
para seriesName
e adicionar o seasonsQuantity
e episodesPerSeason
:
{% block body %}
{{ form_start(seriesForm) }}
{{ form_row(seriesForm.seriesName) }}
{{ form_row(seriesForm.seasonsQuantity) }}
{{ form_row(seriesForm.episodesPerSeason) }}
{{ form_widget(seriesForm.save, {'attr': {'class': 'btn-dark'}}) }}
{{ form_end(seriesForm) }}
{% endblock %}
Depois podemos modificar a aparência desse formulário, por enquanto só vamos checar se ele vai exibir tudo corretamente. Vamos ver no navegador.
Mas está exibindo um erro:
Too few arguments to function App\DTO\SeriesCreateFromInput::construct(), 0 passed in /app/src/Controller/SeriesController.php on line 37 and exactly 3 expected
Nós criamos o DTO sem nenhum dado, por enquanto ele está vazio. Isso, obviamente, não é permitido pelo nosso código. Então vamos tirar o "readonly" do código do nosso DTO e deixar os campos com vazio, zero e zero.
form.html.twig
namespace App\DTO;
class SeriesCreateFromInput
{
public function __construct(
public string $seriesName = '',
public int $seasonsQuantity = 0,
public int $episodesPerSeason = 0,
) {
}
}
Isso é uma alternativa para podermos ter esse dado vazio inicialmente, para ter o nosso formulário vazio também.
Podemos atualizar o navegador. E agora o nosso formulário foi criado corretamente com os campos "Nome", "Qtd Temporadas" e "Ep por Temporada".
Ao inspecionar a página podemos ver que o input type
ainda está input type="text"
. Vamos modificar em SeriesType.php
, depois de opções podemos informar o tipo. O método ass espera alguns parâmetros, o primeiro é o nome do campo que estamos criando, o segundo é o tipo e o terceiro são as opções.
Como estamos utilizando named parameters, não informamos o type porque, por padrão, ele já é o text type. Agora vamos mudar isso para number type em seasonsQuantity
e episodesPerSeason
:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('seriesName', options: ['label' => 'Nome:'])
->add('seasonsQuantity', NumberType::class, options: ['label' => 'Qtd Temporadas:'])
->add('episodesPerSeason', NumberType::class, options: ['label' => 'Ep por Temporada:'])
->add('save', SubmitType::class, ['label' => $options['is_edit'] ? 'Editar' : 'Adicionar'])
->setMethod($options['is_edit'] ? 'PATCH' : 'POST')
;
}
Agora, vamos atualizar a página no navegador. O Symfony adicionou um inputmode="decimal"
nos campos numéricos.
E em celulares, por exemplo, ao selecionarmos estes campos já seria aberto automaticamente o teclado numérico.
Então, o nosso formulário está pronto. Vamos enviar um novo dado preenchendo da seguinte forma:
Nome: Grey's Anatomy
Qtd Temporada: 18
Ep por Temporada: 23
Ao clicar em adicionar, os dados estão vindo corretamente:
SeriesController.php on line 52:
App\DTO\SeriesCreateFromInput {467
seriesName: "Grey's Anatomy"
seasonsQuantity: 18
episodesPerSeason: 23
}
O primeiro passo foi concluído. Agora, em SeriesController.php
, vamos criar essa série de verdade.
Onde está dd($input);
substituiremos pela criação da série, e ela espera pelo menos um nome por parâmetro. Vamos fazer com que o contador já comece com 1 e vá até um número igual à quantidade de temporadas:
$series = new Series($input->seriesName);
for ($i = 1;) $i <= $input->seasonsQuantity; $i++) {
$series->addSeason(new Season($i));
}
Em form.html.twig
vamos criar o construtor com o parâmetro number e inicializar number
direto no construtor.
public function __construct(
#[ORM\Column(type: 'smallint')]
private int $number
){
$this->episodes = new ArrayCollection();
}
Dessa forma, estamos recebendo a propriedade number
e inicializando-a.
Agora, no SeriesController.php
vamos adicionar episódio à season
. Adicionaremos a quantidade que tem em episodesPerSeason
.
$series = new Series($input->seriesName);
for ($i = 1;) $i <= $input->seasonsQuantity; $i++) {
$season = new Season($i);
$input->episodesPerSeason
$series->addSeason($season);
Repare que nosso código está ficando complexo, o ideal seria separar isso para algum lugar. Mas vou deixar isso como desafio para você.
Continuando, no for
teremos outro contador, enquanto o j
for menor ou igual a episódios por temporada, vamos incrementar ele e criar um novo episódio, para fazer um addEpisode
com o season
.
$series = new Series($input->seriesName);
for ($i = 1;) $i <= $input->seasonsQuantity; $i++) {
$season = new Season($i);
$input->episodesPerSeason
$series->addSeason($season);
for ($j = 1; $j <= $input->episodesPerSeason; $j++) {
$season->addEpisode(new Episode($j));
}
$series->addSeason($season);
Então, nesse Episode
receberemos um número por parâmetro, que será o j
.
Vamos entrar em Episode.php
, e mover a linha private int $number;
para o nosso construtor:
public function __construct(
#[ORM\Column(type: 'smallint')]
private int $number
) {
}
Perfeito. Agora nosso construtor espera esse episódio, nossa temporada vai ter todos os episódios e vamos adicionar todas as temporadas na série. No final, podemos adicionar a série utilizando nosso repositório. Aqui podemos utilizar o nome da série ou o nome vindo do input, tanto faz.
$this->addFlash(
'success',
"Série \"{$series->getName()}\" adicionada com sucesso"
Então, vamos lá. Vou tentar adicionar uma série, mas a temporada ainda não terá sido inserida. E também vou tentar adicionar uma temporada e os episódios não terão sido inseridos. Então, podemos utilizar aquele cascade persist, que já fizemos no treinamento de Doctrine.
Vamos começar em Series.php
, onde temos o relacionamento com seasons
. Vamos inserir cascade: ['persist']
:
#[ORM\OneToMany(
mappedBy: 'series',
targetEntity: Season::class,
orphanRemoval: true,
cascade: ['persist']
)]
Com isso, estamos informando que deve persistir em cascata nesse relacionamento com seasons
, ou seja, inseriu a série, já insere as temporadas também.
Vamos fazer a mesma coisa em Season.php
no relacionamento com os episódios:
#[ORM\OneToMany(mappedBy: 'season',
targetEntity: Episode::class,
orphanRemoval: true,
cascade: ['persist']
)]
Algo que me incomoda no Season.php
e no Episode.php
é que temos relacionamentos que são opcionais, não precisaríamos adicioná-los porque não vamos utilizá-los. Num cenário real eu removeria isso, mas por enquanto vou deixar, talvez precisemos disso no futuro:
Season.php
#[ORM\ManyToOne(targetEntity: Series::class, inversedBy: 'seasons')]
#[ORM\JoinColumn(nullable: false)]
private Series $series;
Episode.php
#[ORM\ManyToOne(targetEntity: Season::class, inversedBy: 'episodes')]
#[ORM\JoinColumn(nullable: false)]
private Season $season;
Continuando, temos a série criada por completo. Antes de adicionar, vamos fazer um dd($series);
, para garantir que a série está criada corretamente.
Vamos voltar para o navegador e atualizar a página para reenviar o formulário.
SeriesController.php on line 63:
App\Entity\Series {#908
-seasons: Doctrine_\ArrayCollection {#1226}
-name: "Grey's Anatomy"
}
Temos aqui o nome da série correto, temos a seasons
com 18 elementos. Onde cada uma dessas temporadas também é uma coleção e seus episódios cada um será uma coleção de 23 itens.
Agora já temos nossa modelagem correta, vamos remover a linha dd($series);
do código e ver se o comando para persistir vai funcionar como esperado.
Voltaremos ao navegador, atualizamos a página para enviar. A princípio funcionou, confesso que foi mais rápido do que o esperado. Achei que esse insert iria demorar porque tem bastante coisa acontecendo.
No Symfony Profiler, vamos dar uma olhada nas queries que foram geradas pelo Doctrine. Ele inicializou uma transação e foi adicionando uma temporada de cada vez. Note que ele não fez da melhor forma. E, depois de adicionar todas as temporadas, adicionou episódio por episódio.
No treinamento de Doctrine já aprendemos como melhorar isso, utilizando as queries. Podemos até executar um SQL puro mesmo, que seria uma opção mais rápida.
Então, como já falamos bastante sobre performance no treinamento de Doctrine, não vou otimizar isso. Vou deixar dessa forma que está, executando 435 queries, e fica aqui o desafio para você melhorar isso. Vou deixar um Para Saber Mais só para você relembrar como pode atingir esse objetivo.
Agora, queremos atualizar a exibição para clicarmos em "Grey's Anatomy" e vermos todas as temporadas e talvez um indicador de quantos episódios tem, adicionar algumas informações na exibição. No próximo vídeo trabalharemos essa parte do projeto.
O curso Symfony Framework: cache e segurança possui 151 minutos de vídeos, em um total de 47 atividades. Gostou? Conheça nossos outros cursos de PHP em Programação, ou leia nossos artigos de Programação.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.