Alura > Cursos de Programação > Cursos de PHP > Conteúdos de PHP > Primeiras aulas do curso Laravel: transações, service container e autenticação

Laravel: transações, service container e autenticação

Transações - Apresentação

Boas-vindas ao treinamento de Laravel: transações, service container e autenticação da Alura, sou o Vinícius Dias e vou guiar vocês em mais um treinamento de Laravel. Nesse treinamento vamos nos aprofundar um pouco mais nos nossos conhecimentos, tanto de Laravel quanto o desenvolvimento na totalidade.

Vamos começar aprendendo um pouco sobre consistência de dados e ver como utilizar transações no Laravel, e ainda nesse quesito de banco de dados como podemos separar melhor o código, por exemplo, do controller de um código que manipula o banco de dados.

Então para isso vamos entrar em alguns como repository, e tratando de um repositório vamos ver como utilizar a funcionalidade de service provider do Laravel para podermos ter uma flexibilidade interessante no código e essa parte vai ser bem legal.

Depois disso, vamos adicionar funcionalidade no sistema de séries, vamos permitir que o usuário marque determinados episódios de cada uma das temporadas como assistido e em seguida, exibir quantos episódios de cada temporada foram assistidos.

A seguir, vamos entrar na parte de autenticação. Primeiro, vamos criar a nossa própria autenticação manualmente, com o formulário de login, com o registro de usuário, etc. Em seguida, vamos utilizar essa página de login solicitando e-mail e senha, que já é entregue pronto para nós pelo Prezi.

Então se tentarmos realizar o login como usuário ou senha inválido é retornado um erro, agora caso coloquemos as credenciais corretas somos redirecionados para a página com as séries. Com a aplicação funcionando teremos acesso a todas as funcionalidades que comentei que vamos implementar ao longo desse treinamento.

Vamos implementar bastante coisa, vamos usar recursos bem interessantes do Laravel, inclusive, um recurso externo que é o Laravel Prezi e, por isso, se em qualquer momento deste treinamento você tiver alguma dúvida, não hesite, abra um tópico no fórum, isso é muito importante para que você não fique com dúvidas nesse treinamento para os treinamentos futuros.

Então, abra um tópico no fórum ou te convido também para fazer parte do nosso canal do Discord, que fica no menu superior na plataforma da Alura, você possui acesso ao servidor do Discord onde você pode interagir de forma mais dinâmica.

De novo, não hesite, tire suas dúvidas e espero que você tire bastante proveito desse treinamento, e te espero já no próximo vídeo para aprendermos sobre consistência de dados no momento de criar as nossas séries.

Transações - DB::transaction

Bem-vindos de volta. No último treinamento fizemos a inserção das séries de uma maneira um pouco diferente. Ao criar uma nova série, além de inserir a série em si, vamos criar determinado número de temporadas e para cada temporada um determinado número de episódios.

Por exemplo, se tenho um seriado com três temporadas e dez episódios em cada temporada, vamos inserir um registro na tabela de séries, três registros na tabela de temporadas, um para cada temporada e trinta registros na tabela de episódios. Visto que serão dez episódios para cada temporada.

Já implementamos essa lógica, inclusive, no código otimizamos bastante, fizemos o uso de algumas funcionalidades do Eloquent para ter o bulk insert, para inserirmos vários dados de uma vez só em uma única query (consulta).

Então já usamos bastante essa parte, mas tem um problema, imagine que crio o meu seriado, $serie = Series::create($request->all());, mas na hora de criar as temporadas, Season::insert($seasons);, a rede falha ou ocorre uma instabilidade fechando, assim, a conexão com o banco de dados, ou o espaço em disco no servidor do banco de dados acaba ou o próprio servidor do banco de dados sai do ar. Problemas acontecem.

Com isso, vamos acabar tendo um seriado criado sem nenhuma temporada ou também pode acontecer se conseguir criar o seriado e temporadas, mas no momento de salvar os episódios acabou o espaço em disco, os servidores caíram ou a rede teve um problema e teremos várias temporadas sem nenhum episódio.

Então nesse caso teremos problemas de inconsistência. E para resolver esse problema, quem já possui conhecimento de banco de dados, sabe que é através de transações. Como aprendemos no treinamento de PDO poderíamos fazer algo como beginTransaction(), $pdo->beginTrasaction();, no início do código e no final um commit, $pdo->commit();. E caso alguma exceção seja lançada, poderíamos fazer o rollback, se necessário.

Mas como estamos usando Laravel e suas facilidades, vamos utilizar também as facilidades para lhe darmos com as transações. Assim, podemos usar a facade de DB, lembrando que já importamos no início do código em use illuminate\Support\Facades\DB;, e a função transaction(), DB::transaction();.

Essa função espera por parâmetro uma closure, função anônima e tudo que inserimos nela vai ocorrer dentro de uma transação, isto é, se fizermos um insert, um delete, um update, tudo isso vai estar dentro de uma única transação.

Assim, essa função transaction() inicia uma transação para nós, executa o código que está inserido nela e no final, executa o commit. Só que se dentro da função alguma exceção for lançada, isto é, se algum problema acontecer em algumas das queries que executarmos, então, o rollback também ocorre de forma automática, ou seja, essa função cuida de tudo para nós.

Para melhor visualização, parte do código foi omitido.

public function store(SeriesFormRequest $request)
{
    DB::transaction(function () {

    });
}

Vamos trazer, então, todo este código para dentro da closure:

public function store(SeriesFormRequest $request)
{
    DB::transaction(function () {
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);
    });

    return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
}

Após inserir essa parte do código para dentro da função, teremos dois problemas. O primeiro é que não possuímos acesso a variável request dentro dessa closure, assim, como já aprendemos em treinamentos anteriores faremos o uso dessa variável dentro de DB::transaction(function () use ($request) {. Agora a closure vai ter acesso a esse escopo externo.

DB::transaction(function () use ($request)

O segundo problema, é que fora dessa closure a variável série,{$serie->nome}, não existe visto que foi criada dentro em $serie = Series::create($request->all());. Existem algumas soluções, primeiro farei uma mais feia e depois melhoro.

Vamos criar uma variável serie como nula depois de public function store(SeriesFormRequest $request), então, $serie=null; fora da transação e usar dentro de use ($request, &$serie) por referência. Agora quando definimos ou alteramos essa variável serie em $serie = Series::create($request->all()); estamos alterando também a variável série que colocamos com valor nulo no começo do código, fora da transação. Assim, podemos usar sem problemas.

public function store(SeriesFormRequest $request)
{
    $serie = null;
    DB::transaction(function () use ($requestt, &$serie) {
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);
    });

    return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
}

Vamos salvar e verificar se funcionou, voltando para o navegador em "http://localhost:8000/series/create" vamos criar uma série com o nome "Moon Knight" com uma temporada no campo "Número de temporadas" e e seis episódios em "Eps/Temporada" e selecionar o botão de "adicionar" abaixo do campo "Nome" do lado esquerdo que inserimos o nome da série.

Ao adicionar, foi inserido com sucesso visto que apareceu a mensagem "Série 'Moon Knight' adicionada com sucesso" e está na lista de seriados junto com "Grey's Anatomy" e "The Punisher". Vamos analisar as queries com a requisição de post selecionando "Queries" no menu inferior da tela ao lado de "Route".

Perceba que ele iniciou a transação, "Begin Transaction", executou as quatro queries que já conhecemos de insert into series e seasons, select * from "seasons" e insert into "episodes" e, em seguida, realizou o commit, "Commit Transaction".

Com isso, temos todas as queries dentro de uma única transação e acabamos ganhando até um pouco de performance por trás, mas não é esse o objetivo, e sim a consistência dos dados.

Vamos para um detalhe no código, estamos usando uma variável por referência, &$serie, e inicializando ela como nulo. Se algo acontecer dentro da função íamos acabar usando nulo como valor fora, em {$serie->nome}.

Assim, esse código não está organizado da melhor forma e no próximo vídeo vamos analisar dois detalhes. Primeiro, como melhorar esse código e, segundo, como fazer isso em uma sintaxe alternativa. Vamos aprender isso no próximo vídeo.

Transações - Sintaxe alternativa

Bem-vindos de volta. Então, começamos a usar transações no código e o Laravel possui algumas ferramentas para trabalharmos com transações. O que aprendemos primeiro, foi que através da facade de DB usamos a função transaction() e ela possui algumas particularidades.

A primeira é que ela recebe, além dessa closure, um segundo parâmetro, quando trabalhamos com transações pode ocorrer deadlock, isto é, se uma transação depende de duas tabelas e outra transação depende de outras duas tabelas, só que uma delas é igual.

Por exemplo, a primeira transação depende da tabela de episódios, a segunda transação usa a de episódios mas depende da de temporadas que a primeira transação ainda está dependendo. Basicamente, é como se você perguntasse para a sua mãe se pode sair e ela responde para você verificar com o seu pai e ao perguntar para o pai ele informa para você verificar com a mãe. Isso é um deadlock e quando trabalhamos com transações, isso pode acontecer.

Então podemos informar para o Laravel tentar executar novamente essa transação em caso de deadlock, por exemplo, se quisermos tentar cinco vezes se houver deadlock, basta passarmos o parâmetro cinco Episode::insert($episodes); }, attempts:5);.

No caso, não enxergamos nenhuma possibilidade visto que essa é a única transação que estamos usando, não estamos realizando nenhuma outra transação que utilize as mesmas tabelas. Mas é interessante ter esse conhecimento e vale a pena pesquisar mais sobre deadlock e como lidar nessas situações.

A primeira particularidade já foi esclarecida, agora, a segunda. Não precisamos iniciar a variável serie em um estado inválido, passar por referência, &$serie, etc. Vamos apagar essa criação da variável $serie = null; e sua referência &$serie para fazer de outra forma, que essa função retorna a série. Após o Episode::insert($episodes); vamos digitar return $serie;.

Agora o que a função transaction() vai fazer é pegar o retorno de toda a função da linha 30 até 52 e retornar também. Então agora podemos pegar o retorno da série incluindo $serie = antes de DB::transaction(function () use ($request), isto é, temos as séries em mãos e podemos acessar o nome dela.

$serie = DB::transaction(function () use ($request)

Repare que o código fica bem mais interessante dessa forma:

public function store(SeriesFormRequest $request)
{
    $serie = DB::transaction(function () use ($requestt) {
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);

                return $serie;
    });

    return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
}

Vamos executar o código novamente para garantir que tudo está funcionando. No site vou apagar a série "Moon Knight" selecionando o botão de "X" do lado direito da tela para inserir novamente, então no campo "Nome" vamos digitar "Moon Knight", em "Número de temporadas" apenas um,"1", e em "Eps/temporada" colocaremos seis,"6" e clicamos no botão "adicionar".

Irá aparecer a mensagem "Série 'Moon Knight' adicionada com sucesso" e em seguida a lista dos seriados: Grey's Anatomy, Moon Knight e The Punisher. Para analisarmos as queries na requisição post na parte inferior da tela, basta clicar em "Queries" e note que iniciou a transação, insere as séries, as temporadas e episódios sem erro e, no final, realizar um commit.

Com isso já melhoramos um pouco o código, só que em caso de erro precisaríamos após o public function store(SeriesFormRequest $request) inserir um try{ e fechar as chaves só depois de return $serie;}); e tentar retornar essa resposta, só que em caso de erro, no cenário de um catch, se pegarmos uma exceção qualquer queremos garantir que seja exibido um erro, por exemplo.

Isso é um cenário comum, só que no nosso caso, com esse código vamos ter um nível de indentação, depois outro nível de indentação, assim, o código deixa de ser tão interessante. Se você fez o treinamento de object calisthenics já sabe que é interessante diminuirmos o número de indentações.

Código para fins explicativos com o try:

public function store(SeriesFormRequest $request)
{
        try{
    $serie = DB::transaction(function () use ($requestt) {
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);

                return $serie;
    });

        return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
      } catch (\Throwable $e) {

            }
}

Vamos desfazer o que escrevemos no código para fins explicativos, o catch e o try. Poderíamos, para esse cenário usar uma sintaxe alternativa, ao invés de utilizar o transaction() usarmos o DB::beginTransaction();, assim, podemos remover a função anônima $serie = DB::transaction(function () use ($requestt) e a closure return $serie; e todo o código passa a estar no mesmo escopo que estava antes.

Apenas inserimos a linha DB::beginTransaction(); e no final DB::commit();. E agora, caso queiramos tratar aquele erro, podemos fazer o try catch e no catch adicionar o rollback. Igual fazemos com o PDO, então os métodos que conseguimos acessar no PDO, conseguimos acessar em DB.

public function store(SeriesFormRequest $request)
{
                DB::beginTransaction(); 
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);
                DB::commit();

                DB::rollBack();

    return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
}

Então dessa forma poderíamos trabalhar com essa sintaxe alternativa, mas eu vou preferir deixar naquele cenário porque não vou tratar esse erro, vou manter a função de Transaction(). Então, dessa forma vamos manter a implementação e agora estamos usando as transações.

public function store(SeriesFormRequest $request)
{
    $serie = DB::transaction(function () use ($requestt) {
        $serie = Series::create($request->all());
        $seasons = [];
        for ($i = 1; $i <= $request->seasonsQty; $i++) {
            $seasons[] = [
                    'series_id' => $serie->id,
                    'number' => $i,
            ];
        }
        Season::insert($seasons);

        $episodes = [];
        foreach ($serie->seasons as $season) {
            for ($j = 1; $j <= $request->episodesPerSeason; $j++) {
                    $episodes[] = [
                        'season_id' => $season->id,
                        'number' => $j
                    ];
            }
        }
        Episode::insert($episodes);

                return $serie;
    });

    return to_route(route:'series.index') ->with('mensagem.sucesso', "Série '{$serie->nome}' adicionada com sucesso");
}

Assim, as séries estão inseridas com suas temporadas e episódios e estamos garantindo a consistência. Em caso de erro, vamos visualizar uma mensagem informando e não quero tratar isso, por enquanto.

Agora voltando ao ponto do código, o controller deveria ter um motivo para mudar que é no cenário de precisarmos tratar a requisição de forma diferente. Só que caso precise tratar também de forma diferente no banco de dados teríamos que alterar esse controller.

Isso significa que o controller está com muita responsabilidade, só de analisar esse método grande já conseguimos ver que não está muito interessante. A regra para criar uma série deveria ser uma regra de negócio ou até uma regra da aplicação ou da persistência, está tudo dentro do controller.

Então vamos aprender sobre formas de extrair essa responsabilidade para outro lugar e, de maneira superficial, sobre qualidade de código, assim, no próximo capítulo vamos dar uma limpada e trazer esse código para outro lugar.

Sobre o curso Laravel: transações, service container e autenticação

O curso Laravel: transações, service container e autenticação possui 122 minutos de vídeos, em um total de 44 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