Alura > Cursos de Programação > Cursos de PHP > Conteúdos de PHP > Primeiras aulas do curso Symfony Framework: cache e segurança

Symfony Framework: cache e segurança

Temporadas e episódios - Apresentação

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!

Temporadas e episódios - Criando as entidades

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

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.

Temporadas e episódios - Atualizando o 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.

Sobre o curso Symfony Framework: cache e segurança

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:

Aprenda PHP acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas