Iniciei na programação, contando o inicio da jornada e estudos, lá em 2007 mas meu contato com o primeiro computador foi nos anos 2000 por meio do computador de um tio meu com Windows 98 na época.
Meu principal intuito ao entrar no mundo da programação não era de nenhuma maneira ensinar e sim criar minha loja virtual para vender produtos de um velho sonho que tinha, sonho esse que era ter uma loja de produtos de informática e games (Seguindo os passos de meu pai, comerciante). Sempre via essa tal extensão .php
nas urls dos sites e fóruns, então resolvi que iria começar por aí, buscando saber o que era esse tal de PHP.
Alguém lembra das bancas de revistas, rsrsrs. Fui a uma banca de revistas e comprei um mini-livro, da antiga Digerati, sobre PHP, entretanto, estava me precipitando muito pois não conhecia nada de lógica e o livro tinha aspectos avançados da linguagem, eu noob, achando que tudo é a mesma coisa, penei para encontrar o material correto rsrsrs!
Lembro de ter deixado o livro de lado e partido para entender como de fato começar e depois de muito penar e pesquisar fui pro zero absoluto, estudando lógica de programação e o funcionamento da web, então, o dia do livro chegou, quando finalmente consegui entender a proposta dele além de continuar a buscar mais conhecimentos para evoluir e montar minha loja, que cá entre nós tecnicamente nunca chegou sendo criada por mim…
Acabei que, na época, montando a loja mas montei por meio de serviços de loja pronta na época, entretanto, continuava a estudar programação e Web, me apaixonando cada vez mais. PHP foi minha linguagem de escolha e a que me especializei e uso até hoje, inclusive, alcançando a tão sonhada certificação PHP em 2015.
No percurso e carreira, passando por empresas locais e trabalhos remotos encontrei o Laravel, na versão 3 ainda e de lá pra cá sempre tenho ele como uma ferramenta na minha bagagem de ferramentas, quando preciso entregar aplicações web de forma produtiva e robusta e com muito valor.
Não acredito em bala de prata, entendo que existem casos e casos e a ferramenta ideal para o trabalho do momento!
Mas o Laravel é um excelente framework do mercado e que vêm ganhando muita notoriedade e cada vez mais isso só se mantêm. Ele têm um foco muito grande em produtividade e você vai aprender isso na prática, aqui nesta jornada comigo.
A Code Experts é uma escola de cursos online com foco em prática onde abordamos os conceitos da programação web em projetos práticos e reais. A Code Experts, antes Code Experts Learning, começou como um braço de educação da Code Experts (Criação de Sistemas Web Sob Demanda).
Iniciamos na educação para suprir uma demanda faltante de cursos em nossa cidade, São Luis do Maranhão, e da vontade de transformar outras vidas por meio do ensino de programação, levando o conhecimento e simplificando o caminho para os devs iniciantes.
Dia após dia buscamos melhorar nossas metodológias de forma a gerar valor para nossos alunos, resultando em melhora de nossos materiais e na didática abordada!
O trabalho com educação foi tão grande que resolvemos fazer o merge e unificar tudo em educação, então a Code Experts Learning, de uma braço de educação, tornou-se o corpo todo! Tornou-se Code Experts - Cursos Online de Programação na Prática.
O conhecimento adquirido ao longo dos anos, que inicialmente seria para criar minha loja de produtos de informática e games, tornou-se para criação de nossa plataforma de cursos online lançada em 2016, especificamente Novembro de 2016 no dia 05 e de lá pra cá venho concentrando meus esforços para a cada dia melhorar a experiência dos nossos alunos dentro dela.
O trabalho apenas começou!
Hoje sou desde o zelador ao editor de vídeos, passando por escritor e professor mas busco sempre dá o meu melhor para entregar excelentes materiais para nossos alunos e não farei diferente aqui neste livro!
Conte comigo!
Te convido também a nos conhecer em codeexperts.com.br.
Bom, serei breve aqui.
Escrevi este livro para os programadores PHP e entusiastas que buscam melhorar sua produtividade na criação de Aplicações Web usando a linguagem, entregando softwares mais rápidos e com valor agregado. Nem sempre rápido quer dizer de má qualidade pois o Laravel te permite criar coisas com menos tempo e esforço mas com alto valor agregado e é por este caminho que irei te guiar nestas páginas.
É claro que se você apenas começou com PHP, recomendo fortemente a continuar estudando as bases da linguagem e busque materiais sobre Orientação a Objetos, pois estes conhecimentos irão te adicionar uma excelente base para entender todos os conceitos abordados, não somente no Laravel, mas em frameworks PHP modernos.
Se você está pronto para a jornada, então conte comigo e vamos lá!
O projeto está disponível de forma completa no github e você pode escolher seguir a jornada de duas formas:
O repositório do projeto do livro está organizado por capítulos, sendo cada capítulo uma branch específica. Cada branch está incremental, tendo o código atual do capítulo junto dos códigos dos capítulos anteriores.
Acesse o repositório em bit.ly/laravel-12-ebook-app-code
A branch main será a branch com o app completo, caso queira navegar por capítulos utilize as branchs. Você pode inclusive, ver os commits de cada capítulo(branch) para ver as alterações especifícicas ou ver os commits da branch principal que terá o merge do app completo.
Hoje estou atuando como instrutor independente e inclusive este livro(ebook) vêm sendo escrito, revisado e atualizado de forma independente também. Posso ter deixado passar erros aqui e ali, pode existir também algum ponto em que você possa travar quer seja porque eu deixei algum código por adicionar quer seja por você se perder na jornada.
Para isso criei um tópico no nosso grupo principal no telegram para escutar você e prestar um suporte para te destravar na jornada!
Te convido a participar em t.me/CodeExpertsCursos ou podes buscar no seu telegram pelo @CodeExpertsCursos.
Te espero aqui no grupo também!
Att. Nanderson Castro (Code Experts)
Laravel é um fullstack framework criado por Taylor Otwell. O Laravel foi criado em Junho de 2011 e de lá pra cá vem ganhando muita notoriedade por meio de suas features e facilidades, quando tratamos da criação de aplicações web em geral.
O Laravel foi fortemente inspirado pelo Rails, desde sua forma de trabalho e gerações, a sua estrutura. O Laravel hoje em sua versão 12 e conta com um ecossistema em constante crescimento, envolvendo diversos projetos, componenetes e ferramentas dentro do seu proprio ecossistema.
Conheceremos diversas funcionalidades do framework e veremos como ele traz essa simplicidade tão aclamada pela comunidade de desenvolvedores PHP.
Para conhecimento, o Laravel hoje possui plataformas para Deploy, Criação de Projetos com Foco em Saas, Ferramentas de Suporte ao Desenvolvimento e também projetos para painéis CMS além, claro, de todo o projeto em open-source que temos.
Iniciar um projeto Laravel 12, hoje é recomendado fortemente o uso do Laravel CLI ou Installer. Por meio do installer você será guiado por um utilitário que te guia por escolhas com o fim de iniciar seu projeto com os recursos que você está acostumado. O Laravel traz algumas opções de inicialização de projeto, onde você pode iniciar usando ou não um Starter Kit.
Starter kits são opções que trazem para você um projeto para de fato iniciar app rapidamente, eles vêm com autenticação, registro, perfil e mais e têm o foco de te direcionar pro que de fato importa, sua regra criada dentro desse projeto pré-preparado.
O Laravel possui hoje apenas um StarterKit com 3 opções de stack (tecnologia de ferramentas que vai compor esse starter kit).
As stacks são:
Você pode optar por iniciar o projeto Laravel sem a escolha de uma stack listada acima, que a priore é que iremos fazer aqui nesta jornada!
Dado a questão das stacks faladas anteriormente, vamos iniciar nosso projeto e entender o que precisamos para usar o installer em nossa máquina. Lembrando que para usar o installer vamos precisar do composer em nossa máquina.
Para isso instale o composer através do site getcomposer.org.
Se você chegou até aqui em busca do Laravel e entendeu nosso epílogo
Pra Quem é Este Livro
, você já deve ter o composer em sua máquina e lidar com ele não deve ser mais problema para você!
Para termos sucesso na utilização do Laravel Installer com foco em iniciar um novo projeto, cheque se seu PHP respeita as seguintes configurações:
Para verificar sua versão do PHP de forma rápida basta acessar seu terminal ou cmd(tenha certeza que o PHP está no PATH do seu Sistema) e execute o comando abaixo:
php -v
Para visualizar suas extensões execute em seu terminal o comand abaixo:
php -m
Com o composer em sua máquina Windows, vamos instalar o nosso utilitário, o Laravel Installer.
Primeiramente abra seu prompt de comando, para isso basta utilizar a combinação de teclas CTRL + R
e na janelinha que aparecer digitar: cmd.
Você também pode usar o Windows Terminal e ele pode ser obtido na loja de aplicativos da Microsoft no Windows 10.
Se você executou o CTRL+R e digitou o cmd, pressione ENTER para abrir o prompt.
Com o prompt aberto, digite e execute o comando abaixo, dando um ENTER após a digitação:
composer global require “laravel/installer”Com isso o composer vai baixar o pacote laravel/installer e jogar na pasta mencionada abaixo, dentro da pasta vendor/bin que está no caminho abaixo:
C:/Users/<seu_nome_de_usuario>/AppData/Roaming/Composer/Veja o resultado da instalação abaixo:
Na época de atualização e escrita deste livro, a versão do Installer é a 5.14, que traz consigo as novidades, no wizard de instalação, com foco nas melhorias de ecossistema do Laravel 12
Feito isso, precisamos adicionar o caminho dos binários globais do composer no PATH do Windows. Primeiramente acesse o menu propriedades do Meu Computador, como mostrado abaixo:
Após isso, acesse o menu Configurações Avançadas do Sistema, no menu lateral direito da sua tela. Veja abaixo:
Na tela que aparecer, na parte inferior, acesse o menu Variáveis de Ambiente.
Na janela de Variáveis de Ambiente, na parte inferior, procure por Path e clique em editar, você pode editar o PATH apenas para o seu usuário, selecionando o Path da área acima ou o Path do sistema global selecionando o Path na área abaixo, eu editei somente a do meu usuário, no caso o Path da área acima.
Clicando duas vezes na variável Path ou selecionando e clicando em Editar… você verá uma janela como abaixo:
Na janelinha que aparecer, clique em novo e adicione o Path do composer conforme abaixo:
%USERPROFILE%/AppData/Roaming/Composer/vendor/binVeja uma imagem para facilitar:
Após isso, dê OK até sair de todas as janelas. Se seu prompt continuou aberto durante o processo, feche-o e abra novamente.
Para verificar se o laravel installer é reconhecido, execute em seu cmd o comando: laravel, dê ENTER e obtenha o resultado abaixo:
Agora estamos aptos a iniciar um projeto Laravel, por meio do Laravel Installer em nosso CMD ou Windows Terminal.
Tendo a certeza que o Composer está em sua máquina, execute o comando abaixo em seu terminal:
composer global require “laravel/installer”Após a instalação do pacote precisamos linkar ele no PATH do nosso sistema. Se você usa o bash em seu terminal o arquivo que você pode utilizar para configurar o PATH será o .bash_profile, caso utilize o zsh o arquivo a ser alterado será o .zshrc. Ambos os arquivos encontram-se na pasta do seu usuário, em $HOME.
PS.: Pelo terminal você pode digitar cd ~
e dá um enter que cairá na pasta do seu usuário, pelo seu terminal.
Ao abrir o arquivo correspondente, adicione a seguinte linha ao final do arquivo:
MacOS: PATH="$HOME/.composer/vendor/bin:$PATH"
No Linux você terá duas possibilidades e pasta: $HOME/.config/composer/vendor/bin
ou $HOME/.composer/vendor/bin
Após isso, reinicie seu terminal e execute o comando laravel.
Veja o resultado abaixo:
Lembrando que sua versão será 5.14 ou superior dado o momento em que estou atualizando/escrevendo este livro.
Vamo enfim iniciar nosso novo projeto e que seguiremos na prática, através dele, aqui nesta jornada!
Por meio do Laravel Installer vamos iniciar nosso projeto com o seguinte comando abaixo:
laravel new store_app
Lembre de entrar pelo seu terminal e escolhar sua pasta de escolha, nesta pasta será criada a pasta do projeto conforme o nome que você escolher.
Nesta versão do installer precisaremos escolher a stack conforme mencionei anteriormente mas pra fins de momento vamos optar pela opção none:
Perceba que a opção padrão é none, você pode deixar em branco e simplesmente dá ENTER ou digitar none e ENTER.
Com isso o instaldor baixará todo o projeto Laravel, instalará as dependências deste projeto automaticamente e jogará tudo dentro da pasta store_app
.
A opção de escolhe none
iniciará para nós um projeto sem um starter kit, estamos fazendo isso pelo nosso momento didático aqui no livro. Escolher um starter kit agora tornaria nossa jornada mais dificil pois requer conhecimentos extras que não abordaremos neste livro.
Durante o passo de instalação o Laravel Installer abrirá um opção para selecionarmos o banco de escolha, por padrão o drive do banco é SQlite mas vamos optar pelo MySQL, basta digitar como na imagem e dá ENTER. Veja abaixo:
Após a escolha acima ele pedirá pra você executar as migrações, então opte por SIM
e certifique-se de seu banco está levantado, mais a frente falaremos sobre migrations(migrações).
Confirme também a opção de instalar os assets via NPM para o frontend.
Feito todo esse caminho seu projeto será criado e basta acessar a pasta criada para verificar seu primeiro projeto com Laravel, recém iniciado claro!
Vamos conhecer as pastas e os arquivos que fazem parte da estrutura base do nosso projeto:
O Laravel possui algumas pastas base, como a pasta app
, bootstrap
, database
, config
, public
, resources
, storage
, routes
, tests
e a vendor
. Vamos ver o que cada pasta destas representa ou armazena em sua estrutura.
A pasta app conterá todo o conteúdo do nosso projeto como os models, controllers, serviços, providers, middlewares e outros. Concentraremos muito dos nossos esforços nesta pasta durante a criação e desenvolvimento de nosso projeto.
Os arquivos de configuração, do projeto laravel, encontram-se nesta pasta. Configurações de conexões com banco de dados, configurações para armazenamento de arquivos, configurações de autenticação, mailers, serviços, sessão e outros. Esta pasta contêm arquivos que armazenam os arrays de configuração para cada uma das áreas mencionadas.
Nesta pasta temos algumas subpastas que são: views
, js
, & css
. A priore esta pasta salva os assets frontend que podemos cosumir no projeto e também nossas views da camada de templates do projeto.
Nesta pasta o Laravel salva arquivos de sessions, caches, logs e nós também podemos utilizar para armazenar arquivos mediante uploads em nosso projeto.
Nesta pasta vamos encontrar os arquivos para o mapeamento das rotas de nosso projeto. Rotas estas que permitirão o nosso usuário acessar determinada url e ter o conteúdo processado e esperado.
Mais a frente vamos conhecer melhor essas rotas, no Laravel 12 por padrão teremos apenas os arquivos de rota para Web e Console, as rotas de API e Broadcast Channels são adicionadas mediante necessidade, isso vêm desde a versão 11.
Nesta pasta teremos as classes para teste de nossa aplicação. Testes Unitários, Funcionais e outros.
Aqui teremos os arquivos de migração, na pasta migrations
para nossas tabelas, vamos conhecer mais sobre migrações a frente. Temos, também, os arquivos para os seeds, na pasta seeders
e também as factories, na pasta factories
que também conheceremos mais a frente aqui.
Na pasta bootstrap teremos os arquivos responsáveis por inicializar os participantes do framework Laravel, encaminhando as requisições e indicando como as coisas devem ser executadas.
Nesta pasta, no arquivo app.php é onde configuraremos, conforma necessidade, quesões sobre Middlewares, Erro Handler e mais.
Esta é nossa pasta principal, a que fica exposta para a web e que contêm nosso front controller.
Por meio desta pasta é que recebemos nossas requisições, especificamente no index.php, e a partir daí que o laravel direciona as requisições e começa a executar o que é necessário para aquela demanda recebida pelo browser.
A vendor, como conhecemos, é onde ficam os pacotes de terceiros dentro de nossa aplicação. Pacotes estes, mapeados e instalados pelo composer.
Temos ainda alguns arquivos na raiz do nosso projeto, como o composer.json
e o composer.lock
onde estão definidas as nossas dependêncas e as versões baixadas respectivamente.
Temos também o package.json
que contêm algumas definições de dependências do frontend.
Temos ainda também o server.php
que nos permite emular o mod_rewrite do apache.
Temos também o phpunit.xml
que contêm as configurações para a execução dos testes unitários, funcionais e etc em nossa aplicação.
A parte de asset bundler, para as builds do frontend, é gerenciada pelo ViteJS e seu arquivo de configurações é o vite.config.js
.
Deixei por último o arquivo .env
que contêm as variavéis de ambiente para cada configuração de nossa aplicação.
Neste arquivo encontramos os parâmetros para conexão com o banco, o aplication key: que é o hash único para nossa aplicação. Encontramos outras variáveis neste arquivo que são consumidas pelo app, vale dá uma conferida.
Este arquivo e essas configurações são providas pelo pacote DotEnv do Vance Lucas.
Basicamente esta é a estrutura do nosso projeto Laravel, e nela estaremos enquanto realizamos nossa jornada!
O Laravel possui uma interface de comandos ou command line interface (CLI) chamada de artisan
. Por meio dela podemos melhorar bastante nossa produtividade enquanto desenvolvemos, como por exemplo: Gerar models, controllers, gerenciar nossas filas e muito mais!
Para conhecer todos os comando disponíveis no Artisan, basta executar na raiz do seu projeto o seguinte comando:
php artisan
O resultado deste comando será a lista de comandos e subcomandos disponiveis neste executável de terminal, que é o Artisan.
Sempre que precisar lembrar dos parâmetros de um comando/subcomando específico, lembre sempre da opção
--help
, exemplo:php artisan make:model --help
Para concluirmos nosso primeiro capítulo, vamos iniciar nossa aplicação e testá-la em nosso browser. Para isso acesse o seu projeto via terminal ou cmd no Windows e na raiz, deste projeto, execute o comando abaixo:
php artisan serve
O comando acima disponibilizará sua aplicação, via browser, no seguinte link: http://127.0.0.1:8000
. Veja o resultado ao acessarmos o projeto inicialmente:
Se tudo estiver corretamente configurado, teremos o resultado acima, a tela inicial do nosso projeto Laravel, e sua página inicial padrão.
A título de informação, ao iniciar seu server, você pode escolher uma porta e host diferentes com as opções
--host
e--port
informando os valores que quer mudar, lembre sempre da opção--help
para uma doc do comando em questão. Exemplo:php artisan serve --help
Neste capítulo concluimos as configurações iniciais para criarmos projetos com o Laravel , o que permitiu inclusive criarmos nosso projeto que será incrementado a cada capítulo deste livro.
Agora, continuando para o próximo capítulo vamos começar a entender a estrutura geral do framework por meio da criação do nosso Hello World no Laravel.
Vamos lá!
Este capítulo visa mostrar o Laravel de uma forma geral, o que definirá nosso fluxo de trabalho durante todo o desenvolvimento. Gosto muito de tomar uma abordagem prática, por isso, vamos trabalhar todos os conceitos envolvendo o framework em cima de um projeto prático e direto ao ponto.
O projeto escolhido e que nos traz todo o aparato para entedermos cada parte de um framework fullstack, será a criação de uma loja virtual, além do sistema de comentários.
Então vamos lá colocar a mão na massa e dar inicio, a nossa jornada de conhecimento mas antes não podemos deixar de fazer nosso famoso mantra, criar um Hello World com o framework!
Antes de continuarmos, é interessante sabermos um pouco do modelo base utilizado no organização da estrurua dentro do nosso projeto Laravel, esse modelo ou padrão é o famoso MVC, Model-View-Controller.
A maioria dos frameworks atuais utilizam este padrão para organizar seus componentes/participantes, além de ter um Front Controller
que recebe as requisições recebidas por nossa aplicação e entrega/delega para quem responsável daquela requisição. Geralmente este Front Controller
se encontra na pasta public(Diretério WEBROOT da aplicação), e é inicializado no arquivo index.php dos frameworks, como o é no Laravel.
O modelo MVC possui três camadas base, as que comentei acima, o Model, o Controller e a View. Vamos entender o que é cada parte:
A camada do Model ou Modelo é a camada que normalmente conterá nossas regras de negócio, geralmente possuem as entidades que representam a conversa com o banco de dados mas podem conter classes com regras de negócio específicas e até podem conter classes que realizam algum determinado serviço.
Dentro do Laravel nossos models serão as classes que representam alguma tabela no banco de dados, com poderes de manipulação referentes a esse tratamento com o banco. Você pode encontrar, por default, as classes model dentro da pasta app/Models
.
A camada do Controller ou Controlador é a camada mais fina, digamos assim, pois deve receber a requisição e já demandar para o model em questão, caso necessário, e dado o retorno do model, o controller pega este retorno e entrega para a view ou, como em muitos casos, só carrega uma view caso não necessitemos de operações na camada do Model.
A ideia pro controller é que ele seja o mais simples possivel, portanto, evite adicionar regras e complexidade em seus Controllers.
Dentro do Laravel estes controllers encontram-se na pasta app/Http/Controllers
.
A camada de View ou visualização é a camada de interação do usuário, onde nossos templates vão existir, como as telas do nosso sistema e as páginas de nossos sites.
Nesta camada, também, não é recomendado colocar regra de negócios, como por exemplo: consultas ao banco ou coisas deste tipo. Esta camada é exclusivamente para exibição de resultados e input de dados, via formulários, além de interações Javascripts e outros processos já esperados para uma melhor experiência do usuário.
No Laravel, as views encontram-se na pasta resources/views/
. As views do Laravel são compostas por um template engine, que falarei sobre ele mais a frente, chamado de Blade. O Blade visa simplificar a escrita de pontos dinâmicos em nossas views como o uso loops, condicionais, estrururas customizadas e muito mais.
Para criarmos nosso hello world, vamos seguir o roteiro abaixo:
Hello World
a ser exibida como resultado do acesso;A priore passos bem simples e que vão nos dar um panorama inicial do framework para a partir daí, seguirmos com os conceitos individualmente.
Acesse o projeto iniciado no capítulo passado pelo seu terminal ou cmd no Windows. Na certeza de está na raiz do seu projeto execute o seguinte comando abaixo:
php artisan make:controller HelloWorldController
Ao executar o comando acima teremos o seguinte resultado, pro sucesso da criação do nosso primeiro controller, exibido em nosso terminal ou cmd: Controller <CAMINHO_ATE_O_CONTROLLER> created successfully.
Após isso teremos o nosso controller criado dentro da pasta do nosso projeto, especificamente em: app/Http/Controllers
. E o arquivo criado será: HelloWorldController.php
.
Não esqueça de acessar o projeto em seu editor ou IDE de código de sua preferência.
PS.: Recomendo aqui, caso não tenha um em questão, o Visual Studio Code. Para baixar, acesse code.visualstudio.com.
O código do nosso controller encontra-se abaixo, este código foi gerado junto com o arquivo:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
;
4
5
use
Illuminate
\Http
\Request
;
6
7
class
HelloWorldController
extends
Controller
8
{
9
//
10
}
O resultado acima é a classe do nosso controller, que estende do Controller base e já traz um import para nós, o nosso Request que também conheceremos ele no decorrer do nosso livro.
Vamos criar nosso primeiro método para execução. Abaixo segue o conteúdo do nosso primeiro método:
1
public
function
index
()
2
{
3
$helloWorld
=
'
Hello World
'
;
4
5
return
view
(
'
hello_world.index
'
, compact
(
'
helloWorld
'
))
;
6
}
Do método acima temos um ponto bem interessante, o retorno da função helper chamada view
que recebe como primeiro parâmetro a view desejada e o segundo parâmetro um array associativo com os valores a serem enviados para esta view.
Antes de entendermos como vamos criar a view, com base no primeiro parâmetro do helper view(), preciso te falar que a função compact é uma função do proprio PHP e que pega as variáveis informadas nesta função e joga dentro de um array associativo, respeitando o pensamento de: o nome da variável ser a chave associativa e o valor da variável será o valor desta chave no array criado.
Exemplo, o compact acima, informado no segundo parâmetro do helper view(), terá como resultado o seguinte array associativo:
1
[
2
'helloWorld' => 'Hello World'
3
]
Agora, como podemos criar nossa view de forma que o primeiro parâmetro da função helper view
seja satisfeito? Vamos entender!
Temos o seguinte valor: hello_world.index
.
A última parte da string acima, apresentada pós .
, será o nome da nossa view e deve respeitar o seguinte nome de arquivo, se depois do ponto tenho index
precisarei criar uma view chamada index.blade.php
lá na pasta de views.
Só que antes do ponto temos o valor hello_world
, que neste caso será a pasta onde nosso index.blade.php
estará ou será criado, então, no fim das contas precisaremos criar lá dentro da pasta resources/views
uma pasta chamada hello_world
e dentro desta pasta nosso arquivo index.blade.php
chegando ao caminho completo e o arquivo: resources/views/hello_world/index.blade.php
.
PS.: Se você quiser chamar uma view diretamente que esteja dentro da pasta de views, basta informar apenas o nome da view em questão. Se você tiver mais niveis, em questão de pastas, até chegar na view, é necessário informá-los até o arquivo da view em questão. Você não precisa informar o caminho base resources/views e nem a extensão do arquivos .blade.php, pois o Laravel já inclui automaticamente para você.
Com isso crie o arquivo index.blade.php
e sua pasta hello_world
dentro da pasta views
. Com o seguinte conteúdo abaixo:
1
<h1>
{{
$
helloWorld
}}
</h1>
Acima temos, dentro do elemento h1
, o nosso primeiro contato com o template engine Blade. Usamos acima a notação de print, chamada de interpolação nos template engines, abre {{
fecha }}
, dentro da nossa view e pegamos a nossa variável $helloWorld
vinda lá do nosso controller e exibimos seu valor dentro da tag html do elemento h1.
Agora, que já seguimos os dois passos iniciais do nosso roteiro, precisamos permitir o acesso e execução deste método, método index do nosso controller HelloWorldController, por parte dos nossos usuários mediante a liberação de uma url para acesso.
Como faremos isso? Simples, no momento, vamos criar uma rota que apontará para o método do nosso controller!
Vamos lá que vou te mostrar!
Abra seu arquivo web.php
que se encontra na pasta routes
na raiz do projeto.
Teremos o seguinte conteúdo:
1
<?php
2
3
use
Illuminate\Support\Facades\Route
;
4
5
Route
::
get
(
'/'
,
function
()
{
6
return
view
(
'welcome'
);
7
});
Este arquivo web.php conterá todas as rotas da nossa aplicação, rotas estas que trabalham com a exibição de views dinâmicas em nosso projeto. Tudo que for UI ou User Interface(Interface do Usuário) terá suas rotas definidas neste arquivo.
De cara já vemos a primeira definição de rota, a rota principal /
que exibe a view welcome.blade.php
, que está lá dentro da pasta de views. Esta rota é a rota executado ao acessarmos a página principal de uma aplicação Laravel recém instalada e que futuramente será nossa rota de entrada do site/loja.
Agora vamos definir nossa rota de hello world.
Adicione o código abaixo, após a definição da rota principal que já existe:
1
Route
::
get
(
'hello-world'
,
[
\
App
\
Http
\
Controllers
\
HelloWorldController
::class
,
'index'
\
2
]
);
Acima temos nossa primeira rota definida, a rota escolhida foi hello-world
que executará o método index do controller HelloWorldController, beleza, como isto está definido? Vamos lá!
O segundo parâmetro do método get
, do Route
, é o executavél para esta rota, que pode ser uma função anônima como vimos na definição da rota principal já existente ou um array que respeite Controller, Método
.
Existe ainda uma outra forma, que é usando uma string no formato NamespaceAtéOController@Metodo. Basicamente ficando como abaixo:
Route::get('hello-world', '\\App\\Http\\Controllers\\HelloWorldController@index');
Ao acessarmos em nosso browser a url http://127.0.0.1:8000/hello-world, o Laravel vai buscar por nossa rota internamente em sua coleção de rotas e ao encontrar uma rota que faça o match com hello-world, executará o segundo parâmetro desta rota encontrada, que como comentado pode ser uma funcão anônima ou um método de um controller ou resumindo um Callable
, como fizemos a definição.
Com nossa rota definda, chegamos ao passo 3 e final do nosso roteiro e já podemos iniciar nosso webserver, na raiz do projeto para o teste do nosso Hello World.
Execute o comand abaixo em seu terminal ou cmd:
1
php artisan serve
E acesse em seu browser http://127.0.0.1:8000/hello-world
, onde teremos o resultado abaixo:
O Hello World em nossa tela!
Neste capítulo vimos diretamente o processo de expor uma rota para acesso via browser, a execução de nosso controller através do método index, bem como, a exibição do resultado em uma view.
É claro que cada etapa destas carece de um pouco mais de informações, e também, é claro que cada etapa destas contêm diversos detalhes que serão necessários, para termos um melhor proveito do framework.
A partir de agora iremos debsravar toda estas opções e outros pontos a mais que não foram diretamente mostrados aqui.
Este capítulo serviu para te mostrar um panorama geral que será utilizado no decorrer de sua caminhada com o framework durante o desenvolvimento de suas aplicações, então, vamos continuando que temos bastante coisa para aprender ainda!
Vamos continuar nossa jornada!
Rotas e Controllers são integrantes bem importantes em nossas aplicações Laravel. Neste capítulo vamos conhecer mais opções proporcionadas pelas rotas e como podemos trabalhar com controllers conhecendo um pouco além do que vimos no capítulo passado.
Vamos as rotas então!
As rotas no Laravel nos ajudam a termos um maior controle no mapeamento dos acessos a nossa aplicação, claro que nós definimos nossas urls e expomos pro nosso usuário. Como temos que mapear nossas URLs dentro dos arquivos de rota, fica mais fácil termos controle do que será exposto e também fica mais fácil de customizar como queremos.
Dentro do Laravel temos os arquivos de rotas bem separados, que nos ajudam a organizar melhor tais rotas que dependendo da aplicação podem se tornar bem grande no quesito de definições escritas dentro do arquivo de rotas em questão.
O Laravel possui os seguintes arquivos de rotas: web.php
, api.php
( vem mediante habilitar via comando), channels.php
(mesmo detalhe sobre o api.php, precisamos habilitar dentro do fw) e console.php
.
O arquivo web.php conterá as rotas de sua aplicação com as interfaces para o usuário. Todas as rotas que têm esse fim deverão ser definidas neste arquivo.
Se você for trabalhar com APIs, expondo endpoints para que outras aplicações possam consumir seus recursos, você deverá definir suas rotas com este fim no arquivo api.php.
Esse arquivo não existe em um projeto novo, para habilitá-lo precisamos executar o comando php artisan install:api
com o fim de ambientar toda a parte para APIs REST do framework.
Se você for trabalhar com eventos de Broadcasting suas rotas deverão ser definidas neste arquivo, as rotas aqui dispostas são utilizadas para controle de acesso aos canais usados no Broadcast.
Esse arquivo também não existe em um projeto novo, para habilitá-lo precisamos executar o comando php artisan install:broadcast
com o fim de ambientar toda a parte para mensagens broadcast do framework.
Arquivo para registro de comandos para o console e execução a partir do artisan.
Como vimos no último capítulo, abordei duas formas de definição de rotas em nosso Hello World. Uma era a rota que já existia no arquivo web.php e a outra foi nossa definição de rota para nosso Hello World.
Vamos da uma revisada, uma relembrada:
1
Route
::
get
(
'/'
,
function
()
{
2
return
view('welcome')
;
3
}
);
e
1
Route
::
get
(
'hello-world'
,
[
\
App
\
Http
\
Controllers
\
HelloWorldController
::class
,
'index
\
2
'
]
);
Acima temos duas formas de definição para rotas, com respeito ao que será executado. Lembrando que o primeiro parâmetro do método get é a rota em questão e o segundo parâmetro será um callable: uma função anônima ou um método de um Controller que será executado ao acessarmos a rota.
Na rota inicial, que já tinhamos no arquivo web.php, vemos a utilização da função anônima e em nossa rota usamos a definição de chamada do controller e seu método diretamente.
Um primeiro ponto que podemos abordar sobre os métodos do Route
, como vimos o get
até o momento, é que teremos métodos respeitando os verbos http, como:
Podemos utilizá-los da seguinte maneira:
Podemos usar os métodos conforme os verbos http mostrados, tendo sempre como primeiro parâmetro, a rota em si, e o segundo parâmetro o callable ou executável para esta rota quando acessada.
Se precisarmos usar uma rota que responda a determinados tipos de verbos http, podemos usar o método match
do Route
. Como abaixo:
1
Route
::
match
(
[
'get'
,
'post'
]
,
'products/create'
,
function
()
{
2
return
'Esta
rota
bate
com
o
verbo
GET
e
POST'
;
3
}
);
No exemplo acima a rota products/create
suporte acessos tanto via GET quanto via POST.
Caso queira que uma rota responda para todos os verbos ao mesmo tempo, você pode usar o método any
do Route
:
1
Route
::
any
(
'products'
,
function
()
{
2
return
'Esta
rota
bate
com
todos
os
verbos
HTTP
mencionados
anteriormente'
;
3
}
);
Em determinados momentos você vai precisar apenas renderizar determinadas views como resultado do acesso a sua rota. Para isso temos o método view
do Route
, que nos permite setar uma rota, primeiro parâmetro, definir uma view, segundo parâmetro. Se precisarmos passar algum valor para esta view passamos o array associativo com os dados para nossa view, este sendo o terceiro parâmetro do método.
Veja como é simples:
1
//Exibindo somente a view: 127.0.0.1:8000/bem-vindo -> view resources/views/bemvindo\
2
.blade.php
3
4
Route
::
view
(
'/
bem-vindo
'
,
'
bemvindo
'
)
;
5
6
//Exibindo a view e mandando parâmetros para ela
7
8
Route:
:
view
(
'/
bem-vindo
'
,
'
bemvindo
'
,
[
'
name
'
=>
'
Nanderson
Castro
'
])
;
Bem simples, ao acessarmos a rota bem-vindo
em nosso browser, carregaremos a view bemvindo.blade.php
diretamente como resultado.
Continuando, vamos conhecer um ponto bem importante sobre rotas que é a possibilidade de informamos parâmetros dinâmicos. Parâmetros estes que podem servir para identificar determinado recurso, como uma postagem em um blog por exemplo.
Veja a rota definida abaixo:
1
Route
::
get
(
'/products/{slug}'
,
function
($
slug
)
{
2
return
$slug
;
3
}
);
Temos a rota /products/
após isso definimos um parâmetro dinâmico chamado slug
, dentro de chaves como é solicitado pelo componente de rotas. Em nossa função anônima passamos o parâmetro $slug
que receberá o valor dinâmico e assim, poderemos utilizar ele dentro da nossa função.
Se eu estiver usando um controller e seu método, basta informarmos no método o parâmetro correspondente como informamos na função anônima e assim utilizamos tranquilamente o valor do parâmetro dinâmico.
Com esta rota defininda em nosso arquivo web.php
e nosso server levantado, podemos acessar em nosso browser a seguinte url: http://127.0.0.1:8000/products/teste-parametro-dinamico
.
Resultado:
Como retornamos o parâmetro informado, teremos o valor dinâmico exibido em nosa tela como mostra a imagem acima.
Se você precisar definir parâmetros opcionais para sua rota em questão, basta adicionar a ?
antes do fechamento da chave do parâmetro, na definição da rota. Veja abaixo:
1
Route
::
get
(
'/products/{slug?}'
,
function
($
slug
=
null
)
{
2
3
return
!is_null($slug)
?
$
slug
:
'Comportamento sem a existência do param slug'
;
4
5
}
);
Agora nosso parâmetro slug
é opcional, ou seja, não será necessário informá-lo na rota e isso nos abre precedente para validarmos ou exibirmos os resultados com base na não existência do parâmetro.
Mais um ponto bem útil nas rotas no Laravel.
Podemos validar o formato dos parâmetros aceitos em nossas rotas por meio de expressões regulares para um melhor controle. Para isso podemos utilizar o método where
com este fim:
1
Route
::
get
(
'/user/{id}'
,
function
($
slug
)
{
2
return
$slug
;
3
}
)
4
-
>
where
(
[
'id'
=>
'[0-9]+'
]
);
O método where
espera um array associativo, sendo a chave o nome do parâmetro e o valor a expressão regular (Regex) a ser validada no parâmetro informado.
Se você tiver mais de um parâmetro dinâmico e quiser informar uma regex para tal, basta ir adicionando no array associativo, respeitando o pensamento chave sendo o parâmetro e valor sendo a experessão regular que irá validar o parâmetro informado, verificando se ele respeita a regra da Regex escrita.
Outro ponto bem importante em nossas rotas são apelidos. Mas para que servem? Até agora conhecemos o valor real ou nome real da rota mas podemos chamá-las por meio de seus apelidos também, isso nos ajuda quando precisamos, em um futuro, alterar o nome real das rotas(se isso for necessário de alguma forma).
Quando fazemos referência aos apelidos, podemos alterar tranquilamente o nome real da rota que o peso desta modificação não será tão impactante assim. E como utilizar este apelido?
Vamos pegar nossa última rota do parâmetro dinâmico:
1
Route
::
get
(
'/products/{slug}'
,
function
($
slug
)
{
2
return
$slug
;
3
}
)
4
-
>
name
(
'products.single'
);
Perceba a adição simples que fiz após o método get
antes de fechar com o ;
. Chamei o método name
que me permite adicionar um apelido para a rota em questão, neste caso agora posso chamar o apelido products.single
toda vez que eu precisar usar a rota products/{slug}
.
Em um link em nossa view ao invés de usarmos o link da rota desta maneira:
1
<a
href=
"/products/primeiro-produto"
>
Primeiro Produto</a>
Vamos utilizar desta maneira:
1
<a
href=
"
{{
route
(
'products.single'
,
[
'slug'
=>
'primeiro-produto'
])
}}
"
>
Primeiro Prod\
2
uto</a>
Perceba acima que estamos em uma suposta view e utilizamos um método helper do Blade que é o route
em nosso atributo href da âncora. O primeiro parâmetro do route
é o apelido da rota e se a rota tiver parâmetros dinâmicos, que é o nosso caso, podemos informar o valor do parâmetro, dentro de um array, no segundo parâmetro do helper route
, informando o nome do parâmetro dinâmico e o seu valor.
O método route irá gerar a url correta, informando o parâmetro dinâmico no local correto. Com isso fica mais simples, se precisarmos futuramente modificar o nome real da rota, uma vez que chamamos a rota pelo apelido ao invés de seu nome real.
Podemos definir determinadas configurações para um grupo específico de rotas e para conhecermos o poder do método group, decidi mostrar ele aqui com a definição de um prefixo, método existente no Route também.
Vamos ao código abaixo:
1
Route
::
prefix
(
'products'
)
-
>
group
(
function
()
{
2
3
Route
:
:
get
(
'/'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'index'
]
)
->
name
\
4
(
'products.index'
);
5
6
Route
:
:
get
(
'/create'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'create'
]
\
7
)
->
name
(
'products.create'
);
8
9
Route
:
:
post
(
'/save'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'save'
]
)
->
\
10
name
(
'products.save'
);
11
12
}
);
Perceba no trecho de rotas acima, que utilizei incialmente o método prefix
para definir esse prefixo para o grupo de rotas dentro do método group.
Agora, as rotas dentro do group serão prefixadas com o products
, ficando desta maneira:
Duas rotas acessíveis via GET e uma acessível via POST.
O grupo nos permite esse tipo de configuração, quando precisamos organizar melhor determinadas configurações que se repetirão para mais de um set de rota. Isso melhora até a escrita dos nossos arquivos de rotas e definições.
Vamos melhorar ainda mais nosso set de rotas do momento passado. Podemos, também, definir um apelido base para um grupo de rotas, então vamos melhorar nosso grupo anterior.
Veja como ficou:
1
Route
::
prefix
(
'products'
)
-
>
name
(
'products.'
)
-
>
group
(
function
()
{
2
3
Route
:
:
get
(
'/'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'index'
]
)
->
name
\
4
(
'index'
);
5
6
Route
:
:
get
(
'/create'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'create'
]
\
7
)
->
name
(
'create'
);
8
9
Route
:
:
post
(
'/save'
,
[
\
App
\
Http
\
Controllers
\
ProductController
::class
,
'save'
]
)
->
\
10
name
(
'save'
);
11
12
}
);
Perceba que agora isolei a parte products.
, referente ao apelido das rotas, após a definição do prefixo. Agora as rotas deste grupo além de receberem um prefixo, irão receber um apelido base que será concatenado com os apelidos de cada rota do grupo.
Os apelidos ficarão desta forma:
Esses serão os apelidos das rotas, o mesmo que seria anteriormente mas agora com o detalhe de termos organizado e isolado, centralizando o que era repetido, ou seja, o products.
.
Assim como o prefixo e o apelido podemos centralizar outras configurações nestes arquivos, como middlewares, controllers e muitas outras opções que veremos mais a frente aqui nesta jornada.
Já tivemos nosso primeiro contato com controllers anteriormente. Os controllers são parte importantíssima nesta organização, utilizando o Laravel.
Relembrando, de forma simples, os controllers são o ponto de delegação entre Model e View, recebendo a requisição e entregando para quem é de direito.
Utilizando o artisan podemos gerar, como já fizemos, os nossos controllers. Como fizemos, no capítulo passado, ao executarmos o comando abaixo:
1
php artisan make:controller HelloWorldController
Agora vamos explorar mais opções dentro deste comando e buscar mais produtividade na execução de nossos projetos.
Podemos gerar controllers com métodos para cada operação de CRUD e por meio de uma configuração de rota termos também, as rotas automáticas para cada um destes métodos.
Este tipo de controller chamamos de controller como recurso ou em muitos casos chamamos também de RESTFull Controllers. Vamos gerar e entender como são.
Em seu terminal na raiz do seu projeto execute o comando abaixo:
1
php artisan make:controller UserController --resource
A opção
--resource
pode ser chamada também como-r
Perceba agora que usei o mesmo comando para gerar um novo controller mas, caso acima, adicionei a opção --resource
que criará um controller com os seguintes métodos abaixo:
Perceba que quando iniciamos o controller como recurso, já temos um controller com os métodos acima definidos só no ponto para colocarmos nossas lógicas de inserção, leitura e etc.
Veja o controller gerado abaixo na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
;
4
5
use
Illuminate
\Http
\Request
;
6
7
class
UserController
extends
Controller
8
{
9
/**
10
*
Display
a
listing
of
the
resource
.
11
*
12
*
@
return
\Illuminate
\Http
\Response
13
*/
14
public
function
index
()
15
{
16
//
17
}
18
19
/**
20
*
Show
the
form
for
creating
a
new
resource
.
21
*
22
*
@
return
\Illuminate
\Http
\Response
23
*/
24
public
function
create
()
25
{
26
//
27
}
28
29
/**
30
*
Store
a
newly
created
resource
in
storage
.
31
*
32
*
@
param
\Illuminate
\Http
\Request
$
request
33
*
@
return
\Illuminate
\Http
\Response
34
*/
35
public
function
store
(
Request
$
request
)
36
{
37
//
38
}
39
40
/**
41
*
Display
the
specified
resource
.
42
*
43
*
@
param
int
$
id
44
*
@
return
\Illuminate
\Http
\Response
45
*/
46
public
function
show
(
$
id
)
47
{
48
//
49
}
50
51
/**
52
*
Show
the
form
for
editing
the
specified
resource
.
53
*
54
*
@
param
int
$
id
55
*
@
return
\Illuminate
\Http
\Response
56
*/
57
public
function
edit
(
$
id
)
58
{
59
//
60
}
61
62
/**
63
*
Update
the
specified
resource
in
storage
.
64
*
65
*
@
param
\Illuminate
\Http
\Request
$
request
66
*
@
param
int
$
id
67
*
@
return
\Illuminate
\Http
\Response
68
*/
69
public
function
update
(
Request
$
request
,
$
id
)
70
{
71
//
72
}
73
74
/**
75
*
Remove
the
specified
resource
from
storage
.
76
*
77
*
@
param
int
$
id
78
*
@
return
\Illuminate
\Http
\Response
79
*/
80
public
function
destroy
(
$
id
)
81
{
82
//
83
}
84
}
Agora com o controller como recurso gerado, podemos expor rotas para cada um dos métodos acima. Neste caso, para facilitar mais ainda nossas vidas, temos um método dentro do Route
chamado de resource
que já expõe para nós as rotas apontando para cada um dos métodos vistos acima, simplificando ainda mais as coisas.
Em seu arquivo de rotas web adicione a rota abaixo:
1
Route
::
resource
(
'/users'
,
\
App
\
Http
\
Controllers
\
UserController
::
class
);
Se nós debugarmos as rotas existentes em nosso projeto até o momento, podemos encontrar, focando no controller como recurso e em suas rotas, o resultado destacado abaixo na imagem.
Para debugarmos as rotas existentes no Laravel, definidas dentro de sua aplicação, basta executar na raiz do seu projeto o comando abaixo:
1
php artisan route:list
Obtendo a lista de rotas de sua aplicação. Veja o destaque nas rotas criadas pelo método resource
do Route
:
Além de termos gerado o controller com os métodos para serem utilizados dentro de um CRUD completo, temos também a criação das rotas para cada um dos métodos deste controller, por meio da chamada de apenas um método, o método resource
do Route
.
O método resource
recebe o nome da rota, tal qual um prefixo, como primeiro parâmetro e o nome do controller, gerado como recurso, no segundo parâmetro, simples como vimos.
Perceba que na imagem acima: temos as rotas, temos também apelidos para estas rotas e ainda temos rotas para cada um dos verbos HTTP.
Entenda abaixo:
/users
que aponta para o método index
do UserController
;users/create
que aponta para o método create do Usercontroller
que se refere a tela de exibição do form de criação. A rota users/store
aponta para o método store de UserController
e se refere ao salvar de fato os dados vindos do form de criação;users/{user}
(aponta para UserController@show
) ou users/{user}/edit
(aponta para UserController@edit
) via GET, o edit aqui seria exatamente para uma tela com form de edição;users/{user}
que aponta para o método update de UserController
.users/{user}
e aponta para o método destroy
do UserController
, indicando a remoção de um dado.Perceba acima ou pela imagem que temos rotas com nomes parecidos, em outros cenários a rota mais recente em ordem cima pra baixo seria a primeira a ser executada mas o que difere cada acesso é o verbo HTTP que também vemos na listagem do route:list
ou na lista abaixo eu mostro.
Melhorando a visualização e simplificando:
/users/
, verbo: GET
, controller@método: UserController@index
;/users/
, verbo: POST
, controller@método: UserController@store
;/users/create
, verbo: GET
, controller@método: UserController@create
;/users/{user}
, verbo: GET
, controller@método: UserController@show
;/users/{user}
, verbo: PUT ou PATCH
, controller@método: UserController@update
;/users/{user}
, verbo: DELETE
, controller@método: UserController@destroy
;/users/{user}/edit
, verbo: GET
, controller@método: UserController@edit
;Lembrando que você precisa adicionar sua lógica para cada um dos métodos, entretanto, todas as rotas já estão definidas como vimos acima e quando geramos o controller temos as definições dos métodos também.
Vimos aqui neste capítulo diversas possibilidades sobre as rotas de nossa aplicação e mais opções referentes aos nossos controllers. É claro, que temos mais opções, tanto dentro das rotas quanto na exploração dos controllers.
A cada capítulo vamos conhecendo mais opções para rotas, por exemplo a opção dos middlewares, quando conhecermos os conceitos envolvendo esta parte!
No próximo capítulo iremos falar sobre o envio de dados de um formulário para nosso backend e a manipulação das requests
e do nosso response
.
Até lá!
Neste capítulo vamos conhecer um ponto bem importante n criação das nossas aplicações, como manipular informações recebidas em nossas requests e as particularidades do response HTTP dentro do Laravel.
Aproveito também e já abordo essas requisições com dados vindos de um formulário, onde teremos pontos importantes a serem abordados neste aspecto.
Então vamos lá!
Para começar, vamos criar um formulário para envio dos dados para nosso controller. Para isso crie um arquivo chamado de create.blade.php
dentro da pasta de views, colocando ele dentro da pasta products
, então crie esta pasta também. O caminho completo até o arquivo ficará assim:
resources/views/products/create.blade.php
Veja o formulário abaixo a ser adicionado no create.blade.php
:
1
<form
action=
"
{{
route
(
'products.store'
)
}}
"
method=
"post"
>
2
3
<div
class=
"w-full mb-6"
>
4
<label
class=
"block mb-2"
>
Nome</label>
5
<input
type=
"text"
name=
"name"
class=
"w-full p-2 rounded"
>
6
</div>
7
8
<div
class=
"w-full mb-6"
>
9
<label>
Descrição</label>
10
<input
type=
"text"
name=
"description"
class=
"w-full p-2 rounded"
>
11
</div>
12
13
<div
class=
"w-full mb-6"
>
14
<label>
Conteúdo</label>
15
<textarea
name=
"body"
id=
""
cols=
"30"
rows=
"10"
class=
"w-full p-2 rounded"
>
\
16
</textarea>
17
</div>
18
19
<div
class=
"w-full mb-6"
>
20
<label>
Preço</label>
21
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded"
>
22
</div>
23
24
<div
class=
"w-full mb-6"
>
25
<label>
Slug</label>
26
<input
type=
"text"
name=
"slug"
class=
"w-full p-2 rounded"
>
27
</div>
28
29
<button
class=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
30
font-thin"
>
Criar Produto</button>
31
</form>
No formulário temos 5 campos diretamente, são eles:
Obs.: Utilizei a estrutrua do form pensando no TailwindCSS, mais a frente no livro vamos configurar esse framework css em nosso projeto. O TailwindCSS é um framework css pensado mais no utilitário, ou seja, classes que representam o estilo aplicado. Nele você também pode componentizar como Bootstrap por exemplo e pra isso você mesmo precisa criar seus código pra reutilização.
Nosso formulário trabalhará com envio dos dados via método POST
e já adicionamos, na action do formulário, a chamada de uma rota pelo seu apelido, a products.store
. Vamos gerar nosso controller e nossas rotas para que possamos fazer esse form funcionar.
Gere o controller a partir do seu terminal, com o comando abaixo:
1
php artisan make:controller Admin/ProductController
Perceba mais um aprendizado acima, como coloquei Admin/
antes do nome do nosso controller, ele será criado dentro da pasta Admin
dentro da pasta de controllers. Isso simplifica também nossas gerações, onde podemos especificar pastas a dentro para uma melhor organização dos controllers.
Para este exemplo não vou gerar este controller como recurso porque nosso foco ainda não é gerarmos um crud e sim conhecermos esse trabalho entre as requisições e o envio de dados.
Uma vez criado o controller, adicione os dois métodos abaixo nele:
1
public
function
create
()
2
{
3
return
view
(
'
products.create
'
)
;
4
}
e também:
1
public function store(Request $request)
2
{
3
4
}
O primeiro método será para exibição de nossa view do formulário e o segundo para manipularmos o que for enviado do formulário de criação. Este segundo método veremos mais adiante seu conteúdo.
Até o momento nosso controller fica desta maneira:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
Illuminate
\Http
\Request
;
6
use
App
\Http
\Controllers
\Controller
;
7
8
class
ProductController
extends
Controller
9
{
10
public
function
create
()
11
{
12
return
view
(
'products.create'
);
13
}
14
15
public
function
store
(
Request
$
request
)
16
{
17
18
}
19
}
View criada, controller e métodos definidos vamos adicionar no arquivo de rotas web.php
nossas definições de rota para este trabalho. Vamos lá!
Veja as rotas adicionadas abaixo:
1
Route
::
prefix
(
'admin'
)
-
>
group
(
function
()
{
2
3
Route
:
:
prefix
(
'products'
)
->
name
(
'products.'
)
->
group
(
function
()
{
4
5
Route
::
get
(
'/create'
,
[
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::class
,
'crea
\
6
te'
]
)
-
>
name
(
'create'
)
;
7
8
Route
::
post
(
'/store'
,
[
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::class
,
'stor
\
9
e'
]
)
-
>
name
(
'store'
)
;
10
11
}
);
12
13
}
);
Perceba acima que isolei o prefixo admin para o grupo de rotas que contêm o nossas rotas de product
. E também isolei o prefixo products, bem como o seu apelido products., para o conjunto de rotas pensadas para este capítulo.
No fim ao acessarmos /admin/products/create veremos nosso formulário, ainda sem estilo, carregado em nossa tela. E também ao enviarmos nosso form ele mandará os dados para nossa rota /admin/products/store.
Com nosso servidor ligado, através do comando php artisan serve
podemos acessar nosso link http://127.0.0.1:8000/admin/products/create chegando ao resultado da imagem abaixo:
Feito isso vamos conhecer o request!
Vamos começar este trecho tentando enviar qualquer dado para o nosso backend a partir do formulário. Podemos enviar até o form vazio mesmo, então vamos clicar em Criar Produto, enviando nosso formulário.
Se você recebeu a tela de expirado abaixo, não se preocupe! rsrsrsrsrsr!
Por que isso aconteceu? Perceba, pela sua barra de endereços, que ele enviou a requisição para a rota correta: /admin/products/store, então porque recebemos uma tela de requisição expirada?
Quando enviamos dados via POST, PUT ou PATCH o Laravel faz uma validação na requisição para evitar que fontes externas enviem dados ou falsifiquem nossa requisição. Esse controle é chamado de CSRF (Cross Site Request Forgery).
Obs.: O CSRF não é algo exclusivo do Laravel, é um tópico de segurança e recomendo fortemente a leitura sobre o assunto.
Agora como podemos adicionar este controle de segurança em nosso formulário? Vamos lá então, adicione após a abertura da tag form o seguinte input abaixo:
1
<input type="hidden" name="_token" value="
{{
csrf_token
()
}}
">
Acima estamos enviando o token csrf, para validar a procedência e envio dos dados do nosso formulário, através da requisição. Feito isso, volte para a página do formulário atualize e tente enviar novamente.
Agora você verá tudo branco e a página de expirado já não existe mais, como não temos nada definido no método store
, lá no controller, o resultado será mesmo uma página em branco mas nossa requisição post, vinda do formulário, já bate na execução do método corretamente.
O input acima, com o token csrf, pode ser substituido completamente pelo:
1
{{
csrf_field
()
}}
Que adicionará o input completo como fizemos na mão anteriormente. Ou ainda podemos simplificar mais, utilizando uma diretiva disponível do blade, segue abaixo:
1
@csrf
Que também adiciona o input como fizemos anteriormente. O formulário agora fica desta forma:
1
<form
action=
"
{{
route
(
'products.store'
)
}}
"
method=
"post"
>
2
@csrf
3
4
<div
class=
"w-full mb-6"
>
5
<label
class=
"block mb-2"
>
Nome</label>
6
<input
type=
"text"
name=
"name"
class=
"w-full p-2 rounded"
>
7
</div>
8
9
<div
class=
"w-full mb-6"
>
10
<label>
Descrição</label>
11
<input
type=
"text"
name=
"description"
class=
"w-full p-2 rounded"
>
12
</div>
13
14
<div
class=
"w-full mb-6"
>
15
<label>
Conteúdo</label>
16
<textarea
name=
"body"
id=
""
cols=
"30"
rows=
"10"
class=
"w-full p-2 rounded"
>
<
\
17
/textarea>
18
</div>
19
20
<div
class=
"w-full mb-6"
>
21
<label>
Preço</label>
22
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded"
>
23
</div>
24
25
<div
class=
"w-full mb-6"
>
26
<label>
Slug</label>
27
<input
type=
"text"
name=
"slug"
class=
"w-full p-2 rounded"
>
28
</div>
29
30
<button
class=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
31
font-thin"
>
Criar Produto</button>
32
</form>
Agora que nosso form já está ‘funcionando’ e nossa requisição está chegando no método como podemos manipular os dados dentro do método store
?
Se voltarmos ao método store
do ProductController
, vamos perceber que em sua assinatura temos a definição de um parâmetro.
1
public function store(Request $request)
2
{
3
4
}
O parâmetro $request
, e seu tipo esperado é um objeto Request
do próprio Laravel. O objeto Request
vêm do namespace Illuminate\Http\Request
e ele é automaticamente resolvido pelo container de injeção de dependências do Laravel, mais a frente falaremos sobre esse container.
Com isso, por meio do parâmetro $request
temos a possibilidade de acesso, das mais variadas formas, aos dados enviados na requisição para nosso método, e em nosso caso, vindos do formulário.
Vamos ver um panorama geral dos dados enviados. Adicione o seguinte trecho dentro do método store
:
1
dd
(
$
request
->
all
());
Acima temos contato com mais uma função helper do Laravel, o dd
que simplesmente faz uma dump mais customizado dos dados informados a ele e em seguida joga um die travando a continuação da execução do código. Por isso o dd
ou dump and die
.
Continuando…
Preencha dados em seu formulário e envie novamente a requisição. Veja o resultado:
O resultado acima são todos os campos vindos do nosso formulário, o resultado do dump é o que temos acima mas os dados estão sendo resgatados por meio do método all
do objeto Request
, que traz todos os dados enviados por meio desta requisição tal qual pegassemos diretamente do nosso conhecido $_POST
.
Você pode acessar os campos diretamente, por exemplo, se você quiser acessar o nome do produto, enviado do formulário, você pode acessar das seguintes maneiras:
1
dd
(
$
request
->
get
(
'name'
));
ou
1
dd
(
$
request
->
name
);
ou ainda
1
dd
(
$
request
->
input
(
'name'
));
Isso vale para cada campo que você envia e que deseja recuperar o valor.
Obs.: Só uma observação, e frisando novamente, estou usando o
dd
aqui apenas para debug. Mais a frente vamos concluir esta etapa de manipulação, como por exemplo, pegar estes valores e salvar no banco de dados.
Podemos verificar ainda se determinado campo, parâmetro ou input foi informado em nossa requisição. Para isso podemos utilizar o método has
do objeto request.
Veja abaixo:
1
if
(
$
request
->
has
(
'name'
))
{
2
dd
(
$
request
->
name
);
3
}
Com isso podemos realizar determinadas operações para a existência ou não existência dos campos. Se você deseja verificar a existência de vários inputs, você pode utilizar o método hasAny
, veja abaixo:
1
if
(
$
request
->
hasAny
([
'name'
,
'body'
,
'slug'
]))
{
2
dd
(
$
request
->
name
);
3
}
Podemos recuperar x inputs se precisarmos. Por exemplo, se você quiser recuperar apenas o campo name
e o campo slug
da request:
1
$
request
->
only
([
'name'
,
'slug'
]);
Desta maneira vamos receber um array com os dois campos informados e seus respectivos valores.
Caso você queira ignorar campos específicos, também é possivel, por exemplo:
1
$
request
->
except
([
'name'
]);
Neste caso receberemos um array com todos os campos e somente o campo name
não estará presente neste array. São método bem úteis quando precisamos destes comportamentos.
Parâmetros de url, as famosas query strings, são parâmetros informados em nossa url sempre após a ?
e respeitando chave=valor
e quando temos mais de um parâmetro são concatenados pelo &
. A querystring é a forma que a requisição envia os dados mantendo seu estado na própria URL com já conhecemos quando estudamos no PHP Básico.
Por exemplo, como eu poderia recuperar o parâmetro search
da seguinte url http://127.0.0.1:8000?search=teste
?
Para acessarmos seu valor, nós podemos utilizar o método query
para recuperar este input exclusivo vindo da url.
Por exemplo:
1
$
request
->
query
(
'search'
);
Este método, o query
, assim como o get
e o input
, aceita um segundo parâmetro pro caso da não existência do parâmetro ou input solicitado. Podemos definir um valor default que será carregado caso não tenhamos valor no acesso em questão.
Veja abaixo:
1
$
request
->
query
(
'search'
,
'este valor será retornado caso não tenhamos o parâmetro s\
2
earch na query string'
);
3
4
//Pro input e pro get
5
6
$
request
->
get
(
'title'
,
'este valor será retornado caso não tenhamos o parâmetro titl\
7
e na requisição'
);
8
9
$
request
->
input
(
'title'
,
'este valor será retornado caso não tenhamos o parâmetro ti\
10
tle na requisição'
);
PS.: por padrão esse valor do segundo parâmetro é NULL então você também pode testar direto por seu retorno se o dado veio ou não com valor. Caso queira verificar se tal chave existe aí recorremos ao has como vismo anteriormente.
Dentro das linguagens Web é importante entendermos o protocolo HTTP e a base dessa arquitetura que é a Web, onde trabalhamos com Requests & Responses e no meio temos os processamentos no backend, seja lá qual for a linguagem.
Manipulamos nossa requisição e de certa forma realizamos processamentos até o momento, agora precisamos conhecer a manipulação das responses ou respostas HTTP.
É claro que abordarei aqui dentro da estrutura do framework mas já deixo uma recomendação de, se caso você não tenha conhecimento básico do http pelo menos, procure mais informações sobre o assunto para entender melhor a arquitetura da web, isso vai te abrir muito a mente.
Então, como podemos manipular esse tal response ou respostas http?
Podemos manipular as respostas http em nossos controllers ou funções anônimas de nossas rotas, utilizando a função helper response
.
Veja abaixo uma utilização simples:
1
return
response
(
'
Retornando uma resposta
'
, 200
)
;
Acima retornei uma resposta com o conteúdo Retornando uma resposta
e o status code http 200, que se refere a status de sucesso.
Podemos definir cabeçalhos HTTP em nossa resposta também. Por exemplo, posso dizer que o tipo da minha resposta é um json da seguinte maneira:
1
return
response
([
'msg'
=>
'Retorno do tipo json'
],
200
)
2
->
header
(
'Content-Type'
,
'application/json'
);
Agora o tipo da resposta, que por default seria do tipo ‘text/html’, será do tipo ‘application/json’ por meio da manipulação do cabeçalho http Content-Type
e informando o mime-type por meio deste cabeçalho, em nosso caso, colocando o tipo para json: application/json
.
Podemos ainda manipular cookies e enviar quantos cabeçalhos forem necessários por meio do objeto response. Por exemplo, se você quiser retornar um cookie em uma determinada resposta, basta utilizar como abaixo:
1
return
response
([
'msg'
=>
'Retorno do tipo json'
],
200
)
2
->
cookie
(
'nome_cookie'
,
'valor_cookie'
,
'tempo_em_minutos_de_validade_do\
3
_cookie'
);
Dentro desta manipulação do response, que têm a ver com o retorno de nossas rotas (quer seja em função anônima quer seja no método de um controller), precisamos abordar, também, sobre redirecionamentos.
Em alguns momentos vamos precisar apenas retornar um redirecionamento pós realização de uma determinada execução. Para isso temos a função helper redirect
:
1
return
redirect
(
'
/
'
)
;
Acima creio que está bem intuitivo mas não custa comentar.
Após um determinado processo posso redirecionar o usuário para uma determinada url, acima redireciono ele para a página inicial do nosso website mas podemos melhorar ainda mais esses redirecionamentos. No trecho acima redirecionei ele para uma url chamando o nome real da rota, entretanto, eu te disse anteriormente que é melhor trabalharmos com os apelidos da rota ao invés do seu nome real, correto?
Correto, então não se preocupe!
Podemos redirecionar o usuário para a rota desejada por meio do apelido desta rota.
Veja abaixo:
1
return
redirect
()
->
route
(
'home'
);
Chamo a função redirect sem parâmetros, com isso temos o retorno do objeto \Illuminate\Routing\Redirector
e com isso posso ter acesso ao método route
, que inclusive já utilizamos aqui no livro. Agora basta que eu informe o apelido da rota desejada, e se essa rota possuir parâmetros dinâmicos basta informar em um array no segundo parâmetro do route
como já vimos anteriormente também.
Podemos utilizar também um redirect para o estado anterior de uma requisição, isso é perfeito para momentos de erro no processamento de determinado acesso ou envio de dados. Por exemplo, quando enviamos os dados de um formulário para o backend e temos algum erro de validação nos dados.
Para isso temos a função helper back
.
Por exemplo:
1
return
back
()
;
Que retornará o usuário para o estado anterior da requisição assim como o botão back do browser ou melhor simulando esse comportamento.
Geralmente usamos essa função back
para momentos de erro de processo ou validações, e nestes casos você precisa voltar o usuário pro momento anterior, caso o usuário tenha mandado dados de um formulário podemos retornar os campos já digitados por ele também, como podemos ver abaixo:
1
return
back
()
->
withInput
();
Acima além de voltarmos pro estado da requisição anterior, estamos mandando de volta os inputs digitados. Para manipularmos lá na view, no form e exibir os valores vindos do withInput
podemos usar a função helper old
nos inputs do nosso formulário.
Lembra do nosso formulário? Olha como ele fica após adicionarmos essa possibilidade de pegar o valor novamente dos campos digitados anteriormente pelo usuário:
1
<form
action=
"
{{
route
(
'products.store'
)
}}
"
method=
"post"
>
2
@csrf
3
4
<div
class=
"w-full mb-6"
>
5
<label
class=
"block mb-2"
>
Nome</label>
6
<input
type=
"text"
name=
"name"
class=
"w-full p-2 rounded"
value=
"
{{
old
(
'nam\
7
e'
)
}}
"
>
8
</div>
9
10
<div
class=
"w-full mb-6"
>
11
<label>
Descrição</label>
12
<input
type=
"text"
name=
"description"
class=
"w-full p-2 rounded"
value=
"
{{
o
\
13
ld
(
'description'
)
}}
"
>
14
</div>
15
16
<div
class=
"w-full mb-6"
>
17
<label>
Conteúdo</label>
18
<textarea
name=
"body"
id=
""
cols=
"30"
rows=
"10"
class=
"w-full p-2 rounded"
>
{\
19
{ old('body') }}</textarea>
20
</div>
21
22
<div
class=
"w-full mb-6"
>
23
<label>
Preço</label>
24
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded"
value=
"
{{
old
(
'pr\
25
ice'
)
}}
"
>
26
</div>
27
28
<div
class=
"w-full mb-6"
>
29
<label>
Slug</label>
30
<input
type=
"text"
name=
"slug"
class=
"w-full p-2 rounded"
value=
"
{{
old
(
'slu\
31
g'
)
}}
"
>
32
</div>
33
34
<button
class=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
35
font-thin"
>
Criar Produto</button>
36
</form>
Perceba no textarea
e nos atributos value de cada input, onde temos agora o print do retorno da função helper old
. Nesta função helper informamos o nome do campo que queremos recuperar o valor digitado anteriormente.
Caso aquele campo em questão não esteja na requisição, adicionado pelo withInput
, o campo simplesmente fica em branco e o usuário preenche tranquilamente ou altera, se necessitar, o valor previamente digitado.
Isso é perfeito para o usuário, que não precisará digitar tudo novamente caso tenha algum problema no backend e nós se precisarmos retornar ele pro estado anterior pós uma validação não satisfeita por exemplo.
Neste capítulo trabalhamos com o envio de dados de um formulário para uma determinada rota e aprendemos também a manipular estes valores além de conhecermos mais detalhes sobre o objeto Request e Response.
Vimos ainda, particularidades sobre as repostas HTTP nos resultados de nossos processos, além de conhecermos as possibilidades de redirecionamento que estão diretamente atrelados aos responses uma vez que os redirecionamentos também são uma execução pós-processos.
Agora que já entendemos essas manipulações, vamos conhecer como o Laravel trabalha com banco de dados e como podemos manipular a parte do Model fechando o ciclo MVC dentro do nosso livro.
Nos vemos no próximo capítulo.
Neste capítulo vamos entrar na área, provavelmente a mais esperada, do banco de dados. Conhecendo como o Laravel nos permite realizar a persistência dos dados em nossas aplicações web.
Preciso ressaltar que temos algumas camadas dentro do framework quando tratamos de banco de dados, uma delas é a que vamos abordar neste capítulo, a camada mais baixa, a camada mais estrutural composta por: migrations
, seeds
& model factories
.
Vamos entender o que cada ponto têm de importante aqui.
Comecemos pelas migrations.
As migrations ou migrações são ferramentas que nos auxiliam no versionamento da estrutura de nossas bases de dados. Funciona basicamente como uma documentação da linha histórica com respeito ao crescrimento estrutural do banco, criação das tabelas, ligações e etc.
Vale ressaltar que o conceito migrações não é algo do Laravel mas que ele se utiliza para trazer mais essa opção e facilidade para quem está desenvolvendo. Criar tabelas e seus aparatos se torna mais fácil quando realizamos isso do ponto de vista de código, que após um comando via artisan é traduzido para o banco em questão.
As migrations dentro do nosso projeto podem ser encontradas dentro da pasta database/migrations
. Nesta pasta você já vai encontrar alguns arquivos de migração iniciais, como a migração para a tabela de usuários dentre outras migrações referentes a tabela de jobs(usada para a parte de filas) e algumas a mais.
Vamos da uma olhada na migration da tabela de usuários e entender como é formado um arquivo de migração.
Veja abaixo o arquivo 0001_01_01_000000_create_users_table.php
:
1
<
?
php
2
3
use
Illuminate
\Database
\Migrations
\Migration
;
4
use
Illuminate
\Database
\Schema
\Blueprint
;
5
use
Illuminate
\Support
\Facades
\Schema
;
6
7
return
new
class
extends
Migration
8
{
9
/**
10
*
Run
the
migrations
.
11
*/
12
public
function
up
():
void
13
{
14
Schema
::
create
(
'users'
,
function
(
Blueprint
$
table
)
{
15
$
table
->
id
();
16
$
table
->
string
(
'name'
);
17
$
table
->
string
(
'email'
)
->
unique
();
18
$
table
->
timestamp
(
'email_verified_at'
)
->
nullable
();
19
$
table
->
string
(
'password'
);
20
$
table
->
rememberToken
();
21
$
table
->
timestamps
();
22
});
23
24
Schema
::
create
(
'password_reset_tokens'
,
function
(
Blueprint
$
table
)
{
25
$
table
->
string
(
'email'
)
->
primary
();
26
$
table
->
string
(
'token'
);
27
$
table
->
timestamp
(
'created_at'
)
->
nullable
();
28
});
29
30
Schema
::
create
(
'sessions'
,
function
(
Blueprint
$
table
)
{
31
$
table
->
string
(
'id'
)
->
primary
();
32
$
table
->
foreignId
(
'user_id'
)
->
nullable
()
->
index
();
33
$
table
->
string
(
'ip_address'
,
45
)
->
nullable
();
34
$
table
->
text
(
'user_agent'
)
->
nullable
();
35
$
table
->
longText
(
'payload'
);
36
$
table
->
integer
(
'last_activity'
)
->
index
();
37
});
38
}
39
40
/**
41
*
Reverse
the
migrations
.
42
*/
43
public
function
down
():
void
44
{
45
Schema
::
dropIfExists
(
'users'
);
46
Schema
::
dropIfExists
(
'password_reset_tokens'
);
47
Schema
::
dropIfExists
(
'sessions'
);
48
}
49
};
Perceba acima que a classe de migração, inicialmente extende de uma classe base chamada Migration e traz a definição de dois métodos, o método up
e o método down
.
O método up
será chamado quando essa migração for executada em nosso banco de dados. E o método down
contêm a definição do inverso do método up
, no método down
nós definimos a remoção do que foi aplicado no método up
, isso nos permite voltarmos para estados anteriores pré-execução do último lote de migrações.
Vamos da uma atenção ao método up
, focando no trecho da criação da tabela users
:
1
public
function
up
()
2
{
3
Schema
:
:
create
(
'users'
,
function
(
Blueprint
$
table
)
{
4
$
table
->
id
();
5
$table->string('name')
;
6
$table->string('email')->unique()
;
7
$table->timestamp('email_verified_at')->nullable()
;
8
$table->string('password')
;
9
$table->rememberToken()
;
10
$table->timestamps()
;
11
}
);
12
13
//...
14
}
Temos uma classe base responsável pela definição dos schemas (Schema) da base de dados e é com ela que criamos nossa tabela por meio do método create
, informando como primeiro parâmetro o nome da tabela e o segundo parâmetro será um callback (ou função anônima) onde definiremos os campos e estrutura da tabela em questão, no trecho acima a tabela users
.
Para definir os campos de nossa tabela precisamos do objeto Blueprint
, que nos permite criar os campos e tipos por meio de seus métodos, por isso tipamos o parâmetro $table
, do callback, como Blueprint
.
O Blueprint
contêm métodos para tudo o que é necessário de manipulação de nossa tabela como: geração e remoção de colunas, os mais variados tipos de dados para as colunas em questão, definição de chaves estrangeiras, criação de índices e muito mais.
Podemos criar nossas chaves primárias e auto-incrementáveis utilizando o método id
(método este que já vêm na migration, quando fazemos a criação do arquivo pelo artisan), como temos na linha abaixo:
1
$
table
->
id
();
Podemos definir campos do tipo string (Varchar), como ocorre abaixo:
1
$
table
->
string
(
'name'
);
2
$
table
->
string
(
'email'
)
->
unique
();
Acima temos a definição de dois campos Varchar e por default recebe 255 caracteres, caso queira especificar um tamanho para o campo, basta preencher o segundo parâmetro com o valor inteiro, correspondente ao tamanho do campo desejado.
Perceba também, que para o e-mail atribuimos uma definição na coluna, assinalando este campo como unique
ou seja evitando a duplicação de linhas com o mesmo email, tornando assim, a partir da base, o usuário único por email.
Podemos definir campos de data e hora por meio do método timestamp, veja abaixo:
1
$
table
->
timestamp
(
'email_verified_at'
)
->
nullable
();
O campo email_verified_at
tabém permitirá nulo, isso definido pelo método nullable.
Temos também a definição de mais um campo tipo VARCHAR para a coluna password
:
1
$
table
->
string
(
'password'
);
E por fim temos a chamada do método rememberToken()
e também do timestamps()
, o que são esses métodos ou que eles fazem?
1
$
table
->
rememberToken
();
2
$
table
->
timestamps
();
O método rememberToken
irá criar uma coluna chamada de remember_token, varchar com tamanho 100 e aceitando o valor nulo. Já o método timestamps
irá criar dois campos do tipo timestamp, um chamado de created_at
e outro chamada de updated_at
, ambos para representarem a data de criação e atualização do dado.
Um ponto interessante é que o Laravel controla os valores destes dois campos(created_at, updated_at), para data de criação e atualização de registros, automaticamente via Models.
Se o método up
define a criacão da tabela de users
(usuários) o down
define a remoção desta tabela. Veja como está a definição do método down
:
1
public
function
down
()
2
{
3
Schema
:
:
dropIfExists
(
'users'
);
4
Schema
:
:
dropIfExists
(
'password_reset_tokens'
);
5
Schema
:
:
dropIfExists
(
'sessions'
);
6
}
O método dropIfExists
, do objeto Schema
, permite a remoção da tabela da base se ela existir, conretizando o inverso do método up
. Isso facilita pois podemos voltar um passo anterior, na linha de execução das migrations, desfazendo alterações propostas pela migração em questão.
Desde o Laravel 11 a migração para a tabela de users vêm com as definições para mais duas tabelas, como é muito tranquilo entender a estrutura dessas migrations, e claro, aqui espero que você entenda bem do banco de dados, deixo pra você olhar as outras definições existentes neste arquivo de migração atual.
Agora como pegamos esse código Orientado a Objetos e jogamos para uma base relacional? É o que vamo ver a seguir.
Para executarmos as migrações existentes no projeto podemos utilizar o comando: php artisan migrate
.
Este comando sempre vai executar as migrações mais novas na base de dados, as migrações que já foram executadas são registradas na tabela migrations. A tabela migrations é criada pelo Laravel justamente para controlar quais arquivos de migração foram executados.
Aqui te lembro que quando iniciamos o projeto as migrações já foram executadas, ou seja, essas migrações padrões vindas no framework já estão executadas na nossa base de dados (dê uma olhada na tua base depois). O nome da tua base é o nome que você deu pro seu projeto e o Laravel também cria essa base automaticamente baseada nesse nome, lembrando também, que as variáveis de acesso ao banco estão no arquivo .env.
Veja abaixo como está no projeto atualmente:
1
#Par
âmetros
dentro
do
arquivo
.env
2
3
DB_CONNECTION
=
mysql
4
DB_HOST
=
127
.0
.0
.1
5
DB_PORT
=
3306
6
DB_DATABASE
=
store_app
7
DB_USERNAME
=
root
8
DB_PASSWORD
=
Caso você queira utilizar outro banco de dados, que não mysql, será necessário alterar o drive na variável DB_CONNECTION
.
Por exemplo, temos os seguintes drives disponiveis por padrão:
Ao alterar seu drive você pode rodar o php artisan migrate
novamente e caso teu banco não exista o Laravel perguntará se você deseja que ele crie e assim as migration serão executadas.
Sempre que formos rodar as migrações em nosso projeto, e repetindo, o comando será:
1
php artisan migrate
Veja a imagem abaixo, ele é da versõa anterior do livro mas representa bem o resultado de migratons executadas na base. O log de sucesso será sempre neste caminho:
Se você for na sua base de dados, encontrará a tabela migrations como comentei anteriormente. Nesta tabela está registrada as migrations já executadas em nosso projeto:
E é claro as tabelas também foram criadas e estão em nosso banco agora e o número de tabelas é maior pois o Laravel resumiu várias execuções por arquivo e não mais um arquivo por criação de tabela, como é visto na própria migration de users.
Mas, você me pergunta, como posso criar minhas migrações para tabelas do meu projeto? Certo, vamos fazer isso agora!
Primeiro passo é irmos ao nosso terminal e executarmos o comando para geração de nosso primeiro arquivo de migração:
1
php artisan make:migration create_products_table
O comando acima criará nosso primeiro arquivo de migração dentro da pasta de migrações, chamado de 2025_03_25_150613_create_products_table.php
, o nome do arquivo de migração respeita a data de criação e o nome escolhido, em nosso caso: create_products_table
.
Essa definição da data e timestamp, do momento de geração desta migration, permite o Laravel organizar a ordem das migrações e a linha em que elas devem ser executadas, no caso, sempre da mais antiga para a mais nova/recém criada.
Um ponto interessante é que, como colocamos o nome da migração como create_products_table, o Laravel automaticamente preenche o conteúdo dos métodos up e down, da migration, e adiciona algumas colunas base para a tabela, como id e a definição para as colunas created_at e updated_at.
Isso facilita muito nossa vida durante o desenvolvimento. Veja abaixo:
1
<
?
php
2
3
use
Illuminate
\Database
\Migrations
\Migration
;
4
use
Illuminate
\Database
\Schema
\Blueprint
;
5
use
Illuminate
\Support
\Facades
\Schema
;
6
7
return
new
class
extends
Migration
8
{
9
/**
10
*
Run
the
migrations
.
11
*/
12
public
function
up
():
void
13
{
14
Schema
::
create
(
'products'
,
function
(
Blueprint
$
table
)
{
15
$
table
->
id
();
16
$
table
->
timestamps
();
17
});
18
}
19
20
/**
21
*
Reverse
the
migrations
.
22
*/
23
public
function
down
():
void
24
{
25
Schema
::
dropIfExists
(
'products'
);
26
}
27
};
Agora vamos as nossas adições, a adição dos nossos campos para nossa tabela de produtos. Criaremos os seguintes campos:
Veja abaixo o código a ser adicionado após o método id, na migração:
1
$
table
->
string
(
'name'
);
2
$
table
->
string
(
'description'
);
3
$
table
->
text
(
'body'
);
4
$
table
->
integer
(
'price'
);
5
$
table
->
integer
(
'in_stock'
);
6
$
table
->
string
(
'slug'
);
7
$
table
->
boolean
(
'is_active'
);
Simples, acima realizamos as definições dos nossos campos. Agora estamos aptos a executar esta migração em nosso banco de dados, para isso vamos ao nosso terminal executar o comando que já conhecemos.
Veja abaixo:
1
php artisan migrate
Ao executarmos o comando acima novamente, o Laravel só executará as migrações que ainda não foram executadas, por isso que temos o registro na tabela migrations feita automaticamente pelo framework. Veremos que apenas a migração para a tabela de produtos é que será executada desta vez.
Veja o resultado:
Este processo de trabalho com migrations será parte do seu desenvolvimento e mais a frente dedicaremos um capítulo aos relacionamentos na base e dentro dos models (que ainda veremos também).
É importante salientar que aqui na camada de banco é importante que você entenda bem desta parte para lhe dá melhor no que se trata a tua base de dados e as facilidades que o framework traz para você.
O Laravel possui definições, a partir do Blueprint, de tudo que é necessário para o mapeamento dos tipos das tuas colunas, configuração de CONSTRAINTS e muito mais. Lembrando que pro processo reverso também temos métodos especificos para remover chaves estrangeiras, remover colunas etc, ou seja, nas migrações temos o aparato necessário para a tratativa estutural do nosso banco de dados.
Por hora vamos aos seeds e as factories.
Quando estamos em nosso ambiente de desenvolvimento sempre precisamos de dados no banco para testes e como sabemos, isso é um saco durante o processo de desenvolvimento. Os seeds nos permitem automatizar este processo simplificando nosso trabalho. Podemos, por meio das seeds, alimentar nosso banco de dados com informações para que possamos testar nossos CRUDs por exemplo.
Você pode encontrar os seeds do sistema dentro da pasta database/seeders
, dentro desta pasta você vai encontrar o arquivo DatabaseSeeder.php
que é o arquivo principal que executa todas as outras seeds que tivermos nesta pasta ou pode concentrar nele as execuções diretamente.
Veja seu conteúdo atual do arquivo:
1
<
?
php
2
3
namespace
Database
\Seeders
;
4
5
use
App
\Models
\User
;
6
//
use
Illuminate
\Database
\Console
\Seeds
\WithoutModelEvents
;
7
use
Illuminate
\Database
\Seeder
;
8
9
class
DatabaseSeeder
extends
Seeder
10
{
11
/**
12
*
Seed
the
application
's database.
13
*/
14
public
function
run
():
void
15
{
16
//
User
::
factory
(
10
)
->
create
();
17
18
User
::
factory
()
->
create
([
19
'name'
=>
'Test User'
,
20
'email'
=>
'test@example.com'
,
21
]);
22
}
23
}
Perceba a linha comentada //User::factory(10)->create();
: esta linha é um exemplo para a chamada de uma factory, que nos permite gerar dados fakes em massa.
Já o trecho abaixo, que também é uma chamada de factory:
1
User
::
factory
()
->
create
(
[
2
'name' => 'Test User',
3
'email' => 'test@example.com',
4
]
);
Cria apenas um usuário, baseado na classe factory UserFactory
da pasta database/factories
, diretamente na base de dados, claro preciso executar o comando para rodar a seed e veremos em breve.
Vamos criar dois arquivos de seed para este momento, primeiro o arquivo que já temos referenciado acima, o UsersTableSeeder
e também vamos criar o ProductsTableSeeder
.
Tá! Mas como podemos criar isso? Eu te mostro, é bem simples!
Execute os comandos abaixo, primeiro executando a geração de um, depois do outro:
1
php artisan make:seeder UsersTableSeeder
Após a execução acima, execute a criação do outro arquivo seeder:
1
php artisan make:seeder ProductsTableSeeder
Veja o resultado:
Após a criação, veja que temos dois arquivos, a mais, criados dentro da pasta de seeders, o UsersTableSeeder.php
e também o ProductsTableSeeder.php
.
Agora o que faremos com eles?
Podemos utilizar ambos os arquivos de seed para gerarmos 1 usuário e 1 produto para nossa base. Como fazer isso?
Abra o arquivo UsersTableSeeder.php
e adicione o código abaixo no método run
:
1
DB
:
:
table
(
'users'
)
->
insert
(
[
2
'name' => 'Primeiro Usuário',
3
'email' => 'email@email.com',
4
'password' => bcrypt('secret')
5
]
);
PS.: Não esqueça de add o namespace pro DB:
use Illuminate\Support\Facades\DB;
Acima temos o primeiro contato com o objeto DB
, que nos permite a execução de queries SQL no mais baixo nível, em relação a parte do ORM que veremos mais a frente. Nele informo a tabela que quero realizar a inserção do dado, por meio do método table
e logo após, aninhando, chamo o método insert
informando um array associativo, respeitando as colunas como chave e seus valores a serem salvos.
Perceba que no campo de senha do usuário utilizo o helper bcrypt para encryptar a senha do nosso usuário.
Após isso adicione o conteúdo do método run
do ProductsTableSeeder
:
1
DB
::
table
(
'products'
)
-
>
insert
(
[
2
'name'
=>
'Primeiro Produto'
,
3
'description'
=>
'Produto teste com seeds'
,
4
'body'
=>
'Conteúdo do Produto'
,
5
'in_stock'
=>
34
,
6
'price'
=>
(
14.99
*
100
),
7
'is_active'
=>
1
,
8
'slug'
=>
'primeira-produto'
9
]
);
Agora vamos voltar lá no DatabaseSeeder
e adicionar a chamada para estas duas classes Seed, que acabamos de criar e definir: UsersTableSeeder
& ProductsTableSeeder
.
O conteúdo do arquivo DatabaseSeeder
ficará assim, veja ele todo na íntegra abaixo:
1
<
?
php
2
3
namespace
Database
\Seeders
;
4
5
use
App
\Models
\User
;
6
//
use
Illuminate
\Database
\Console
\Seeds
\WithoutModelEvents
;
7
use
Illuminate
\Database
\Seeder
;
8
9
class
DatabaseSeeder
extends
Seeder
10
{
11
/**
12
*
Seed
the
application
's database.
13
*/
14
public
function
run
():
void
15
{
16
//
User
::
factory
(
10
)
->
create
();
17
18
//
User
::
factory
()
->
create
([
19
//
'name'
=>
'Test User'
,
20
//
'email'
=>
'test@example.com'
,
21
//
]);
22
23
//
Chamada
dos
nosso
seeders
24
$
this
->
call
(
UsersTableSeeder
::
class
);
25
$
this
->
call
(
ProductsTableSeeder
::
class
);
26
}
27
}
Este passo feito, vamos ao terminal e conhecer mais um comando. Desta vez para execução dos nossos seeds, alimentando dados em nossa base.
Em seu terminal e na raiz do projeto execute o comando abaixo:
1
php artisan db:seed
Veja o resultado:
Se você for ao seu banco e consultar as tabelas verá que os dados foram criados corretamente como definimos nas classes de seed.
Se precisarmos de mais dados, como por exemplo, inserir 30 produtos e fazer isso só com seeds teríamos um trabalho colossal mas este trabalho se simplifica por meio do que chamamos de Model Factories.
Então vamos crescer mais dentro do Laravel conhecendo mais esta funcionalidade que nos auxilia neste camada/etapa do desenvolvimento.
Vamos continuando…
Quando precisamos realizar a geração de muitos dados de forma mais automatizada podemos utilizar as Factories ou Model Factories para realizar este trabalho.
Aqui, de certa forma, teremos algum contato sobre models mas não vou me ater a eles (Models) no momento, pois o próximo capítulo será sobre esta camada e o tratamento com banco de dados. Como precisamos de models, para uso das Factories, na geração automatizada de dados para nossa aplicação, vou começar primeiramente com o model que já vem no projeto recém criado, o model Users.
As factories definem as regras para criação de dados fictícios em nossas tabelas, com base no model escolhido e conforme a quantidade especificada. No fim das contas, são chamados de factories ou fábricas por serem um esboço para criação de dados e junto com os seeds realizamos a inserção dos dados na base.
Você pode encontrar os arquivos de factories na pasta database/factories
.
Nesta pasta já temos um arquivo de factory, o UserFactory.php
. Veja o conteúdo dele abaixo:
1
<
?
php
2
3
namespace
Database
\Factories
;
4
5
use
Illuminate
\Database
\Eloquent
\Factories
\Factory
;
6
use
Illuminate
\Support
\Facades
\Hash
;
7
use
Illuminate
\Support
\Str
;
8
9
/**
10
*
@
extends
\Illuminate
\Database
\Eloquent
\Factories
\Factory
<
\App
\Models
\User
>
11
*/
12
class
UserFactory
extends
Factory
13
{
14
/**
15
*
The
current
password
being
used
by
the
factory
.
16
*/
17
protected
static
?
string
$
password
;
18
19
/**
20
*
Define
the
model
's default state.
21
*
22
*
@
return
array
<
string
,
mixed
>
23
*/
24
public
function
definition
():
array
25
{
26
return
[
27
'name'
=>
fake
()
->
name
(),
28
'email'
=>
fake
()
->
unique
()
->
safeEmail
(),
29
'email_verified_at'
=>
now
(),
30
'password'
=>
static
::
$
password
??
=
Hash
::
make
(
'password'
),
31
'remember_token'
=>
Str
::
random
(
10
),
32
];
33
}
34
35
/**
36
*
Indicate
that
the
model
's email address should be unverified.
37
*/
38
public
function
unverified
():
static
39
{
40
return
$
this
->
state
(
fn
(
array
$
attributes
)
=>
[
41
'email_verified_at'
=>
null
,
42
]);
43
}
44
}
As Factories, no Laravel desde a v8, receberam diversos incrementos e o primeiro deles é a estrutura de classe que foi adicionada em sua escrita. Antes as factories eram arquivos php com o código definido de forma imperativa.
Esta classe factory, do UserFactory, e outras classes que iremos gerar no futuro, são bem tranquilas de entender. Primeiramente o Laravel faz o autodiscover da classe de factory por meio do padrão <Model>Factory e sempre que precisarmos executar a criação destes dados fazemos por meio do model, que recebe uma trait pra lhe dá corretamente com a sua classe factory.
No método definition, podemos definir o esboço para geração dos nossos dados fakes, perceba que o array retornado neste método respeita exatamente a estrutura da tabela users. É importante ressaltar que os dados fakes são gerados pelo pacote Faker que já está associado na classe base a qual nossa classe factory extende e este pacote faker nos permite gerar todo tipo de dado falso(pra testes), como números de telefone, nomes, frases, textos e muitos mais.
Esta classe UserFactory, possui ainda mais um método que se utiliza do pensamento de estado durante a execução desta factory, que foi adicionado as factories no Laravel 8. Basicamente podemos criar métodos que podem ser chamados durante a execução desta factory de forma a gerar x dados com status específicos, como você pode ver exatamamente na ideia do método unverified. Posso criar x usuários com o status de email não verificado.
PS.: Sempre que precisar de dados fake para sua coluna em questão, dê uma olhada nas possibilidades disponiveis no link https://github.com/fzaninotto/Faker#formatters.
Como podemos executar esta factory existente?
Vamos ver a seguir.
Como comentado, vamos executar nossa inserção de dados utilizando o dado esboçado pela factory. Como o título do trecho diz, precisamos combinar a geração dos dados, utilizando a factory, junto com a execução via seeds. Agora, vamos alterar nosso UsersTableSeeder para utilizar as factories.
Comente o conteúdo do método run
do UsersTableSeeder e adicione a chamada abaixo:
1
\
App
\
Models
\
User
::
factory
(
10
)
-
>
create
();
Ao invés de usarmos o objeto DB
, como fizemos anteriormente, chamaremos a partir do model User o método factory, que recebe como parâmetro a quantidade de dados que queremos criar, neste caso 10 e logo após podemos chamar o método create que é quem irá de fato alimentar a tabela com estes dados.
Depois desta definição na classe UsersTableSeeder
vamos ao terminal e vamos rodar somente o seed para a classe de seed de usuário.
Para isto execute o comando abaixo:
1
php artisan db:seed --class=UsersTableSeeder
Resultado:
Acima conhecemos mais uma possibilidade dos seeds, podemos executar seeds especificando a classe que queremos executar, em nosso caso apenas o conteúdo do UsersTableSeeder informando o parâmetro --class
e o nome da classe.
Se você for ao seu banco, além do dado que já haviamos inserido no momento anterior com o seed, você verá mais 10 registros que foram gerados neste último comando, por meio da execução da factory via execução dos seeds. Perceba que isso simplifica bastante nossas vidas, pois se quisermos mais dados fakes, basta definirmos a quantidade desejada no parâmetro do método factory.
Por hora sobre factories e seeds é o que queria mostrar dado o escopo do nosso capítulo, em breve, nos próximos capítulos trabalharemos mais com estas camadas quando começarmos a focar nos conhecimentos dos Models e darmos largada de vez ao nosso app exemplo aqui do livro.
Agora que já conhecemos como criar tabelas e como alimentá-las com dados falsos para nosso desenvolvimento, vamos entender como podemos reverter as coisas quando tratamos das migrações e suas criações e alterações.
Primeramente caso precisemos voltar o último lote de migrações executados, podemos usar o comando abaixo:
1
php artisan migrate:rollback
O comando migrate:rollback, como comentado, desfará tudo que as migrações do último lote de execução fizeram. Em nosso caso, foi a criação da tabela de produtos. Você pode voltar também as execução por meio de passos, informando o parâmetro --step
:
1
php artisan migrate:rollback --step=2
Neste caso acima, ele desfará as duas migrações mais recentes, nesse desfazer de coisas que o método down da migration faz em suas definições.
Lembrando que nos processos de reverter coisas em migrations os métodos
down
é que serão chamadas e executados das migrações mais novas para as mais antigas.
Podemos ainda realizar operações em cima de todas as migrações, desfazendo-as, de uma vez só com o comando abaixo:
1
php artisan migrate:reset
O comando desfará todas as migrações executadas em sua base, deixando apenas a tabela migrations mas vazia. Caso queiramos resetar e re-executar as migrações novamente de uma vez só, podemos executar o comando abaixo:
1
php artisan migrate:refresh
Ou ainda executar tudo do zero novamente com o comando fresh, que apaga todas as tabelas e executa as migrations novamente:
1
php artisan migrate:fresh
Este comando acima, o migrate:fresh
, executa um drop table na base antes de re-executar as migrations então tome cuidado caso tenha tabelas que não estão mapeadas por migrations, pois com o drop elas podem ser removidas e este ponto é o que diferencia o fresh do refresh.
O refresh faz um rollback e re-executa as migrações sem mexer na base no que diz respeito a coisas fora das migrações e o fresh faz um drop (ALL) table para poder executar as migrações novamente.
Caso queira voltar todos os passos executados nas migrations e na re-execução, executar as seeds junto, podemos informar o parâmetro --seed
como opção ao comando refresh
.
Veja abaixo:
1
php artisan migrate:refresh --seed
Com isso teremos a execução reversa das migrações, sua re-execução e ainda a execução das seeds no banco de dados. Veja o resultado do comando abaixo:
Isso facilita muito nossas vidas!
Podemos usar o parâmetro --seed
tanto no migrate:refresh, como no migrate:fresh e também na execução do comando migrate.
Bom, este capítulo foi bem denso e trabalhoso mas com muitas vantagens demonstradas pelo framework Laravel. Recomendo fortemente que você execute bastante os comandos aprendidos aqui e teste cada nuance dos comandos para reverter coisas no banco via migrations.
No próximo capítulo vamos subir o nível de utilização da camada de banco, tratando diretamente com os Models e suas representações.
Então até lá!
Continuando nosso trabalho com a camada de dados e persitência, vamos subir o nível, conhecendo a camada dos models e como podemos trabalhar buscas, inserções, atualizações, remoções via objetos.
Vamos começar primeiro pelas queries e ir crescendo nosso conhecimento no decorrer deste capítulo. Para isto vamos usar nosso controller ProductController
que se encontra dentro da pasta Admin
em controllers.
Mas antes…
No Laravel, os models são a representação, do ponto de vista de objetos, das tabelas do nosso banco de dados.
Por exemplo, por convenção do framework, se eu tenho uma tabela chamada products
na base, a representação em model desta tabela será uma classe chamada de Product
. Se eu tenho uma tabela users
, sua representação via model será uma classe chamada de User
.
O Laravel automaticamente tentará, por convenção, resolver sua tabela no plural por ter o pensamento, na base, de uma coleção de dados, ou seja, se tenho um model Product e como comentado, o Laravel tentará buscar os dados da tabela products.
É possível mudar essa mágica do framework, sobscrevendo a propriedade protegida $table
dentro do model que deseja mudar, colocando o nome da sua tabela caso ela não siga a convenção do plural resolvida pelo Laravel.
Vamos começar a entender como interagir com os dados.
Por exemplo, como posso pegar todos as postagens via Model? Para isto temos um método chamada de all
que faz este trabalho para nós.
Por exemplo:
1
return
\
App
\
Models
\
Product
::
all
();
O resultado do método acima se fossemos pensar em uma query na base, seria uma sql como esta:
1
select * from products
Lembrando que ainda não criamos nosso primeiro model, então vamos a esta tarefa. Execute na raiz do projeto o seguinte comando gerador:
1
php artisan make:model Product
Veja o conteúdo do model abaixo:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Model
;
6
7
class
Product
extends
Model
8
{
9
//
10
}
Veja nosso model acima, bem simples inclusive, basicamente todo o processo para manipular o banco é herdado da classe Model, do Eloquent que é o ORM responsável por trabalhar esta camada de Objeto para o Relacional do Banco de Dados.
Neste model, caso tivessemos gerado a factory junto, ele viria com a trait para manipulação das factories, factories essas que vimos anteriormente aqui na jornada.
Se, por ventura, você quiser utilizar um nome de tabela de sua escolha e não quiser que o Laravel resolva o nome dela de forma mágica, você pode sobscrever o atributo $table
dentro do seu model como mostrado no conteúdo abaixo:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Model
;
6
7
class
Product
extends
Model
8
{
9
protected
$
table
=
'nome_da_sua_tabela'
;
10
}
Acima ao invés do Laravel tentar encontrar a tabela no plural ele pegará o valor do atributo $table
.
Deixarei o conteúdo acima para exemplo mas utilizarei a convenção em nossos models sem a utilização do atributo $table
.
Certo mas então o que é o Eloquent?
O Eloquent é o ORM padrão do Laravel, é a camada, via objetos, para manipulação dos dados de seu banco. O ORM é a camada que traduz sua estrutura de objetos, Models, para a camada relacional do seu banco de dados.
Veja um exemplo de inserção de um produto utilizando o Eloquent:
1
//O códiga poderia está em um método do controller
2
3
$
product
=
new
\
App
\
Models
\
Product
();
4
$
product
->
name
=
'Product salvo com eloquent e active record'
;
5
$
product
->
description
=
'Descrição product'
;
6
$
product
->
body
=
'Conteúdo do product'
;
7
$
product
->
slug
=
'product-salvo-com-eloquent-e-active-record'
;
8
$
product
->
in_stock
=
30
;
9
$
product
->
price
=
(
299.90
*
100
);
10
$
product
->
is_active
=
true
;
11
12
//Aqui a inserção do produto é , de fato, realizado na tabela.
13
$
product
->
save
();
Veja como é simples, dou inicio uma nova instância de Product
, chamo as colunas como atributos do objeto e por fim para salvar os dados atribuídos a cada um dos atributos, utilizo o método save
do model para realizar a operação de criação do nosso produto.
Aqui no livro vou abordar mais conceitos do Eloquent de forma prática e pontuando os comportamentos. Me utilizarei, para salvar e atualizar os dados, do conceito de Mass Assignment que explicarei mais a frente.
PS.: Antes de prosseguirmos, recomendo fortemente a leitura sobre Active Record caso queira conhecer esse padrão, o modelo de inserção apresentado acima se utiliza deste padrão. Padrão este que têm muitos contras por parte da comunidade mas vale o conhecimento toda via.
Lembra que já temos um controller para utilização e criação de um CRUD para produtos?
Por meio deste CRUD vamos focar nos pontos mais cruciais para você conhecer o trabalho do Eloquent em nossas aplicações. Nosso controller encontra-se na pasta dos controllers, dentro da pasta Admin
e o controller ProductController
.
Primeiramente vamos definir nosso método index. Trabalhando do ponto de vista dos models eu consigo realizar operações de busca dos dados em minhas tabelas, como mostrado anteriormente posso buscar todos os registros do banco de dados por meio do método all
.
Trecho abaixo:
1
return
\
App
\
Models
\
Product
::
all
();
Ou posso retornar os dados paginados, para exibição em uma tabela html na view. Então vamos começar a implementar o método index no controller ProductController
que está na pasta Admin
.
Veja o conteúdo dele abaixo:
1
public
function
index
()
2
{
3
$products
=
Post
:
:
paginate
(
15
);
4
5
dd($products)
;
//no
próximo
capítulo
vamos
mandar
para
view...
6
}
Veja o código acima, por enquanto, ainda não vamos utilizar a view, retornaremos a ela e os pontos do Blade no próximo capítulo. Voltando ao código acima, perceba que chamei o Product::paginate
, informando que quero 15 produtos, neste caso, os dados vão vir do banco paginados e teremos os 15 produtos por cada tela da paginação.
Não esqueça de importar o Model Product em seu controller:
1
use App\Models\Product;
Agora, vamos lá no arquivo de rotas e realizar uma pequena alteração no que já havíamos feito, para o conjunto de rotas de produto, então, o que está assim:
1
Route
::
prefix
(
'admin'
)
-
>
group
(
function
()
{
2
3
Route
:
:
prefix
(
'products'
)
->
name
(
'products.'
)
->
group
(
function
(
)
{
4
5
Route
::
get
(
'/create'
,
[
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::class
,
\
6
'create'
]
)
->
name
(
'create'
);
7
8
Route
:
:
post
(
'/store'
,
[
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::class
,
\
9
'store'
]
)
->
name
(
'store'
);
10
}
);
11
}
);
Vamos atualizar para a chamada do controller como recurso, ficando como vemos abaixo:
1
Route
::
prefix
(
'admin'
)
-
>
group
(
function
()
{
2
3
Route
:
:
resource
(
'products'
,
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::
class
);
4
5
}
);
Perceba, que agora simplificamos mais ainda nossas rotas dentro do grupo para o admin. Quando trabalhamos no capítulo sobre request e response, os métodos create
e store
, que já existem no controller, foram pensados já para deixá-los no pensamento do método resource
e os controllers como recurso.
Se você for ao seu browser agora e acessar http://127.0.0.1:8000/admin/products
você verá o resultado abaixo:
Veja que expandi o atributo items
e seu nivel a dentro, onde temos a coleção de dados retornada. Veja que tivemos 1 item retornado, em nosso caso o produto criado durante o aprendizado de seeds.
Quandos se trata de navegação nos dados paginados o passo é bem simples, basta informarmos na url o parâmetro page=<pagina-que-queremos>
(como querystring) por exemplo.
O retorno do método paginate
do eloquent, e como podemos ver na imagem anterior, é um objeto do tipo Illuminate\Pagination\LengthAwarePaginator
que dentre muitos detalhes possui também o método para exibir a paginação em nossas telas na camada de views.
Quando formos trabalhar com as views e o Blade, veremos uma forma simples de criar a páginação no frontend, simplificando esta navegação.
Para buscarmos dados específicos, podemos trabalhar com diversos métodos. Por exemplo, se você quiser buscar um produto pelo id dele, você pode usar o método find
.
Veja:
1
Product
::
find
(
1
);
Ou ainda, buscando pelo id, você pode usar o método findOrFail
, que caso o Eloquent não encontre o produto em questão, irá lançar uma Exception causando um 404 NOT Found
. Podemos usar desta maneira:
1
Product
::
findOrFail
(
1
);
Vamos criar agora o método em nosso controller ProductController
para recuperação de um produto, para nossa tela de edição. Veja o conteúdo do método edit
e já adicione ele em seu controler:
1
public
function
edit
($
product
)
2
{
3
$product
=
Product
:
:
findOrFail
(
$
product
);
4
5
dd($product)
;
//em
breve
mandaremos
pra
view
6
}
Usarei o findOrFail
para termos o melhor controle para quando o produto não for encontrado, comportamento mencionado anteriormente. Já aproveite e adicione o método show
no controller e nele apenas vamos redirecionar para a tela de edição uma vez que não teremos uma tela de visualização de um produto específico.
1
public
function
show
(
$
product
)
2
{
3
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
]);
4
}
Abaixo eu destaco só o atributo original
que traz o dados retornados do método findOrFail
, visualizamos acessando http://127.0.0.1:8000/admin/products/1/edit
:
Nosso controller até o momento está abaixo, com os dois métodos criados no capítulo sobre requests e junto do index
, edit
e o show
criados agora.
Veja na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Product
;
7
use
Illuminate
\Http
\Request
;
8
9
class
ProductController
extends
Controller
10
{
11
public
function
index
()
12
{
13
$
products
=
Product
::
paginate
(
15
);
14
15
dd
(
$
products
);
//
no
próximo
capítulo
vamos
mandar
para
view
...
16
}
17
18
public
function
create
()
19
{
20
return
view
(
'products.create'
);
21
}
22
23
public
function
store
(
Request
$
request
)
24
{
25
dd
(
$
request
->
all
());
26
}
27
28
public
function
show
(
$
product
)
29
{
30
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
]);
31
}
32
33
public
function
edit
(
$
product
)
34
{
35
$
product
=
Product
::
findOrFail
(
$
product
);
36
37
dd
(
$
product
);
//
em
breve
mandaremos
pra
view
38
}
39
}
Agora vamos ao método store
pois nele vamos trabalhar a inserção de dados propriamente dita!
Como mencionei anteriormente, poderíamos utilizar a criação de dados usando Active Record, por meio da referência dos atributos dinâmicos, baseado nas colunas do banco para aquela tabela (produtos) e por meio do método save
criar um dado.
Como estamos tratando aqui de criação, vou mostrar a título de conhecimento o salvar dos dados usando Active Record dentro de nosso método, veja como ficaria.
Altere o conteúdo do método store
, já existente, para o conteúdo abaixo:
1
public
function
store
(
Request
$
request
)
2
{
3
$
data
=
$
request
->
all
();
4
5
/**
6
Preço recebido, exe.: 19,99 -> passa pelo replace -> 19.99 e salvamos ele como i\
7
nteiro multiplicando por 100 jogando as casa decimais 2 casas para direita.
8
*/
9
$
price
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
10
11
$
product
=
new
Product
();
12
13
$
product
->
name
=
$
data
[
'name'
];
14
$
product
->
description
=
$
data
[
'description'
];
15
$
product
->
body
=
$
data
[
'body'
];
16
$
product
->
in_stock
=
48
;
//está fixo mas vamos adiciona-lo no formulário ainda
17
$
product
->
price
=
$
price
*
100
;
18
$
product
->
slug
=
$
data
[
'slug'
];
19
$
product
->
is_active
=
true
;
20
21
dd
(
$
product
->
save
());
//veja o resultado no browser
22
}
Veja que agora pego os dados de cada campo a partir da request, vindas do formulário, e repasso para cada atributo de nosso model e na chamada do método save
nós criamos este produto na base.
PS.: Aqui no ebook irei trabalhar o preço dos produtos como inteiro para termos mais precisão nas ações de lidar com as manipulações em produtos, por isso faço o replace que seria feito de fato para salvar o dado como float mas aqui estamos trabalhando com inteiro, então, multiplicamos por 100 esse preço ao salvar o dado, movendo a virgula duas casa para a direita deixando nosso preço como inteiro.
PS2.: Em breve vamos delegar estas manipulações para nosso model para limpar melhor nossos controllers e ao recuperar o dado vamos trabalhar ele de forma flutuante com precisão 2, para isso vamos dividir o preço por 100 trazendo a vírgula 2 casa para esquerda.
Para testarmos, vamos ao nosso link http://127.0.0.1/admin/products/create
e envie os dados via form. Veja o resultado na imagem abaixo:
Perceba que o resultado do método save
foi o valor boleano true
, confirmando assim a criação do registro em nossa base. Se você quiser atualizar um registro usando Active Record, basta, ao invés de instanciar um model post, passar o resultado de um find por exemplo ou um dado buscado na base:
1
$
price
=
(
float
)
str_replace
(
[
'.'
,
','
]
,
[
''
,
'.'
]
,
$
data
[
'price'
]
);
2
3
$
product
=
Product
::
findOrFail
(
1
);
4
5
$
product-
>
name
=
$
data
[
'name'
]
;
6
$
product-
>
description
=
$
data
[
'description'
]
;
7
$
product-
>
body
=
$
data
[
'body'
]
;
8
$
product-
>
in_stock
=
48
;
9
$
product-
>
price
=
$
price
*
100
;
10
$
product-
>
slug
=
$
data
[
'slug'
]
;
11
$
product-
>
is_active
=
true
;
12
13
dd
($
product-
>
save
());
//
aqui
ele
vai
atualizar
Como temos a referência na variável $product
de um dado recém-buscado na base, ao chamarmos o método save
o Eloquent irá atualizar este registro ao invés de criar um novo.
Lembrando que você, ao atualizar, pode chamar apenas as colunas que deseja atualizar o dado de fato.
Este trecho foi um rápido desmontrativo do Active Record no Eloquent, quero te mostrar uma técnica mais direta e que é mais utilizada hoje em dia dentro do Laravel, via Eloquent. Esta técnica é o que chamamos de Mass Assignment
ou Atribuição em Massa
.
Vamos conhecer esta técnica.
Mass Assignment ou Atribuição em Massa é uma forma de inserir ou atualizar os dados por meio de uma única chamada e de uma vez só quando necessitamos de realizar estas operações.
Por exemplo, eu poderia passar todo o array vindo da request e já salvar isso direto no banco por meio de um método do Eloquent
, o método create
.
Então vamos a alteração, mais uma vez, do nosso método store
, que está assim:
1
public
function
store
(
Request
$
request
)
2
{
3
$
data
=
$
request
->
all
();
4
5
$
price
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
6
7
$
product
=
new
Product
();
8
9
$
product
->
name
=
$
data
[
'name'
];
10
$
product
->
description
=
$
data
[
'description'
];
11
$
product
->
body
=
$
data
[
'body'
];
12
$
product
->
in_stock
=
48
;
//está fixo mas vamos adiciona-lo no formulário ainda
13
$
product
->
price
=
$
price
*
100
;
14
$
product
->
slug
=
$
data
[
'slug'
];
15
$
product
->
is_active
=
true
;
16
17
dd
(
$
product
->
save
());
//veja o resultado no browser
18
}
Agora, passará a ficar assim:
1
public
function
store
(
Request
$
request
)
2
{
3
$data
=
$request->all()
;
4
5
$price
=
(float)
str_replace(
[
'.'
,
','
]
,
[
''
,
'.'
]
,
$data
[
'price'
]
)
;
6
7
$data
[
'in_stock'
]
=
48
;
8
$data
[
'price'
]
=
$price
*
100
;
9
$data
[
'is_active'
]
=
true
;
10
11
dd(
Product
:
:
create
(
$
data
));
//veja
o
resultado
no
browser
12
}
Perceba a redução acima, ao invés de chamarmos cada atributo baseado na coluna de nossa tabela, chamamos apenas o método create
passando para ele nosso array recuperado da request, lembrando que este array associativo respeita chave(coluna da tabela) e valor desta chave, o valor a ser inserido.
Se você for ao browser e testar isso enviando os dados do formulário, perceberá que teremos uma exception sobre a adição de um campo na propriedade $fillable
do model, aqui entra um ponto importante.
Antes de comentar o erro, você pode está se perguntando: Esta atribuição em massa não pode ser problemática, já que ela pelo visto aceita tudo?!
Veja a exception lançada:
Para resolver a exception lançada acima, sobre o atributo $fillable
e o seu questionamento ao mesmo tempo, nós precisamos de fato definir este bendito atributo $fillable
lá no model, o model do momento Product
.
Agora para que serve este atributo? Tecnicamente ele é bem simples.
Como estamos passando esta atribuição de uma vez só, precisamos indicar para o Model/Eloquent que ao salvarmos ou atualizarmos os dados usando a atribuição em massa quais são os atributos que ele deve permitir o passar dos valores a serem salvos na tabela representada pelo Model.
Não esqueça que o array associativo, passado ao métodos create, deve respeitar as colunas da tabela como chave de cada linha do array a ser inserido. No model definimos uma lista com as colunas/atributos permitdos na Atribuição em Massa, definindo a propriedade $fillable
e apenas aqueles campos listado neste atributo é que receberão o valor a ser inserido.
Então vamos adicionar ele em nosso model Product
e logo após comentarmos mais um pouco sobre este detalhe.
Veja a alteração em Product.php
:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Factories
\HasFactory
;
6
use
Illuminate
\Database
\Eloquent
\Model
;
7
8
class
Product
extends
Model
9
{
10
protected
$
fillable
=
[
11
'name'
,
12
'description'
,
13
'body'
,
14
'slug'
,
15
'is_active'
,
16
'in_stock'
,
17
'price'
18
];
19
}
Agora com os campos adicionados no atributo $fillable
vamos enviar os dados novamente e atualizando o link com o erro anterior e clicando em reenviar no browser.
Veja o resultado do dd, vindo do método create
do Eloquent, em nossa tela:
O método create
ao criar um dado, retorna este dado criado junto com seu id na base como resultado populado no model em questão, aqui o model Product. Veja o conteúdo da informação abrindo a propriedade original
.
A segurança do método create
, usando a atribuição em massa, se dá pela propriedade $fillable
no model que uma vez definida e tendo as colunas permitidas, só teremos o preechimento das informações para a coluna mapeada nesta propriedade.
Agora como fazemos a atualização do dados massa?
Vamos lá.
Para atualizarmos os dados, vamos trabalhar aqui com nossa view de edição e conhecer mais alguns detalhes do Laravel, crie lá dentro da pasta resources/views/products
o arquivo edit.blade.php
e adicione o conteúdo abaixo:
1
<form
action=
"
{{
route
(
'products.update'
,
[
'product'
=>
$
product-
>
id
])
}}
"
method=
"p\
2
ost"
>
3
4
@csrf
5
@method('PUT')
6
<div
class=
"w-full mb-6"
>
7
<label
class=
"block mb-2"
>
Nome</label>
8
<input
type=
"text"
name=
"name"
class=
"w-full p-2 rounded"
value=
"
{{
$
product
\
9
->
name
}}
"
>
10
</div>
11
12
<div
class=
"w-full mb-6"
>
13
<label>
Descrição</label>
14
<input
type=
"text"
name=
"description"
class=
"w-full p-2 rounded"
value=
"
{{
$\
15
product-
>
description
}}
"
>
16
</div>
17
18
<div
class=
"w-full mb-6"
>
19
<label>
Conteúdo</label>
20
<textarea
name=
"body"
id=
""
cols=
"30"
rows=
"10"
class=
"w-full p-2 rounded"
>
{\
21
{ $product->body }}</textarea>
22
</div>
23
24
<div
class=
"w-full mb-6"
>
25
<label>
Preço</label>
26
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded"
value=
"
{{
number_
\
27
format
(
$
product-
>
price
,
2
,
','
,
'.'
)
}}
"
>
28
</div>
29
30
<div
class=
"w-full mb-6"
>
31
<label>
Slug</label>
32
<input
type=
"text"
name=
"slug"
class=
"w-full p-2 rounded"
value=
"
{{
$
product
\
33
->
slug
}}
"
>
34
</div>
35
36
<button
class=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
37
font-thin"
>
Atualizar
38
Produto</button>
39
</form>
Agora, lá no método edit
do ProductController
substitua a linha do dd
pelo trecho abaixo:
1
$
product
->
price
=
$
product
->
price
/
100
;
//voltando pro ponto flutuante
2
3
return
view
(
'products.edit'
,
compact
(
'product'
));
Se você acessar o link http://127.0.0.1:8000/admin/products/1/edit
você obterá o resultado abaixo:
Veja nosso formulário de edição acima já preenchido com os valores pegos do banco pelo Eloquent e enviados para a view.
Agora vamos entender os códigos do formulário de edição acima. Vamos lá.
Primeiramente atente a chamada da rota na action do formulário:
1
<form action="
{{
route
(
'products.update'
,
[
'product'
=>
$
product-
>
id
])
}}
" method="p\
2
ost">
Enviaremos nossos dados para a rota de apelido products.update
atribuída pelo método resource
do Route
, informamos o nome do parâmetro dinâmico da rota no array do segundo parâmetro da função helper route
que será o id do produto, estes dados serão enviados para o método update
lá do ProductController
, que vamos criar ainda.
Temos agora mais uma alteração/novidade, a chamada da diretiva @method
. Vamos entender ela:
Sabemos que os formulários html só suportam os verbos http: post
e get
. Por meio da diretiva @method
fazemos o Laravel interpretar o formulário em questão com o verbo definido na diretiva, ou seja, como usei o valor PUT
este formulário será interpretado pelo Laravel como sendo enviado via verbo PUT e cairá para a execução do método update
do controller, que é o método que recebe as solicitações quando usamos o verbo PUT
ou PATCH
em nossa requisição.
Caso você ainda não tenha ouvido falar deste verbos HTTP além do GET e POST, basicamente o HTTP permite que realizemos requisições com mais contexto do que simplesmente recuperar(GET) e salvar (POST). Temos verbos HTTP para o pensamento de cada operação de manipulação de um dado, que normalmente chamamos de Recurso no âmbito da requisição. Os verbos HTTP mais conhecidos são:
- GET: traz o pensamento de recuperar um dado, basicamente sempre que acessamos um link na web usamos este verbo;
- POST: traz o pensamento de criação de um novo dado;
- PUT: traz o pensamento de atualizar um dado por completo;
- PATCH: traz o pensamento de atualizar partes de um dado;
- DELETE: o nome é intuitivo, traz o pensamento de remover um dado;
- OPTIONS: retorna os verbos aceitos em um determinado endpoint;
Continuando, agora em nossos inputs recebemos os valores vindos lá do controller e adicionado no atributo value de cada input. Quando fazemos uma busca pela postagem desejada, ele retorna um objeto populado com os dados desta postagem, o que nos resta é acessarmos eles respeitando os nomes das colunas mas chamando como atributos do objeto e isto pode ser visto em cada atributo value
dos inputs do nosso formulário de edição.
Agora precisamos definir o método para manipulação dos dados enviados do formulário de edição, para isso crie um método chamado de update
em seu controller com o conteúdo abaixo:
1
public
function
update
(
$product
,
Request
$request
)
2
{
3
//Atualizando com mass assignment
4
$data
=
$request
->
all
();
5
$price
=
(
float
)
str_replace
([
'.'
,
','
],
['',
'.'
],
$data
['
price
']);
6
$data
['
price
']
=
$price
*
100
;
7
8
$product
=
Product
::
findOrFail
(
$product
);
9
$product
->
update
(
$data
);
10
11
return
redirect
()
->
route
('
products
.
edit
',
['
product
'
=>
$product
->
id
]);
12
}
Perceba que busquei o produto usando o método findOrFail
e passando o id vindo da url
, no parâmetro dinâmico. Neste caso, como quero atualizar, o método para este processo em massa, é o método do eloquent chamado de update
, que me retorna um booleano para o sucesso ou falha desta execução.
Você pode testar sua edição acessando http://127.0.0.1:8000/admin/products/<id-produto>/edit
, o sucesso da atualização é o redirecionamento para a tela de edição então verifique se o dado que você atualizou persistiu atualizando a própria página do browser na tela de edição após submeter o formulário e ele redirecionar para a edição novamente.
Vamos ao passo final do CRUD, a remoção de um dado com Eloquent.
Bom, agora para completarmos nosso ciclo de CRUD, vamos deletar produtos de nossa base. Isso será realizado pelo método delete
do Eloquent
.
Veja o método abaixo a ser adicionado no ProductController
:
1
public
function
destroy
($
product
)
2
{
3
$product
=
Product
:
:
findOrFail
(
$
product
);
4
5
dd($product->delete())
;
6
}
O método do controller como recurso que responde a chamada para remoção de um dado é o destroy
, entretanto, o método do Eloquent
que irá remover um dado é o delete
que retorna um boleano para o caso de sucesso ou falha da operação de remoção do dado na base.
Perceba que, como queremos remover uma postagem, precisamos buscar por ela via Eloquent
para então removermos.
Agora precisamos adicionar o botão de remoção lá na tela de edição, neste caso como precisamos simular o envio do verbo http DELETE, vou precisar usar um form para o botão de remoção.
Adicione o form abaixo, após o formulário de edição:
1
<form
action=
"
{{
route
(
'products.destroy'
,
[
'product'
=>
$
product-
>
id
])
}}
"
method=
"po\
2
st"
>
3
@csrf
4
@method('DELETE')
5
<button
type=
"submit"
>
Remover Produto</button>
6
</form>
Perceba também que temos adicionado o controle csrf para esta operação, além da definição da diretiva @method com o verbo http DELETE
.
A tela de edição fica assim:
Ao clicar no botão remover, você verá o resultado na tela, true para sucesso na remoção ou false para o caso de falha. Após removido se tentarmos acessar a mesma postagem teremos uma tela de 404 em nossa cara:
Um pequeno e último detalhe ao invés de realizarmos um dd após o processo de remoção vamos redirecionar para a listagem de produtos. Veja o método destroy
agora:
1
public
function
destroy
($
product
)
2
{
3
$product
=
Product
:
:
findOrFail
(
$
product
);
4
$product->delete()
;
5
6
return
redirect()->route('products.index')
;
7
}
Façamos o mesmo no método store, como retorno do método store
coloque o redirect para a tela de edição do produto recém criado, como mostrado no método completo abaixo:
1
public
function
store
(
Request
$
request
)
2
{
3
$data
=
$request->all()
;
4
5
$price
=
(float)
str_replace(
[
'.'
,
','
]
,
[
''
,
'.'
]
,
$data
[
'price'
]
)
;
6
7
$data
[
'in_stock'
]
=
48
;
//está
fixo
mas
vamos
adiciona-lo
no
formulário
ainda
8
$data
[
'price'
]
=
$price
*
100
;
9
$data
[
'is_active'
]
=
true
;
10
11
$product
=
Product
:
:
create
(
$
data
);
12
13
return
redirect()->route('products.edit',
[
'product'
=>
$product
->
id
]
)
;
14
}
Agora completamos as 4 etapas de um CRUD utilizando o Eloquent, que nos permite trabalhar com o banco pela visão de objetos.
Neste capítulo conhecemos diversas possibilidades usando o Eloquent, o ORM padrão do Laravel. Com isso realizamos a criação de um CRUD completo usando nosso Model Product
por meio dos métodos disponíveis e herdados do Model
do Eloquent, que nos permitiu realizar estas operações: seleção, criação, atualização e ainda remoção de produto no banco de dados.
Para completarmos o ciclo e deixarmos as coisas mais dinâmicas e integradas, precisamos conhecer o Blade, o famoso template engine do Laravel e que nos permite escrever views de forma mais dinâmica e rápida.
Conheceremos o Blade no próximo capítulo. Até lá!
Olá tudo bem? Vamos continuar nosso livro e desta vez vamos conhecer o famoso template engine do Laravel, o Blade.
O Blade é o template engine default do Laravel e traz consigo diversas estruturas que simplificam muito nosso trabalho:
- na criação das views;O Blade ao longo do tempo veio e vem recebendo diversos incrementos e pretendo mostrar, na prática, estas possibilidades usando seu poder para construirmos nossas views.
Então vamos criar as views de nossa aplicação utilizando o poder do Blade!
Vamos começar pelo layout, em nosso Hello World com Laravel nós tivemos um contato com o blade quando mandamos nossa mensagem (Hello World) para a view e exibimos ela com o interpolador {{ }}
, que nos permite exibir informações passadas a ele. Antes de entrarmos em pontos como este que comentei, vamos definir e organizar um template base usando o blade para melhor dispor as views do nosso painel.
Primeiramente crie uma pasta chamada de layouts
dentro da pasta de views e dentro da pasta layouts crie o arquivo app.blade.php
. Adicione o seguinte conteúdo e logo após farei os comentários:
1
<!doctype html>
2
<html
lang=
"en"
>
3
4
<head>
5
<meta
charset=
"UTF-8"
>
6
<meta
name=
"viewport"
7
content=
"width=device-width, user-scalable=no, initial-scale=1.0, maximum-sc\
8
ale=1.0, minimum-scale=1.0"
>
9
<meta
http-equiv=
"X-UA-Compatible"
content=
"ie=edge"
>
10
<title>
Loja - Code Experts</title>
11
<script
src=
"https://cdn.tailwindcss.com"
></script>
12
</head>
13
14
<body>
15
<nav
class=
"w-full mb-10 flex justify-between bg-gray-900 px-4"
>
16
<a
href=
"/"
class=
"text-white py-4"
>
Experts Store 12</a>
17
<ul>
18
<li
class=
"py-4"
>
19
<a
href=
"
{{
route
(
'products.index'
)
}}
"
20
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-\
21
in-out duration-300"
>
Produtos</a>
22
</li>
23
</ul>
24
</nav>
25
<div
id=
"container"
class=
"max-w-7xl mx-auto p-6"
>
26
<!-- Aqui será inserido o conteúdo das páginas filhas -->
27
28
@yield('content')
29
30
<!-- Aqui será inserido o conteúdo das páginas filhas -->
31
</div>
32
</body>
33
34
</html>
Acima temos a definição do nosso layout base, aqui entramos no primeiro conceito do Blade, neste caso, na herança de templates. Se você perceber no layout, defini dentro do body na div#container
uma diretiva chamada de @yield
que me permite apontar onde os templates devem exibir seus conteúdos.
Por exemplo, vamos entender como isso funciona fazendo nossa view create.blade.php
herdar do nosso layout app.blade.php
. Veja as alterações que fiz na view create.blade.php
:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('products.store') }}"
method
=
"post"
>
5
@
csrf
6
7
<
div
class
=
"w-full mb-6"
>
8
<
label
class
=
"block mb-2"
>
Nome
</
label
>
9
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border border-g\
10
ray-900"
value
=
"{{ old('name') }}"
>
11
</
div
>
12
13
<
div
class
=
"w-full mb-6"
>
14
<
label
>
Descrição
</
label
>
15
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border b\
16
order-gray-900"
17
value
=
"{{ old('description') }}"
>
18
</
div
>
19
20
<
div
class
=
"w-full mb-6"
>
21
<
label
>
Conteúdo
</
label
>
22
<
textarea
name
=
"body"
id
=
""
cols
=
"30"
rows
=
"10"
class
=
"w-full p-2 rounde\
23
d border border-gray-900"
>
{{
old
(
'
body
'
)
}}
</
textarea
>
24
</
div
>
25
26
<
div
class
=
"w-full mb-6"
>
27
<
label
>
Preço
</
label
>
28
<
input
type
=
"text"
name
=
"price"
class
=
"w-full p-2 rounded border border-\
29
gray-900"
30
value
=
"{{ old('price') }}"
>
31
</
div
>
32
33
<
div
class
=
"w-full mb-6"
>
34
<
label
>
Slug
</
label
>
35
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border border-g\
36
ray-900"
37
value
=
"{{ old('slug') }}"
>
38
</
div
>
39
40
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
41
hite font-thin"
>
Criar
Produto
</
button
>
42
</
form
>
43
@
endsection
O trecho de template acima, além da adição dos trecho de herança, eu adicionei algumas classes nos inputs para exibir uma borda neles. Lembre de pegar este código e atualizar em seu projeto.
Aqui temos o conteúdo do formulário envolvido por uma diretiva chamada de @section
que recebe o valor content
e a definição de onde essa diretiva termina, indicada pelo @endsection
. O que isso quer dizer?!
A diretiva @section
define o conteúdo que será substituido no layout principal, ou seja, quando eu acessar essa view ele vai herdar o que tem em app.blade.php
e onde eu defini o @yield('content')
, será adicionado o conteúdo que temos na diretiva @section
, no caso da view create.blade.php
o que estiver na section content
e exibirá o conteúdo do formulário junto do layout base.
Mas Nanderson, onde está definido que o create.blade.php
herda de layout.blade.php
? Não comentei de propósito mas ele se encontra como sendo a primeira linha da nossa view, veja a definição que aponta de qual template create.blade.php
herda, por meio da diretiva @extends
, o layout pai. Neste caso digo que a view create.blade.php
herda de app.blade.php
informando a diretiva @extends
da seguinte maneira: @extends('layouts.app')
.
Sendo que layouts
é a pasta dentro de views e app
o arquivo app.blade.php
, onde sabemos que o Laravel irá incluir a extensão internamente e linkar o caminho completo até a pasta layouts.
Alterei também a view edit.blade.php
, segue o conteúdo:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('products.update', ['product' => $product->id]) }}"
method
=
"p\
5
ost"
>
6
@
csrf
7
@
method
(
'
PUT
'
)
8
<
div
class
=
"w-full mb-6"
>
9
<
label
class
=
"block mb-2"
>
Nome
</
label
>
10
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border border-g\
11
ray-900"
value
=
"{{ $product->name }}"
>
12
</
div
>
13
14
<
div
class
=
"w-full mb-6"
>
15
<
label
>
Descrição
</
label
>
16
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border b\
17
order-gray-900"
18
value
=
"{{ $product->description }}"
>
19
</
div
>
20
21
<
div
class
=
"w-full mb-6"
>
22
<
label
>
Conteúdo
</
label
>
23
<
textarea
name
=
"body"
id
=
""
cols
=
"30"
rows
=
"10"
class
=
"w-full p-2 rounde\
24
d border border-gray-900"
>
{{
$product
->
body
}}
</
textarea
>
25
</
div
>
26
27
<
div
class
=
"w-full mb-6"
>
28
<
label
>
Preço
</
label
>
29
<
input
type
=
"text"
name
=
"price"
class
=
"w-full p-2 rounded border border-\
30
gray-900"
31
value
=
"{{ number_format($product->price, 2, ',', '.') }}"
>
32
</
div
>
33
34
<
div
class
=
"w-full mb-6"
>
35
<
label
>
Slug
</
label
>
36
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border border-g\
37
ray-900"
38
value
=
"{{ $product->slug }}"
>
39
</
div
>
40
41
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
42
hite font-thin"
>
Atualizar
43
Produto
</
button
>
44
</
form
>
45
46
<
form
action
=
"{{ route('products.destroy', ['product' => $product->id]) }}"
meth
\
47
od
=
"post"
>
48
@
csrf
49
@
method
(
'
DELETE
'
)
50
<
button
type
=
"submit"
class
=
"px-4 py-2 bg-red-700 border border-red-900 roun\
51
ded text-white font-thin mt-8"
>
Remover
52
Produto
</
button
>
53
</
form
>
54
@
endsection
Alterei as bordas dos inputs aqui também além de adicionar alguns estilos no button do form de remoção.
Veja o formulário na íntegra na imagem abaixo:
Aqui já temos alguns estilos aplicados por conta que desde o ínicio nosso form já possuía algumas classes utilitárias do TailwindCSS(tailwindcss.com), não comentarei muito dele aqui neste ebook mas tentarei na maior parte das vezes explicar os trechos que forem importantes e claro, se você conhece bem CSS o tailwindcss será algo mais tranquilo pra você.
Lá no Layout base, o app.blade.php
, por hora mantêm o link do cdn do tailwindcss. O trecho do layout base que uso o cdn é destacado abaixo:
1
<script
src=
"https://cdn.tailwindcss.com"
></script>
Agora vamos para a nossa listagem dos produtos e conhecer mais possibilidades do Blade.
Sabemos que para iterarmos em cima de uma coleção de dados precisamos usar laços de repetição, o Blade nos traz algumas possibilidades interessantes. A primeira delas é a possibilidade de utilição do foreach, como vemos abaixo:
1
@
foreach
(
$products
as
$product
)
2
<
li
>
$product
->
name
</
li
>
3
@
endforeach
Desta maneira a iteração na coleção de produtos, vindas do banco de dados, será no mesmos moldes do foreach do PHP, e claro, no procesamento desta view essa diretiva se tornará um foreach nativo.
Mas aqui, quero utilizar um foreach não muito convencional, na forma que vamos escrever, mas é claro que com condicionais e combinando com os laços chegaríamos no mesmo resultado.
Mas Nanderson, do que você está falando!?
Por exemplo, poderíamos fazer um controle condicional pro caso de não existirem produtos na base e somente, se existirem, exibissemos a tabela com os produtos.
Por exemplo, veja o trecho abaixo:
1
@
if
(
count
(
$products
))
2
@
foreach
(
$products
as
$product
)
3
<
li
>
{{
$product
->
name
}}
</
li
>
4
@
endforeach
5
@
else
6
<
h2
>
Nenhum
produto
cadastrada
!</
h2
>
7
@
endif
Acima te apresento o controle condicional ou como usar os se…senão (if…else) via diretivas do Blade. Primeiramente verificamos se o valor de $products
é verdadeiro, se verdadeiro nós realizamos os loops, senão, exibimos uma mensagem padrão. O verdadeiro aqui pra $products
é termos produtos na coleção.
Agora podemos melhorar ainda mais essa escrita usando outra diretiva do blade. Com a diretiva de loop chamada de @forelse
conseguimos chegar no mesmo resultado acima com pouco esforço e é ela que vamos utilizar, então vamos ao conteúdo do nosso index.blade.php
, que não existe ainda, então crie o arquivo index.blade.php
lá dentro da pasta das views de produto: resources/views/products/
.
Veja seu conteúdo abaixo:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
div
class
=
"w-full mb-10 flex justify-between"
>
5
<
h2
class
=
"font-bold text-xl"
>
Produtos
</
h2
>
6
<
a
href
=
"{{ route('products.create') }}"
7
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
8
font-thin"
>
Criar
Produto
</
a
>
9
</
div
>
10
<
div
class
=
"w-full p-5"
>
11
<
table
class
=
"w-full"
>
12
<
thead
>
13
<
tr
class
=
"border-b border-gray-600"
>
14
<
th
class
=
"px-4 py-2 text-xl text-left"
>
#
</
th
>
15
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Produto
</
th
>
16
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Status
</
th
>
17
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Criado
em
</
th
>
18
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Ações
</
th
>
19
</
tr
>
20
</
thead
>
21
<
tbody
>
22
@
forelse
(
$products
as
$product
)
23
<
tr
>
24
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
id
\
25
}}
</
td
>
26
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
nam
\
27
e
}}
</
td
>
28
<
td
class
=
"px-4 py-2 text-normal text-left"
>
29
@
if
(
$product
->
is_active
)
30
<
span
class
=
"p-1 rounded bg-green-500 text-green-900\
31
"
>
Ativo
</
span
>
32
@
else
33
<
span
class
=
"p-1 rounded bg-red-500 text-red-900"
>
In
\
34
ativo
</
span
>
35
@
endif
36
</
td
>
37
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
cre
\
38
ated_at
->
format
(
'
d
/
m
/
Y
H
:
i
'
)
}}
</
td
>
39
<
td
class
=
"w-[15%] px-4 py-2 text-normal text-left"
>
40
<
div
class
=
"flex justify-around gap-x-2"
>
41
<
a
href
=
"{{ route('products.show', ['product' => $pr\
42
oduct->id]) }}"
43
class
=
"px-4 py-2 bg-blue-700 border border-blue-\
44
900 rounded text-white font-thin"
>
45
Editar
46
</
a
>
47
<
form
action
=
"{{ route('products.destroy', ['product\
48
' => $product->id]) }}"
method
=
"post"
>
49
@
csrf
50
@
method
(
'
DELETE
'
)
51
<
button
type
=
"submit"
52
class
=
"px-4 py-2 bg-red-700 border border-re\
53
d-900 rounded text-white font-thin"
>
Remover
</
button
>
54
</
form
>
55
</
div
>
56
</
td
>
57
</
tr
>
58
@
empty
59
<
tr
>
60
<
td
colspan
=
"4"
>
Nada
encontrado
!</
td
>
61
</
tr
>
62
@
endforelse
63
</
tbody
>
64
</
table
>
65
</
div
>
66
@
endsection
Não esqueça de substituir, lá no controller ProductController
, no método index
, a linha do dd
pela linha abaixo:
1
return
view
(
'
products.index
'
, compact
(
'
products
'
))
;
Vamos aos pontos principais da view index.blade.php
. Perceba que aqui usei a diretiva:
1
@
forelse
(
$products
as
$product
)
2
...
3
@
empty
4
...
5
@
endforelse
Para iterar as produtos vindas do controller, neste caso o @forelse
fará o seguinte:
Se não existir produtos, ele cairá na execução do bloco @empty
, onde adicionamos um tr
com td
e a mensagem Nada Encontrado!
. Veja o bloco do @empty
abaixo:
1
@
empty
2
<
tr
>
3
<
td
colspan
=
"4"
>
Nada
encontrado
!</
td
>
4
</
tr
>
5
@
endforelse
Existindo produtos, que é o nosso caso, teremos a exibição da tabela com os produtos corretamente exibida. Vamos analisar o bloco do @forelse
.
Veja somente ele abaixo:
1
...
2
@
forelse
(
$products
as
$product
)
3
<
tr
>
4
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
id
}}
</
td
>
5
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
name
}}
</
td
>
6
<
td
class
=
"px-4 py-2 text-normal text-left"
>
7
@
if
(
$product
->
is_active
)
8
<
span
class
=
"p-1 rounded bg-green-500 text-green-900"
>
Ativo
</
span
>
9
@
else
10
<
span
class
=
"p-1 rounded bg-red-500 text-red-900"
>
Inativo
</
span
>
11
@
endif
12
</
td
>
13
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$product
->
created_at
->
format
(
\
14
'
d
/
m
/
Y
H
:
i
'
)
}}
</
td
>
15
<
td
class
=
"w-[15%] px-4 py-2 text-normal text-left"
>
16
<
div
class
=
"flex justify-around gap-x-2"
>
17
<
a
href
=
"{{ route('products.show', ['product' => $product->id]) }}"
18
class
=
"px-4 py-2 bg-blue-700 border border-blue-900 rounded text\
19
-white font-thin"
>
20
Editar
21
</
a
>
22
<
form
action
=
"{{ route('products.destroy', ['product' => $product->i\
23
d]) }}"
method
=
"post"
>
24
@
csrf
25
@
method
(
'
DELETE
'
)
26
<
button
type
=
"submit"
27
class
=
"px-4 py-2 bg-red-700 border border-red-900 rounded te\
28
xt-white font-thin"
>
Remover
</
button
>
29
</
form
>
30
</
div
>
31
</
td
>
32
</
tr
>
33
@
empty
34
...
O que vale destacar aqui é a utilização da diretiva de controle condicional para exibição da mensagem Ativo
ou Inativo
, usei também, no campo da data de criação da postagem, o método format a parti da propriedade created_at
para formatar a data a lá BR.
Nota: Os campos created_at e updated_at, internamente no Laravel, sempre recebem uma instância do pacote chamado de Carbon, que é um pacote para manipulação de datas e vêm junto no Laravel, apesar de não ser do framework. Com isso podemos formatar estas duas datas chamando o método format diretamente da propriedade, exemplo
{{ $produto->created_at->format('d/m/Y H:i:s') }}
.
Temos ainda, para a coluna de Ações, o botão de edição onde linkamos a rota de edição usando o apelido dela por meio da função helper route
e passando o id dinâmico do produto como segundo parâmetro.
Sobre o botão da remoção do produto, apenas peguei o form que tinhamos criado para remoção do produto no último capítulo.
Veja o resultado da nossa tela de listagem das postagens:
Antes de trabalharmos a paginação, na verdade exibi-la em nossa tela, vamos criar uma factory para nossos produtos. Neste ponto como já trabalhamos bastante com models e juntando com o conhecimento prévio de factories do capítulo 5, estamos bem para este ponto.
Execute a criação da factory por meio do comando abaixo:
1
php artisan make:factory ProductFactory
Em sua classe ProductFactory
adicione a definição da geração dos dados fakes. Segue o exemplo abaixo:
1
public
function
definition
()
:
array
2
{
3
$
name
=
fake
()
->
words
(
3
,
true
);
4
return
[
5
'name'
=>
$
name
,
6
'description'
=>
fake
()
->
sentence
,
7
'body'
=>
fake
()
->
paragraphs
(
3
,
true
),
8
'price'
=>
fake
()
->
randomFloat
(
2
,
10
,
3000
)
*
100
,
9
'in_stock'
=>
rand
(
30
,
1000
),
10
'slug'
=>
str
(
$
name
)
->
slug
(),
11
'is_active'
=>
rand
(
0
,
1
)
12
];
13
}
Os métodos vindos do fake são bem intuitivos e todos que uso vêm a partir do helper fake
, para randomizar os inteiros do stock e is_active usei o rand do próprio PHP.
Um ponto a comentar é que pro slug usei o helper str
do Laravel que me permite, mediante o passar de uma string nele, manipular de diversas formas a string em questão. Aqui na factory uso para gerarmos o slug baseado no nome randômico do produto.
Antes de usarmos nossa factory precisamos configurar ela em nosso model. Um ponto importante pro seu futuro, caso queira poupar este trabalho que faremos abaixo você pode no futuro gerar seu model junto de uma factory e ele já vir configurado. Para isso basta informar o parâmetro -f
na geração do model:
1
php artisan make:model <SEU_MODEL> -f
Caso queira gerar uma migração junto, você pode informar o parâmetro -m
ou aninhar na geração da factory junto a geração do model. Exemplo:
1
php artisan make:model <SEU_MODEL> -fm
Como geramos o model sem associação com factories precisamos configurar e é bem simples na verdade. Em seu model Product, importe a trait de factories como abaixo:
1
use Illuminate\Database\Eloquent\Factories\HasFactory;
E utilizar a trait em seu model Product:
1
use HasFactory;
Nosso model Product até o momento está assim:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Model
;
6
use
Illuminate
\Database
\Eloquent
\Factories
\HasFactory
;
7
8
class
Product
extends
Model
9
{
10
use
HasFactory
;
11
12
protected
$
fillable
=
[
13
'name'
,
14
'description'
,
15
'body'
,
16
'slug'
,
17
'is_active'
,
18
'in_stock'
,
19
'price'
20
];
21
}
Feito isso vamos modificar nosso seeder para chamarmos a factory ProductFactory.
Para usarmos nossa factory vamos adicioná-la, tal qual fizemos com a UserFactory, lá no ProductsTableSeeder
.
Veja como ficou o método run:
1
public
function
run
():
void
2
{
3
//
DB
:
:
table
(
'products'
)
->
insert
(
[
4
// 'name' => 'Primeiro Produto',
5
// 'description' => 'Produto teste com seeds',
6
// 'body' => 'Conteúdo do Produto',
7
// 'in_stock' => 34,
8
// 'price' => (14.99 * 100),
9
// 'is_active' => 1,
10
// 'slug' => 'primeira-produto'
11
// ]);
12
13
\
App
\
Models
\
Product
::factory
(
30
)
->
create
();
14
}
Coloquei para gerar 30 produtos aleatórios, a quantidade aqui fica a seu critério. Com isso temos dados suficientes para vermos e testarmos a paginaçõa na tela de Listagem.
Como usamos o método paginate
lá no nosso controller, podemos por meio da coleção de produtos que recebemos na view, dentro da variável $products
, exibir a paginação de forma bem simples e direta!
Adicione o trecho abaixo, logo após o fechamento da tag table
da sua view index.blade.php
:
1
<div
class=
"w-full mt-10"
>
2
{{
$
products-
>
links
()
}}
3
</div>
O seguinte trecho exibirá nosso html de navegação entre cada tela da paginação, veja o resultado obtido:
Simples assim, agora já temos uma paginação tanto nos dados vindos do banco quando na exibição dos links da navegação de forma simples e direta!
PS.: No Laravel 8 o layout padrão do paginate mudou de Bootstrap para um framework CSS chamado de TailwindCSS. Caso esteja usando bootstrapcss no estilo da paginação, procure o arquivo app/Providers/AppServiceProvider.php e no método boot deste arquivo, adicione o seguinte trecho:
Paginator::useBootstrap();
.Não esqueça de importar o Paginator:
use Illuminate\Pagination\Paginator;
Desta forma o paginator irá usar o layout com bootstrap ao invés do tailwindcss e o
AppServiceProvider
disponibiliza esta config de forma global para nós.
Neste capítulo nós fechamos o ciclo MVC iniciado lá no começo do livro. Além de termos tido contato com diversas diretivas do blade, que juntadas as que vimos aqui já te dão uma excelente base de aprendizado e aplicação!
É claro que ainda veremos mais opções do blade mas isso vou diluindo no decorrer dos próximos capítulos.
Agora que temos nosso CRUD completo, em se tratando de persitência no banco e retornando para as views os resultados, além de termos dado um tapa nas nossas views, vamos para o próximo capítulo onde vamos trabalhar os relacionamentos de banco de dados tanto na base de dados como em nossos models.
Vamos lá!
Olá, tudo bem? Espero que sim!
Vamos começar agora um trecho que é necessário ter bastante atenção e cuidado, vamos falar sobre relacionamentos de uma base relacional (nosso banco de dados) e a representação destes relacionamentos do ponto de vista de Objetos, representadados por nossos Models junto com o Eloquent.
Como venho fazendo vamos mostrar os relacionamentos e suas nuances aplicados em nossa Loja, onde teremos as seguintes representações:
Vamos iniciar pelo primeiro relacionamento, o 1:N entre Produto e Fotos.
Primeiramente vamos criar nosso model para a tabela de Fotos do Produto. Para isso crie o model, com uma factory e uma migration junto:
1
php artisan make:model ProductPhoto -mf
Feito isso, adicione na migration gerada <timestamp>-create_product_photos_table
as seguintes colunas:
1
$
table
->
foreignId
(
'product_id'
)
->
constrained
()
->
cascadeOnDelete
();
2
$
table
->
string
(
'photo'
);
Já execute as migrações assim que incluir as colunas da tabela product_photos, que será criada com base nessa migration recém adicionada no projeto.
Acima destaco apenas a primeira linha, onde criamos nossa coluna para referência da chave estrangeira que apontará para a tabela de produtos. Lembrando que se você chegou neste ponto em sua carreira o banco de dados não deve ser problema mais para você!
Ao definirmos a coluna product_id
como foreignId automaticamente o laravel colocará ela como bingInteger & unsigned. O método constrained
fará a criação da chave estrangeira adicionando a referência para a coluna id
lá da tabela products
.
O Laravel buscará com base no parâmetro do foreignId o nome da tabela, ignorando o _id
e buscando a tabela no plura, no caso products e neste caso ela satisfaz pois o nome da nossa tabela de fato é products
. Caso tenha uma tabela que foge dessa convenção pode informar como primeiro parâmetro do método constrained
e tendo a coluna remota diferente de id pode informá-la no segundo parâmetro do mesmo método.
O método, aninhado, cascadeOnDelete
indica que ao removermos um produto seus dados associados serão apagados. Essa estratégia é uma das opções ligadas a chave estrangeira e não é uma verdade pois dependerá muito da sua regra esse tipo de comportamento.
Com estas configurações definidas na tabela product_photos
vamos mapear esse relacionamento do ponto de vista dos models.
Primeiramente vamos criar o método do ponto de vista de Product em relação ao model ProductPhoto. Veja o método abaixo adicionado ao model Product
:
1
public
function
photos
():
HasMany
2
{
3
return
$this->hasMany(
ProductPhoto
:
:
class
);
4
}
Não esqueça de importar o use Illuminate\Database\Eloquent\Relations\HasMany;
.
Definimos o método acima para representar a ligação do ponto de vista de Product, ponto de vista esse que indica que o produto pode ter 1 ou mais fotos relacionadas a ele.
O método hasMany
indicará essa ligação e por padrão e baseada no nome do model buscará a coluna, em product_photos, chamada de product_id
por isso criamos ela com este padrão inclusive.
Se por ventura você usou um nome de coluna diferente, que representa a referência na sua tabela. Por exemplo, não usou o product_id
mas sim produtos_id
. Neste caso você precisa informar para o Eloquent o nome da coluna, veja um trecho exemplo abaixo:
1
return
$
this-
>
hasMany
(
ProductPhoto
::
class
,
'produtos_id'
);
Agora como digo que o model ProductPhoto
pertence a um produto? Vamos lá no model ProductPhoto
e defini o método de ligação:
1
public
function
product
():
BelongsTo
2
{
3
return
$this->belongsTo(
Product
:
:
class
);
4
}
Não esqueça do import: use Illuminate\Database\Eloquent\Relations\BelongsTo;
e já que estamos aqui adicone em ProductPhoto a propriedade $fillable como abaixo:
1
public $fillable = ['photo'];
Pontos importantes, aqui em ProductPhoto
adicionei um método de ligação que indica que este model pertence(belongs) ao(to) model Product
. O método belongsTo resolverá o nome da coluna referência por meio do nome do método de ligação.
Como o método de ligação é product
o Laravel entenderá que o nome da coluna é product_id
, se por ventura o método fosse produto ele entenderia que a coluna é produto_id
. Se o nome da sua coluna está diferente você pode manipular tanto informando o nome do segundo parâmetro do método belongsTo
(chamando dentro do método product
que é nosso método de ligação) ou manipular o nome do método de ligação de forma que o laravel manipule e coloque o nome da coluna corretamente.
Da forma que mapeamos já estamos respeitando, em ambos os lados, as configurações de relacionamento corretamente.
As definições do ponto de vista do Model são estas, agora, para entendermos melhor vamos realizar algumas queries para testarmos esta relação.
Antes de fazermos algumas queries neste relacionamento, precisamos criar alguns dados para tal. Neste ponto precisamos entender como gerar dados fakes relacionados, no caso criar X Produtos com X Fotos por exemplo.
Por enquanto não trabalharemos com upload de arquivos, manteremos as fotos somente via dados fake, então, em seu ProductPhotoFactory adicione o seguinte conteúdo no método definition
:
1
return
[
2
'photo'
=>
fake
()
->
imageUrl
(
width
:
480
,
height
:
480
)
3
]
;
Na web temos diversos sites que permitem que chamemos links com configurações de larguraxaltura e estes links já retorna uma imagem fake para gente. Usando o método imageUrl
, do faker, teremos isso de forma facilitada.
Uma vez definido a factory de photos, vamos alterar nosso ProductTableSeeder. Veja o código abaixo:
1
\
App
\
Models
\
Product
::
factory
(
30
)
2
-
>
hasPhotos
(
5
)
//
linha
adicionada
3
-
>
create
();
Perceba que adicionei o método hasPhotos
indicando que quero criar 5 fotos e essas fotos já associadas ao produto que será criado. Perceba que o nome do método hasPhotos
, tirando o has, respeita exatamente o nome de ligação dentro do model Product.
O laravel entenderá e buscará por este método de ligação para realizar a criação dos dados associados e como estamos no contexto de factories utilizará a factory, também, do ProductPhoto.
Vamos resetar os dados da nossa base e já executar as migrações juntos:
1
php artisan migrate:refresh --seed
Resultado:
Agora que temos o mapeamento 1:N~N:1(Um pra Muitos & Muitos pra Um) tanto nos models quanto na base e temos os dados associados. Testaremos e entenderemos alguns acessos via os métodos de ligação.
Primeiramente vamos realizar algumas queries via model Product. Por exemplo caso queiramos pegar as fotos do Produto, podemos fazer como abaixo:
1
$
product
=
\
App
\
Models
\
Product
::
find
(
2
);
2
$
product-
>
photos
;
Para melhor mostrar esses testes e queries vou utilizar um terminal inteligente que vêm no artisan chamado de Tinker
. O tinker permite que façamos diversas interações com nossa aplicação como ponto de teste.
Para acessar vamos executar o comando abaixo em nosso terminal ou cmd:
1
php artisan tinker
Veja:
Peguemos o trecho anterior e executemos dentro desse cli inteligente:
Então peguemos agora as fotos do produto:
Ao acessarmos, a partir de um model buscado na base, a propriedade que têm o mesmo nome do método de ligação o Laravel já retorna os objetos associados a este model depedendo da ligação mapeada.
Aí comentado acima está uma grande diferença entre acessar a ligação como propriedade e como o método, método este que está lá mapeado.
Se eu chamar o método o retorno será, tal qual está mapeado no próprio model e esse comportamento serve pra todos os tipos de ligação. Comportamento de acessar a ligação como método ou propriedade.
Veja:
Acessando como atributo temos acesso a collection, que já traz os dados associados. Já acessando o método, que de fato está definido no model, temos o retorno do objeto HasMany.
Caso você precise realizar mais queries a partir desta relação lembre sempre de chamar o método de ligação, isso vale pra toda as relações via Model.
Podemos verificar, também quantos dados temos associados. Exemplo:
1
$
product
->
photos
->
count
();
Acima chamamos o count existente na collection, esse método também existe no query builder então podemos chamá-lo também a partir do método:
1
$
product
->
photos
()
->
count
();
Do ponto de vista de ProductPhoto podemos trabalhar a ligação com o mesmo pensamento, só têm uma diferença nos dados. Como estamos ligados a 1 produto, os retornos serão o dado pai populado em um model Product e não uma collection.
Veja:
1
$
productPhoto
=
\
App
\
Models
\
ProductPhoto
::
find
(
1
);
2
3
$
productPhoto-
>
product
;
Destaco na foto apenas a chamada do produto pai da foto de id 1:
O pensamento acessar o método de ligação como propriedade ou método é o mesmo aqui também.
Quando você precisar realizar queries na relação lembre sempre de chamar o método de ligação.
Por hora é isso que quero mostrar aqui no mapeamento 1:N, vamos conhecer o MuitosPraMuitos(M:M).
Ah, pra sair do tinker
digite quit
e dê ENTER
. rsrsrs…
Agora neste ponto vamos começar a parte da relação de Muitos para Muitos, a relação aqui será entre Produtos e Categorias. Primeiramente vamos criar o model Category(Categoria), suas migrations e todo o CRUD deste participante.
Podemos já começar criando nosso model com todo o aparato necessário de uma vez só, criar o model, a migration e o controller como recurso de uma vez só!
Execute na raiz do seu projeto o comando abaixo:
1
php artisan make:model Category -mcr
Considerações:
-m
: cria a migration deste model;-c
: cria o controller deste model;-r
: cria o controller como recurso para este model.PS.: Você pode passar os parâmetros separados, ou como na imagem juntos.
Veja o resultado:
Obs.: O comando irá criar o controller na pasta de Controllers normalmente, apenas mova este controller para a pasta Admin
e corrija o namespace, de namespace App\Http\Controllers;
para namespace App\Http\Controllers\Admin;
e adicione o import do controller base: use App\Http\Controllers\Controller;
.
Veja o controller na íntegra depois das observações acima:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Category
;
7
use
Illuminate
\Http
\Request
;
8
9
class
CategoryController
extends
Controller
10
{
11
/**
12
*
Display
a
listing
of
the
resource
.
13
*/
14
public
function
index
()
15
{
16
//
17
}
18
19
/**
20
*
Show
the
form
for
creating
a
new
resource
.
21
*/
22
public
function
create
()
23
{
24
//
25
}
26
27
/**
28
*
Store
a
newly
created
resource
in
storage
.
29
*/
30
public
function
store
(
Request
$
request
)
31
{
32
//
33
}
34
35
/**
36
*
Display
the
specified
resource
.
37
*/
38
public
function
show
(
Category
$
category
)
39
{
40
//
41
}
42
43
/**
44
*
Show
the
form
for
editing
the
specified
resource
.
45
*/
46
public
function
edit
(
Category
$
category
)
47
{
48
//
49
}
50
51
/**
52
*
Update
the
specified
resource
in
storage
.
53
*/
54
public
function
update
(
Request
$
request
,
Category
$
category
)
55
{
56
//
57
}
58
59
/**
60
*
Remove
the
specified
resource
from
storage
.
61
*/
62
public
function
destroy
(
Category
$
category
)
63
{
64
//
65
}
66
}
Por enquanto vamos deixá-lo assim, vamos da uma atenção lá para nossa migration, acesse a pasta de migrations e abra o arquivo: <ARQUIVO_TIMESTAMP>_create_categories_table.php
.
Adicione o seguintes campos abaixo:
1
$
table
->
string
(
'name'
);
2
$
table
->
string
(
'description'
)
->
nullable
();
3
$
table
->
string
(
'slug'
);
Como você pode ver o Laravel pegou o nome do nosso model e já preparou nossa migration para o plural, pela convenção já comentada aqui.
Neste caso teremos a tabela categories
com os campos:
Após as alterações que comentei anteriormente, vamos criar uma nova migration, a migração para nossa tabela pivot ou intermediária, para a relação M:M.
Muitos para Muitos permite que um produto tenha várias categorias e uma categoria tenha vários produtos, desta forma precisamos de uma tabela intermediária para manter esta relação/ligação.
Execute em seu terminal o comando abaixo para criação da migração para esta tabela intermediária:
1
php artisan make:migration create_category_product_table
Veja o conteúdo na íntegra da migração já com as configs da tabela:
1
<
?
php
2
3
use
Illuminate
\Database
\Migrations
\Migration
;
4
use
Illuminate
\Database
\Schema
\Blueprint
;
5
use
Illuminate
\Support
\Facades
\Schema
;
6
7
return
new
class
extends
Migration
8
{
9
/**
10
*
Run
the
migrations
.
11
*/
12
public
function
up
():
void
13
{
14
Schema
::
create
(
'category_product'
,
function
(
Blueprint
$
table
)
{
15
$
table
->
id
();
16
17
$
table
->
foreignId
(
'category_id'
)
->
constrained
()
->
cascadeOnDelete
();
18
$
table
->
foreignId
(
'product_id'
)
->
constrained
()
->
cascadeOnDelete
();
19
20
$
table
->
timestamps
();
21
});
22
}
23
24
/**
25
*
Reverse
the
migrations
.
26
*/
27
public
function
down
():
void
28
{
29
Schema
::
dropIfExists
(
'category_product'
);
30
}
31
};
Nesta tabela precisamos apenas das referências, uma apontando para o produto e o outro para a categoria, além das chaves estrangeiras referenciando cada tabela para o id de cada uma mediante uso dos métodos que já conhecemos.
Agora podemos executar estas migrações na base de dados, vamos ao terminal executar o comando abaixo:
1
php artisan migrate
Veja o resultado:
Com as tabelas no ponto e definidas, como podemos mapear nossa relação do ponto de vista do model?
Existe no Eloquent o método para esta relação, o belongsToMany
(traduzindo Pertence a Muitos) que estará nos dois models por conta da tabela intermediária.
Então, vamos adicionar o método abaixo dentro do nosso model Product
:
1
public
function
categories
()
2
{
3
return
$this->belongsToMany(
Category
:
:
class
);
4
}
E no model Category
adicione o método abaixo:
1
public
function
products
()
2
{
3
return
$this->belongsToMany(
Product
:
:
class
);
4
}
O método belongsToMany
nos permite o mapeamento na relação de muitos p/ muitos entre os dois models, como ambos apontam para a mesma tabela, o método de ambos os lados também é o mesmo.
Aqui vale um aprendizado e um adendo, o segundo parâmetro do método belongsToMany
é o nome da tabela intermediária e o Laravel já resolve o nome dela procurando uma tabela com o nome de ambos os models no singular e em ordem alfabética. Então seria Category, Product ficando assim: category_product
exatamente como colocamos.
Caso sua tabela fuja desse padrão informe o nome dela como segundo parâmetro do método belongsToMany
.
Antes de criarmos nosso produto com suas categorias, vamos criar rapidamente o CRUD de categorias com alguns detalhes a mais a serem adicionados também em nosso CRUD de produtos melhorando assim nossa aplicação.
Vamos criar nosso CRUD de categorias, praticamente algo que já conhecemos então irei organizar as coisas um pouco diferente.
Agora vamos ao nosso controller, irei colocar método por método do controller de Categorias, o CategoryController
, para vermos os pontos importantes sobre cada método. Ao final coloco ele na íntegra para conferência.
Vamos ao primeiro método, o método index e o construtor:
1
public
function
__construct
(
private
Category
$
category
)
2
{
3
$
this
->
category
=
$
category
;
4
}
5
6
/**
7
*
Display
a
listing
of
the
resource
.
8
*/
9
public
function
index
()
10
{
11
$
categories
=
$
this
->
category
->
paginate
(
15
);
12
13
return
view
(
'categories.index'
,
compact
(
'categories'
));
14
}
O primeiro ponto importante é a utilização do nosso model como dependência do construtor da classe controller. Com isso teremos por meio do container de serviços do Laravel a instância do nosso model sempre que nosso controller for chamado. Isso simplifica muito as coisas e nos ajuda a mantermos nosso controller mais focado.
Continuando para o método index
, a difernça direta aqui é que ao invés de chamar o paginate
assim Category::paginate(15)
, chamamos pelo atributo do controller, $category, que receberá a instância do nosso model.
O método a seguir é o create, que dispensa comentários até então, veja ele:
1
/**
2
* Show the form for creating a new resource.
3
*/
4
public
function
create()
5
{
6
return
view(
'categories.create'
)
;
7
}
Vamos continuando para o método store, onde de fato persistimos nossos dados, neste caso categoria. Veja ele e logo após os comentários:
1
/**
2
* Store a newly created resource in storage.
3
*/
4
public
function
store
(
Request
$
request
)
5
{
6
$
data
=
$
request
->
all
();
7
8
$
this
->
category
->
create
(
$
data
);
9
10
return
redirect
()
->
route
(
'categories.index'
);
11
}
Aqui mais uma referência ao nosso atributo, que contêm a instância do nosso model. Usamos o mass assignment que já conhecemos.
O método show têm um pequeno detalhe que preciso comentar, veja:
1
/**
2
* Display the specified resource.
3
*/
4
public
function
show
(
Category
$
category
)
5
{
6
return
redirect
()
->
route
(
'categories.edit'
,
[
'category'
=>
$
category
->
id
]);
7
}
Perceba de cara o parâmetro tipado do método show
, tipado para aceitar apenas instâncias do nosso model Category.
Um ponto importante aqui é que quando temos um parâmetro tipado assim, o Laravel automaticamente vai converter este parâmetro para um objeto populado para o id informado via url, por isso que não estou realizando um find
ou findOrFail
aqui, porque quando chegarmos na execução deste método do controller, já teremos a instância do objeto Category
populada com os dados do banco, a este processo damos o nome de Route Model Biding.
O método show
apenas redireciona para a tela de edição que também vêm com a ideia de Route Model Biding
.
Veja nosso edit e também nosso update abaixo, o update já com o uso do Update em Massa, como já conhecemos também:
1
/**
2
* Show the form for editing the specified resource.
3
*/
4
public
function
edit
(
Category
$
category
)
5
{
6
return
view
(
'categories.edit'
,
compact
(
'category'
));
7
}
8
9
/**
10
* Update the specified resource in storage.
11
*/
12
public
function
update
(
Request
$
request
,
Category
$
category
)
13
{
14
$
data
=
$
request
->
all
();
15
16
$
category
->
update
(
$
data
);
17
18
return
redirect
()
->
route
(
'categories.edit'
,
[
'category'
=>
$
category
->
id
]);
19
}
A remoção/delete também segue o mesmo pensamento, veja:
1
/**
2
* Remove the specified resource from storage.
3
*/
4
public
function
destroy
(
Category
$
category
)
5
{
6
$
category
->
delete
();
7
8
return
redirect
()
->
route
(
'categories.index'
);
9
}
Perceba que por conta do Route Model Binding o passo de find
ou findOrFail
são cortados pois o Laravel já injetará o model populado no método do controller que recebe a tipagem do model no parâmetro que espera o valor dinâmico vindo da rota. Caso o model não seja encontrado o Laravel já lança o 404 NOT FOUND.
Não esqueça dos parâmetros convertidos automaticamente para o model populado com base no id, resolvido dinâmicamente pelo Laravel.
Ah, e não esqueça do atributo $fillable
lá no model Category, para o mass assignment:
1
protected $fillable = [
2
'name',
3
'description',
4
'slug'
5
];
Veja o controller CategoryController
na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Category
;
7
use
Illuminate
\Http
\Request
;
8
9
class
CategoryController
extends
Controller
10
{
11
public
function
__construct
(
private
Category
$
category
)
12
{
13
$
this
->
category
=
$
category
;
14
}
15
16
/**
17
*
Display
a
listing
of
the
resource
.
18
*/
19
public
function
index
()
20
{
21
$
categories
=
$
this
->
category
->
paginate
(
15
);
22
23
return
view
(
'categories.index'
,
compact
(
'categories'
));
24
}
25
26
/**
27
*
Show
the
form
for
creating
a
new
resource
.
28
*/
29
public
function
create
()
30
{
31
return
view
(
'categories.create'
);
32
}
33
34
/**
35
*
Store
a
newly
created
resource
in
storage
.
36
*/
37
public
function
store
(
Request
$
request
)
38
{
39
$
data
=
$
request
->
all
();
40
41
$
category
=
$
this
->
category
->
create
(
$
data
);
42
43
return
redirect
()
->
route
(
'categories.edit'
,
[
'category'
=>
$
category
->
id
]);
44
}
45
46
/**
47
*
Display
the
specified
resource
.
48
*/
49
public
function
show
(
Category
$
category
)
50
{
51
return
redirect
()
->
route
(
'categories.edit'
,
[
'category'
=>
$
category
->
id
]);
52
}
53
54
/**
55
*
Show
the
form
for
editing
the
specified
resource
.
56
*/
57
public
function
edit
(
Category
$
category
)
58
{
59
return
view
(
'categories.edit'
,
compact
(
'category'
));
60
}
61
62
/**
63
*
Update
the
specified
resource
in
storage
.
64
*/
65
public
function
update
(
Request
$
request
,
Category
$
category
)
66
{
67
$
data
=
$
request
->
all
();
68
69
$
category
->
update
(
$
data
);
70
71
return
redirect
()
->
route
(
'categories.edit'
,
[
'category'
=>
$
category
->
id
]);
72
}
73
74
/**
75
*
Remove
the
specified
resource
from
storage
.
76
*/
77
public
function
destroy
(
Category
$
category
)
78
{
79
$
category
->
delete
();
80
81
return
redirect
()
->
route
(
'categories.index'
);
82
}
83
}
Agora, vamos as views.
As views não têm novidades, mas antes, crie a pasta categories
, dentro da pasta views, e dentro os arquivos:
Com os conteúdos:
index.blade.php:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
div
class
=
"w-full mb-10 flex justify-between"
>
5
<
h2
class
=
"font-bold text-xl"
>
Categorias
</
h2
>
6
<
a
href
=
"{{ route('categories.create') }}"
7
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
8
font-thin"
>
Criar
Categoria
</
a
>
9
</
div
>
10
<
div
class
=
"w-full p-5"
>
11
<
table
class
=
"w-full"
>
12
<
thead
>
13
<
tr
class
=
"border-b border-gray-600"
>
14
<
th
class
=
"px-4 py-2 text-xl text-left"
>
#
</
th
>
15
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Categoria
</
th
>
16
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Criado
em
</
th
>
17
<
th
class
=
"px-4 py-2 text-xl text-left"
>
Ações
</
th
>
18
</
tr
>
19
</
thead
>
20
<
tbody
>
21
@
forelse
(
$categories
as
$category
)
22
<
tr
>
23
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$category
->
id
\
24
}}
</
td
>
25
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$category
->
na
\
26
me
}}
</
td
>
27
<
td
class
=
"px-4 py-2 text-normal text-left"
>
{{
$category
->
cr
\
28
eated_at
->
format
(
'
d
/
m
/
Y
H
:
i
'
)
}}
</
td
>
29
<
td
class
=
"w-[15%] px-4 py-2 text-normal text-left"
>
30
<
div
class
=
"flex justify-around gap-x-2"
>
31
<
a
href
=
"{{ route('categories.show', ['category' => \
32
$category->id]) }}"
33
class
=
"px-4 py-2 bg-blue-700 border border-blue-\
34
900 rounded text-white font-thin"
>
35
Editar
36
</
a
>
37
<
form
action
=
"{{ route('categories.destroy', ['categ\
38
ory' => $category->id]) }}"
39
method
=
"post"
>
40
@
csrf
41
@
method
(
'
DELETE
'
)
42
<
button
type
=
"submit"
43
class
=
"px-4 py-2 bg-red-700 border border-re\
44
d-900 rounded text-white font-thin"
>
Remover
</
button
>
45
</
form
>
46
</
div
>
47
</
td
>
48
</
tr
>
49
@
empty
50
<
tr
>
51
<
td
colspan
=
"4"
>
Nada
encontrado
!</
td
>
52
</
tr
>
53
@
endforelse
54
</
tbody
>
55
</
table
>
56
</
div
>
57
<
div
class
=
"w-full mt-10"
>
58
{{
$categories
->
links
()
}}
59
</
div
>
60
@
endsection
create.blade.php:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('categories.store') }}"
method
=
"post"
>
5
@
csrf
6
7
<
div
class
=
"w-full mb-6"
>
8
<
label
class
=
"block mb-2"
>
Nome
</
label
>
9
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border border-g\
10
ray-900"
value
=
"{{ old('name') }}"
>
11
</
div
>
12
13
<
div
class
=
"w-full mb-6"
>
14
<
label
>
Descrição
</
label
>
15
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border b\
16
order-gray-900"
17
value
=
"{{ old('description') }}"
>
18
</
div
>
19
20
<
div
class
=
"w-full mb-6"
>
21
<
label
>
Slug
</
label
>
22
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border border-g\
23
ray-900"
24
value
=
"{{ old('slug') }}"
>
25
</
div
>
26
27
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
28
hite font-thin"
>
Criar
Categoria
</
button
>
29
</
form
>
30
@
endsection
edit.blade.php:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('categories.update', ['category' => $category->id]) }}"
m
\
5
ethod
=
"post"
>
6
@
csrf
7
@
method
(
'
PUT
'
)
8
<
div
class
=
"w-full mb-6"
>
9
<
label
class
=
"block mb-2"
>
Nome
</
label
>
10
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border border-g\
11
ray-900"
12
value
=
"{{ $category->name }}"
>
13
</
div
>
14
15
<
div
class
=
"w-full mb-6"
>
16
<
label
>
Descrição
</
label
>
17
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border b\
18
order-gray-900"
19
value
=
"{{ $category->description }}"
>
20
</
div
>
21
22
<
div
class
=
"w-full mb-6"
>
23
<
label
>
Slug
</
label
>
24
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border border-g\
25
ray-900"
26
value
=
"{{ $category->slug }}"
>
27
</
div
>
28
29
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
30
hite font-thin"
>
Atualizar
31
Categoria
</
button
>
32
</
form
>
33
34
<
form
action
=
"{{ route('categories.destroy', ['category' => $category->id]) }}"
\
35
method
=
"post"
>
36
@
csrf
37
@
method
(
'
DELETE
'
)
38
<
button
type
=
"submit"
class
=
"px-4 py-2 bg-red-700 border border-red-900 roun\
39
ded text-white font-thin mt-8"
>
Remover
40
Categoria
</
button
>
41
</
form
>
42
@
endsection
Ah e não esqueça de linkar no menu do layout.blade.php
o menu de categorias, veja o trecho adicionado logo após o link de produtos:
1
<li
class=
"py-4"
>
2
<a
href=
"
{{
route
(
'categories.index'
)
}}
"
3
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-in-out durat\
4
ion-300"
>
Categorias</a>
5
</li>
Na tag do elemento ul
destes menus coloque mais uma classe chamada de flex para alinharmos os menus horizontalmente. Veja o ul completo:
1
<ul
class=
"flex"
>
2
<li
class=
"py-4"
>
3
<a
href=
"
{{
route
(
'products.index'
)
}}
"
4
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-in-out d\
5
uration-300"
>
Produtos</a>
6
</li>
7
<li
class=
"py-4"
>
8
<a
href=
"
{{
route
(
'categories.index'
)
}}
"
9
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-in-out d\
10
uration-300"
>
Categorias</a>
11
</li>
12
</ul>
Outro ponto que não podemos esquecer é a definição de rota para nossa área de categorias, no arquivo de rotas web.php adicione o trecho abaixo logo após o trecho das rotas de produto:
1
Route
::
resource
(
'categories'
,
\
App
\
Http
\
Controllers
\
Admin
\
CategoryController
::
class
);
Até o momento nossas rotas estão assim:
1
<?php
2
3
use
Illuminate\Support\Facades\Route
;
4
5
Route
::
get
(
'/'
,
function
()
{
6
return
view
(
'welcome'
);
7
});
8
9
Route
::
prefix
(
'admin'
)
->
group
(
function
()
{
10
11
Route
::
resource
(
'products'
,
\App\Http\Controllers\Admin\ProductController
::
class\
12
);
13
Route
::
resource
(
'categories'
,
\App\Http\Controllers\Admin\CategoryController
::
cl\
14
ass
);
15
});
Agora que nosso CRUD de categorias está pronto, faça alguns testes e crie algumas categorias para termos insumo no momento da inclusão das categorias para um produto via form.
Abaixo, deixo nosso controller ProductController
alterado com os pensamentos feitos no controller de categorias.
Como não criamos ele diretamente como recursos pelo terminal, ele vai está sem alguns comentários como vimos no CategoryController
mas isso são detalhes.
Veja ele na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Product
;
7
use
Illuminate
\Http
\Request
;
8
9
class
ProductController
extends
Controller
10
{
11
public
function
__construct
(
private
Product
$
product
)
{}
12
13
public
function
index
()
14
{
15
$
products
=
$
this
->
product
->
paginate
(
15
);
16
17
return
view
(
'products.index'
,
compact
(
'products'
));
18
}
19
20
public
function
create
()
21
{
22
return
view
(
'products.create'
);
23
}
24
25
public
function
store
(
Request
$
request
)
26
{
27
$
data
=
$
request
->
all
();
28
29
$
price
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
30
31
$
data
[
'in_stock'
]
=
48
;
//
está
fixo
mas
vamos
adiciona
-
lo
no
formulário
a
\
32
inda
33
$
data
[
'price'
]
=
$
price
*
100
;
34
$
data
[
'is_active'
]
=
true
;
35
36
$
product
=
$
this
->
product
->
create
(
$
data
);
37
38
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
->
id
]);
39
}
40
41
public
function
show
(
$
product
)
42
{
43
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
]);
44
}
45
46
public
function
edit
(
$
product
)
47
{
48
$
product
=
$
this
->
product
->
findOrFail
(
$
product
);
49
50
$
product
->
price
=
$
product
->
price
/
100
;
51
52
return
view
(
'products.edit'
,
compact
(
'product'
));
53
}
54
55
public
function
update
(
$
product
,
Request
$
request
)
56
{
57
//
Atualizando
com
mass
assignment
58
$
data
=
$
request
->
all
();
59
$
price
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
60
$
data
[
'price'
]
=
$
price
*
100
;
61
62
$
product
=
$
this
->
product
->
findOrFail
(
$
product
);
63
$
product
->
update
(
$
data
);
64
65
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
->
id
]);
66
}
67
68
public
function
destroy
(
$
product
)
69
{
70
$
product
=
$
this
->
product
->
findOrFail
(
$
product
);
71
$
product
->
delete
();
72
73
return
redirect
()
->
route
(
'products.index'
);
74
}
75
}
Não fiz a adição do Route Model Biding aqui no ProductController, caso queira praticar pode se basear no CategoryController para tal.
Aplicada as melhorias no controller ProductController
, vamos para à alterações necessárias para inserção da relação muitos para muitos entre Produtos e Categorias.
Para inserção de muitos para muitos, nós temos 3 métodos principais, o método attach
, o detach
e o sync
que particularmente gosto de utilizar. Utilizarei aqui o sync
mas darei uma rápida amostra do que os 2 outros fazem.
O método attach
recebe um array com os ids a serem incluídos na referência, se estivermos salvando do ponto de vista de Product os ids serão de categorias e vice-versa.
Veja o exemplo:
1
$
product
->
categories
()
->
attach
([
1
,
2
]);
Com o attach acima estou adicionando para um produto duas categorias, as de id 1 e 2 respectivamente. Se em uma tela de edição eu quiser remover a categoria 1 deste produto, eu posso utilizar o detach, como abaixo:
1
$
product
->
categories
()
->
detach
([
1
]);
Agora como o sync
funciona, ele é bem simples. Basicamente ele irá sincronizar as referências da ligação, por exemplo:
1
$
product
->
categories
()
->
sync
([
1
,
2
]);
Acima o sync
irá inserir as duas referências caso elas não existam na ligação, dentro da tabela intermediária. Uma vez salva, se eu utilizar o sync
novamente da maneira abaixo:
1
$
product
->
categories
()
->
sync
([
1
]);
Ele irá remover os ids que não estiverem no array informado a ele, neste caso, a categoria de id 2 será removida da ligação, então, no sync o que vale são os ids informados a ele.
Ainda é possível utilizar o método toggle, que remove os dados ligados que estiverem na ligação e adiciona os que não estiverem. Isso no modelo de liga-desliga de fato. No sync o que prevalece sempre é o que está no array passado ao sync.
Exemplo da chamada do toggle:
1
$
product
->
categories
()
->
toggle
([
1
,
2
,
3
]);
Vamos adicionar a possibilidade de adição de categorias em nossa criação e edição de produtos. O primeiro passo é mandarmos para as views de criação e edição nossas categorias.
Veja como ficou os métodos create
e edit
lá no ProductController
:
create
1
public
function
create
(
Category
$
category
)
2
{
3
$
categories
=
$
category
->
all
([
'id'
,
'name'
]);
4
5
return
view
(
'products.create'
,
compact
(
'categories'
));
6
}
edit
1
public
function
edit
(
$
product
,
Category
$
category
)
2
{
3
$
product
=
$
this
->
product
->
findOrFail
(
$
product
);
4
$
categories
=
$
category
->
all
([
'id'
,
'name'
]);
5
6
$
product
->
price
=
$
product
->
price
/
100
;
7
8
return
view
(
'products.edit'
,
compact
(
'product'
,
'categories'
));
9
}
Não esqueça de importar o model Category no ProductController
Agora mandamos para cada view as categorias existentes em nossa base, neste caso passei um array para o método all
que me retornará apenas o id e o nome de cada categoria, que é o que necessitamos pra este momento. A query final para esta busca de categorias seria algo como: select id, name from categories
.
Perceba também que injetei o model Category nos métodos create
e edit
, desta forma o Laravel me entregará uma instância de Category e posso trabalhar minhas queries como eu quiser, como fizemos com o método all
por exemplo.
Agora precisamos exibir essas categorias em nossos formulários de criação e edição do Produto. Adicione o trecho abaixo em ambos os formulários:
1
<
div
class
=
"w-full mb-6"
>
2
<
label
class
=
"w-full mb-4"
>
Categorias
</
label
>
3
4
<
div
class
=
"px-5 grid grid-cols-3"
>
5
@
foreach
(
$categories
as
$category
)
6
<
div
>
7
<
input
type
=
"checkbox"
name
=
"categories[]"
value
=
"{{ $category->id }\
8
}"
>
9
{{
$category
->
name
}}
10
</
div
>
11
@
endforeach
12
</
div
>
13
</
div
>
Este trecho exibirá diversas checkboxes com as categorias e seus ids como valores para cada checkbox.
Perceba um detalhe importante, o checkbox nos permite marcarmos quantos itens quisermos, neste caso como preciso mandar um array de ids(das categorias) para sincronizar, usei a notação no nome do campo com colchetes([]
) desta maneira: categories[]
. Assim enviaremos na requisição um array com os ids selecionados no campo categories
, vindo dos checkboxes.
Uma vez feita esta alteração, vamos adicionar a possibilidade de save da relação, adicionando assim as categorias do produto. Para isto, basta adicionarmos logo após a linha de criação e atualização dentro dos seus métodos (strore
, update
), a chamada do sync como vemos abaixo:
1
$
product
->
categories
()
->
sync
(
$
data
[
'categories'
]);
Veja o método store e o update (ProductController
) na íntegra, pós alteração:
store
1
public
function
store
(
Request
$
request
)
2
{
3
$
data
=
$
request
->
all
();
4
5
$
price
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
6
7
$
data
[
'in_stock'
]
=
48
;
//está fixo mas vamos adiciona-lo no formulário ainda
8
$
data
[
'price'
]
=
$
price
*
100
;
9
$
data
[
'is_active'
]
=
true
;
10
11
$
product
=
$
this
->
product
->
create
(
$
data
);
12
13
$
product
->
categories
()
->
sync
(
$
data
[
'categories'
]);
14
15
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
->
id
]);
16
}
update
1
public
function
update
(
$product
,
Request
$request
)
2
{
3
//Atualizando com mass assignment
4
$data
=
$request
->
all
();
5
$price
=
(
float
)
str_replace
([
'.'
,
','
],
['',
'.'
],
$data
['
price
']);
6
$data
['
price
']
=
$price
*
100
;
7
8
$product
=
$this
->
product
->
findOrFail
(
$product
);
9
$product
->
update
(
$data
);
10
11
$product
->
categories
()
->
sync
(
$data
['
categories
']);
12
13
return
redirect
()
->
route
('
products
.
edit
',
['
product
'
=>
$product
->
id
]);
14
}
Desta forma já adicionamos a possibilidade de inserção e edição das categorias para o produto.
Agora precisamos selecionar as categorias do produto na edição, para sabermos quais as categorias deste produto quando entrarmos na tela de edição.
Como temos uma relação de muitos para muitos, se acessarmos por exemplo:
1
$
product
->
categories
;
Teremos uma Collection
, como vimos anteriormente, com as categorias existentes para o produto buscado. Essa Collection nos permite, dentre outras coisas, verificar se determinado Objeto existe dentro da coleção, por meio do método contains
.
O que precisamos é marcar com checked
cada input que bater com a categoria existente na relação, dentro do loop de categorias teremos cada categoria em questão, o que precisamos fazer é passar para o método contains
cada linha do loop, para que o Laravel verifique se aquela categoria existe na relação do produto, que está aberto na tela de edição!
Vamos a alteração para entedermos melhor. Na view de edição (edit.blade.php
) de produto: o que está asssim, dentro do loop de categorias:
1
<div>
2
<input
type=
"checkbox"
name=
"categories[]"
value=
"
{{
$
category-
>
id
}}
"
>
3
{{
$
category-
>
name
}}
4
</div>
Ficará assim:
1
<div>
2
<input
type=
"checkbox"
name=
"categories[]"
value=
"
{{
$
category-
>
id
}}
"
@checked(\
3
$product-
>
categories->contains($category))>
4
5
{{
$
category-
>
name
}}
6
</div>
Perceba que no input checkbos usei uma diretiva do Laravel, a @checked
, que me permite facilmente habilitar ou não o check para o inpit em questão e isso baseado no valor passado como parâmetro pro @checked
que deve ser um teste booleano.
Como estamos fazendo o laço nas categorias, em cada linha teremos o objeto Category populado com a categoria em questão. Com isso podemos verificar se na relação, do Produto editado, existe aquele objeto disponível na relação usando o método contains
.
Veja melhor a linha:
1
@checked
(
$
product
->
categories
->
contains
(
$
category
))
Se a categoria existir na ligação com o Produto, o contains
retornará verdadeiro, dando o check no nosso input. Selecione uma categoria e atualize seu produto, verifique que agora a(s) categoria(s) pertencentes ao produto estará ou estarão checkada(s).
Na tela de inserção só precisamos selecionar as categorias que desejarmos que elas já serão salvas pelo
sync
e isso já fizemos. Faça seus testes!
Com isso chegamos ao ponto final proposto aqui para este capítulo.
O conteúdo abordado aqui foi bem denso e nos custou bastante alteração, entretanto, agora temos um projeto bem mais robusto e mais organizado do ponto de vista de execuções, relação e organização que ainda pode melhorar mais!
No próximo capítulo vamos conhecer a parte que compete a autenticação e como podemos privar o acesso aos nossos CRUDs a apenas usuários autenticados.
Até lá!
Neste capítulo vamos criar a autenticação para acesso ao painel administrativo. O mais interessante é que o Laravel já vêm com praticamente tudo pronto para criarmos esta autenticação de forma rápida e produtiva quer seja codificando quer seja usando paineis chamados de Starter Kits.
Na versão 12 o Laravel simplificou a quantidade de Starter Kits disponíveis deixando apenas um de forma visual mas com 3 opções de escolha com comentei lá no ínicio. O escopo deste livro não abordará esse Starter Kit com algum stack pois requer conhecimentos que não iremos abordar aqui neste volume, então criaremos nossa autenticação mas usando tudo que o Laravel têm que serve de automação para este fim, autenticar usuários.
O Laravel já têm todo o aparato para criarmos ou, melhor, habilitarmos a parte de auth no projeto. Internamente o framework possui o conceito de Guards. Guards servem para gerenciar a parte de autenticação de forma guiada a driver, onde poderemos ter guards para o gerenciamento de autenticação em APIs, na área Web e se necessitarmos de algo customizados podemos criar nosso próprio Guard customizado.
O framework também já traz toda a parte de autenticação configurada para o model user e os arquivos referentes a parte de configurações ligadas a autenticação se encontram em config/auth.php
. Neste arquivo encontramos o drive Guard padrão, que seria o web, e onde o Laravel busca sua fonte de dados para validar o usuário que está tentando autenticar!
Com essas ponderações vamos falar alguns detalhes sobre sessions no Laravel para iniciarmos nossa autenticação.
Imagino que session não seja problema para você, uma vez que Session aprendemos lá no ínicio com PHP.
Trabalhar com sessions no Laravel é algo bem bem tranquilo e para isso temos um helper que nos auxilia, o helper: session
.
O helper session nos auxilia a salvar, resgatar e atualizar os dados de sessão e possui alguns métodos para isso.
Por padrão o drive de armazenamento de sessão é o database. Os dados de sessão são armazenado na tabela sessions.
O Laravel suporta diversos drivers para sessão, abaixo listo alguns deles:
storage/framework/sessions
;Existem outros suportes mas aqui listei os principais. Vamos as manipulações.
Podemos colocar dados na sessão de duas formas. Primeira:
1
session(['key' => 'value']);
ou podemos chamar o helper session sem parâmetros, com isso teremos uma instância de Illuminate\Session\SessionManager
e com isso podemos chamar o método de gerenciamento desejado:
1
session
()
->
put
(
'key'
,
'value'
);
Podemos ainda armazenar arrays na sessão e lidar com a notação de .
para salvar dados para chaves específicas. Veja:
1
session
()
->
put
(
'user'
,
[
'name'
=>
'Teste'
]);
Posso adicionar uma nova chave ao user da sessão:
1
session
()
->
push
(
'user.email'
,
'email@email.com'
);
Acima estamos adicionando para o array de user na sessão a chave email e o email deste usuário.
Para recuperar valores da sessão podemos informar a chave diretamente no helper session
:
1
session('user');
ou podemos chamar o método get
a partir da instância de SessionManager
:
1
session
()
->
get
(
'user'
);
Podemos ainda recuperar o valor da sessão e já remover ele na mesma chamada, com o pull
:
1
session
()
->
pull
(
'user'
,
'posso_passar_um_valor_default_de_retorno_aqui_caso_a_chave_\
2
nao_exista'
);
Se você estiver trabalhando com contadores na sessão o session também possui métodos de incremento e decremento:
1
session
()
->
increment
(
'contador);
1
session
()
->
decrement
(
'contador);
Esses dois métodos possuem um segundo parâmetro caso seu incremento e decremento precisem ser modificados em >=2
.
Removendo uma chave específica da sessão:
1
session
()
->
forget
(
'key'
);
Ou uma lista de chaves:
1
session
()
->
forget
([
'key1'
,
'key2'
]);
Ou ainda, podemos limpar toda a sessão:
1
session
()
->
flush
();
Podemos regerar o ID da sessão também, para evitar problemas como session fixation
. Para isso você pode usar como abaixo:
1
session
()
->
regenerate
();
E podemos ainda regerar o ID da sessão removendo todos os dados com o:
1
session
()
->
invalidate
();
Existe uma possibilidade nas sessões do Laravel no qual podemos armazenar dados que serão removidos assim que forem lidos. Esse mecanismo via session é o que chamamos de flash e serve perfeitamente para, dentre outras coisas, armazenamos mensagens de execução entre uma URL e outra como forma de avisar o usuário do processo que ele está fazendo.
Para isso podemos usar da forma abaixo:
1
session
()
->
flash
(
'success'
,
'Produto Criado com Sucesso!'
);
Desta forma poderíamos utilizar para enviar mensagens de feedback, e faremos isso em breve!
Podemos acessar o session a partir da instância do Request, exemplo:
$request->session()...
. Caso precise utilizar a sessão a partir da Requisição atual.
Vamos começar gerando nosso controller para a autenticação, para isso execute o comando de geração abaixo:
1
php artisan make:controller Auth/LoginController
Adicione o método login neste controller. Segue abaixo e logo depois vamos as explicações:
1
public
function
login
(
Request
$request
)
:
RedirectResponse
2
{
3
$credentials
=
$request
->
only
('
email
',
'
password
');
4
5
if
(
!
Auth
::
attempt
(
$credentials
,
(
bool
)
$request
->
rememberme
))
{
6
abort
(
401
);
7
}
8
9
$request
->
session
()
->
regenerate
();
10
11
return
redirect
()
->
intended
(
route
('
products
.
index
',
absolute:
false
));
12
}
Em primeiro pegamos da requisição apenas o email e senha do usuário, algo que já conhecemos mas aqui como o método only pego apenas os campos que quero. Desta forma teremos na variável credentials o array como abaixo:
1
[
2
'email' => 'email@usuario.com',
3
'password' => 'XYZ'
4
]
Por meio da facade Auth
chamo o método attempt
que tentará fazer o login do usuário, login esse que será satisfeito se as credenciais estiverem corretas. O attempt
ainda aceita um segundo parâmetro, parâmetro booleano que indica o famoso Lembrar de mim, que também vamos adicionar no formulário de login mais abaixo.
No if checkamos o retorno do attempt, se verdadeiro passamos adiante mas caso falso(o usuário errou a sua senha ou email) abortamos a aplicação com o status code 401 (não autenticado).
Em breve faremos um redirect aí voltando para a tela de login com a mensagem de validação. Faremos essas melhorias nos capítulos posteriores.
As credenciais estando corretas nós regeramos o ID da sessão e redirecionamos o usuário para a URL que ele estava anteriormente, se ele veio de um REFER anterior claro, caso não tenha vindo o redirecionamento será para a listagem de Produtos no Admin.
Abaixo segue na íntegra nossa view de login, criei essa view no caminho abaixo dentro da pasta de views:
1
auth/login.blade.php
Segue o código:
1
<!DOCTYPE html>
2
<
html
lang
=
"en"
>
3
4
<
head
>
5
<
meta
charset
=
"UTF-8"
>
6
<
meta
name
=
"viewport"
content
=
"width=device-width, initial-scale=1.0"
>
7
<
meta
http-equiv
=
"X-UA-Compatible"
content
=
"ie=edge"
>
8
<
title
>
Logar - Painel Administrativo</
title
>
9
<
script
src
=
"https://cdn.tailwindcss.com"
></
script
>
10
</
head
>
11
12
<
body
class
=
"bg-fixed bg-gradient-to-b from-10% to-80% from-purple-800 to-black"
>
13
14
<
div
class
=
"w-full h-full absolute flex justify-center items-center"
>
15
16
<
div
class
=
"w-[390px] p-5 rounded bg-black shadow shadow-purple-600"
>
17
<
h2
class
=
"w-full text-center mb-4 text-2xl font-extrabold text-white"
>
E\
18
xperts Store
19
</
h2
>
20
<
form
action
=
"
{{
route
(
'login'
)
}}
"
method
=
"post"
>
21
@csrf
22
<
div
class
=
"w-full mb-6"
>
23
<
label
for
=
"email"
class
=
"text-white mb-2"
>
E-mail</
label
>
24
<
input
type
=
"email"
name
=
"email"
id
=
"email"
placeholder
=
"Seu e-m\
25
ail..."
required
26
class
=
"w-full p-2 rounded border border-gray-900"
>
27
</
div
>
28
29
<
div
class
=
"w-full mb-6"
>
30
<
label
for
=
"password"
class
=
"text-white mb-2"
>
Senha</
label
>
31
<
input
type
=
"password"
name
=
"password"
id
=
"password"
placeholder
\
32
="
Sua
senha
..."
33
class
=
"w-full p-2 rounded border border-gray-900"
>
34
</
div
>
35
36
<
div
class
=
"w-full mb-6 flex gap-x-4"
>
37
<
input
type
=
"checkbox"
name
=
"rememberme"
id
=
"rememberme"
class
=
"\
38
p-2 rounded border border-gray-900"
>
39
<
label
for
=
"rememberme"
class
=
"text-white"
>
Lembrar de mim</
label
>
40
</
div
>
41
42
<
button
43
class
=
"py-2 px-4 text-thin text-xl rounded border border-purple-\
44
900 bg-black text-purple-900 hover:bg-purple-900 hover:text-white transition ease-in\
45
-out duration-300"
>
Acessar</
button
>
46
</
form
>
47
48
</
div
>
49
</
div
>
50
</
body
>
51
52
</
html
>
Essa tela de login não estende de nenhum template então nela já contêm tudo que precisamos tanto os inputs como o apontar para o processamento desta tela e também seus estilos.
Sobre estilos até o momento, meu intuito não é abordar o TailwindCSS aqui mas em breve vamos ambientar ele no projeto na camada de assets frontend e lá explico os principais detalhes que o permeiam. Por hora veja os códigos e tente ir se familiarizando com as classes, inclusive se você já entende bem de CSS as classes são mais fáceis de entender.
Abaixo segue as duas rotas, tanto para view de login como para o processamento lá no controller AuthController:
1
Route
::
view
(
'login'
,
'auth.login'
)
-
>
name
(
'login.view'
);
2
Route
::
post
(
'login'
,
[
\
App
\
Http
\
Controllers
\
Auth
\
LoginController
::class
,
'login'
]
)
-
>
\
3
name
(
'login'
);
Usei o route view para exibir a tela de login e a definição pro post vindo do formulário irá pro nosso método login do AuthController. Coloque estas rotas antes ou depois do grupo do Admin, eu coloquei antes.
Para título de situação, nosso arquivo de rotas está assim até o momento:
1
<?php
2
3
use
Illuminate\Support\Facades\Route
;
4
5
Route
::
get
(
'/'
,
function
()
{
6
return
view
(
'welcome'
);
7
});
8
9
Route
::
view
(
'login'
,
'auth.login'
)
->
name
(
'login.view'
);
10
Route
::
post
(
'login'
,
[
\App\Http\Controllers\Auth\LoginController
::
class
,
'login'
])
->
\
11
name
(
'login'
);
12
13
Route
::
prefix
(
'admin'
)
->
group
(
function
()
{
14
15
Route
::
resource
(
'products'
,
\App\Http\Controllers\Admin\ProductController
::
class\
16
);
17
Route
::
resource
(
'categories'
,
\App\Http\Controllers\Admin\CategoryController
::
cl\
18
ass
);
19
});
Abaixo está nosso método logout
para colocarmos no AuthController:
1
public
function
logout
(
Request
$request
)
:
RedirectResponse
2
{
3
Auth
::
guard
('
web
')
->
logout
();
4
5
$request
->
session
()
->
invalidate
();
6
$request
->
session
()
->
regenerateToken
();
7
8
return
redirect
('
/
login
');
9
}
Perceba que uso o guard web para deslogar o usuário ativo nesse guard apenas, depois invlidamos nossa sessão e regeramos o token CSRF. Por fim mando o usuário de volta pra tela de login.
Processo bem simples, direto e usando os recursos do próprio Laravel.
Vamos criar nossa rota pro logout e linkar em nosso painel o link desse logout.
Rota, coloquei depois das rotas do login:
1
Route
::
post
(
'logout'
,
[
\
App
\
Http
\
Controllers
\
Auth
\
LoginController
::class
,
'logout'
]
)
\
2
-
>
name
(
'logout'
);
Abaixo adicione mais um link no nav do app.blade.php:
1
<li
class=
"py-4"
>
2
<form
action=
"
{{
route
(
'logout'
)
}}
"
method=
"post"
id=
"logout"
>
@csrf</form>
3
4
<a
href=
"#"
5
class=
"px-6 py-4 bg-red-800 hover:bg-red-950 text-white transition ease-in-o\
6
ut duration-300"
7
onclick=
"event.preventDefault(); document.querySelector('form#logout').submi\
8
t()"
>
Sair</a>
9
</li>
Aqui fiz uma pequena manipulação de Javascript para submeter nosso form e deslogar o usuário. Mantemos o form e o envio do logout via POST para termos o controle CSRF em nossa requisição.
Defini o form dentro da li
que contêm a ação pra rota logout e o envio via POST, dentro do controle apenas seto nossa diretiva @csrf
que já conhecemos. O link do menu será o gatilho deste form, por isso defino o atributo onclick
, que ao clicarmos no link ignoramos o comportamento deste elemento com event.preventDefault()
.
Ainda no onclick
, após ignorar o comportamento padrão do link, eu busco pelo form de id logout
e eu mesmo submeto ele, enviando assim nossa requisição post e mandando o controle CSRF
junto.
Desta forma teremos nossa autenticação funcionando corretamente, tanto login como logout agora e devidamente linkados.
Vamos agora colocar o controle de acesso ao paínel apenas a usuários autenticados, para isso vamos difinir o middleware auth em nosso grupo do Admin
mas antes o que seria esse tal de middleware?
Middlewares em aplicações web são pedaços de lógicas que são colocadas entre a requisição do usuário e a execução propriamente dita da nossa rota. O Laravel possui diversos middlewares padrão, por exemplo o próprio middleware de check do usuário autenticado.
Podemos criar nossos próprios middlewares e mais a frente aqui na jornada vamos fazer isso mas por hora adicione o método middleware, como abaixo, em seu grupo do admin:
1
->
middleware
(
'auth'
)
Ficando assim, o trecho do grupo admin no arquivo web.php da pasta routes:
1
Route
::
prefix
(
'admin'
)
-
>
middleware
(
'auth'
)
-
>
group
(
function
()
{
2
3
Route
:
:
resource
(
'products'
,
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::
class
\
4
);
5
Route
:
:
resource
(
'categories'
,
\
App
\
Http
\
Controllers
\
Admin
\
CategoryController
::
cl
\
6
ass
);
7
}
);
Desta forma tudo que estiver dentro do grupo será protegido e apenas poderá ser acessado por usuários logados/autenticados.
Teste seu acesso, a esta altura devemos ter pelo menos um usuário na base. Procure o email deste usuário e combine com a senha padrão gerada que é password
.
Antes de tentar logar você pode tentar acessar sua rota de admin/products e verá que você será redirecionado para a tela de login. Teste e veja tudo funcionando corretamente!
Antes de vermos como acessar as info do usuário logado, nossa tela de login ficou desta forma:
Podemos acessar tanto nos controllers como nas views nosso usuário autenticado por meio do helper auth()
, o método user
vindo deste helper nos traz as informações do usuário atualmente autenticado.
1
auth
()
->
user
();
Podemos utilizar em nossos controller essas informações através da request também. Veja:
1
$
request
->
user
();
Caso precise pegar apenas o id do usuário pode utilizar o método id()
diretamente:
1
auth
()
->
id
();
Vamos exibir em nosso paínel o nome do nosso usuário autenticado, adicione a li
abaixo na sua ul do nav no arquivo app.blade.php
:
1
<li
class=
"py-4"
>
2
<span
class=
" font-thin ml-4 text-white"
>
Olá, {{
auth
()->
user
()->
name
}}
</span>
3
</li>
Se quiser exibir apenas o primeiro nome, pode fazer como abaixo:
1
<li
class=
"py-4"
>
2
<span
class=
" font-thin ml-4 text-white"
>
Olá, {{ str(auth()->user()->name)->expl\
3
ode(' ')[0] }}</span>
4
</li>
Usamos o helper str()
para manipular o nome do nosso usuário autenticado, fazemos o explode nos espaços e pegamos a primeira casa do array dos nomes, no qual será o primeiro nome do usuário.
Neste capítulo conhecemos a parte de autenticação no Laravel e de fato percebemos que com poucos esforços conseguimos criar toda a estrutura de autenticação e controle para nosso painel administrativo.
Claro que quanto mais você avançar no Laravel perceberá que este processo será mais simplificado ainda pois o Laravel dispões de Starter Kit + Stack (Livewire, React ou Vue) para facilitar justamente este processo onde já na inicialização de um novo projeto você já pode trazer o painel com autenticação com poucos esforços.
As opções de stack pro starter kit oficial do Laravel 12 fogem do escopo do nosso livro pois cada stack requer conhecimentos específicos que precisariam ser colocadas em um livro focado mas toda a base que você precisa de Laravel estamos abordado aqui e em um futuro será mais simples você adotar um starter kit de escolha.
Então é isso, no vemos no próximo capítulo onde vamos trabalhar a parte de Validação dos Dados.
No vemos lá!
Olá tudo bem, espero que sim!
Estamos quase chegando na reta final da nossa jornada e neste capítulo iremos conhecer as validações dentro do Laravel. Lembrando que continuaremos abordando os conceitos como parte prática dentro do nosso projeto feito até aqui.
Podemos usar validação de duas maneiras incialmente. Uma delas é usando Form Requests e a outra é usando o objeto Validator e criando nossas validações customizadas direto em nossos controllers.
Para este capítulo e em nossa jornada vamos conhecer o uso de validações por meio do Form Request, pois ele nos ajuda a tirar esta lógica do nosso controller e isola em uma classe exclusiva para este fim. Então vamos entender como gerar este form request dentro do laravel e em nossa aplicação.
Vamos lá!
Primeiramente vamos gerar nosso primeiro form request e logo em seguida iremos comentar sobre o código desta classe. Em seu terminal execute o comando abaixo, na raiz do seu projeto:
1
php artisan make:request ProductRequest
Uma pasta será criada dentro da pasta Http
, a pasta Requests
, e lá estará nosso ProductRequest. Veja o conteúdo dele abaixo:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
ProductRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
false
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
//
27
];
28
}
29
}
O Form Request trará de cara dois métodos iniciais, o authorize
e o rules
. Vamos entender para que servem:
authorize
: Este método é serve para verificar se determinado acesso está autorizado na sua aplicação, retornando false a requisição é automaticamente bloqueada na rota em que você utilizar este Form Request e retornando true a requisição passará normalmente, caindo para as regras de validação em rules
. No authorize
você poderia verificar, por exemplo, se determinado usuário teria a permissão necessária para o acesso requisitado checando se este usuário, por exemplo, é dono do dado que está tentando criar/atualizar no momento;rules
: No método rules você define as regras para validação e assim que a requisição do formulário for enviada, o Laravel usa estas regras e valida os dados antes mesmo de chegarem em seu controller e no método correspondente.Podemos usar o Form Request para substituir o Request em nossos métodos que necessitam dele, isso trará a pitada extra de validação que será executada antes da requisição bater na execução do nosso método.
Vamos entender como montar as regras de nossa validação.
A estrutura para as validações respeitam basicamente o nome dos campos dos inputs de seu formulário e o uso das validações disponiveis, para cada tipo de dado, disponibilizados pelo Laravel. O Laravel possui diversos validadores, como por exemplo, citando alguns e deixando referência para os outros:
Existem diversos validadores e recomendo que você veja o que se enquadra melhor para a validação dentro da sua aplicação. Para visualizar as opções acesse: https://laravel.com/docs/12.x/validation#available-validation-rules.
Dito isso, vamos montar nossas regras de validação para os campos do nosso formulário de criação e edição do Produto. Vamos lá.
Primeiramente irei colocar alguns campos como obrigatórios para termos um primeiro contato com as validações, veja o trecho do método rules
do ProductRequest
:
1
/**
2
* Get the validation rules that apply to the request.
3
*
4
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>\
5
|string>
6
*/
7
public
function
rules
()
:
array
8
{
9
10
return
[
11
'name' => 'required',
12
'description' => 'nullable',
13
'body' => 'required',
14
'price' => 'required',
15
'in_stock' => 'required',
16
'is_active' => 'required',
17
'slug' => 'required',
18
'categories' => 'required'
19
]
;
20
}
Acima listei no array, de retorno do método rules
, o nome dos nossos campos do formulário e defini para cada campo a validação para campos obrigatórios.
Podemos ainda utilizar mais validadores para cada um dos campos, e isso é possivel quando usamos o pipe |
e informamos outro validador para o campo escolhido. Se esse validador aceitar parâmetros nós informados por meio de um :
, como por exemplo no validador abaixo.
Posso colocar um tamanho minimo ou máximo para nossa descrição(description
), veja o método alterado:
1
/**
2
* Get the validation rules that apply to the request.
3
*
4
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>\
5
|string>
6
*/
7
public
function
rules
()
:
array
8
{
9
10
return
[
11
'name' => 'required',
12
'description' => 'nullable|min:20',
13
'body' => 'required',
14
'price' => 'required',
15
'in_stock' => 'required',
16
'is_active' => 'required',
17
'slug' => 'required',
18
'categories' => 'required'
19
]
;
20
}
Acima digo que nosso campo de descrição além de não ser obrigatório com o validador nullable
, também deve ter um valor minimo a ser digitado e esse valor minimo, informado depois do :
é 20 caracteres. O validador nullable
ignora o campo se ele vier vazio mas ao termos valor e existindo outras regras de validação pro campo, essas regras serão aplicadas.
Lembrando que no exemplo acima mostrei a organização dos validadores como string mas se você preferir pode utilizar em lista via array. Exemplo:
1
...
2
'description' => ['nullable', 'min:20'],
3
...
Agora, como utilizar o Form Request e também exibir estas validação em nossas views?
Antes de prosseguirmos vamos adicionar o input do status em nosso formulário e modificar o trecho do input de preço adicinoando o campo referente ao estoque. Altere seu create.blade.php
do Produto com o input abaixo:
input Status:
1
<div
class=
"w-full mb-6"
>
2
<label
class=
"w-full mb-4"
>
Status</label>
3
4
<div
class=
"w-full mb-6"
>
5
<select
name=
"is_active"
class=
"w-full p-2 rounded border border-gray-900"
>
6
<option
value=
""
>
Selecione o status do produto</option>
7
<option
value=
"1"
@selected(old('is_active'))
>
Ativo</option>
8
<option
value=
"0"
@selected(!old('is_active'))
>
Inativo</option>
9
</select>
10
</div>
11
</div>
O mesmo input status, para o edit.blade.php do produto:
1
<div
class=
"w-full mb-6"
>
2
<label
class=
"w-full mb-4"
>
Status</label>
3
4
<div
class=
"w-full mb-6"
>
5
<select
name=
"is_active"
class=
"w-full p-2 rounded border border-gray-900"
>
6
<option
value=
""
>
Selecione o status do produto</option>
7
<option
value=
"1"
@selected($product-
>
is_active)>Ativo</option>
8
<option
value=
"0"
@selected(!$product-
>
is_active)>Inativo</option>
9
</select>
10
</div>
11
</div>
Perceba aqui uma nova diretiva do blade, usei o @selected
que assim como o @checked
espera um valor booleano para ativar ou não ativar o atributo selected desta tag do elemento select. No criar eu verifico o valor do helper old, já no editar pegamos o valor direto do produto recebido do controller.
PS.: Eu coloquei ele antes do trecho dos inputs das categorias.
Abaixo segue o input preço + o estoque, neste caso remova a div do preço atual e coloque esta. Segue o da tela de criação e edição:
Trecho no create.blade.php
1
<div
class=
"w-full mb-6 flex gap-x-4"
>
2
3
<div
class=
"w-[50%]"
>
4
<label>
Preço</label>
5
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded border border-gray-900"
6
value=
"
{{
old
(
'price'
)
}}
"
>
7
</div>
8
9
<div
class=
"w-[50%]"
>
10
<label>
Estoque</label>
11
<input
type=
"numer"
name=
"in_stock"
class=
"w-full p-2 rounded border border-gray\
12
-900"
13
value=
"
{{
old
(
'in_stock'
)
}}
"
>
14
</div>
15
16
</div>
Trecho no edit.blade.php:
1
<div
class=
"w-full mb-6 flex gap-x-4"
>
2
3
<div
class=
"w-[50%]"
>
4
<label>
Preço</label>
5
<input
type=
"text"
name=
"price"
class=
"w-full p-2 rounded border border-gray\
6
-900"
7
value=
"
{{
number_format
(
$
product-
>
price
,
2
,
','
,
'.'
)
}}
"
>
8
</div>
9
10
<div
class=
"w-[50%]"
>
11
<label>
Estoque</label>
12
<input
type=
"numer"
name=
"in_stock"
class=
"w-full p-2 rounded border border-gray\
13
-900"
14
value=
"
{{
$
product-
>
in_stock
}}
"
>
15
</div>
16
17
</div>
Alterações feitas, remova do seu controller qualquer chave no método store e no método update referentes ao is_active
e ao in_stock
.
Feito isso, vamos continuando.
É um processo bem simples, só precisamos trocar a referência nos métodos que usam o Request, trocando o Request
pelo ProductRequest
nos métodos store
e update
, do ProductController
.
Que estão assim:
store:
1
...
2
public function store(Request $request)
3
{
4
...
update:
1
...
2
public function update($product, Request $request)
3
{
4
...
Ficarão assim:
store:
1
...
2
3
public function store(ProductRequest $request)
4
{
5
6
...
update:
1
...
2
public function update($post, ProductRequest $request)
3
{
4
...
Simples, simples assim. Não precisamos alterar mais nada nos controllers, isso se dá por que o Form Request extende do Request, por isso não precisamos alterar nada, e o acesso aos métodos para manipulação do dados vindos da requisição continuam disponíveis.
Obs.: Como não vamos utilizar regras no método authorize
do ProductRequest
ao invés de retornar false
retorne true
e não esqueça de importar o ProductRequest
no ProductController
.
1
use App\Http\Requests\ProductRequest;
Veja na íntegra como ProductRequest
está:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
ProductRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
26
return
[
27
'name'
=>
'required'
,
28
'description'
=>
'nullable'
,
29
'body'
=>
'required'
,
30
'price'
=>
'required'
,
31
'in_stock'
=>
'required'
,
32
'is_active'
=>
'required'
,
33
'slug'
=>
'required'
,
34
'categories'
=>
'required'
35
];
36
}
37
}
Agora precisamos testar essas validações e exibir pro usuário as mensagens dos validadores de cada input em nosso formulário. Agora sim, vamos exibir as validações e suas mensagens em nossas views.
Um ponto importante e recomendo que faça essa alteração. Em seus controllers onde recebe os dados da requisição. Onde chamamos o
$request->all()
troque todos as referências por$request->validated()
.O método
validated
trará o array onde somente os dados que foram validados e passaram nessa validação estarão.
O Laravel disponibiliza uma variável que pode ser acessada em todas as views da aplicação, chamada de $errors
. A variável $errors
receberá as informações dos dados que não passaram na validação, durante o envio dos dados do formulário. No Laravel podemos interagir com estes erros diretamente através de uma diretiva exclusiva para isso.
Por meio da diretiva @error
podemos tratar estas exibições de forma bem simples, vamos entender como ela funciona.
Aplicamos as validações com base nos nomes dos campos de nossos formulários, pois são esses os identificadores das informações enviadas em nossa requisição. Para recuperarmos erros específicos de cada campo podemos usar a diretiva @error
como abaixo:
1
@
error
(
'
name
'
)
2
<
h1
>
Existe
um
erro
de
validação
para
o
input
do
nome
do
produto
</
h1
>
3
@
enderror
A diretiva retornará true caso exista um erro de validação para o campo informado. Desta forma, você pode exibir as mensagens de validação para este campo e esta mensagem está na variável $message
criada pela diretiva e que contêm a mensagem real do erro aplicado pela validação.
Veja:
1
@
error
(
'
name
'
)
2
<
h1
>
{{
$message
}}
</
h1
>
3
@
enderror
Acima, se existir erros de validação para o campo name
a mensagem do erro real, lançada pelo Laravel, será exibida dentro do h1
.
Agora vamos entender como usar esta diretiva em nossos formulários.
Vou trazer uma pequena amostragem da aplicação da diretiva @error
para um input específico, pois no todo é mais copia e cola e o que difere é o input name passado.
Segue o input name com a diretiva aplicada:
1
<
div
class
=
"w-full mb-6"
>
2
<
label
class
=
"block mb-2 @error('name') text-red-900 @enderror"
>
Nome
</
label
>
3
4
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border @error('name') b\
5
order-red-900 @else border-gray-900 @enderror"
value
=
"{{ old('name') }}"
>
6
7
@
error
(
'
name
'
)
8
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400 text-re\
9
d-900"
>
{{
$message
}}
</
div
>
10
@
enderror
11
12
</
div
>
Perceba que utilizei a diretiva error e customizei com TailwindCSS o estilo da mensagem de erro para se adequar visualmente a algo que não passou na validação. Perceba o input e a label também, neles apliquei a diretiva @error
para exibir a cor da borda do input para vermelho e o texto da label também em vermelhor, neste caso é mais um exemplo que você pode aplicar.
Visualmente veja como fica:
Agora é só replicarmos este pensamento para cada um dos inputs. Veja o formulário de criação completo abaixo:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('products.store') }}"
method
=
"post"
>
5
@
csrf
6
7
<
div
class
=
"w-full mb-6"
>
8
<
label
class
=
"block mb-2 @error('name') text-red-900 @enderror"
>
Nome
</
la
\
9
bel
>
10
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border @error('\
11
name') border-red-900 @else border-gray-900 @enderror"
value
=
"{{ old('name') }}"
>
12
13
@
error
(
'
name
'
)
14
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
15
text-red-900"
>
{{
$message
}}
</
div
>
16
@
enderror
17
</
div
>
18
19
<
div
class
=
"w-full mb-6"
>
20
<
label
class
=
"block mb-2 @error('description') text-red-900 @enderror"
>
\
21
Descrição
</
label
>
22
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border \
23
@error('description') border-red-900 @else border-gray-900 @enderror"
24
value
=
"{{ old('description') }}"
>
25
26
@
error
(
'
description
'
)
27
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
28
text-red-900"
>
{{
$message
}}
</
div
>
29
@
enderror
30
</
div
>
31
32
<
div
class
=
"w-full mb-6"
>
33
<
label
class
=
"block mb-2 @error('body') text-red-900 @enderror"
>
Conteúdo
\
34
</
label
>
35
<
textarea
name
=
"body"
id
=
""
cols
=
"30"
rows
=
"10"
class
=
"w-full p-2 rounde\
36
d border @error('body') border-red-900 @else border-gray-900 @enderror"
>
{{
old
(
'
body
\
37
'
)
}}
</
textarea
>
38
39
@
error
(
'
body
'
)
40
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
41
text-red-900"
>
{{
$message
}}
</
div
>
42
@
enderror
43
44
</
div
>
45
46
<
div
class
=
"w-full mb-6 flex gap-x-4"
>
47
<
div
class
=
"w-[50%]"
>
48
<
label
class
=
"block mb-2 @error('price') text-red-900 @enderror"
>
Pre
\
49
ç
o
</
label
>
50
<
input
type
=
"text"
name
=
"price"
class
=
"w-full p-2 rounded border @er\
51
ror('price') border-red-900 @else border-gray-900 @enderror"
52
value
=
"{{ old('price') }}"
>
53
54
@
error
(
'
price
'
)
55
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
56
text-red-900"
>
{{
$message
}}
</
div
>
57
@
enderror
58
</
div
>
59
<
div
class
=
"w-[50%]"
>
60
<
label
class
=
"block mb-2 @error('in_stock') text-red-900 @enderror"
>
\
61
Estoque
</
label
>
62
<
input
type
=
"numer"
name
=
"in_stock"
class
=
"w-full p-2 rounded border\
63
@error('in_stock') border-red-900 @else border-gray-900 @enderror"
64
value
=
"{{ old('in_stock') }}"
>
65
66
@
error
(
'
in_stock
'
)
67
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
68
-400 text-red-900"
>
{{
$message
}}
</
div
>
69
@
enderror
70
</
div
>
71
</
div
>
72
73
<
div
class
=
"w-full mb-6"
>
74
<
label
class
=
"block mb-2 @error('slug') text-red-900 @enderror"
>
Slug
</
la
\
75
bel
>
76
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border @error('\
77
slug') border-red-900 @else border-gray-900 @enderror"
78
value
=
"{{ old('slug') }}"
>
79
80
@
error
(
'
slug
'
)
81
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
82
text-red-900"
>
{{
$message
}}
</
div
>
83
@
enderror
84
</
div
>
85
86
<
div
class
=
"w-full mb-6"
>
87
<
label
class
=
"w-full mb-4 @error('is_active') text-red-900 @enderror"
>
St
\
88
atus
</
label
>
89
90
<
div
class
=
"w-full mb-6"
>
91
<
select
name
=
"is_active"
class
=
"w-full p-2 rounded border @error('is\
92
_active') border-red-900 @else border-gray-900 @enderror"
>
93
<
option
value
=
""
>
Selecione
o
status
do
produto
</
option
>
94
<
option
value
=
"1"
@
selected
(
old
(
'
is_active
'
))
>
Ativo
</
option
>
95
<
option
value
=
"0"
@
selected
(
!
old
(
'
is_active
'
))
>
Inativo
</
option
>
96
</
select
>
97
98
@
error
(
'
is_active
'
)
99
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
100
-400 text-red-900"
>
{{
$message
}}
</
div
>
101
@
enderror
102
</
div
>
103
</
div
>
104
105
<
div
class
=
"w-full mb-6"
>
106
107
@
error
(
'
categories
'
)
108
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
109
text-red-900"
>
{{
$message
}}
</
div
>
110
@
enderror
111
112
<
label
class
=
"w-full mb-4 @error('categories') text-red-900 @enderror"
>
C
\
113
ategorias
</
label
>
114
115
<
div
class
=
"px-5 grid grid-cols-3"
>
116
@
foreach
(
$categories
as
$category
)
117
<
div
>
118
<
input
type
=
"checkbox"
name
=
"categories[]"
value
=
"{{ $catego\
119
ry->id }}"
>
120
{{
$category
->
name
}}
121
</
div
>
122
@
endforeach
123
</
div
>
124
</
div
>
125
126
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
127
hite font-thin"
>
Criar
Produto
</
button
>
128
</
form
>
129
@
endsection
Veja o resultado dos inputs já com a validação e essa validação sendo aplicada e exibida:
O de edição também seguirá o mesmo pensamento, veja o formulário de edição alterado abaixo:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('products.update', ['product' => $product->id]) }}"
metho
\
5
d
=
"post"
>
6
@
csrf
7
@
method
(
'
PUT
'
)
8
<
div
class
=
"w-full mb-6"
>
9
<
label
class
=
"block mb-2 @error('name') text-red-900 @enderror"
>
Nome
</
l
\
10
abel
>
11
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border @error('\
12
name') border-red-900 @else border-gray-900 @enderror"
value
=
"{{ $product->name }}"
>
13
@
error
(
'
name
'
)
14
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
15
text-red-900"
>
{{
$message
}}
</
div
>
16
@
enderror
17
</
div
>
18
19
<
div
class
=
"w-full mb-6"
>
20
<
label
class
=
"block mb-2 @error('description') text-red-900 @enderror"
>
D
\
21
escrição
</
label
>
22
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border @\
23
error('description') border-red-900 @else border-gray-900 @enderror"
24
value
=
"{{ $product->description }}"
>
25
26
@
error
(
'
description
'
)
27
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
28
text-red-900"
>
{{
$message
}}
</
div
>
29
@
enderror
30
</
div
>
31
32
<
div
class
=
"w-full mb-6"
>
33
<
label
class
=
"block mb-2 @error('body') text-red-900 @enderror"
>
Conteúdo
\
34
</
label
>
35
<
textarea
name
=
"body"
id
=
""
cols
=
"30"
rows
=
"10"
class
=
"w-full p-2 rounde\
36
d border @error('body') border-red-900 @else border-gray-900 @enderror"
>
{{
$product
-
\
37
>
body
}}
</
textarea
>
38
@
error
(
'
body
'
)
39
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
40
text-red-900"
>
{{
$message
}}
</
div
>
41
@
enderror
42
</
div
>
43
44
<
div
class
=
"w-full mb-6"
>
45
46
</
div
>
47
48
<
div
class
=
"w-full mb-6 flex gap-x-4"
>
49
<
div
class
=
"w-[50%]"
>
50
<
label
class
=
"block mb-2 @error('price') text-red-900 @enderror"
>
Pre
\
51
ç
o
</
label
>
52
<
input
type
=
"text"
name
=
"price"
class
=
"w-full p-2 rounded border @er\
53
ror('price') border-red-900 @else border-gray-900 @enderror"
54
value
=
"{{ number_format($product->price, 2, ',', '.') }}"
>
55
@
error
(
'
price
'
)
56
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
57
-400 text-red-900"
>
{{
$message
}}
</
div
>
58
@
enderror
59
</
div
>
60
<
div
class
=
"w-[50%]"
>
61
<
label
class
=
"block mb-2 @error('in_stock') text-red-900 @enderror"
>
\
62
Estoque
</
label
>
63
<
input
type
=
"numer"
name
=
"in_stock"
class
=
"w-full p-2 rounded border @er\
64
ror('in_stock') border-red-900 @else border-gray-900 @enderror"
65
value
=
"{{ $product->in_stock }}"
>
66
67
@
error
(
'
in_stock
'
)
68
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
69
-400 text-red-900"
>
{{
$message
}}
</
div
>
70
@
enderror
71
</
div
>
72
</
div
>
73
74
<
div
class
=
"w-full mb-6"
>
75
<
label
class
=
"block mb-2 @error('slug') text-red-900 @enderror"
>
Slug
</
la
\
76
bel
>
77
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border @error('\
78
slug') border-red-900 @else border-gray-900 @enderror"
79
value
=
"{{ $product->slug }}"
>
80
81
@
error
(
'
slug
'
)
82
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
83
text-red-900"
>
{{
$message
}}
</
div
>
84
@
enderror
85
</
div
>
86
87
<
div
class
=
"w-full mb-6"
>
88
<
label
class
=
"w-full mb-4 @error('is_active') text-red-900 @enderror"
>
St
\
89
atus
</
label
>
90
91
<
div
class
=
"w-full mb-6"
>
92
<
select
name
=
"is_active"
class
=
"w-full p-2 rounded border @error('is\
93
_active') border-red-900 @else border-gray-900 @enderror"
>
94
<
option
value
=
""
>
Selecione
o
status
do
produto
</
option
>
95
<
option
value
=
"1"
@
selected
(
$product
->
is_active
)
>
Ativo
</
option
>
96
<
option
value
=
"0"
@
selected
(
!
$product
->
is_active
)
>
Inativo
</
optio
\
97
n
>
98
</
select
>
99
100
@
error
(
'
is_active
'
)
101
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
102
-400 text-red-900"
>
{{
$message
}}
</
div
>
103
@
enderror
104
</
div
>
105
</
div
>
106
107
<
div
class
=
"w-full mb-6"
>
108
@
error
(
'
categories
'
)
109
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
110
text-red-900"
>
{{
$message
}}
</
div
>
111
@
enderror
112
113
<
label
class
=
"w-full mb-4 @error('categories') text-red-900 @enderror"
>
C
\
114
ategorias
</
label
>
115
116
<
div
class
=
"px-5 grid grid-cols-3"
>
117
@
foreach
(
$categories
as
$category
)
118
<
div
>
119
<
input
type
=
"checkbox"
name
=
"categories[]"
value
=
"{{ $catego\
120
ry->id }}"
@
checked
(
$product
->
categories
->
contains
(
$category
))
>
121
{{
$category
->
name
}}
122
</
div
>
123
@
endforeach
124
</
div
>
125
</
div
>
126
127
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
128
hite font-thin"
>
Atualizar
129
Produto
</
button
>
130
</
form
>
131
132
<
form
action
=
"{{ route('products.destroy', ['product' => $product->id]) }}"
meth
\
133
od
=
"post"
>
134
@
csrf
135
@
method
(
'
DELETE
'
)
136
<
button
type
=
"submit"
class
=
"px-4 py-2 bg-red-700 border border-red-900 roun\
137
ded text-white font-thin mt-8"
>
Remover
138
Produto
</
button
>
139
</
form
>
140
@
endsection
Como é repeteco vamos as intruções de alteração diretamente!
Primeiramente vamos gerar nosso CategoryRequest
para criarmos nossas regras de validação. Execute a geração em seu terminal com o comando abaixo:
1
php artisan make:request CategoryRequest
Veja abaixo o conteúdo do CategoryRequest
completo e com as regras de validação:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
CategoryRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'name'
=>
'required'
,
27
'description'
=>
'nullable|min:20'
,
28
'slug'
=>
'required'
29
];
30
}
31
}
Troque os requests do store
e do update
do CategoryController
assim como fizemos no ProductController
. E por fim veja os formulários alterados da área de categorias:
create.blade.php
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('categories.store') }}"
method
=
"post"
>
5
@
csrf
6
7
<
div
class
=
"w-full mb-6"
>
8
<
label
class
=
"block mb-2 @error('name') text-red-900 @enderror"
>
Nome
</
l
\
9
abel
>
10
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border @error('\
11
name') border-red-900 @else border-gray-900 @enderror"
value
=
"{{ old('name') }}"
>
12
13
@
error
(
'
name
'
)
14
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
15
text-red-900"
>
{{
$message
}}
</
div
>
16
@
enderror
17
</
div
>
18
19
<
div
class
=
"w-full mb-6"
>
20
<
label
class
=
"block mb-2 @error('description') text-red-900 @enderror"
>
\
21
Descrição
</
label
>
22
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border @\
23
error('description') border-red-900 @else border-gray-900 @enderror"
24
value
=
"{{ old('description') }}"
>
25
26
@
error
(
'
description
'
)
27
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
28
text-red-900"
>
{{
$message
}}
</
div
>
29
@
enderror
30
</
div
>
31
32
<
div
class
=
"w-full mb-6"
>
33
<
label
class
=
"block mb-2 @error('slug') text-red-900 @enderror"
>
Slug
</
la
\
34
bel
>
35
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border @error('\
36
slug') border-red-900 @else border-gray-900 @enderror"
37
value
=
"{{ old('slug') }}"
>
38
39
@
error
(
'
slug
'
)
40
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
41
text-red-900"
>
{{
$message
}}
</
div
>
42
@
enderror
43
</
div
>
44
45
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
46
hite font-thin"
>
Criar
Categoria
</
button
>
47
</
form
>
48
@
endsection
edit.blade.php
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
form
action
=
"{{ route('categories.update', ['category' => $category->id]) }}"
m
\
5
ethod
=
"post"
>
6
@
csrf
7
@
method
(
'
PUT
'
)
8
<
div
class
=
"w-full mb-6"
>
9
<
label
class
=
"block mb-2 @error('name') text-red-900 @enderror"
>
Nome
</
la
\
10
bel
>
11
<
input
type
=
"text"
name
=
"name"
class
=
"w-full p-2 rounded border @error('\
12
name') border-red-900 @else border-gray-900 @enderror"
13
value
=
"{{ $category->name }}"
>
14
15
@
error
(
'
name
'
)
16
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
17
text-red-900"
>
{{
$message
}}
</
div
>
18
@
enderror
19
</
div
>
20
21
<
div
class
=
"w-full mb-6"
>
22
<
label
class
=
"block mb-2 @error('description') text-red-900 @enderror"
>
\
23
Descrição
</
label
>
24
<
input
type
=
"text"
name
=
"description"
class
=
"w-full p-2 rounded border @\
25
error('description') border-red-900 @else border-gray-900 @enderror"
26
value
=
"{{ $category->description }}"
>
27
28
@
error
(
'
description
'
)
29
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
30
text-red-900"
>
{{
$message
}}
</
div
>
31
@
enderror
32
</
div
>
33
34
<
div
class
=
"w-full mb-6"
>
35
<
label
class
=
"block mb-2 @error('slug') text-red-900 @enderror"
>
Slug
</
l
\
36
abel
>
37
<
input
type
=
"text"
name
=
"slug"
class
=
"w-full p-2 rounded border @error('\
38
slug') border-red-900 @else border-gray-900 @enderror"
39
value
=
"{{ $category->slug }}"
>
40
41
@
error
(
'
slug
'
)
42
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400\
43
text-red-900"
>
{{
$message
}}
</
div
>
44
@
enderror
45
</
div
>
46
47
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-w\
48
hite font-thin"
>
Atualizar
49
Categoria
</
button
>
50
</
form
>
51
52
<
form
action
=
"{{ route('categories.destroy', ['category' => $category->id]) }}"
\
53
method
=
"post"
>
54
@
csrf
55
@
method
(
'
DELETE
'
)
56
<
button
type
=
"submit"
class
=
"px-4 py-2 bg-red-700 border border-red-900 roun\
57
ded text-white font-thin mt-8"
>
Remover
58
Categoria
</
button
>
59
</
form
>
60
@
endsection
Agora que aplicamos as validações ao nosso formulário e área de categorias vamos adicionar também as validações para nossa área de Login, nosso form de Login.
Também vamos gerar um form request, veja:
1
php artisan make:request Auth/LoginRequest
Perceba que aqui informei uma pasta assim como fizemos pra nossos controllers do Admin, o Laravel irá criar dentro da pasta de requests a pasta Auth
e nosso LoginRequest
estará lá dentro.
Veja o form login na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Requests
\Auth
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
LoginRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'email'
=>
[
'required'
,
'string'
,
'email'
],
27
'password'
=>
[
'required'
,
'string'
],
28
];
29
}
30
}
Aqui é mais do mesmo, não esqueça de, no método login
do LoginController
adicionar o form request no lugar do request padrão. Adicione também a diretiva @error
no seu formulário de login. Como já sabemos fazer, deixarei essa alteração a seu cargo em seu projeto.
Existe um pequeno detalhe, e já que estamos falando de validação, no processo de login que podemos modificar. Lembra que ao testarmos se o login está correto via método attempt
do AuthManager, retornamos um 401 via abort helper?
Substitua essa linha pelo trecho abaixo, em seu método login
no LoginController
:
1
throw
ValidationException
::
withMessages
(
[
2
'email'
=>
'Acesso Inválido...'
,
3
]
);
Ao invés de realizarmos um abort vamos lançar uma exception do tipo ValidationException
onde informamos a chave email
referente ao nosso input e indicamos o erro de Acesso inválido
pro caso do erro nas credenciais.
Essa mensagem cairá na parte de mensagens de validação e será exibida no trecho da diretiva @error
pro nosso e-mail no formulário de login.
Tente passar um email e senha errados e veja o que acontece. No caso veja a tela do formulário ao cair na validação de credenciais inválidas:
Veja o método login completo e mais recente:
1
public
function
login
(
LoginRequest
$request
)
:
RedirectResponse
2
{
3
$credentials
=
$request
->
only
('
email
',
'
password
');
4
5
if
(
!
Auth
::
attempt
(
$credentials
,
(
bool
)
$request
->
rememberme
))
{
6
throw
ValidationException
::
withMessages
([
7
'
email
'
=>
'
Acesso
Inválido
...',
8
]);
9
}
10
11
$request
->
session
()
->
regenerate
();
12
13
return
redirect
()
->
intended
(
route
('
products
.
index
',
absolute:
false
));
14
}
Não esqueça de importar o ValidationException
:
1
use Illuminate\Validation\ValidationException;
Feito isso vamos entender das traduções pro momento, das nossas mensagens de validação nos Forms Request.
Dentro do form request podemos ainda traduzir as mensagens de erro, simplesmente sobscrevendo o método messages
que está na classe pai do nosso form request.
Por exemplo, vamos traduzir as mensagens de validação lá do ProductRequest
, logo após o método rules
adicione o método abaixo:
1
public
function
messages
()
2
{
3
return
[
4
'
required
'
=>
'
Este campo é obrigatório
'
,
5
'
min
'
=>
'
Sua descrição deve ter pelo menos :min caracteres
'
6
];
7
}
O método messages retornará um array, referenciando o nome dos validadores como indice de cada linha e para cada linha, referentes ao validador, o valor será a mensagem que você quer que seja exibida. Agora sempre que a validação ocorrer, as mensagens escolhidas irão aparecer.
Note que consigo acessar, na chave min
o valor digitado para a validação de quantidade mínima de caracteres, por meio da notação :min
.
Para saber mais sobre as possibilidades com respeito as mensagens de validações recomendo a documentação: https://laravel.com/docs/12.x/validation#working-with-error-messages.
Veja o ProductRequest
na íntegra:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
ProductRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
26
return
[
27
'name'
=>
'required'
,
28
'description'
=>
'nullable|min:20'
,
29
'body'
=>
'required'
,
30
'price'
=>
'required'
,
31
'in_stock'
=>
'required'
,
32
'is_active'
=>
'required'
,
33
'slug'
=>
'required'
,
34
'categories'
=>
'required'
35
];
36
}
37
38
public
function
messages
()
39
{
40
return
[
41
'required'
=>
'Este campo é obrigatório'
,
42
'min'
=>
'Sua descrição deve ter pelo menos :min caracteres'
43
];
44
}
45
}
Veja também o CategoryRequest e o LoginRequest já com o método messages
adicionado:
CategoryRequest:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
CategoryRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'name'
=>
'required'
,
27
'description'
=>
'nullable|min:20'
,
28
'slug'
=>
'required'
29
];
30
}
31
32
public
function
messages
()
33
{
34
return
[
35
'required'
=>
'Este campo é obrigatório'
,
36
'min'
=>
'Sua descrição deve ter pelo menos :min caracteres'
37
];
38
}
39
}
LoginRequest:
1
<
?
php
2
3
namespace
App
\Http
\Requests
\Auth
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
LoginRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'email'
=>
[
'required'
,
'string'
,
'email'
],
27
'password'
=>
[
'required'
,
'string'
],
28
];
29
}
30
31
public
function
messages
()
32
{
33
return
[
34
'required'
=>
'Este campo é obrigatório'
,
35
'min'
=>
'Sua descrição deve ter pelo menos :min caracteres'
,
36
'email'
=>
'Digite um e-mail válido'
37
];
38
}
39
}
Neste capítulo nós conhecemos as principais formas de trabalho com validações de dados dentro do Laravel, em particular utilizando os Forms Request que facilitam muito, tanto na concentração da validação e execução desta validação, quanto na organização dos participantes, onde tiramos do nosso controller este detalhe e isolando diretamente na classe Form Request.
O Laravel possui diversos validadores e as possibilidades são imensas, além da simplificidade e facilidade na utilização destes recursos e foi o que vimos aqui. Vamos ao próximo capítulo para trabalharmos a parte de upload dentro do Laravel, onde vamos adicionar o upload das fotos do produto.
Então, até o próximo capítulo!
Neste capítulo vamos entender como funciona o upload de arquivos no Laravel, criando o upload das fotos do produto além de entender um pouco do funcionamento geral e configurações de upload no Laravel.
Primeiramente podemos recuperar um arquivo, vindo de um input do tipo file
, usando o método do request, o método file
. Por exemplo: quando enviarmos lá da tela de editar o produto o campo fotos, podemos recuperar as informações como abaixo.
1
$
photos
=
$
request
->
file
(
'photos'
);
Poderíamos acessar também como abaixo:
1
$
photos
=
$
request
->
photos
;
Este método irá retornar um objeto do tipo Illuminate\Http\UploadedFile
com as informações do aquivo enviado. Onde poderemos recuperar diversas informações, como por exemplo o nome real do arquivo:
1
$
avatar
->
getClientOriginalName
();
Ou mesmo a extensão do arquivo:
1
$
avatar
->
extension
();
Podemos de cara já realizar o upload deste arquivo, sem muito esforço, simplesmente chamando o método store
:
1
$
path
=
$
avatar
->
store
(
'products/photos'
);
O método store
por padrão pegará o caminho do drive default configurado lá no filesystems.php
dentro da pasta config
do projeto. E moverá a imagem para esta pasta padrão, criando a pasta products/photos
se ela não existir e jogando nesta pasta nossos uploads.
O Laravel já manda o arquivo com um nome modificado via hash para evitar conflitos de nome. O caminho padrão é storage/app/private
mas se você quiser salvar em outro caminho, por disco, como é chamado a configuração, você precisa informar o segundo parâmetro.
Por exemplo, vamos salvar as imagens no public da storage onde futuramente linkaremos com a pasta public do projeto. O disco que representa este caminho é chamado de public
que aponta para storage/app/public
e se quisermos referenciar ele, temos que chamar como abaixo:
1
$
ref
=
$
photo
->
store
(
'products/photos'
,
'public'
);
O retorno do método store
é o nome da imagem mais a pasta, por exemplo:
1
products/photos/loWy0OYzb5CDoyMEN1QGQ86jlyMCvJJmOfEDa5Ue.jpeg
Esse nome + caminho é o que salvaremos na coluna photo da tabela product_photos
. Agora vamos conhecer as configurações de armazenamento lá no filesystems.php
.
config/filesystems.php:
1
<?php
2
3
return
[
4
5
/*
6
|--------------------------------------------------------------------------
7
| Default Filesystem Disk
8
|--------------------------------------------------------------------------
9
|
10
| Here you may specify the default filesystem disk that should be used
11
| by the framework. The "local" disk, as well as a variety of cloud
12
| based disks are available to your application for file storage.
13
|
14
*/
15
16
'default'
=>
env
(
'FILESYSTEM_DISK'
,
'local'
),
17
18
/*
19
|--------------------------------------------------------------------------
20
| Filesystem Disks
21
|--------------------------------------------------------------------------
22
|
23
| Below you may configure as many filesystem disks as necessary, and you
24
| may even configure multiple disks for the same driver. Examples for
25
| most supported storage drivers are configured here for reference.
26
|
27
| Supported drivers: "local", "ftp", "sftp", "s3"
28
|
29
*/
30
31
'disks'
=>
[
32
33
'local'
=>
[
34
'driver'
=>
'local'
,
35
'root'
=>
storage_path
(
'app/private'
),
36
'serve'
=>
true
,
37
'throw'
=>
false
,
38
'report'
=>
false
,
39
],
40
41
'public'
=>
[
42
'driver'
=>
'local'
,
43
'root'
=>
storage_path
(
'app/public'
),
44
'url'
=>
env
(
'APP_URL'
)
.
'/storage'
,
45
'visibility'
=>
'public'
,
46
'throw'
=>
false
,
47
'report'
=>
false
,
48
],
49
50
's3'
=>
[
51
'driver'
=>
's3'
,
52
'key'
=>
env
(
'AWS_ACCESS_KEY_ID'
),
53
'secret'
=>
env
(
'AWS_SECRET_ACCESS_KEY'
),
54
'region'
=>
env
(
'AWS_DEFAULT_REGION'
),
55
'bucket'
=>
env
(
'AWS_BUCKET'
),
56
'url'
=>
env
(
'AWS_URL'
),
57
'endpoint'
=>
env
(
'AWS_ENDPOINT'
),
58
'use_path_style_endpoint'
=>
env
(
'AWS_USE_PATH_STYLE_ENDPOINT'
,
false
),
59
'throw'
=>
false
,
60
'report'
=>
false
,
61
],
62
63
],
64
65
/*
66
|--------------------------------------------------------------------------
67
| Symbolic Links
68
|--------------------------------------------------------------------------
69
|
70
| Here you may configure the symbolic links that will be created when the
71
| `storage:link` Artisan command is executed. The array keys should be
72
| the locations of the links and the values should be their targets.
73
|
74
*/
75
76
'links'
=>
[
77
public_path
(
'storage'
)
=>
storage_path
(
'app/public'
),
78
],
79
80
];
Perceba que temos a chave default logo de cara, que espera o valor vindo lá do .env na variável FILESYSTEM_DISK
, se ela não existir lá no .env, teremos por padrão o valor local
como disco para salvarmos nossos arquivos.
Veja:
1
'default' => env('FILESYSTEM_DISK', 'local'),
Logo, seguindo pelo arquivo, temos os discos (disks) disponíveis e configurados para armazenamento de arquivos. São eles:
app/private
dentro de storage
;app/public
dentro de storage
;S3
serão necessárias para armazenamento na nuvem.Veja os discos abaixo:
1
'
disks
'
=>
[
2
3
'
local
'
=>
[
4
'
driver
'
=>
'
local
',
5
'
root
'
=>
storage_path
('
app
/
private
'),
6
'
serve
'
=>
true
,
7
'
throw
'
=>
false
,
8
'
report
'
=>
false
,
9
],
10
11
'
public
'
=>
[
12
'
driver
'
=>
'
local
',
13
'
root
'
=>
storage_path
('
app
/
public
'),
14
'
url
'
=>
env
('
APP_URL
').'
/
storage
',
15
'
visibility
'
=>
'
public
',
16
'
throw
'
=>
false
,
17
'
report
'
=>
false
,
18
],
19
20
'
s3
'
=>
[
21
'
driver
'
=>
'
s3
',
22
'
key
'
=>
env
('
AWS_ACCESS_KEY_ID
'),
23
'
secret
'
=>
env
('
AWS_SECRET_ACCESS_KEY
'),
24
'
region
'
=>
env
('
AWS_DEFAULT_REGION
'),
25
'
bucket
'
=>
env
('
AWS_BUCKET
'),
26
'
url
'
=>
env
('
AWS_URL
'),
27
'
endpoint
'
=>
env
('
AWS_ENDPOINT
'),
28
'
use_path_style_endpoint
'
=>
env
('
AWS_USE_PATH_STYLE_ENDPOINT
',
false
),
29
'
throw
'
=>
false
,
30
'
report
'
=>
false
,
31
],
32
],
Cada um têm o nome do driver, o caminho, url e a visibilidade, entretanto, o S3, como é um servidor e serviço remoto e ainda privado necessita de configurações extras para que você possa realizar o upload neste disco.
Na última configuração do arquivo, temos a possibilidade de definir caminhos para a linkagem desses caminhos na pasta public do projeto usando o comando:php artisan storage:link
. Realizaremos este processo mais a frente mas se você quiser que mais links simbólicos sejam criados no momento desta execução, você pode especificar o caminho no array de configuração abaixo. Sendo a chave o caminho do link simbólico e o valor o caminho para onde link simbólico apontará:
1
'links' => [
2
public_path('storage') => storage_path('app/public'),
3
],
Perceba que a config já traz a possibilidade de criarmos um link simbólico chamado de storage
na pasta public do projeto e este link simbólico apontará para, a partir da raiz do projeto, storage/app/public
.
Vamos a adição dos uploads para as fotos do produto. Insumos que formos precisando e que eu já tiver comentado em outros capítulos irei apenas deixar os códigos aqui para leitura e cópia.
Primeiramente vamos criar um controller exclusivo para as fotos do produto. Criamos nosso controller com o comando abaixo:
1
php artisan make:controller Admin/ProductPhotoController
Aproveite e já crie um form request para adicionarmos as validações para as fotos enviadas via upload. Veja:
1
php artisan make:request ProductPhotoRequest
Veja o conteúdo do request recém criada, onde adicionamos para o campo foto a validação para images:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
ProductPhotoRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'photos.*'
=>
[
'image'
]
27
];
28
}
29
30
public
function
messages
():
array
31
{
32
return
[
33
'image'
=>
'Existe uma imagem inválida dentre as que você está tentando
\
34
enviar...'
35
];
36
}
37
}
Aqui entra um pequeno detalhe importante. Como vamos receber um array com várias imagens por conta da possibilidade de escolha de múltiplas fotos, precisamos informar a notação .*
na chave photos
. Essa notação indica ao Laravel que valide individualmente cada arquivo no array que virá, via requisição, contendo as fotos escolhidas.
Vamos começar a montar nosso controller. Aqui teremos mais do que já vimos então serei bem direto nos pontos mostrados aqui. Segue nosso index no ProductPhotoController:
1
public
function
index
(
Product
$
product
)
2
{
3
return
view
(
'products.photos-upload'
,
compact
(
'product'
));
4
}
Aqui têm um ponto importante, como vamos aninhar as rotas de fotos sob as rotas de produtos vamos precisar sempre da referência do Produto pai referente ao processo feito com a(s) foto(s).
Vamos ao nosso método store:
1
public
function
store
(
Product
$
product
,
ProductPhotoRequest
$
request
)
2
{
3
$photos
=
$request->validated('photos')
;
4
5
if(!$photos)
{
6
throw
ValidationException
:
:
withMessages
(
[
7
'photos.*'
=>
'Selecione as fotos pro produto primeiro...'
,
8
]
);
9
}
10
11
$
photosReference
=
[]
;
12
13
foreach
($
photos
as
$
photo
)
{
14
$photosReference
[]
=
[
'photo'
=>
$photo
->
store
(
'product/photos'
,
'public'
)
]
;
15
}
16
17
$
product-
>
photos
()
-
>
createMany
($
photosReference
);
18
19
session
()
-
>
flash
(
'success'
,
'Fotos upada com sucesso!'
);
20
return
redirect
()
-
>
back
();
21
}
No trecho acima faço uma validação para o não envio de fotos, por alguma razão a notação de .*
não pega no array vazio pro required e individualmente em cada linha. Poderia fazer de outras formas mas esta resolve bem e é algo que já usamos lá no login.
Como recebemos um array de fotos, iteramos sob este array e realizamos o upload como comentado lá no ínicio do capítulo. Aqui têm um ponto bem importante, perceba que monto um array com o retorno do método store e armazeno isso na chave ‘photo’ e criando uma lista de arrays com o formato abaixo:
1
[
2
['photo' => '...'],
3
['photo' => '...'],
4
]
Dessa forma consigo utilizar o método createMany através do método de ligação do produto com fotos. O método createMany
me permite criar vários dados de uma vez, respeitando a mesma ideia do create mas contendo uma coleção de arrays e inserindo todos de uma vez.
Basea-se no array exemplo que mostrei acima, onde teremos todas as fotos(referência) salvas na base e já pegando o ID do Produto pai por conta da ligação.
PS.: Perceba que aqui já começo a colocar as mensagens na sessão flash e mais a frente iremos criar nosso trecho de exibição dessas mensagens nas telas.
Vamos ao método destroy
:
1
public
function
destroy
(
Product
$
product
,
string
$
photo
)
2
{
3
$photo
=
$product->photos()->findOrFail($photo)
;
4
5
$disk
=
Storage
:
:
disk
(
'public'
);
6
7
if($disk->exists($photo->photo))
{
8
$disk->delete($photo->photo)
;
9
}
10
11
$
photo-
>
delete
();
12
13
session
()
-
>
flash
(
'success'
,
'Foto removida com sucesso!'
);
14
return
redirect
()
-
>
back
();
15
}
Aqui buscamos a fotos por meio da ligação com produto, e o processo de remoção envolve:
O método do Storage é bem descritivo, onde podemos escolher um disco e realizar checks e ações nos arquivos deste disco. Aqui verificamos a existência da imagem no disco public
e existindo remover ela com o método delete
e passando a referência da imagem salva na coluna photo
.
Segue abaixo o controller na íntegra para consulta e verificação dos imports também:
ProductPhotoController
:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Admin
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Http
\Requests
\ProductPhotoRequest
;
7
use
App
\Models
\Product
;
8
use
Illuminate
\Validation
\ValidationException
;
9
use
Illuminate
\Http
\Request
;
10
use
Illuminate
\Support
\Facades
\Storage
;
11
12
class
ProductPhotoController
extends
Controller
13
{
14
public
function
index
(
Product
$
product
)
15
{
16
return
view
(
'products.photos-upload'
,
compact
(
'product'
));
17
}
18
19
public
function
store
(
Product
$
product
,
ProductPhotoRequest
$
request
)
20
{
21
$
photos
=
$
request
->
validated
(
'photos'
);
22
23
if
(
!$
photos
)
{
24
throw
ValidationException
::
withMessages
([
25
'photos.*'
=>
'Selecione as fotos pro produto primeiro...'
,
26
]);
27
}
28
29
$
photosReference
=
[];
30
31
foreach
(
$
photos
as
$
photo
)
{
32
$
photosReference
[]
=
[
'photo'
=>
$
photo
->
store
(
'product/photos'
,
'public
\
33
'
)];
34
}
35
36
$
product
->
photos
()
->
createMany
(
$
photosReference
);
37
38
session
()
->
flash
(
'success'
,
'Fotos upada com sucesso!'
);
39
return
redirect
()
->
back
();
40
}
41
42
public
function
destroy
(
Product
$
product
,
string
$
photo
)
43
{
44
$
photo
=
$
product
->
photos
()
->
findOrFail
(
$
photo
);
45
46
$
disk
=
Storage
::
disk
(
'public'
);
47
48
if
(
$
disk
->
exists
(
$
photo
->
photo
))
{
49
$
disk
->
delete
(
$
photo
->
photo
);
50
}
51
52
$
photo
->
delete
();
53
54
session
()
->
flash
(
'success'
,
'Foto removida com sucesso!'
);
55
return
redirect
()
->
back
();
56
}
57
}
Vamos para nossa tela de upload, depois adicionamos nossas rotas.
Nossa view da tela para upload conterá um pouco de javascript e muito tailwindcss. Recomendo fortemente que tente lê a parte das classes do tailwindcss para entender bem os estilos ali aplicados e como comentei anteriormente o tailwind é focado no baixo nível, cada classe está mais perto do que é o estilo do que outros frameworks css.
Deixo abaixo a view completo e vamos aos pontos importantes:
View - resources/views/products/photos-upload.blade.php
:
1
@
extends
(
'
layouts
.
app
'
)
2
3
@
section
(
'
content
'
)
4
<
div
class
=
"mb-10"
>
5
&
raquo
;
<
a
href
=
"{{route('products.edit', ['product' => $product->id])}}"
cl
\
6
ass
=
"text-sm underline"
>
Edição
Produto
</
a
>
7
</
div
>
8
<
div
class
=
"w-full mx-auto"
>
9
<
form
action
=
"{{ route('photos.store', ['product' => $product->id]) }}"
meth
\
10
od
=
"post"
enctype
=
"multipart/form-data"
>
11
@
csrf
12
13
<
div
class
=
"w-full mb-6"
>
14
<
label
class
=
"block mb-2 @error('photos.*') text-red-900 @enderror"
>
\
15
Selecione
as
fotos
do
produto
abaixo
:
</
label
>
16
<
input
type
=
"file"
name
=
"photos[]"
class
=
"w-full p-2 rounded border \
17
@error('photos.*') border-red-900 @else border-gray-900 @enderror"
multiple
required
\
18
>
19
20
@
error
(
'
photos
.
*
'
)
21
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red\
22
-400 text-red-900"
>
{{
$message
}}
</
div
>
23
@
enderror
24
</
div
>
25
26
27
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded te\
28
xt-white font-thin"
>
Salvar
Fotos
</
button
>
29
</
form
>
30
</
div
>
31
<
div
class
=
"w-full mx-auto mt-10"
>
32
@
if
(
$product
->
photos
->
count
())
33
<
div
class
=
"grid grid-cols-5 gap-x-3 border-t border-gray-400 py-4"
>
34
@
foreach
(
$product
->
photos
as
$photo
)
35
<
div
class
=
"relative group"
>
36
<
form
action
=
"{{route('photos.destroy', ['product' => $produ\
37
ct->id, 'photo' => $photo->id])}}"
id
=
"submitDestroy{{$photo->id}}"
method
=
"post"
>
@
c
\
38
srf
@
method
(
'
DELETE
'
)
</
form
>
39
40
<
button
class
=
"absolute top-1 right-1 text-white bg-red-700 \
41
p-2 rounded cursor-pointer hidden group-hover:block"
id
=
"destroyPhoto"
data
-
photo
=
"{\
42
{$photo->id}}"
>
43
44
<
svg
xmlns
=
"http://www.w3.org/2000/svg"
fill
=
"none"
view
\
45
Box
=
"0 0 24 24"
stroke
-
width
=
"1.5"
stroke
=
"currentColor"
class
=
"size-6"
>
46
<
path
stroke
-
linecap
=
"round"
stroke
-
linejoin
=
"round"
\
47
d
=
"m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.16\
48
5L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5\
49
.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0\
50
a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 \
51
0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/
\
52
>
53
</
svg
>
54
55
</
button
>
56
57
<
img
src
=
"{{asset('storage/' . $photo->photo)}}"
alt
=
""
clas
\
58
s
=
"p-1 rounded shadow bg-white"
>
59
</
div
>
60
@
endforeach
61
</
div
>
62
@
else
63
<
div
class
=
"w-full mt-4 text-red-700"
>
64
Sem
fotos
para
este
produto
!
65
</
div
>
66
@
endif
67
</
div
>
68
69
@
push
(
'
scripts
'
)
70
<
script
>
71
const
button
=
document
.
querySelectorAll
(
'
button
#
destroyPhoto
'
)
72
73
button
.
forEach
(
el
=>
{
74
console
.
log
(
el
)
75
el
.
addEventListener
(
'
click
'
,
e
=>
{
76
e
.
preventDefault
();
77
78
if
(
!
confirm
(
'
Deseja
mesmo
remover
a
foto
?
'
))
return
;
79
80
const
photoId
=
el
.
dataset
.
photo
;
81
82
document
.
querySelector
(
`
form
#
submitDestroy$
{
photoId
}
`
).
submit
();
83
})
84
});
85
</
script
>
86
@
endpush
87
@
endsection
Um panorâma geral dessa tela, nela temos tanto o formulário para upload como a listagem das fotos deste produto. As fotos do produto pego a partir da referência via ligação.
Vou destacar abaixo inicialmente o formulário da página:
1
<
form
action
=
"{{ route('photos.store', ['product' => $product->id]) }}"
method
=
"post\
2
"
enctype
=
"multipart/form-data"
>
3
@
csrf
4
5
<
div
class
=
"w-full mb-6"
>
6
<
label
class
=
"block mb-2 @error('photos.*') text-red-900 @enderror"
>
Selecion
\
7
e
as
fotos
do
produto
abaixo
:
</
label
>
8
<
input
type
=
"file"
name
=
"photos[]"
class
=
"w-full p-2 rounded border @error('\
9
photos.*') border-red-900 @else border-gray-900 @enderror"
multiple
required
>
10
11
@
error
(
'
photos
.
*
'
)
12
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg-red-400 tex\
13
t-red-900"
>
{{
$message
}}
</
div
>
14
@
enderror
15
</
div
>
16
17
18
<
button
class
=
"px-4 py-2 bg-green-700 border border-green-900 rounded text-white\
19
font-thin"
>
Salvar
Fotos
</
button
>
20
</
form
>
Perceba que na diretiva @error
chamo exatamente a chave como usei na validação: photos.*
. Desta forma pegamos a mensagem corretamente para a validação dos itens como imagem válidas.
Nosso input permite upload múltiple por meio do atributo multiple
e o nome do nosso input respeita a notação para enviarmos os itens como array na requisição: name="photos[]"
.
Acima destaquei os principais pontos, não esqueça de pegar o trecho e executar localmente e entender ou rever mais nuances aqui existentes neta tela.
A tela ainda possui um trecho onde exibimos as imagens, se existirem, para o produto escolhido. Vamos ao trecho:
1
@
if
(
$product
->
photos
->
count
())
2
<
div
class
=
"grid grid-cols-5 gap-x-3 border-t border-gray-400 py-4"
>
3
@
foreach
(
$product
->
photos
as
$photo
)
4
<
div
class
=
"relative group"
>
5
<
form
action
=
"{{route('photos.destroy', ['product' => $product->id, \
6
'photo' => $photo->id])}}"
id
=
"submitDestroy{{$photo->id}}"
method
=
"post"
>
@
csrf
@
met
\
7
hod
(
'
DELETE
'
)
</
form
>
8
9
<
button
class
=
"absolute top-1 right-1 text-white bg-red-700 p-2 roun\
10
ded cursor-pointer hidden group-hover:block"
id
=
"destroyPhoto"
data
-
photo
=
"{{$photo-\
11
>id}}"
>
12
13
<
svg
xmlns
=
"http://www.w3.org/2000/svg"
fill
=
"none"
viewBox
=
"0 0\
14
24 24"
stroke
-
width
=
"1.5"
stroke
=
"currentColor"
class
=
"size-6"
>
15
<
path
stroke
-
linecap
=
"round"
stroke
-
linejoin
=
"round"
d
=
"m14.\
16
74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 \
17
19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.4\
18
56 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 4\
19
8.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.\
20
32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
21
</
svg
>
22
23
</
button
>
24
25
<
img
src
=
"{{asset('storage/' . $photo->photo)}}"
alt
=
""
class
=
"p-1 r\
26
ounded shadow bg-white"
>
27
</
div
>
28
@
endforeach
29
</
div
>
30
@
else
31
<
div
class
=
"w-full mt-4 text-red-700"
>
32
Sem
fotos
para
este
produto
!
33
</
div
>
34
@
endif
Esse loop é codicional onde verifico via propriedade da ligação a existência de fotos na collection ligada a produtos. Seguindo aquela explicação sobre acessar como método de ligação e propriedade de ligação.
Como nosso remover da imagem está via método destroy
(que espera um DELETE), monto no loop formulários únicos para associá-los a cada button onde ao passar o mouse na imagem este button aparece com um ícone de lixeira (svg que peguei no site heroicons.com).
Esse button ao ser clicado será interceptado por nosso javascript e assim realizaremo a submissão deste form para processar o remover da imagem escolhida.
No button que está sob a imagem, via estilo, defino um atributo dataset
(data-photo) para salvar a referência do id da imagem para via JS submeter o formulário associado ao button e a imagem.
1
<button
class=
"absolute top-1 right-1 text-white bg-red-700 p-2 rounded cursor-point\
2
er hidden group-hover:block"
id=
"destroyPhoto"
data-photo=
"
{{
$
photo-
>
id
}}
"
>
3
4
<svg
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
stroke-w\
5
idth=
"1.5"
stroke=
"currentColor"
class=
"size-6"
>
6
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"m14.74 9-.346 9m-4.7\
7
88 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25\
8
0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.1\
9
08 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478\
10
-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2\
11
.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
12
</svg>
13
14
</button>
Perceba também que via javascript buscarei pelos ids do button e pelo id dos formulários.
Aqui teremos mais um ponto nesta tela referente a possibilidade do Blade. Utilizarei para organizar o javascript deste template e fazer com que ele exista no âmbito geral da página a diretiva
@push
.O
@push
será adicionado no template principal onde existir a diretiva@stack
no pensamente pilha de fato. Desta forma conseguimos enviar nossos códigos de scripts do contexto daquela view para o escopo geral da tela HTML. Poderia fazer isso com section também mas stack e push me ajudam neste ponto também.
Abaixo segue o trecho do nosso javascript existente na tela:
1
@
push
(
'
scripts
'
)
2
<
script
>
3
const
button
=
document
.
querySelectorAll
(
'
button
#
destroyPhoto
'
)
4
5
button
.
forEach
(
el
=>
{
6
console
.
log
(
el
)
7
el
.
addEventListener
(
'
click
'
,
e
=>
{
8
e
.
preventDefault
();
9
10
if
(
!
confirm
(
'
Deseja
mesmo
remover
a
foto
?
'
))
return
;
11
12
const
photoId
=
el
.
dataset
.
photo
;
13
14
document
.
querySelector
(
`
form
#
submitDestroy$
{
photoId
}
`
).
submit
();
15
})
16
});
17
</
script
>
18
@
endpush
Nomeei nossa stack como scripts
, por esta razão chamamos o @push('scripts')
para que este trecho de JS seja empilhado exatamente na definição da @stack('scripts')
.
Começo nosso script buscando pelo button
de ID destroyPhoto:
1
const
button
=
document
.
querySelectorAll
(
'button#destroyPhoto'
)
Como teremos vários buttons mediante a quantidade de imagens, itero em cada um deles para adicionar para cada um de forma individual o escutar do evento de clique, com o método addEventListener
.
Ao o usuário clicar na imagem, ignore o comportamento padrão do elemento com o e.preventDefault();
. Continuando, uso o confirm
do js para exibir um pequeno popup de confirmação, se o usuário confirmar temos o true e se ele cancelar teremos o false. Negando o cancelar do usuário e condicionando isso, simplesmente ignoro o processo de remoção.
Se o usuário confirmar que quer remover continuamos o script, onde pego o ID da foto a partir do dataset salvo na tag do elemento button e realizo a busca do formulário único e assim submeto ele para realizar o processo de remoção de fato da imagem.
Pegando o ID da foto via dataset:
1
const
photoId
=
el
.
dataset
.
photo
;
`
E o trecho onde busco o form para realizar a submissão e concretizar a remoção da imagem.
1
document.querySelector(`form#submitDestroy${
photoId
}
`).submit();
Agora que destaquei os principais pontos desta tela, adicione no arquivo app.blade.php
antes do fechamento da tag body
o trecho abaixo:
1
@stack
(
'scripts'
)
Perceba que comecei a utilizar as mensagens de feedback do processo, jogando as mensagens na sessão flash. Crie o arquivo messages.blade.php
na pasta includes
(precisa ser criada também) dentro de views.
Neste arquivo definiremos três blocos para mensagens de sucesso, erro e warning. Como o trecho é auto-explicativo e já falamos de sessão aqui nesta jornada.
Deixo o código completo abaixo:
1
@
if
(
session
(
'
success
'
))
2
<
div
class
=
"my-10 p-5 rounded border-green-900 bg-green-400 text-green-900 font-\
3
bold"
>
{{
session
(
'
success
'
)}}
</
div
>
4
@
endif
5
6
@
if
(
session
(
'
error
'
))
7
<
div
class
=
"my-10 p-5 rounded border-green-900 bg-red-400 text-red-900 font-bold\
8
"
>
{{
session
(
'
error
'
)}}
</
div
>
9
@
endif
10
11
@
if
(
session
(
'
warning
'
))
12
<
div
class
=
"my-10 p-5 rounded border-green-900 bg-yellow-400 text-yellow-900 fon\
13
t-bold"
>
{{
session
(
'
warning
'
)}}
</
div
>
14
@
endif
Inclua este arquivo no seu app.blade.php
acima do trecho do yeld('content')
. Veja abaixo:
1
@include
(
'includes.messages'
)
A diretiva @include
trabalha tal qual o include no PHP, com um adicional de você, se precisar, passar variáveis para este include via segundo parâmetro desta diretiva. Veja na íntegra como está nosso app.blade.php
após as alterações:
1
<!doctype html>
2
<html
lang=
"en"
>
3
4
<head>
5
<meta
charset=
"UTF-8"
>
6
<meta
name=
"viewport"
7
content=
"width=device-width, user-scalable=no, initial-scale=1.0, maximum-sc\
8
ale=1.0, minimum-scale=1.0"
>
9
<meta
http-equiv=
"X-UA-Compatible"
content=
"ie=edge"
>
10
<title>
Loja - Code Experts</title>
11
<script
src=
"https://cdn.tailwindcss.com"
></script>
12
</head>
13
14
<body>
15
<nav
class=
"w-full mb-10 flex justify-between bg-gray-900 px-4"
>
16
<a
href=
"/"
class=
"text-white py-4"
>
Experts Store 12</a>
17
<ul
class=
"flex"
>
18
<li
class=
"py-4"
>
19
<a
href=
"
{{
route
(
'products.index'
)
}}
"
20
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-\
21
in-out duration-300"
>
Produtos</a>
22
</li>
23
<li
class=
"py-4"
>
24
<a
href=
"
{{
route
(
'categories.index'
)
}}
"
25
class=
"text-white px-6 py-4 hover:bg-purple-800 transition ease-\
26
in-out duration-300"
>
Categorias</a>
27
</li>
28
<li
class=
"py-4"
>
29
<form
action=
"
{{
route
(
'logout'
)
}}
"
method=
"post"
id=
"logout"
>
@csrf\
30
</form>
31
<a
href=
"#"
32
class=
"px-6 py-4 bg-red-800 hover:bg-red-950 text-white transiti\
33
on ease-in-out duration-300"
34
onclick=
"event.preventDefault(); document.querySelector('form#lo\
35
gout').submit()"
>
Sair</a>
36
</li>
37
<li
class=
"py-4"
>
38
<span
class=
" font-thin ml-4 text-white"
>
Olá, {{
str
(
auth
()->
user
()-
\
39
>
name
)->
explode
(
' '
)[
0
]
}}
</span>
40
</li>
41
</ul>
42
</nav>
43
<div
id=
"container"
class=
"max-w-7xl mx-auto p-6"
>
44
@include('includes.messages')
45
46
@yield('content')
47
</div>
48
49
@stack('scripts')
50
</body>
51
52
</html>
Eu adicionei também na tela da edição do Produto o link para a tela de upload de fotos. Adicione acima do seu form de edição de produto o trecho abaixo:
1
<div
class=
"w-full mb-10 flex justify-end"
>
2
<a
href=
"
{{
route
(
'photos.index'
,
[
'product'
=>
$
product-
>
id
])
}}
"
class=
"px-4 py-\
3
2 rounded bg-purple-600 border border-purple-900 text-white font-bold hover:bg-purpl\
4
e-900 transition duration-300 ease-in-out"
>
Gerenciar Fotos</a>
5
</div>
E por fim, claro, as rotas para a tela e processos da área de uplaods:
1
Route
::
resource
(
'products/{product}/photos'
,
\
App
\
Http
\
Controllers
\
Admin
\
ProductPhot
\
2
oController
::
class
)
3
-
>
only
(
[
'index'
,
'store'
,
'destroy'
]
);
Coloque a definição acima dentro do grupo do admin nas rotas do arquivo web.php
.
No trecho acima destaco o uso do método only
, utilizei ele aqui para expor apenas as rotas para o index, store e destroy pois precisamos apenas destas três rotas.
Com isso concluímos nossa área de upload de fotos.
Trabalhar com upload de arquivos no Laravel é algo íncrivel e simples no contexto geral. O Laravel já traz todo o arcabouço necessário para isso, inclusive se precisarmos subir os arquivos no S3 da Amazon, podemos realizar o processo sem muito esforço.
Agora que temos nosso gerenciamento com muitas possibilidades, vamos partir para nosso Front e criar o processo público onde nossos usuários(clientes) poderão realizar suas compras e gerenciar seus pedidos.
Vamos lá!
Olá, tudo bem? Espero que sim!
Chegamos ao capítulo onde iremos criar o front da nossa loja. Como a navegação de produtos e a parte do nosso carrinho de compras.
Então vamos lá!
Antes de iniciarmos, quero falar um pouco sobre a parte de Tag Componentes do blade, minha intenção aqui é te ambientar deste ponto de forma a te preparar quando começar a desbravar os starter kits baseados em blade na camada de template. As tag componentes ou x tag componentes
não são algo novo ou exclusivo 12+ mas foi uma novidade lá no Laravel 7 e é bastante utilizado nas estruturas de starter kit do Laravel como forma de reutilização na camada de template com Blade.
Os blade tag componentes têm algumas particularidades e podem ser associados a camada PHP + Blade, em nosso caso focarei apenas na parte Blade ou o nome correto: componentes de forma anônima. Estes componentes residem em uma pasta chamada de components
que fica ou pode ser criada na pasta de views do framework.
Você pode criar seu(s) componentes de forma manual ou via comando. Exemplo:
1
php artisan make:component exemplo --view
O parâmetro --view
indica que quero apenas criar meu componente anônimo sem está atrelado a uma classe PHP (recomendo criar um componente sem o –view para você olhar a estrutura, ao final do capítulo deixarei uma referência de um vídeo onde mostrei essa novidade pro caso de você se aprofundar mais). O componente será criado na pasta resources/views/components
e o arquivo será o exemplo.blade.php
.
Nesse componente podemos interagir de N formas, desde isolando pontos reutilizáveis a passar para este dados a serem trabalhados de forma individual. Por exemplo, podemos isolar uma lista e sua iteração em cima destes componentes. A passagem de valores para este componente se assemelha tal qual componentes nos frameworks JS, inclusive são sua inspiração e recebem os mesmo nome props
ou passagem via props
.
Por exemplo, vamos fazer deste componente gerado um pequeno Hello World
. Veja abaixo seu conteúdo:
1
@props([
2
'name'=> null
3
])
4
5
<div>
6
<h1>
Olá, {{
$
name
}}
- Hello World!</h1>
7
</div>
Olhando o trecho acima perceba que inicialmente definimos em nosso componente a diretiva @props
onde listamos os parâmetros que podem ser passados para nosso componente. Perceba que o nome da props ou das props, deve ser utilizada no template como variáveis, se você estivesse utilizando o componente com a camada PHP, você receberia estas props via construtor sem a necessidade de informar na camada de template mas como estamos com um componente anônimo é importante você listar props que receberão os valores para serem utilizadas na finalidade que seu componente espera e foi idealizado.
Nosso exemplo, simplemente printo a mensagem junto do nome dinâmico recebido por quem chamar e passar o valor pro nosso componente. Em qualquer template do blade você pode chamar este componente ou outros(respeitando o nome do componente) da seguinte forma:
1
<x-exemplo :name="$name" />
Perceba que nosso tag componente recebe o prefixo x-
indicando que este é um componente do Blade, logo após informamos o nome do componente sem a extensão do arquivo e ignorando o caminho de pastas até ele (o blade se vira para carregar desde que esteja na pasta correta e com a extensão .blade.php
).
Nossa prop é informada tendo o nome prefixado por :
indicando que é uma prop dinâmica, imagine também que $name
é uma variável que veio do meu controller para a view que chamou o componente. Com isso teremos nossa mensagem exibida junto do valor que vier na variável $name
que está sendo passada pro nosso componente.
PS.: Lembrando que o parâmetro
:name
deve ser o mesmo nome listado na diretiva @props, o nome da variável dinâmica pode mudar pois contêm o valor externo sendo passando para o componente consumir internamente.
Um outro conceito importante nos componentes é a parte de slots e se você já conhece componentes JS vai entender bem tranquilamente esta ideia no Blade, toda a ideia de componentes na verdade.
Slots nos permitem utilizarmos os componentes com uma tag de abertura e fechamento, envolvendo um conteúdo e este conteúdo ser parte integrante do conteúdo do componente. Podemos utilizar o slot padrão ou podemos utilizar slots nomeados para organizar melhor o conteúdo e sua localização dentro do componete.
Por exemplo, na utilização:
1
<x-exemplo
:name"="$name"
>
2
<strong>
Conteúdo vai para o slot padrão...</strong>
3
</x-exemplo>
Internamete no componente basta que eu use o {{ $slot }}
e o conteúdo será jogado exatamente onde ele está definido dentro do componente:
1
@props([
2
'name'=> null
3
])
4
<div>
5
<h1>
Olá, {{
$
name
}}
- Hello World!</h1>
6
7
<hr>
8
9
{{
$
slot
}}
10
</div>
Se precisarmos organizar os slots via nome, podemos trabalhar da seguinte maneira. Na utilização:
1
<x-exemplo
:name"="$name"
>
2
3
<x-slot:header>
Conteúdo pro slot nomeado...</x-slot>
4
5
<strong>
Conteúdo vai para o slot padrão...</strong>
6
7
</x-exemplo>
Perceba que na utilização do componente chamei o componente x-slot
que me pemite depois dos :
informar o nome do slot nomeado que está definido no componente exemplo
. Para definir o local dele, podemos simplementes printar a variável $header
dentro do componente que o blade entregará o conteúdo do slot a ser exibido. Veja:
1
@props([
2
'name'=> null
3
])
4
<div>
5
6
{{
$
header
}}
<!-- slot nomeado, chamado de header -->
7
<hr>
8
9
<h1>
Olá, {{
$
name
}}
- Hello World!</h1>
10
11
<hr>
12
{{
$
slot
}}
<!-- slot padrão -->
13
</div>
Os slots nomeados facilitam muito quando precisamos organizar os conteúdos dentro do componente. Em componentes dentro do blade podemos realizar diversas outras operações como fazer bind de atributos usados no componente, fazer bind de atributos nos slots.Recomendo fortemente que você busque mais conteúdo sobre esta parte que não é muito mas já vai te ajudar bem quando precisar reutilizar partes das suas views com blade.
Este conteúdo mostrado aqui já é o suficiente para que possamos usar a técnica de layout base mas agora via componente. Um jeito diferente do que te mostrei no inicio e dentro do admin mas que vai te abrir muito a mente para os seus estudos futuros com starter kits oficiais do Laravel 12.
Vamos criar o componente para o layout da nossa loja na parte do front ou parte pública pros nossos clientes. Veja o conteúdo na integra e logo após foco nos principais pontos.
Veja o conteúdo do nosso layout como componente criado em resources/views/components/
e o componente é site.blade.php
:
1
<!
doctype
html
>
2
<
html
lang
=
"en"
>
3
<
head
>
4
<
meta
charset
=
"UTF-8"
>
5
<
meta
name
=
"viewport"
6
content
=
"width=device-width, user-scalable=no, initial-scale=1.0, maximum-\
7
scale=1.0, minimum-scale=1.0"
>
8
<
meta
http
-
equiv
=
"X-UA-Compatible"
content
=
"ie=edge"
>
9
<
title
>
Experts
Store
</
title
>
10
@
vite
([
'
resources
/
css
/
app
.
css
'
,
'
resources
/
js
/
app
.
js
'
])
11
</
head
>
12
<
body
class
=
"bg-fixed bg-gradient-to-b from-10% to-80% from-purple-800 to-black"
>
13
<
header
class
=
"w-full mb-10"
>
14
<
div
class
=
"max-w-7xl mx-auto flex justify-between items-center py-4"
>
15
<
a
href
=
"{{route('site.home')}}"
class
=
"font-extrabold text-white text-x\
16
l"
>
Experts
Store
</
a
>
17
18
<
nav
class
=
"mr-4"
>
19
<
ul
class
=
"flex items-center gap-x-4"
>
20
<
li
><
a
href
=
"{{route('site.home')}}"
21
class
=
"text-white hover:underline hover:font-bold transitio\
22
n duration-300 ease-in-out @if(request()->routeIs('site.home')) font-bold @else font\
23
-thin @endif"
>
Home
</
a
></
li
>
24
<
li
class
=
"relative"
>
25
<
a
href
=
"#sera_link_pro_cart"
26
class
=
"flex gap-x-2 items-center text-white hover:under\
27
line hover:font-bold transition duration-300 ease-in-out @if(request()->routeIs('sit\
28
e.home')) font-bold @else font-thin @endif"
>
29
<
svg
xmlns
=
"http://www.w3.org/2000/svg"
fill
=
"none"
view
\
30
Box
=
"0 0 24 24"
stroke
-
width
=
"1.5"
stroke
=
"currentColor"
class
=
"size-6"
>
31
<
path
stroke
-
linecap
=
"round"
stroke
-
linejoin
=
"round"
\
32
d
=
"M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m\
33
-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 \
34
14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 \
35
1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
36
</
svg
>
37
@
if
(
session
()
->
has
(
'
cart
'
)
&&
count
(
session
(
'
cart
'
)))
38
<
span
class
=
"rounded-xl p-1 text-white bg-red-700"
>
39
{{
count
(
session
(
'
cart
'
))}}
40
</
span
>
41
@
endif
42
</
a
>
43
</
li
>
44
</
ul
>
45
</
nav
>
46
</
div
>
47
</
header
>
48
<
div
class
=
"max-w-7xl mx-auto"
>
49
@
include
(
'
includes
.
messages
'
)
50
51
{{
$slot
}}
52
</
div
>
53
<
footer
class
=
"w-full mt-10"
>
54
<
div
class
=
"max-w-7xl mx-auto py-4 text-center border-t border-purple-500"
>
55
<
small
class
=
"text-white "
>
Experts
Store
&
copy
;
Todos
os
Direitos
Reserv
\
56
ados
</
small
>
57
</
div
>
58
</
footer
>
59
@
stack
(
'
scripts
'
)
60
</
body
>
61
</
html
>
Nesse layout já deixei no ponto o ícone do nosso carrinho e como já conhecemos session no Laravel, já tomei a liberdade de deixar o contador de itens no carrinho, pegos da sessão, prontos neste código acima. Em breve vamos linkar a index do carrinho mas por hora o link que envolve o ícone do carrinho segue com o #sera_link_pro_cart
.
Perceba no layout o ponto em que uso o $slot
. O conteúdo que nosso componente envolver será colocado neste local do componente, local onde está a variável $slot
. Um ponto importante e diferente aqui é que chamei nossos assets via diretiva @vite
lá na parte head
do HTML.
O vite é um asset bundler, criado inicialmente pelo Evan You (criador do VueJS), que permite realizar diversas operações como gerar bundlers otimizados baseados nos assets frontends da aplicação, gerenciar scripts, organizar e compilar css, automatizar testes e muito mais. O Laravel até este ponto já traz uma integração pro Vite na aplicação e inclusive nosso app, desde o ínicio, já conta com o TailwindCSS configurado.
Desta forma, para usarmos esses assets gerenciados pelo ViteJS chamamos os arquivos de entrada via diretiva @vite
como abaixo:
1
@vite
(
[
'resources/css/app.css', 'resources/js/app.js'
]
)
Temos listado acima os entrypoint(arquivos de entrada) para geração do bundler pros assets na parte javascript e também na parte CSS que aqui já está configurado com TailwindCSS. Desta forma vamos precisar realizar os builds dos assets e isso pode ser feito automaticamente em DEV, usando o comando em seu terminal: npm run dev
.
É claro que a esta altura esse comando pode ser um problema pra você, como assim? O npm normalmente vêm junto da instalação do NodeJS, então, se você já tiver o node o comando npm é algo usual pra você. Caso não tenha, é necessário instalar o Node e junto você terá acesso ao NPM (Node Package Manager) que permite, além de instalar dependências de front, executar scripts que estão no arquivo package.json do projeto.
Instalar o Node é bem tranquilo e imagino que você não terá problemas quanto a isso, aqui vale salientar que em repositórios Linux o NodeJs e Npm são pacotes distintos, neste caso você precisará instalar ambos para que tudo funcione corretamente.
O comando npm run dev
ficará escutando alterações nas suas views e arquivos entrypoint, e a medida que você for alterando e salvando o comando atualiza suas telas como forma de mostrar/refletir suas alterações. Como o proprio comando sugere, ele deve ser usado apenas em desenvolvimento, pois ao executar o comando um mini server para o frontend será levantado pelo Vite.
No futuro quando você encerrar sua primeira release, o comando para gerar os assets para produção será o npm run build
, que você pode executar em DEV mas se torna um trabalho cansativo quando você pensar que têm N alterações e para cada alteração ter de executar o build rsrsrs. Por isso ele é usado para gerar o build de Produção.
Aqui na parte do front da loja serei o mais dinâmico possível uma vez que teremos muito repeteco de conhecimento. Vamos dá uma olhada nas rotas que criei para estas áreas:
web.php:
1
Route
::
get
(
'/'
,
[
\
App
\
Http
\
Controllers
\
Site
\
HomeController
::class
,
'index'
]
)
-
>
name
(
'\
2
site.home'
);
3
Route
::
get
(
'/product/{product:slug}'
,
[
\
App
\
Http
\
Controllers
\
Site
\
HomeController
::cl
\
4
ass
,
'single'
]
)
-
>
name
(
'site.single'
);
Baseado nas rotas acima, vamos precisar criar nosso HomeController que conterá os métodos para nossa home e nossa single do produto. Perceba, em específico na rota da single, que usamos o parâmetro dinâmico com o escopo de slug. Isso nos ajudará a, ao recebermos a instância do Model no método single esta instância vir populada com o Produto baseado no Slug informado na URL. Lembrando que o :slug
se refere a coluna slug onde o resolvedor do parâmetro buscará o dado a ser populado na instância do Model injetado no método Single.
Veja na íntegra nosso controller e vamos a mais alguns conhecimentos:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Site
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Product
;
7
use
Illuminate
\Http
\Request
;
8
9
class
HomeController
extends
Controller
10
{
11
public
function
index
(
Product
$
product
)
12
{
13
$
products
=
$
product
->
whereIsActive
(
true
)
->
orderBy
(
'id'
,
'DESC'
)
->
take
(
10
)
->
\
14
get
();
15
16
return
view
(
'site.home'
,
compact
(
'products'
));
17
}
18
19
public
function
single
(
Product
$
product
)
20
{
21
$
product
->
load
(
'photos'
);
22
23
return
view
(
'site.single'
,
compact
(
'product'
));
24
}
25
}
Veja o método single e index, ambos recebem via injeção nosso model Product. Existe uma pequena diferença em ambos com respeito a esta injeção, no index como não tenho parâmetro dinâmico receberei apenas uma instância do Model para ser utilizada, já na single receberemos uma instância do Model populado com o Produto encontrado para o slug dinâmico informado e caso ele não exista receberemos um 404 lançado pelo framework antes mesmo de tentar executar o método do nosso controller.
No index perceba também que usei o método whereIsActive
que será traduzido pelo framework para where('is_active', true)
(onde is_active seja igual a true), usando métodos mágicos desta forma você poupa muita escrita. Por exemplo, você quer buscar o produto baseado no slug, você faria algo assim:
1
->
where
(
'slug'
,
$
slug
);
Com a escrita via método mágico, você pode fazer assim:
1
->
whereSlug
(
$
slug
);
Desta forma trago mais um exemplo para você fixar esta escrita, lembrando que a coluna deve existir na sua tabela caso contrário receberá um erro. Continuando no index perceba que peguei apenas 10 produtos, com o método taken
e ordenei pelo id mais recente. O query builder do Laravel permite que criemos queries das mais variadas formas, então considere aqui um ínicio para esta jornada na parte de queries no Laravel, quanto mais você for buscando prosseguir na jornada mais facilitadores encontrará para criar suas queries e realizar suas execuções no banco de dados.
No método single, usei o método load
para enviar junto do produto recebido as fotos deste produto. Desta forma podemos utilizá-las na view para exibir as fotos do produto.
Aqui faço uma pausa para falar de mais um conceito beeem importante no Eloquent, que é Lazy e Eager Loading. Quando falamos de lazy loading ou carregamento (lento,preguiçoso) tratamos dos dados relacionados apenas quando acessamos a ligação, ou seja, imagina que eu busco um produto:
1
Product
::
find
(
10
);
Este produto por padrão trará apenas os dados dele próprio, mesmo tendo dados relacionados. Quando chamamos posterior algo como $product->photos
é somente neste momento que teremos a query e a busca das fotos do produto buscado. Então somente quando precisamos do dado é que ele será buscado e entregue dando a característica do Lazy Loading.
Já no Eager Loading, você terá seu dado junto dos dados relacionados ao buscar por eles. Usando eager loading para trazer o produto e suas fotos, por exemplo, teríamos algos como abaixo:
1
Product
::
with
(
'photos'
)
-
>
find
(
10
);
Perceba que via método with
já faço o loading da relação e logo em seguida tenho a busca do produto de id 10, desta forma nosso resultado será o model Product com os dados do produto 10 junto dos dados relacionados, neste caso as fotos. Se você tiver mais relações e quer trazer todas juntas, basta informa com a adição de mais parâmetros ou passar um array informando o nome das relações baseados no(s) método(s) de ligação em seu Model.
Exemplo:
1
Product
::
with
(
'relacao1'
,
'relacao2'
);
2
3
//
ou
4
5
Product
::
with
(
[
'relacao1'
,
'relacao2'
]
);
Lembrando que podemos também executar queries, por exemplo condicionais, para trazer apenas dados específicos na chamada do método with. Por exemplo imagina que eu quisesse apenas as fotos ativas, poderia fazer desta forma:
1
Product
::
with
(
[
'photos'
=>
fn
(
$query
)
=>
$query
->
where
(
'status'
,
true
)
]
)
-
>
find
(
10
);
Desta forma podemos realizar condicionais nas querys dos dados relacionados e trazidos via Eager Loading. As possibilidades são inúmeras e recomendo já anotar aí mais um ponto a explorar após a leitura deste material!
Mas Nanderson, você usou o método load
e não with
. Nesse caso estou usando o Lazy Eager Loading, pois como o Laravel já me entregou o model populado com o Produto baseado no slug do parâmetro dinâmico, usei este mecanismo para atachar as fotos do produto em tempo de execução antes de ir para a view.
Neste ponto temos a vantagem no Eager Loading quando pensamos no problema N+1 em queries no banco. Por exemplo, imagina que você tenha um endereço para o cliente e ao buscar 10 clientes você quer o endereço de cada um deles. Ao todo teremos 10 queries para os endereços + a query da busca dos clientes. Exemplo:
1
$
customers
=
\
App
\
Models
\
Customer
::
take
(
10
)
-
>
get
();
2
3
foreach
($
customers
as
$
customer
)
{
4
echo
$customer->address->address
;
5
}
Com o padrão lazy loading teríamos a query dos clientes + 1 query para cada chamada de endereço. Com eager loading essas buscas seriam reduzidas, pois seriam trazidas em lote. Por exemplo com a adição do with, baseado no mesmo trecho acima:
1
$
customers
=
\
App
\
Models
\
Customer
::
with
(
'address'
)
-
>
take
(
10
)
-
>
get
();
2
3
foreach
($
customers
as
$
customer
)
{
4
echo
$customer->address->address
;
5
}
Desta forma teríamos uma query tal qual vemos abaixo:
1
-- query pros customers
2
SELECT * FROM customers LIMIT 10;
3
4
-- query pros endereços
5
6
SELECT * FROM address WHERE customer_id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Perceba que ao invés de 11 queries, temos apenas duas queries quando usamos eager loading, ou seja, o ponto aqui não é termos duas queries mas sim a redução destas queries na chamada para aquela busca no banco de dados. Vão existir cenários pequenos que o Lazy Loading será o suficiente para você mas sempre que você identificar telas que demandam de muita chamada a relacionamentos tenha o cuidado de buscar seus dados com eager loading e claro sempre que possível pagine seus resultados.
Existe mais um conhecimento que existe no Eloquent e é aplicado em seus Models, a possibilidade de modificar os dados da sua tabela em tempo de execução tanto na chamada do dado quanto na criação deste dado. Por exemplo, criarei um accessor e mutator para nossa coluna do preço (lembra que estamos usando ela como inteiro mas precisamos formatar para exibir e também quando vamos salvar e/ou atualizar). Um accessor e um mutator no model deixará este ponto mais organizado.
Veja o método abaixo que coloquei no Product.php
model:
1
public
function
price
():
Attribute
2
{
3
return
new
Attribute(
4
get
:
fn
(
$
value
)
=>
$
value
/
100
,
5
set
:
fn
(
$
value
)
=>
$
value
*
100
6
);
7
}
Acima temos um método chamado de price
que será lido pelo Laravel em prioridade quanto eu tentar acessar a propriedade price
do nosso produto. O callback para o parâmetro* get
, pega nosso valor inteiro e colocar a vírgula duas casas pra esquerda trazendo nosso ponto decimal pro lugar correto, já o parâmetro set
recebe o callback que pegerá o valor informado para propriedade price
e colocar o ponto decimal duas casas para a direita deixando nosso inteiro no ponto a ser salvo. O accessor seria o get
e o mutator seria oset
, o accessor age quando pego o valor do preço do produto e o mutator quando vou salvar ou atualizar este preço.
*usei acima os parâmetros nomeados do PHP 8 no construtor do Attribute
Desta forma podemos organizar melhor também nossos controllers, no momento dos saves do produto. Ainda no model Product, criei um accessor para colocar uma thumb na home baseado na ligação de Produto e Fotos.
Veja abaixo:
1
public
function
thumb
():
Attribute
2
{
3
return
new
Attribute(
get
:
fn
(
$
value
)
=>
$
this-
>
photos
?->
first
()
?->
photo
);
4
}
PS.: Não esqueça de importar no model Product o cast Attribute usado na definição dos accessors e mutators:
use Illuminate\Database\Eloquent\Casts\Attribute;
Neste caso, thumb não existe na tabela então ele vira uma propriedade virtual do objeto/model Produto. Quando acessado trará, se existir, a foto thumb relacionada ao produto, que acima pego a primeira foto da ligação. Você verá esta chamada quando codarmos a view front da loja.
Abaixo segue o model completo para consulta:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Casts
\Attribute
;
6
use
Illuminate
\Database
\Eloquent
\Model
;
7
use
Illuminate
\Database
\Eloquent
\Factories
\HasFactory
;
8
use
Illuminate
\Database
\Eloquent
\Relations
\HasMany
;
9
10
class
Product
extends
Model
11
{
12
use
HasFactory
;
13
14
protected
$
fillable
=
[
15
'name'
,
16
'description'
,
17
'body'
,
18
'slug'
,
19
'is_active'
,
20
'in_stock'
,
21
'price'
22
];
23
24
public
function
photos
():
HasMany
25
{
26
return
$
this
->
hasMany
(
ProductPhoto
::
class
);
27
}
28
29
public
function
categories
()
30
{
31
return
$
this
->
belongsToMany
(
Category
::
class
);
32
}
33
34
/**
35
*
Accessors
36
*/
37
public
function
thumb
():
Attribute
38
{
39
return
new
Attribute
(
get
:
fn
(
$
value
)
=>
$
this
->
photos
?
->
first
()
?
->
photo
);
40
}
41
42
public
function
price
():
Attribute
43
{
44
return
new
Attribute
(
45
get
:
fn
(
$
value
)
=>
$
value
/
100
,
46
set
:
fn
(
$
value
)
=>
$
value
*
100
47
);
48
}
49
}
Baseado no accessor/mutator de price, veja o método store e update alterados lá no ProductController
da pasta Admin
:
store:
1
public
function
store
(
ProductRequest
$
request
)
2
{
3
$
data
=
$
request
->
all
();
4
$
data
[
'price'
]
=
(
float
)
str_replace
([
'.'
,
','
],
[
''
,
'.'
],
$
data
[
'price'
]);
5
6
$
product
=
$
this
->
product
->
create
(
$
data
);
7
8
$
product
->
categories
()
->
sync
(
$
data
[
'categories'
]);
9
10
return
redirect
()
->
route
(
'products.edit'
,
[
'product'
=>
$
product
->
id
]);
11
}
update:
1
public
function
update
(
$product
,
ProductRequest
$request
)
2
{
3
//Atualizando com mass assignment
4
$data
=
$request
->
all
();
5
$data
['
price
']
=
(
float
)
str_replace
([
'.'
,
','
],
['',
'.'
],
$data
['
price
']);
6
7
$product
=
$this
->
product
->
findOrFail
(
$product
);
8
$product
->
update
(
$data
);
9
10
$product
->
categories
()
->
sync
(
$data
['
categories
']);
11
12
return
redirect
()
->
route
('
products
.
edit
',
['
product
'
=>
$product
->
id
]);
13
}
Também remova esta linha do método edit
no Admin/ProductController
:
1
$
product
->
price
=
$
product
->
price
/
100
;
A nossa view da home está bem tranquila, segue ela abaixo:
View resources/views/site/home.blade.php
:
1
<
x
-
layouts
.
site
>
2
<
div
class
=
"w-full mt-20"
>
3
4
<
div
class
=
"border-b border-purple-500 mb-10 pb-4 flex justify-between items\
5
-center"
>
6
<
h1
class
=
"text-4xl font-bold text-white"
>
Produtos
</
h1
>
7
</
div
>
8
<
div
class
=
"max-w-7xl grid grid-cols-3 gap-14"
>
9
@
foreach
(
$products
as
$product
)
10
<
div
class
=
"w-96 min-h-[460px] relative bg-white border border-purpl\
11
e-500 rounded mb-10 shadow shadow-purple-500"
>
12
<
img
src
=
"{{$product->thumb ? asset('storage/' . $product->thumb\
13
) : asset('storage/no-photo.jpg') }}"
14
alt
=
"Foto Produto: {{$product->name}}"
15
class
=
"w-full h-52 rounded-t"
>
16
17
<
div
class
=
"p-4"
>
18
<
h4
class
=
"text-xl font-bold capitalize"
>
{{
$product
->
name
}}
<
\
19
/
h4
>
20
21
<
p
class
=
"text-xl font-thin py-2"
>
{{
$product
->
description
}}
<
\
22
/
p
>
23
24
<
h2
class
=
"text-3xl mt-4 mb-10 font-bold text-red-800"
>
R$
{{
\
25
number_format
(
$product
->
price
,
2
,
','
,
'.'
)}}
</
h2
>
26
27
<
a
href
=
"{{route('site.single', ['product' => $product->slug\
28
])}}"
29
class
=
"absolute bottom-2 px-8 py-4 mt-5 bg-gradient-\
30
to-b from-purple-500 to-purple-800
31
font
-
bold
text
-
white
transition
-
all
duration
-300
\
32
ease
-
in
-
out
rounded
33
hover
:
to
-
purple
-800
hover
:
from
-
purple
-900
">
34
Ver
produto
35
</
a
>
36
</
div
>
37
</
div
>
38
@
endforeach
39
</
div
>
40
</
div
>
41
</
x
-
layouts
.
site
>
Perceba que envolvo nosso componente de layout no conteúdo da view acima, e todo o conteúdo envolvido será jogado na variável $slot
do componente. Um ponto aqui, só pra constar, é que como nosso componente site.blade.php está na pasta layouts
dentro de components da pasta de views, precisamos referenciar o caminho na chamada do tag componente, como você ver acima: `<x-layouts.site></x-layouts.site>.
Nossa Home ficou assim:
Nossa view para a single do produto segue abaixo, em seguida pontuo os pontos importantes e novos:
resources/views/site/single.blade.php
:
1
<
x
-
layouts
.
site
>
2
<
div
class
=
"w-full mt-20"
>
3
4
@
php
$photos
=
$product
->
photos
;
@
endphp
5
6
<
div
class
=
"max-w-7xl @if($photos->count()) flex gap-x-4 @endif"
>
7
@
php
$photos
=
$product
->
photos
;
@
endphp
8
@
if
(
$photos
->
count
())
9
<
div
class
=
"w-[40%] p-2"
>
10
<
img
src
=
"{{ asset('storage/' . $photos->first()->photo) }}"
11
alt
=
"Foto Produto: {{$product->name}}"
12
class
=
"w-full p-1 rounded bg-purple-500 shadow shadow-black rounded-\
13
t"
id
=
"mainPhoto"
>
14
15
<
div
class
=
"w-full mt-4 flex gap-x-2"
>
16
@
foreach
(
$photos
as
$photo
)
17
<
img
src
=
"{{ asset('storage/' . $photo->photo) }}"
18
alt
=
"Foto Produto: {{$product->name}}"
19
class
=
"w-[25%] p-1 rounded bg-purple-500 shadow shadow-black\
20
rounded-t photoList"
>
21
@
endforeach
22
</
div
>
23
</
div
>
24
@
endif
25
<
div
class
=
"@if($photos->count()) w-[60%] @else w-full @endif pl-12"
>
26
<
h1
class
=
"text-4xl font-bold text-white mb-4"
>
Produto
:
{{
$product
->
\
27
name
}}
</
h1
>
28
<
p
class
=
"text-white"
>
{{
$product
->
description
}}
</
p
>
29
<
h2
class
=
"text-3xl mt-4 mb-10 font-bold text-white"
>
R$
{{
number_for
\
30
mat
(
$product
->
price
,
2
,
','
,
'.'
)}}
</
h2
>
31
32
<
form
action
=
"{{route('site.cart.add')}}"
method
=
"post"
>
33
@
csrf
34
35
<
div
class
=
"flex items-center gap-x-2"
>
36
<
label
class
=
"text-white"
>
Quantidade
</
label
>
37
<
input
type
=
"hidden"
name
=
"cart[product]"
value
=
"{{$product-\
38
>slug}}"
>
39
<
input
type
=
"number"
min
=
"1"
name
=
"cart[quantity]"
required
\
40
class
=
"w-[80px] p-4 rounded border-purple-900 text-white bg-black text-2xl"
value
=
"1\
41
"
>
42
@
error
(
'
cart
.
product
'
)
<
span
class
=
"text-xl font-bold text-w\
43
hite bg-red-800 rounded p-1"
>
{{
$message
}}
</
span
>
@
enderror
44
@
error
(
'
cart
.
quantity
'
)
<
span
class
=
"text-xl font-bold text-\
45
white bg-red-800 rounded p-1"
>
{{
$message
}}
</
span
>
@
enderror
46
</
div
>
47
<
button
48
class
=
"px-8 py-4 mt-5 bg-gradient-to-b from-purple-500 to-pu\
49
rple-800
50
font
-
bold
text
-
white
transition
-
all
duration
-300
ease
-
in
\
51
-
out
rounded
52
hover
:
to
-
purple
-800
hover
:
from
-
purple
-900
">
53
COMPRAR
54
</
button
>
55
</
form
>
56
</
div
>
57
58
</
div
>
59
<
div
class
=
"w-full mt-10 py-10 border-t border-purple-500"
>
60
<
p
class
=
"text-white text-xl"
>
{{
$product
->
body
}}
</
p
>
61
</
div
>
62
</
div
>
63
64
@
push
(
'
scripts
'
)
65
<
script
>
66
const
mainPhoto
=
document
.
querySelector
(
'
img
#
mainPhoto
'
)
67
const
listPhotos
=
document
.
querySelectorAll
(
'
img
.
photoList
'
)
68
69
listPhotos
.
forEach
(
photoEl
=>
{
70
photoEl
.
addEventListener
(
'
mouseover
'
,
e
=>
{
71
mainPhoto
.
src
=
e
.
target
.
src
;
72
})
73
})
74
</
script
>
75
@
endpush
76
</
x
-
layouts
.
site
>
De ínicio chamo a atenção para a diretiva @php @endphp
, que nos permite na view blade criar código vanilla PHP. Aqui uso apenas para definir uma variável que utilizarei na view, guardando as fotos do produto em questão.
Nesta view criei algumas condicionais baseadas na existência das fotos onde altero o layout caso o produto não tenha foto e adequo o layout pro caso de termos foto pro produto como forma de exibi-las organizadamente.
Essa view também traz um pequeno script onde aplico um efeito ao passar o mouse na lista de imagens, fazendo com que a imagem que você estiver passando o mouse seja destacada na imagem principal. Trecho do script destacado abaixo:
1
@
push
(
'
scripts
'
)
2
<
script
>
3
const
mainPhoto
=
document
.
querySelector
(
'
img
#
mainPhoto
'
)
4
const
listPhotos
=
document
.
querySelectorAll
(
'
img
.
photoList
'
)
5
6
listPhotos
.
forEach
(
photoEl
=>
{
7
photoEl
.
addEventListener
(
'
mouseover
'
,
e
=>
{
8
mainPhoto
.
src
=
e
.
target
.
src
;
9
})
10
})
11
</
script
>
12
@
endpush
Perceba que pra cada foto da lista de fotos adicionao um listener pro evento de mouseover (passar do mouse sobre o elemento). Com isso pego o source da imagem que está ativa no hover e substituo na tag img principal dando o efeito comentado anteriormente. É um script bem simples de entender numa lida tranquila.
Nesta view ainda coloquei um form para que possamos enviar os dados do produto pro carrinho que criaremos em breve, além das info do produto mando também a quantidade desejada.
Como vamos aplicar algumas validações nestes dados do produto->carrinho, já coloquei também a diretivar @error para exibir as mensagens de validação.
Depois veja nosso repositório, como comentei no ínicio do livro, para pegar este códigos de forma mais livre e legível.
A single do produto que têm fotos:
Single do produto sem foto:
Nosso carrinho de produtos será gerenciado via sessão, então aqui teremos muito conhecimento que já mencionei na jornada junto do conhecimento prévio que você já têm de PHP.
Vamos gerar nosso CartController:
1
php artisan make:controller Site/CartController
Gere também o FormRequest, CartRequest:
1
php artisan make:request CartRequest
Veja o conteúdo do CartRequest abaixo:
1
<
?
php
2
3
namespace
App
\Http
\Requests
;
4
5
use
Illuminate
\Foundation
\Http
\FormRequest
;
6
7
class
CartRequest
extends
FormRequest
8
{
9
/**
10
*
Determine
if
the
user
is
authorized
to
make
this
request
.
11
*/
12
public
function
authorize
():
bool
13
{
14
return
true
;
15
}
16
17
/**
18
*
Get
the
validation
rules
that
apply
to
the
request
.
19
*
20
*
@
return
array
<
string
,
\Illuminate
\Contracts
\Validation
\ValidationRule
|
array
<
m
\
21
ixed
>|
string
>
22
*/
23
public
function
rules
():
array
24
{
25
return
[
26
'cart.quantity'
=>
'integer|min:1'
,
27
'cart.product'
=>
'required|string'
28
];
29
}
30
31
public
function
messages
()
32
{
33
return
[
34
'cart.quantity.min'
=>
'Quantidade Inválida!'
,
35
'cart.product.required'
=>
'Escolha o produto para adicionar no carrinho
\
36
!'
37
];
38
}
39
}
Validei tanto o envio do campo product
quanto do quantity
, onde o usuário deve selecionar pelo menos a quantidade 1
para o produto escolhido. Também traduzi as mensagens dentro do form request como já vimos anteriormente.
Segue o conteúdo na íntegra do nosso CartController:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Site
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Http
\Requests
\CartRequest
;
7
use
App
\Models
\Product
;
8
use
App
\Services
\CartService
;
9
use
Illuminate
\Http
\Request
;
10
11
class
CartController
extends
Controller
12
{
13
public
function
__construct
(
private
CartService
$
cartService
)
14
{
15
}
16
17
public
function
index
()
18
{
19
$
cartItems
=
$
this
->
cartService
->
all
();
20
return
view
(
'site.cart'
,
compact
(
'cartItems'
));
21
}
22
23
public
function
add
(
CartRequest
$
request
,
Product
$
product
)
24
{
25
$
item
=
$
request
->
validated
(
'cart'
);
26
27
$
product
=
$
product
->
whereSlug
(
$
item
[
'product'
])
->
firstOrFail
();
28
29
$
this
->
cartService
->
add
([
30
'id'
=>
$
product
->
id
,
31
'quantity'
=>
$
item
[
'quantity'
],
32
'slug'
=>
$
product
->
slug
,
33
'name'
=>
$
product
->
name
,
34
'photo'
=>
$
product
->
thumb
,
35
'price'
=>
$
product
->
price
36
]);
37
38
session
()
->
flash
(
'success'
,
'Produto Adicionado com Sucesso!'
);
39
return
redirect
()
->
back
();
40
}
41
42
public
function
remove
(
string
$
item
)
43
{
44
if
(
!$
this
->
cartService
->
has
(
$
item
,
column
:
'slug'
))
{
45
return
redirect
()
->
route
(
'site.index'
);
46
}
47
48
$
this
->
cartService
->
remove
(
$
item
);
49
50
return
redirect
()
->
route
(
'site.cart.index'
);
51
}
52
53
public
function
cancel
()
54
{
55
$
this
->
cartService
->
clear
();
56
57
return
redirect
()
->
route
(
'site.home'
);
58
}
59
}
Um ponto bem importante aqui neste controller é que delegamos todo o processo de gerenciamento do carrinho pra um service, que vamos criar ainda. Este service gerencia tanto as adições como remoções, limpeza do carrinho e recuperação dos itens do carrinho.
O método index do CartController carregará a tela principal do carrinho, exibindo os itens existente ali mas antes vamos ao CartService abaixo:
Crie manualmente o arquivo no caminho abaixo:
app/Services/CartService.php
:
1
<?php
2
3
namespace
App\Services
;
4
5
use
Illuminate\Session\SessionManager
;
6
7
class
CartService
8
{
9
private
const
SESSION_KEY
=
'cart'
;
10
11
public
function
__construct
(
private
SessionManager
$session
)
12
{
13
if
(
!
$this
->
session
->
has
(
self
::
SESSION_KEY
))
$this
->
session
->
put
(
self
::
SESSIO\
14
N_KEY
,
[]);
15
}
16
17
public
function
all
()
18
{
19
return
$this
->
session
->
get
(
self
::
SESSION_KEY
);
20
}
21
22
public
function
count
()
23
{
24
return
count
(
$this
->
all
());
25
}
26
27
public
function
has
(
string
$item
,
string
$column
=
'id'
)
28
{
29
$itemsList
=
array_column
(
$this
->
all
(),
$column
);
30
31
return
in_array
(
$item
,
$itemsList
);
32
}
33
34
public
function
add
(
array
$item
)
35
{
36
if
(
$this
->
session
->
has
(
self
::
SESSION_KEY
))
{
37
38
if
(
$this
->
has
(
$item
[
'slug'
],
'slug'
))
{
39
$items
=
$this
->
increaseExistentProduct
(
$this
->
all
(),
$item
);
40
41
$this
->
session
->
put
(
self
::
SESSION_KEY
,
$items
);
42
43
return
;
44
}
45
46
$this
->
session
->
push
(
self
::
SESSION_KEY
,
$item
);
47
48
}
else
{
49
$this
->
session
->
put
(
self
::
SESSION_KEY
,
[
$item
]);
50
}
51
}
52
53
public
function
remove
(
string
$item
)
54
{
55
$cart
=
$this
->
all
();
56
57
$this
->
session
->
put
(
self
::
SESSION_KEY
,
array_filter
(
$cart
,
function
(
$line
)
u\
58
se
(
$item
)
{
59
return
$line
[
'slug'
]
!==
$item
;
60
}));
61
}
62
63
public
function
clear
()
64
{
65
$this
->
session
->
put
(
self
::
SESSION_KEY
,
[]);
66
}
67
68
private
function
increaseExistentProduct
(
$items
,
$item
)
69
{
70
return
array_map
(
function
(
$itemLine
)
use
(
$item
){
71
if
(
$item
[
'slug'
]
==
$itemLine
[
'slug'
])
{
72
$itemLine
[
'quantity'
]
+=
$item
[
'quantity'
];
73
}
74
return
$itemLine
;
75
},
$items
);
76
}
77
}
Este CartService reune diversos conhecimentos que já passamos aqui e que imagino que você tenha na bagagem com PHP. Deixo aqui um exercício para que você leia e entenda este service que gerencia nosso carrinho na sessão via Manager Session do Laravel, este gerenciador de sessão é entregue também pelo helper session()
que já conhecemos.
Antes de criarmos a view index do carrinho, listo abaixo as rotas para a área do carrinho:
web.php
:
1
Route
::
prefix
(
'cart'
)
-
>
name
(
'site.cart.'
)
-
>
group
(
function
()
{
2
Route
:
:
get
(
'/'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CartController
::class
,
'index'
]
)
->
na
\
3
me
(
'index'
);
4
Route
:
:
post
(
'/add'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CartController
::class
,
'add'
]
)
->
\
5
name
(
'add'
);
6
Route
:
:
delete
(
'/remove/{item}'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CartController
::clas
\
7
s
,
'remove'
]
)
->
name
(
'remove'
);
8
Route
:
:
get
(
'/cancel'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CartController
::class
,
'cancel
\
9
'
]
)
->
name
(
'cancel'
);
10
}
);
Temos rota pro index, pra adição e remoção do item e também pro cancelar do carrinho.
Veja abaixo a view cart.blade.php
:
resources/views/site/cart.blade.php
:
1
<
x
-
layouts
.
site
>
2
3
<
div
class
=
"w-full"
>
4
<
div
class
=
"border-b border-gray-300 pb-4 flex justify-between items-center"
>
5
<
h1
class
=
"text-4xl font-thin text-white"
>
6
<
a
href
=
"{{route('site.home')}}"
class
=
"text-xl hover:underline hove\
7
r:text-black transition duration-300 ease-in-out"
>
Home
</
a
>
<
span
class
=
"text-xl"
>&
ra
\
8
quo
;
</
span
>
Carrinho
9
</
h1
>
10
</
div
>
11
12
<
div
class
=
"w-full"
>
13
@
cartcount
(
$cartItems
)
14
@
php
$totalCart
=
0
;
@
endphp
15
@
foreach
(
$cartItems
as
$item
)
16
<
div
class
=
"w-full py-8 border-b border-gray-300 flex justify-ar\
17
ound items-center gap-x-5"
>
18
<
div
class
=
"w-[10%]"
>
19
<
img
src
=
"{{$item['photo'] ? asset('storage/' . $item['p\
20
hoto']) : asset('storage/no-photo.jpg') }}"
21
alt
=
"Foto Produto: {{$item['name']}}"
22
class
=
"w-full rounded-t"
>
23
</
div
>
24
<
div
class
=
"w-2/3 flex flex-col"
>
25
<
div
class
=
"text-left"
>
26
<
h4
class
=
"font-bold capitalize text-white"
>
{{
$item
\
27
[
'
name
'
]}}
-
R$
{{
number_format
(
$item
[
'
price
'
],
2
,
','
,
'.'
)}}
({{
$item
[
'
quantity
'
]}
\
28
}
x
)
</
h4
>
29
@
php
$subtotal
=
$item
[
'
price
'
]
*
$item
[
'
quantity
'
];
\
30
@
endphp
31
<
h2
class
=
"text-xl font-thin text-white"
>
Subtotal
:
\
32
R$
{{
number_format
(
$subtotal
,
2
,
','
,
'.'
)}}
</
h2
>
33
</
div
>
34
</
div
>
35
<
div
>
36
<
form
action
=
"{{route('site.cart.remove', ['item' => $it\
37
em['slug']])}}"
method
=
"POST"
>
38
@
csrf
39
@
method
(
'
DELETE
'
)
40
<
button
class
=
"text-white cursor-pointer"
>
41
<
svg
xmlns
=
"http://www.w3.org/2000/svg"
fill
=
"no\
42
ne"
viewBox
=
"0 0 24 24"
stroke
-
width
=
"1.5"
stroke
=
"currentColor"
class
=
"w-6 h-6 hove\
43
r:text-red-800"
>
44
<
path
stroke
-
linecap
=
"round"
stroke
-
linejoin
\
45
=
"round"
d
=
"M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-\
46
1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L\
47
4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.1\
48
65m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.\
49
964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
\
50
/>
51
</
svg
>
52
53
</
button
>
54
</
form
>
55
</
div
>
56
</
div
>
57
@
php
$totalCart
+=
$subtotal
;
@
endphp
58
@
endforeach
59
60
<
div
class
=
"w-full py-8 border-b border-gray-300 flex justify-start \
61
text-white"
>
62
<
h2
class
=
"w-[20%] text-2xl font-extrabold"
>
Total
:
</
h2
>
63
<
strong
class
=
"w-2/3 text-left text-2xl font-extrabold"
>
R$
{{
nu
\
64
mber_format
(
$totalCart
,
2
,
','
,
'.'
)}}
</
strong
>
65
</
div
>
66
67
<
div
class
=
"w-full py-8 mt-10 flex justify-around gap-5 text-white"
>
68
<
a
href
=
"{{route('site.cart.cancel')}}"
class
=
"px-5 py-2 rounded\
69
text-white text-2xl font-bold bg-red-600"
>
Cancelar
</
a
>
70
<
a
href
=
"#"
wire
:
navigate
class
=
"px-5 py-2 rounded text-white te\
71
xt-2xl font-bold bg-green-600"
>
Continuar
</
a
>
72
</
div
>
73
@
else
74
<
h3
class
=
"w-full text-center text-white text-4xl font-thin py-4"
>
Ne
\
75
nhum
item
no
seu
carrinho
de
compras
!</
h3
>
76
@
endcartcount
77
</
div
>
78
</
div
>
79
</
x
-
layouts
.
site
>
Nossa view é bem tranquila também, iteramos nos produtos da sessão e realizamos as exibições do total e subtotal baseados no preço do produto e na sua quantidade existente no carrinho. Inclusive lá no service, temos o incremento da quantidade se o usuário tentar adicionar um produto que já exista na sessão do carrinho.
Existe um detalhe que trouxe nessa view mais como exploração e demonstração. O blade possui diversas diretivas como vimos diversas vezes durante nossa jornada, diretivas que começam com @
e seguido pelo nome da diretiva. Assim como existem diretivas padrão no Blade, podemos criar diretivas customizadas e em nossa tela você percebe que criei uma chamada @cartcount
e também criei uma de fechamento @endcartcount
.
Para isso, vamos colocar a definição dessas diretivas customizadas de forma global no framework. Para este caso adicionemos o trecho abaixo lá no arquivo AppServiceProvider
na pasta app/Providers
.
Coloque o trecho abaixo no método boot
:
1
Blade
::
directive
(
'cartcount'
,
function
(
string
$expression
)
{
2
return
"
<?php
if
(
count
(
$expression
))
:
?>
"
;
3
});
4
5
6
Blade
::
directive
(
'endcartcount'
,
function
(
string
$expression
)
{
7
return
'
<?php
endif
;
?>
'
;
8
});
PS.: Não esqueça de importar a facade Blade no AppServiceProvider:
use Illuminate\Support\Facades\Blade;
.
O método directive permite que criemos diretivas customizadas e aqui simplemesmente criei uma para envolver a condicional baseada no count de itens em nosso carrinho. O valor que eu usar na diretiva será passado para o parâmetro $expression
do callback, que é o segundo parâmetro do método directive
.
Neste caso, passo somente os itens do carrinho que é usado no count e testado pelo if que já conhecemos.
Exemplo:
1
@
cartcount
(
$itemsCart
)
2
<!--
Existindo
itens
no
cart
exibe
-->
3
4
<!--
posso
combinar
um
else
via
diretiva
padrão
pois
respeita
a
estrutura
do
if
-->
5
@
else
6
7
<!--
Algo
para
a
não
existência
de
itens
no
cart
-->
8
9
@
endcartcount
Veja a tela do carrinho:
Com as rotas do carrinho existindo no projeto, adicione o link pro index do carrinho no ícone do cart no componente layout site. Se você teve erro na sua single com respeito a url do cart add no form da single do produto, ela deve funcionar agora.
Aqui deixo um exercício pra você, a tela de produtos por categoria se torna fácil agora. Já buscamos bastante dado relacionado e buscar os produtos de uma categoria será algo bem tranquilo nesse momento pra você.
Tente criar as rotas pra tela da single do produto por categorias e entregar nesta tela os produtos pela categoria selecionada por seu slug, numa ideia de slug que usamos para exibir a single do produto.
Você pode até reutilizar a tela home pois os produtos vindos da ligação com a categoria, vêm em collection para ser iterados. Talvez no máximo você precise controlar pro caso da categoria não ter nenhum produto relacionado.
Acredito que você não terá problemas neste ponto, mas existindo problemas busque nosso grupo no telegram que comentei lá no ínicio do livro.
Bom, então chegamos ao fim de mais um capítulo aqui em nossa jornada, tivemos diversos pontos aqui nesta parte do front da nossa loja. Revise com calma cada código e não deixe de buscar mais e mais sobre os assuntos que pincelei aqui.
Temos também um grupo e um canal onde podemos conversar sobre os assuntos abordados aqui em nossa jornada, deixei ele no ínicio também.
No próximo capítulo vamos criar um mock pro processo de pagamento onde abordarei sobre o Container de Serviços do Laravel.
Nos vemos lá!
Neste capítulos vamos criar um mock para simular o processo de pagamentos em nosso site, utilizaremos aqui uma pequena ideia abordada em Arquitetura Limpa onde criaremos uma espécie de contrato para que os serviços de pagamento respeitem caso queiram ser utilizados como opção de pagamento.
Falei aqui de um jeito bem formal mas basicamente seria um contrato que eu devo segui para que eu integre um PagSeguro, PagarMe e outros gateways conhecidos. Isso vai me ajudar a ter menos motivos para modificar a lógica de pagamentos sempre que precisar de um gateway novo e claro que, como disse, aqui é um conceito bem pontual abordado em Clean Architecture (Arquitetura Limpa) e o conceito em si vai muito além do que o que eu mostro aqui.
Vamos compartilhar este serviço de pagamento via Container de Serviços do Laravel e com isso tenho oportunidade de trazer pra você os conceitos desta camada do framework.
No mundo da Orientação a Objetos temos diversos conceitos que visam facilitar as conversa e relacionamento dos objetos, além de irmos para conceitos mais abstratos buscando essa melhor organização e design.
Começo abordando aqui o pensamento de Injeção de Dependências como forma de chegarmos ao Container de Serviços que também é chamado de Container DI (Dependencie Injection Container ou Container de Injeção de Dependência).
Existem diversos cenários no mundo OO em que criamos diversos acoplamentos entre nossas classes, tornando determinado Objeto dependente e responsável pela instância de outro a qual este objeto necessita para realizar alguma operação.
Este cenário de dependência e responsabilidade pela instância de outro, conforme o tempo passa fica bem dificil de gerenciar, manter e no fim acaba tornando o crescimento truncado ainda mais se pensarmos nas diversas possíveis dependências que um determinado Objeto possa ter. Nesse cenário existe uma forma de relacionar o objeto de forma a não deixar este objeto responsável, ainda que dependente, da instância de outro objeto. Podemos recorrer a associação, onde um Objeto, pode existir sem a existência de outro dependente.
O mecanismo utilizado neste contexto, e ao retirarmos a responsabilidade da instância, é a Injeção da Dependência. Onde fora da classe dependente criamos a instância da sua dependência e a injetamos no objeto que espera aquela instância.
Pense num exemplo simples onde temos um objeto Query que depende de uma conexão com o BD e fora do objeto Query podemos criar a instância da conexão com BD e injetar essa instância no objeto Query para que ele possa trabalhar corretamente.
1
$
connection
=
new
MySQLConnector
(...);
2
3
$
queryBuilder
=
new
QueryBuilder
(
$
connection
);
4
5
return
$
queryBuilder
->
where
(...)
->
orderBy
(...)
->
get
();
Perceba que queryBuilder não precisa saber quem é a conexão e apenas precisa receber sua dependência requerida para trabalhar, ele(Query) também não precisa carregar a responsabilidade da instância da conexão e se formos mais abstratos ainda, pode inclusive tornar Query dependente de uma interface Connection, deixando a injeção da dependência da conexao mais maleável a quem respeitar a interface esperada.
Em cenários como este é normal termos algumas, não muitas, dependências e gerenciar isso manualmente pode ser um problema e é nesse ponto que automatizar e gerenciar de forma centralizada estas injeções faz sentido o uso de um Container de Serviços ou Container de Injeção de Dependências.
O Container de Serviços contêm as instâncias a serem compartilhadas no contexto global da aplicação, em um framework como o Laravel é uma parte crucial do ciclo de vida da aplicação pois compartilha diversos componentes a serem utilizados internamente no funcionamento das diversas parte do framerwork. Neste container você também pode e deve compartilhar seus serviços para que possam ser consumidos na sua aplicação, ao criar pacotes é possível compartilhar seus serviços para serem chamados via container sem problemas.
Inclusive você pode simplemente usar o utilitário do container apenas para instanciar seu objeto, além de poder fazer binds de interface e utilizar a instância Singleton do seu serviço, onde independentemente da chamada o container sempre entregará a mesma instância no ciclo de vida do momento da execução.
O laravel sempre compartilha em seus providers uma instância do container de serviços mas você pode invocar utilizado o helper app()
se assim desejar utilizar em outros momentos. Como comentei, iremos utilizar o container para compartilhar nosso serviço de Pagamento como bind de Interface, para isto criei na pasta app\Services
o namespace Payments
onde teremos nossa interface e uma classe concreta baseada nesta interface para exemplo.
Na pasta crie primeiro a interface:
1
<?php
2
namespace
App\Services\Payments
;
3
4
interface
PaymentInterface
5
{
6
public
function
doPayment
(
array
$data
)
:
array
;
7
}
Perceba que a priore obrigaremos aos participantes apenas a implementação do método doPayment
e sua assinatura, onde esperamos os dados para passar pro gateway de escolha. A classe concreta que criei se chama PayHappyService
(rs) e como comentei não teremos lógica nenhuma de pagamento aqui mas na maioria dos gateways que já usei essa interface pode resolver bem suas classes concretas.
Segue abaixo o conteúdo da classe PayHappyService
:
1
<?php
2
3
namespace
App\Services\Payments
;
4
5
class
PayHappyService
implements
PaymentInterface
6
{
7
public
function
doPayment
(
array
$data
)
:
array
8
{
9
//payment Algorithm
10
return
[
'gateway_code'
=>
str
()
->
uuid
()];
11
}
12
}
Nossa interface aqui funcionará com um contrato de fato, onde dependeremos de um concreto que seja do tipo da interface PaymentInterface
, com isso não dependeremos de um concreto e se precisarmos num futuro, poderemos trocar a instância da classe concreta sem muita dor de cabeça, perceba que não estou falando dor de cabeça zero mas o mínimo atrito na mudança é esperado.
Para definirmos no container de serviços do Laravel o bind da nossa interface e já pontuarmos uma classe concreta para satisfazer essa dependência vamos ao provider AppServiceProvider mas antes deixa eu esclarecer rapidamente o que são os providers no Laravel!
No ciclo de vida de sua aplicação Laravel os Service Providers são outro ponto beeem importante pois são eles que configuram as diversas áreas do framework além de permitirem a criação e integração de pacotes de terceiros no framework. Estes pacotes podem inclusive se configurar totalmente via Service Providers.
No Laravel 11 os Services Providers que ficavam explícitos na estrutura do framework agora fazem parte dos internos e suas configurações como pontos para a parte de Middlewares, Rotas, Error Handlers e outros podem ser efetuados no arquivo app na pasta bootstrap
. Um provider inicialmente existe explicitamente na estrutura do projeto e esse provider é o AppServiceProvider
que desde sempre nos permite configurarmos pontos que precisam ser globais em nossos projetos, o bind no container é um exemplo.
Esses providers contam com dois métodos a serem explorados inicialmente, o boot e o register e inclusive foi nesse provider que colocamos as configs da diretiva customizada do Blade no capítulo anterior. Pontos de bind no container de serviço é recomendado colocar estas configurações no método register
, outras configurações como a parte de diretivas customizadas e etc podem ser colocadas no método boot
.
Para realizarmos o bind por interface de nosso serviço de pagamentos podemos chamar o container via helper app
e acionar o método bind
:
1
app
()
-
>
bind
(
\
App
\
Services
\
Payments
\
PaymentInterface
::
class
,
\
App
\
Services
\
Payments
\\
2
PayHappyService
::
class
);
A linha acima é adicionada no método register
do AppServiceProvider
, veja na íntegra:
1
<
?
php
2
3
namespace
App
\Providers
;
4
5
use
Illuminate
\Support
\Facades
\Blade
;
6
use
Illuminate
\Support
\ServiceProvider
;
7
8
class
AppServiceProvider
extends
ServiceProvider
9
{
10
/**
11
*
Register
any
application
services
.
12
*/
13
public
function
register
():
void
14
{
15
app
()
->
bind
(
\App
\Services
\Payments
\PaymentInterface
::
class
,
\App
\Services
\Pa
\
16
yments
\PayHappyService
::
class
);
17
}
18
19
/**
20
*
Bootstrap
any
application
services
.
21
*/
22
public
function
boot
():
void
23
{
24
Blade
::
directive
(
'cartcount'
,
function
(
string
$
expression
)
{
25
return
"<?php if(count($expression)): ?>"
;
26
});
27
28
29
Blade
::
directive
(
'endcartcount'
,
function
(
string
$
expression
)
{
30
return
'<?php endif; ?>'
;
31
});
32
33
}
34
}
O bind a priore é bem simples e o poder que teremos no desacoplamento e futuras modificações com pouco atrito é o que brilha aqui com todo o conceito envolvido. Com este bind podemos agora utilizar a tipagem pela interface PaymentInterface
e o container de serviços do Laravel já entregará nosso objeto concreto.
Com este trabalho feito vamos ao nosso recurso de Checkout e tudo que engloba toda a parte de Pedido e utilização do que já bindamos aqui!
Vamos começar por nossa estrutura de pedidos, onde irei organizar da seguinte maneira:
Desta forma podemos ter os dados principais do pedido em sua tabela e os itens(produtos) ficam ligados na tabela itens do pedido. Para isso vamos gerar nossos models com suas respectivas migrations:
1
php artisan make:model Order -m
2
3
php artisan make:model OrderItem -m
Segue abaixo as colunas das tabelas para serem adicionadas nas migrações(migrations):
migration: create_orders_table:
1
$
table
->
foreignId
(
'user_id'
)
->
constrained
()
->
cascadeOnDelete
();
2
$
table
->
uuid
(
'code'
)
->
unique
();
3
$
table
->
uuid
(
'gateway_code'
)
->
nullable
();
4
$
table
->
string
(
'status'
)
->
default
(
'WAITING_PAYMENT'
);
migration: create_order_items_table:
1
$
table
->
foreignId
(
'order_id'
)
->
constrained
()
->
cascadeOnDelete
();
2
$
table
->
foreignId
(
'product_id'
)
->
constrained
();
3
$
table
->
integer
(
'quantity'
);
PS.: Apenas adicione as definições acima, coloquei entre os métodos id e timestamps
Segue abaixo os models já com suas ligações, nesta altura imagino que a leitura do código abaixo seja o suficiente:
Order:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Model
;
6
use
Illuminate
\Database
\Eloquent
\Relations
\BelongsTo
;
7
use
Illuminate
\Database
\Eloquent
\Relations
\HasMany
;
8
9
class
Order
extends
Model
10
{
11
protected
$
fillable
=
[
12
'user_id'
,
'code'
,
'status'
,
'gateway_code'
13
];
14
15
public
function
customer
():
BelongsTo
16
{
17
return
$
this
->
belongsTo
(
User
::
class
,
'user_id'
);
18
}
19
20
public
function
items
():
HasMany
21
{
22
return
$
this
->
hasMany
(
OrderItem
::
class
);
23
}
24
}
OrderItem:
1
<
?
php
2
3
namespace
App
\Models
;
4
5
use
Illuminate
\Database
\Eloquent
\Model
;
6
use
Illuminate
\Database
\Eloquent
\Relations
\BelongsTo
;
7
8
class
OrderItem
extends
Model
9
{
10
protected
$
fillable
=
[
11
'product_id'
,
'quantity'
12
];
13
14
public
function
order
():
BelongsTo
15
{
16
return
$
this
->
belongsTo
(
Order
::
class
);
17
}
18
19
public
function
product
():
BelongsTo
20
{
21
return
$
this
->
belongsTo
(
Product
::
class
);
22
}
23
}
Definido nossas migrations e nossos models, não esqueça de executar estas migrações na base com o comando já conhecido:
1
php artisan migrate
Vamos gerar nosso controller e suas definições e por fim partiremos para as views e rotas:
1
php artisan make:controller Site/CheckoutController
Neste controller vamos ter três métodos a priore: um pra tela de pagamento, para o processamento e tela de obrigado. Veja o controller e depois focamos no método que processa o pagamento, que na minha visão é o que demanda mais da nossa atenção:
Controller: CheckoutController:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Site
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Models
\Order
;
7
use
App
\Services
\CartService
;
8
use
App
\Services
\Payments
\PaymentInterface
;
9
use
Illuminate
\Http
\Request
;
10
use
Illuminate
\Support
\Str
;
11
12
class
CheckoutController
extends
Controller
13
{
14
public
function
__construct
(
private
Order
$
modelOrder
)
15
{
16
}
17
18
public
function
index
()
19
{
20
return
view
(
'site.checkout'
);
21
}
22
23
public
function
process
(
PaymentInterface
$
payment
,
CartService
$
cart
)
24
{
25
if
(
!$
cart
->
count
())
{
26
session
()
->
flash
(
'warning'
,
'Sem Itens para Processar Checkout!'
);
27
return
redirect
()
->
route
(
'site.home'
);
28
}
29
30
$
customer
=
auth
()
->
user
();
31
32
$
order
=
$
this
->
modelOrder
->
create
([
33
'code'
=>
Str
::
uuid
(),
34
'user_id'
=>
$
customer
->
id
35
]);
36
37
$
saveOrderItems
=
[];
38
foreach
(
$
cart
->
all
()
as
$
item
)
{
39
$
saveOrderItems
[]
=
[
40
'quantity'
=>
$
item
[
'quantity'
],
41
'product_id'
=>
$
item
[
'id'
],
42
];
43
}
44
$
order
->
items
()
->
createMany
(
$
saveOrderItems
);
45
46
$
gatewayCode
=
$
payment
->
doPayment
([
47
'our_code'
=>
$
order
->
id
,
48
'name'
=>
$
customer
->
name
,
49
'email'
=>
$
customer
->
email
,
50
'cardToken'
=>
'XYZ'
,
51
'paymentMethod'
=>
'CREDIT_CARD'
52
])[
'gateway_code'
];
53
54
55
$
order
->
update
([
'gateway_code'
=>
$
gatewayCode
]);
56
57
session
()
->
forget
(
CartService
::
SESSION_KEY
);
58
59
return
redirect
()
->
route
(
'site.checkout.thanks'
,
$
order
);
60
}
61
62
public
function
thanks
(
Order
$
order
)
63
{
64
return
view
(
'site.thanks'
,
compact
(
'order'
));
65
}
66
}
Vamos dá um zoom no método process
:
1
public
function
process
(
PaymentInterface
$
payment
,
CartService
$
cart
)
2
{
3
if(!$cart->count())
{
4
session()->flash('warning',
'Sem
Itens
para
Processar
Checkout!')
;
5
return
redirect()->route('site.home')
;
6
}
7
8
$
customer
=
auth
()
-
>
user
();
9
10
$
order
=
$
this-
>
modelOrder-
>
create
(
[
11
'code'
=>
Str
::uuid
(),
12
'user_id'
=>
$customer
->
id
13
]
);
14
15
$
saveOrderItems
=
[]
;
16
foreach
($
cart-
>
all
()
as
$
item
)
{
17
$saveOrderItems
[]
=
[
18
'quantity'
=>
$item
[
'quantity'
]
,
19
'product_id'
=>
$item
[
'id'
]
,
20
]
;
21
}
22
$
order-
>
items
()
-
>
createMany
($
saveOrderItems
);
23
24
$
gatewayCode
=
$
payment-
>
doPayment
(
[
25
'our_code'
=>
$order
->
id
,
26
'name'
=>
$customer
->
name
,
27
'email'
=>
$customer
->
email
,
28
'cardToken'
=>
'XYZ'
,
29
'paymentMethod'
=>
'CREDIT_CARD'
30
]
)
[
'gateway_code'
]
;
31
32
33
$
order-
>
update
(
[
'gateway_code'
=>
$gatewayCode
]
);
34
35
session
()
-
>
forget
(
CartService
::
SESSION_KEY
);
36
37
return
redirect
()
-
>
route
(
'site.checkout.thanks'
,
$
order
);
38
}
Perceba que aqui explicito duas dependências, sendo uma delas nosso objeto de pagamento que deve ser do tipo PaymentoInterface
e isso é possível, possível termos um concreto, por conta do bind no container de serviços do Laravel e deixando nosso controller livre da dependência de uma classe concreta o que nos ajudará muito na troca por outro gateway e claro que este ponto é só o ínicio do que você pode alcançar com este tipo de abstração.
Um ponto que irei comentar aqui para este método será no que estou fazendo no todo dele e não em cada ponto linha a linha, tudo bem? Basicamente neste método pego os itens do carrinho, existindo eu gero uma coleção com estes produtos a serem salvos em massa através do método createMany
via associação de Pedido(Order) com Itens Pedido(OrderItem).
Após isso, todo o processo de pagamento é mocado(como se) mas deixei bem explícito alguns detalhes pra fins de visualização de como é o business. Perceba que passei os dados para nosso objeto de pagamento como se eu estivesse mesmo passando os dados para um serviço real(PagSeguro, PagarMe etc.) e é claro que estes dados vão ser diferentes dependendo do gateway utilizado, como comentei aqui é mais pra termos algo próximo do real para te dá a visão melhor do processo.
O código do gateway que retornei do nosso HappyPaymentService seria algo retornado da API de um serviço real que sincronizaríamos em nossa base para futuras consultas, ao receber este código atualizo nosso Pedido(Order) com o código em questão, finalizando assim nosso processo de registro do pedido e pagamento.
Por fim redireciono nosso cliente para a tela de obrigado com alguns dados do pedido.
O restante do controller de checkout é bem simples e não carece, ao meu ver, de explicações uma vez que temos apenas chamadas para views :D
Dentro da pasta de views, na pasta site, crie os arquivos checkout.blade.php
e o thanks.blade.php
. Segue os códigos abaixo:
checkout.blade.php:
1
<x-layouts.site>
2
<div
class=
"border-b border-gray-300 pb-4 flex justify-between items-center"
>
3
<h1
class=
"text-4xl font-thin text-white"
>
4
<a
href=
"
{{
route
(
'site.home'
)
}}
"
class=
"text-xl hover:underline hover:te\
5
xt-black transition duration-300 ease-in-out"
>
Home</a>
<span
class=
"text-xl"
>
»
\
6
</span>
Checkout
7
</h1>
8
</div>
9
<div
class=
"w-full h-[480px] flex justify-center items-center"
>
10
<div
class=
"w-[380px] rounded p-4"
>
11
<form
action=
"
{{
route
(
'site.checkout.process'
)
}}
"
method=
"POST"
>
12
@csrf
13
<div
class=
"w-full mb-6"
>
14
<label
for=
"card_number"
class=
"text-white mb-2"
>
Número Cartão<
/\
15
label>
16
<input
type=
"text"
name=
"card_number"
id=
"card_number"
placehold\
17
er=
"Número Cartão..."
required
18
class=
"w-full p-2 rounded border border-gray-900 bg-white"
>
19
</div>
20
21
<div
class=
"w-full mb-6 flex gap-x-2"
>
22
<div
class=
"w-1/2"
>
23
<label
for=
"card_date"
class=
"text-white mb-2"
>
Validade Cart\
24
ão</label>
25
<input
type=
"text"
name=
"card_date"
id=
"card_date"
placehold\
26
er=
"Validade Cartão..."
required
27
class=
"w-full p-2 rounded border border-gray-900 bg-whit\
28
e"
>
29
</div>
30
31
<div
class=
"w-1/2"
>
32
<label
for=
"card_cvv"
class=
"text-white mb-2"
>
CVV Cartão<
/la\
33
bel>
34
<input
type=
"text"
name=
"card_cvv"
id=
"card_cvv"
placeholder\
35
="CVV
Cartão..."
required
36
class=
"w-full p-2 rounded border border-gray-900 bg-whit\
37
e"
>
38
</div>
39
</div>
40
41
<button
class=
"px-5 py-2 rounded text-white text-5xl font-bold bg-gr\
42
een-600"
>
43
PAGAR
44
</button>
45
</form>
46
</div>
47
</div>
48
</x-layouts.site>
Apesar de não termos um processo de cartão e pagamento, criei uma tela com alguns campos para informar cartão de crédito, como comentei algo mais de perfumaria neste momento uma vez que mocamos o pagamento.
thanks.blade.php:
1
<x-layouts.site>
2
<div
class=
"w-full mt-20 text-white"
>
3
<h3
class=
"text-2xl font-extrabold mb-4"
>
Obrigado, compra concluída!</h3>
4
5
<p
class=
"text-xl"
>
6
Código compra: <strong>
{{
$
order-
>
code
}}
</strong>
<br>
7
Sua compra foi registrada e está aguardando a confirmação do pagamento, \
8
assim que o status for modificado te notificaremos via e-mail.
9
</p>
10
11
</div>
12
</x-layouts.site>
Abaixo segue no grupo de rotas pro checkout:
1
Route
::
prefix
(
'checkout'
)
-
>
name
(
'site.checkout.'
)
-
>
middleware
(
'auth'
)
-
>
group
(
functio
\
2
n
()
{
3
Route
:
:
get
(
'/'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CheckoutController
::class
,
'index'
]
)
\
4
->
name
(
'index'
);
5
Route
:
:
post
(
'/process'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CheckoutController
::class
,
'
\
6
process'
]
)
->
name
(
'process'
);
7
Route
:
:
get
(
'/thanks/{order}'
,
[
\
App
\
Http
\
Controllers
\
Site
\
CheckoutController
::c
\
8
lass
,
'thanks'
]
)
->
name
(
'thanks'
);
9
}
);
Caso tenha dúvidas quanto ao código recorra ao github que deixei nos capítulos introdutórios ou nosso grupo no telegram.
O checkout é algo que é bem pessoal do usuário e inclusive no momento de finalização precisamos da referência deste usuário(cliente) para associar ao pedido. Por esta razão precisamos colocar este grupo de rotas sob autenticação como mostro no código acima ao utilizar o middleware auth.
Para concluirmos nosso processo de checkout precisamos linkar o checkout no carrinho e também criar a tela de registro pro cliente que não possui cadastro.
Como criaremos a tela de cadastro e estamos pedindo que nosso cliente esteja autenticado durante o checkout, eu coloquei um link abaixo do btn de login pra tela de registro.
Claro, vamos antes gerar o que é preciso pra tela de registro/cadastro e posteriormente vamos colocar este link que comentei. Vamos gerar nosso RegisterController
e seu form request:
1
php artisan make:controller Auth/RegisterController
2
3
php artisan make:request Auth/RegisterRequest
Segue na íntegra o conteúdo do controller e do Form Request:
RegisterController:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Auth
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
App
\Http
\Requests
\Auth
\RegisterRequest
;
7
use
App
\Models
\User
;
8
use
App
\Services
\CartService
;
9
use
Illuminate
\Auth
\Events
\Registered
;
10
use
Illuminate
\Http
\Request
;
11
12
class
RegisterController
extends
Controller
13
{
14
public
function
__construct
(
private
User
$
userModel
)
15
{
16
17
}
18
public
function
index
()
19
{
20
return
view
(
'auth.register'
);
21
}
22
23
public
function
store
(
RegisterRequest
$
request
,
CartService
$
cart
)
24
{
25
$
data
=
$
request
->
validated
();
26
$
data
[
'password'
]
=
bcrypt
(
$
data
[
'password'
]);
27
28
$
user
=
$
this
->
userModel
->
create
(
$
data
);
29
30
event
(
new
Registered
(
$
user
));
31
32
auth
()
->
login
(
$
user
);
33
34
if
(
$
cart
->
count
())
{
35
return
redirect
()
->
route
(
'site.checkout.index'
);
36
}
37
38
return
redirect
()
->
route
(
'site.home'
);
39
}
40
}
No controller acima perceba que realizamos o cadastro do cliente e usamos o helper auth para logar o usuário informando o model gerado pós criação. Adicionei a este controller a verificação da existência do carrinho com itens na sessão e existindo já mandamos o cliente para a tela do checkout.
Neste caso você também pode encaminhar o usuário pra tela do carrinho e deixar ele decidir se quer prosseguir no checkout ou fazer outras operações, daí, basta modiciar a rota do redirecionamento.
PS.: Um ponto importante também, é a parte em que no cadastro do usuário utilizamos o helper bcrypt para realizar o hash da senha do nosso usuário. Atente a isso e nunca salve senhas em plain text em qualquer lugar e aplicação que seja!
PS2.: Respeitando o funcionamento do Laravel emiti aqui o evento de usuário registrado, isso permite ter listeners que podem escutar este evento e agir conforme lógica esperada. Fiz isso inclusive pra se manter bem próximo de como é feito em um Starter Kit, no último capítulo falarei sobre eventos e você terá mais clareza sobre este ponto.
Adicione o conteúdo das regras de validação ao RegisterRequest gerado:
1
return
[
2
'name'
=>
'required|string|max:255'
,
3
'email'
=>
'required|string|lowercase|email|max:255|unique:'
.
User
::class
,
4
'password'
=>
[
'required'
,
'confirmed'
,
Rules
\
Password
::defaults
()
]
,
5
];
Pontos importantes das validações acima, o Laravel possui uma classe utilitária para as regras de validação de senha e basicamente usamos o método defaults que pede senha minima de 8 caracteres, valida string e etc. Esta classe possui diversos métodos extras que você pode utilizar e recomendo fortemente que você dê uma espiada no conteúdo desta classe.
Não esqueça de adicionar o namespace base da classe Password:
1
use Illuminate\Validation\Rules;
Outra validação importante de frisar aqui, é o validador confirmed
para a senha, este validador permite que coloquemos no form um input de confirmação de senha e que deve seguir a seguinte regra:
PS.: Um lembrete simples, não esqueça de retornar true no método authorize do RegisterRequest.
Gerado o controller e a request, vamos adicionar a rota para o cadastro e criar a view com o form de cadastro. Segue abaixo ambos:
resources/views/auth/register.blade.php:
1
<
x
-
layouts
.
site
>
2
<
div
class
=
"flex justify-center items-center"
>
3
4
<
div
class
=
"w-[690px] p-5 rounded bg-black shadow shadow-purple-600"
>
5
<
h2
class
=
"w-full text-center mb-4 text-2xl font-extrabold text-white"
>
E
\
6
xperts
Store
</
h2
>
7
<
form
action
=
"{{ route('register.store') }}"
method
=
"post"
>
8
@
csrf
9
<
div
class
=
"w-full mb-6"
>
10
<
label
for
=
"name"
class
=
"text-white mb-2"
>
Nome
Completo
</
label
>
11
<
input
type
=
"text"
name
=
"name"
id
=
"name"
placeholder
=
"Nome Compl\
12
eto..."
required
13
class
=
"w-full p-2 rounded border border-gray-900 text-white"
>
14
@
error
(
'
name
'
)
15
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg\
16
-red-400 text-red-900"
>
{{
$message
}}
</
div
>
17
@
enderror
18
</
div
>
19
20
<
div
class
=
"w-full mb-6"
>
21
<
label
for
=
"email"
class
=
"text-white mb-2"
>
E
-
mail
</
label
>
22
<
input
type
=
"email"
name
=
"email"
id
=
"email"
placeholder
=
"Seu e-m\
23
ail..."
required
24
class
=
"w-full p-2 rounded border border-gray-900 text-white"
>
25
@
error
(
'
email
'
)
26
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg\
27
-red-400 text-red-900"
>
{{
$message
}}
</
div
>
28
@
enderror
29
</
div
>
30
31
<
div
class
=
"w-full mb-6"
>
32
<
label
for
=
"password"
class
=
"text-white mb-2"
>
Senha
</
label
>
33
<
input
type
=
"password"
name
=
"password"
id
=
"password"
placeholder
\
34
=
"Sua senha..."
35
class
=
"w-full p-2 rounded border border-gray-900 text-white"
>
36
37
@
error
(
'
password
'
)
38
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg\
39
-red-400 text-red-900"
>
{{
$message
}}
</
div
>
40
@
enderror
41
</
div
>
42
43
<
div
class
=
"w-full mb-6"
>
44
<
label
for
=
"password_confirmation"
class
=
"text-white mb-2"
>
Confi
\
45
rmar
Senha
</
label
>
46
<
input
type
=
"password"
name
=
"password_confirmation"
id
=
"password\
47
_confirmation"
placeholder
=
"Confirme Sua senha..."
48
class
=
"w-full p-2 rounded border border-gray-900 text-white"
>
49
50
@
error
(
'
password_confirmation
'
)
51
<
div
class
=
"w-full mt-2 p-2 rounded border border-red-900 bg\
52
-red-400 text-red-900"
>
{{
$message
}}
</
div
>
53
@
enderror
54
</
div
>
55
56
<
button
57
class
=
"py-2 px-4 text-thin text-xl cursor-pointer rounded border\
58
border-purple-900 bg-black text-purple-900 hover:bg-purple-900 hover:text-white tra\
59
nsition ease-in-out duration-300"
>
60
Cadastrar
61
</
button
>
62
</
form
>
63
64
<
a
href
=
"{{route('login')}}"
class
=
"block mt-4 text-white hover:underlin\
65
e hover:font-bold"
>
Já
possui
cadastro
?
Clique
aqui
!</
a
>
66
</
div
>
67
</
div
>
68
</
x
-
layouts
.
site
>
Adicione a rota pra tela de cadastro no web.php:
1
Route
::
get
('
register
',
[
\App\Http\Controllers\Auth\RegisterController::class,
'
index
\
2
'])
->
name
('
register
.
index
');
3
4
Route
::
post
('
register
',
[
\App\Http\Controllers\Auth\RegisterController::class,
'
stor
\
5
e
'])
->
name
('
register
.
store
');
Coloquei as rotas do cadastro próximo das rotas do login pra deixar o arquivo de rotas organizado. Acima temos a rota de exibição do form e a rota que recebe os dados pra processar a criação.
Vamos adicionar, agora, o link pra tela de registro na tela do login. Logo abaixo do button de logar coloque o link do trecho abaixo:
Alteração na view login.blade.php:
1
<a
href=
"
{{
route
(
'register.index'
)
}}
"
class=
"block mt-4 text-white hover:underline h\
2
over:font-bold"
>
Não possui cadastro? Clique aqui!</a>
Com todos os insumos anteriores, vamos adicionar na view do carrinho a rota do botão pro checkout. Adicione a rota por apelido a nossa tag a que levaria pro checkout:
1
{{
route
(
'site.checkout.index'
)
}}
A tag completa:
1
<a
href=
"
{{
route
(
'site.checkout.index'
)
}}
"
class=
"px-5 py-2 rounded text-white text-\
2
2xl font-bold bg-green-600"
>
Continuar</a>
A tag está ao lado da tag de cancelar o carrinho.
Um ponto legal no trecho acima é que mandamos o link pra tela do checkout e não havendo sessão, nosso usuário é enviado pra tela de login podendo escolher clicar no link de criar conta também. Ele escolhendo logar, como o link referência era nosso checkout, após o login ele volta pra tela de checkout por conta do redirect()->intended()
lá no LoginController. Ponto simples mas facilita muito a experiência do usuário.
Feito isso podemos realizar todo o processo de compra do usuário, adicionando itens no carrinho, clicando em continuar e prosseguir pra a finalização do pagamento.
1. Escolhemos o produto:
2. Vamos ao carrinho:
3. Podemos simular o pagamento:
4. Temos nossa tela de obrigado:
Pra gente completar nosso ciclo, vamos criar uma tela, mesmo que simples, para nosso cliente visualizar seus pedidos. Comecemos gerando o controller para tal:
1
php artisan make:controller Customer/MyOrdersController
Segue o conteúdo:
1
<
?
php
2
3
namespace
App
\Http
\Controllers
\Customer
;
4
5
use
App
\Http
\Controllers
\Controller
;
6
use
Illuminate
\Http
\Request
;
7
8
class
MyOrdersController
extends
Controller
9
{
10
public
function
index
()
11
{
12
$
orders
=
auth
()
->
user
()
->
orders
()
->
orderBy
(
'id'
,
'DESC'
)
->
paginate
(
5
);
13
14
return
view
(
'customer.my-orders'
,
compact
(
'orders'
));
15
}
16
}
Perceba aqui, que através do usuário autenticado pego os pedidos dele de forma paginada, ordenada pelas mais recente e limitando 5 pedidos por página. Lembrando que o método user()
do auth retorna um model User
com os dados do usuário autenticado.
Outro ponto, ainda não temos o método de ligação em User. Então vamos adicionar o método orders
no model User:
1
public
function
orders
():
HasMany
2
{
3
return
$this->hasMany(
Order
:
:
class
);
4
}
Método acima que indica que o usuário tem N pedidos e o pedido pertence a 1 usuário.
Pontos que já conhecemos bem, vamos a tela dos pedidos e posteriormente as rotas:
resources/views/customer/my-orders.blade.php:
1
<
x
-
layouts
.
site
>
2
<
div
class
=
"max-w-7xl mx-auto"
>
3
<
div
class
=
"w-full mb-8 text-white py-4 border-b border-purple-500"
>
4
Olá
,
{{
request
()
->
user
()
->
name
}}
5
</
div
>
6
7
<
div
class
=
"w-full mb-8 text-white py-4"
>
8
<
h2
class
=
"text-2xl font-extrabold block mb-8 text-white"
>
Meus
Pedidos
</
\
9
h2
>
10
@
forelse
(
$orders
as
$order
)
11
<
div
class
=
"w-full rounded bg-white shadow p-4 my-8"
>
12
<
h2
class
=
"text-xl font-bold text-purple-900 mb-4 block"
>
13
Pedido
:
{{
$order
->
code
}}
14
</
h2
>
15
16
<
p
class
=
"font-thin p-2 text-xl text-purple-900"
>
17
Produtos
:
<
br
>
18
<
ul
class
=
"p-8"
>
19
@
foreach
(
$order
->
items
as
$item
)
20
<
li
class
=
"text-purple-900 list-decimal"
>
{{
$item
->
produc
\
21
t
->
name
}}
-
{{
number_format
(
$item
->
product
->
price
,
2
,
','
,
'.'
)}}
({{
$item
->
quantity
\
22
}})
x
</
li
>
23
@
endforeach
24
</
ul
>
25
</
p
>
26
27
<
p
class
=
"font-thin p-2 text-xl text-purple-900"
>
28
Total
Pedido
:
R$
{{
number_format
(
$order
->
totalOrder
(),
2
,
'
,
\
29
'
,
'.'
)}}
30
</
p
>
31
</
div
>
32
@
empty
33
@
endforelse
34
</
div
>
35
36
</
div
>
37
</
x
-
layouts
.
site
>
Acima deixo pra você explorar a tela e desvendar os detalhes existentes nela. Faço isso, pois não temos muito diferente do que já vimos na jornada e do que você têm de bagagem com PHP.
Um ponto importante nesta tela, é o total do pedido que adicionei. Utilizei a ligação de Order com Items e via collection retornada, dos itens do pedido, fiz um reduce para somar e gerar o total.
O método foi adicionado no model Order:
1
public
function
totalOrder
()
2
{
3
$
total
=
$
this
->
items
->
reduce
(
function
(
?
int
$
total
,
$
item
)
{
4
return
$
total
+=
(
$
item
->
quantity
*
$
item
->
product
->
price
);
5
});
6
7
return
$
total
;
8
}
Adicione a rota pro painel dos pedidos no arquivo web.php:
1
Route
::
middleware
(
'auth'
)
-
>
prefix
(
'customers'
)
-
>
name
(
'customers.'
)
-
>
group
(
function
()
{
2
Route
:
:
get
(
'my-orders'
,
[
\
App
\
Http
\
Controllers
\
Customer
\
MyOrdersController
::clas
\
3
s
,
'index'
]
)
->
name
(
'my-orders'
);
4
}
);
Adicione a tag do link no componente layout do site, ao lado do link da home:
1
<
li
><
a
href
=
"{{route('customers.my-orders')}}"
2
class
=
"text-white hover:underline hover:font-bold transition duration-300 ease-\
3
in-out @if(request()->routeIs('customers.my-orders')) font-bold @else font-thin @end\
4
if"
>
Pedidos
</
a
></
li
>
Lembre sempre de consultar o repositório conforme comentei no ínicio, para se encontrar melhor nestas telas que têm muito html.
Concluímos aqui nossa jornada de compra pro nosso cliente e junto tivemos a oportunidade de conhecer diversos conceitos do framework desde os pontos de Sessão nos capítulos anteriores, a parte de Container neste capítulo.
Nossa jornada está quase chegando ao fim mas quero falar um pouco sobre Eventos & ACL no framework e pra isso vamos continuando no próximo capítulo.
Até lá!
Neste capítulo vamos conhecer o Controle de Acesso no Laravel e também falarei sobre Eventos, inclusive aplicando eventos para gerenciar nosso estoque tanto durante a conclusão do pedido quanto na possibilidade do usuário cancelar.
Primeiramente vamos começar pela parte de Controle de Acessos ou Autorização onde podemos utilizar para organizar acessos via papéis, entretanto, o Laravel não organiza a camada de autorização por papéis e etc mas podemos organizar estes papéis por fora e aplicá-los dentro do framework nos mecanismos disponibilizados para o controle de autorização dentro do sistema.
Vamos começar esta parte falando dos principais pensamentos na camada de autorização do framework, a priore temos dois mecanismo:
Gates: Gates nos permitem organizar controles com pensamentos mais globais de teste, onde podemos definir a label da regra e mediante um callback criar a lógica de acesso baseada em retorno booleano. True para pode acessar e False para a não possibilidade, e claro, esse retorno booleano será baseado na sua regra de acesso para aquele regra ou ability(como o framework chama).
Policie: Policies também são pensadas de forma global mas existe uma diferença promordial em comparação a Gates. A diferença é que policies estão diretamente ligadas ao model e suas operações, onde você pode controlar e criar regras de acesso para cada operação que você pode fazer em cima de um dado que aquele model compete. Ações como listar todos, criar, atualizar, remover e etc. Policies são classes com as regras pelas operações via métodos.
Um ponto bem importante em autorização no Laravel é que tanto o callback da Gate quanto o método da Policy receberão o usuário autenticado como primeiro parâmetro. Importante lembrar isso pois os controles serão feitos em cima do usuário autenticado.
Hoje o Laravel recomenda que coloquemos nossas definições de gates dentro do AppServiceProvider ou em um Provider criado por nós para este fim. Como já temos o AppServiceProvider, colocaremos neles nosso exemplo e posteriormente nosso controle para usuários clientes e usuários admin.
A definição da gate pode ser feita via facade Gate:
1
\
Illuminate
\
Support
\
Facades
\
Gate
::
define
(
'exemplo'
,
fn
(
\
App
\
Models
\
User
$
user
)
=
\
2
>
$
user-
>
isAdmin
());
O exemplo acima define uma gate com a ability(regra) chamada de exemplo e tem o teste baseado na verificação se usuário autenticado é admin ou não. Usei uma arrow function(PHP 7.4) para simplificar o callback e implicitamente sabemos que encapsulado no método isAdmin teríamos uma lógica para verificar se o usuário é admin ou não.
Essa definição colocada no método boot
do AppServiceProvider
nos ajuda a termos o registro global dela por meio da Gate em nossa aplicação.
Os testes baseadas nestes controles é basicamente o mesmo, tanto para Gate quanto para Policies, então vamos logo entender como definir Policies e posteriormente podemos conhecer algumas formas de verificar estas regras tanto em controllers como em views e rotas.
O termo Facade vêm de um padrão de projeto que o Laravel utiliza bastante e este padrão visa simplificar o acesso a interfaces de objetos complexos. O Laravel utiliza Facades para diversos componentes internos e você também pode criar Facades e compartilhar no framework de forma customizada.
Como as policies são classes, nós podemos gerar via artisan. Veja:
1
php artisan make:policie ProductPolicy
A classe Policy é gerada na pasta app/Policies e normalmente está atrelada a um Model. Da forma que geramos, a clase vêm apenas com um construtor e você pode definir os métodos de forma mais livre, entretanto, podemos gerar ela já associada a um model diretamente informando o parâmetro -m
e o nome do model.
Exemplo:
1
php artisan make:policy ProductPolicy -m Product --force
O parâmetro –force aqui é pra sobscrevermos a policy gerada anteriormente, se a policy for nova não precisa informá-lo e inclusive verifique se a policy que existe não possui conteúdo, pois o parâmetro –force pode ser destrutivo quando pensar em usar no dia a dia este parâmetro no comando.
Desta forma nossa policy já virá populada com os métodos para controle baseados na operação. Veja:
1
<?php
2
3
namespace
App\Policies
;
4
5
use
App\Models\Product
;
6
use
App\Models\User
;
7
use
Illuminate\Auth\Access\Response
;
8
9
class
ProductPolicy
10
{
11
/**
12
* Determine whether the user can view any models.
13
*/
14
public
function
viewAny
(
User
$user
)
:
bool
15
{
16
return
false
;
17
}
18
19
/**
20
* Determine whether the user can view the model.
21
*/
22
public
function
view
(
User
$user
,
Product
$product
)
:
bool
23
{
24
return
false
;
25
}
26
27
/**
28
* Determine whether the user can create models.
29
*/
30
public
function
create
(
User
$user
)
:
bool
31
{
32
return
false
;
33
}
34
35
/**
36
* Determine whether the user can update the model.
37
*/
38
public
function
update
(
User
$user
,
Product
$product
)
:
bool
39
{
40
return
false
;
41
}
42
43
/**
44
* Determine whether the user can delete the model.
45
*/
46
public
function
delete
(
User
$user
,
Product
$product
)
:
bool
47
{
48
return
false
;
49
}
50
51
/**
52
* Determine whether the user can restore the model.
53
*/
54
public
function
restore
(
User
$user
,
Product
$product
)
:
bool
55
{
56
return
false
;
57
}
58
59
/**
60
* Determine whether the user can permanently delete the model.
61
*/
62
public
function
forceDelete
(
User
$user
,
Product
$product
)
:
bool
63
{
64
return
false
;
65
}
66
}
Olhando a Policy fica simples entendermos os pontos de controle nas operações. Exemplo:
(Pode/Não pode)
Abrindo aqui uma explicação, pois não abordei aqui, para a parte de Soft Deletes. Soft Deletes nos permitem remover um dado de forma lógica, este dado não é removido da base apenas recebe uma data de remoção via nova coluna adicionada na tabela do model em questão. Coluna esta chamada de
deleted_at
que é preenchida pelo próprio Laravel ao removermos o dado normalmente.O model deve ser configurado como tendo Soft Deletes e a coluna mencionada deve ser adicionada a sua tabela via migration. O processo de apagar continua o mesmo, inclusive os dados com a flag do soft delete é ignorado pelo model, podendo ser recuperados por métodos específicos do Eloquent, facilitando a listagem de dados em Lixeira por exemplo.
Perceba que como a policy está associada diretamente a um model, cada método recebe tanto a instância do usuário autenticado quanto a instância do model associado. Os testes serão efetuados em cima dos dados daquele gerenciamento pelo model.
Lembrando que os métodos da policy devem retornar booleano baseado no teste do poder ou não poder acessar determinado conteúdo. Por exemplo:
1
...
2
3
public
function
update
(
User
$
user
,
Product
$
product
)
:
bool
4
{
5
return
$
product
->
user_id
===
$
user
->
id
;
6
}
7
...
O exemplo acima verifica se o produto a ser alterado pertence ao usuário autenticado. Se o teste retornar verdadeiro, ele poderá atualizar o produto, caso falso não poderá alterá.
Acima é apenas um exemplo mas acredito que dá pra você ter ideias do que controlar.
Lembrando também que nos form requests temos o método authorize que pode ser usado para controle de criação e atualização de dados também.
Exsitem diversas formas de utilizar as permissões nas aplicações Laravel. Podemos usar nos controllers por meio da facade gate, podemos usar nas rotas por meio do método can
e podemos utilizar nas views via diretiva @can
.
Caso queira testar os acessos no seu controller nos métodos específicos, pode usar os métodos denies ou allows via Gate
. Exemplo:
1
\
Illuminate
\
Support
\
Facades
\
Gate
::
allows
(
'exemplo'
);
1
\
Illuminate
\
Support
\
Facades
\
Gate
::
denies
(
'exemplo'
);
Acima teremos o retorno verdadeiro no método allows
se a permissão exemplo
, definida via Gate, retornar verdadeiro indicando que o usuário pode fazer a ação. Temos o denies
que terá o verdadeiro invertido, ou seja, o denies é verdadeiro se a permissão retornar falso pro teste da ação.
As policies também podem ser testadas via Gate e tanto a policy quanto as regras via gate não precisam receber a instância do usuário autenticado pois o Laravel já resolve.
Testando a policy, lembre do nome do método e da instância do model a ser validada. Exemplo:
1
\Illuminate\Support\Facades\Gate::allows('update', Product::find(2))
Em ações de policies que envolvem um dado específico, como atualizar, remover e visualizar um é necessário lembra de passar a instância com os dados do model a ser validado. Lembra do pequeno trecho do update na policy de produto.
O Laravel normalmente possui diversos caminhos para testar e fazer as coisas. Podemos inclusive através do usuário autenticado validar as permissões por meio dos métodos can
(similar ao allows) e cannot
(similar ao denies).
Os métodos allows e denies retornam booleano e podem ser combinados com controles condicionais. Se você quiser ser mais simplista pode usar o método authorize da facade Gate, que disparará uma exception quando o usuário não puder realizar a ação. Exemplo:
1
Gate
::
authorize
(
'update'
,
Product
::
find
(
2
));
A exception lançada é do tipo: Illuminate\Auth\Access\AuthorizationException
.
O método authorize por já lançar uma exceção não precisa ser combinada com condicionais e pode está apenas como linha já no inicio do método que receberá o check.
Nas views podemos utilizar a diretiva @can
ou @cannot
, imagino que você já entendeu como funcionam. Existem o pensamento aqui de o que o usuário não pode fazer é bom que não seja exibido, então estas diretivas são bem úteis para este fim.
Você pode ocultar o botão de editar por exemplo se o usuário pode/ou não pode atualizar um dado. Exemplo:
1
@
can
(
'
update
'
,
$product
)
2
<
button
>
Editar
</
button
>
3
@
endcan
Lembrando que acima o $product virá da linha do loop na tabela html por exemplo ou se fosse um dado single ` $product` já seria populada com o model pro teste, e por aí vai.
Nas rotas podemos utilizar o método can como método aninhado na config da rota para validar policies. Exemplo:
1
Route
::
get
(
'products/{product}'
,
ProductController
::
class
,
'update'
)
2
->
can
(
'update'
,
'product'
)
ou
1
Route
::
get
(
'products/{product}'
,
ProductController
::
class
,
'update'
)
2
->
can
(
'update'
,
Product
::
class
)
O Laravel injeta no teste do can o que é necessário pro teste baseado no dados vindo no parâmetro dinâmico da rota. O controle aqui é feito com a ideia de middleware. Inclusive temos middleware para testar também. Exemplo:
1
Route
::
get
(
'products/{product}'
,
ProductController
::
class
,
'update'
)
2
-
>
middleware
(
'can:update,product'
);
3
4
//
ou
5
6
Route
::
get
(
'products/{product}'
,
ProductController
::
class
,
'update'
)
7
-
>
middleware
(
'can:update,\App\Models\Product'
);
Pode confundir mas o can chamado como middleware também aceita as abilities ou permissões feitas via Gate.
Perceba que temos diversos caminhos para teste das permissões definidas via Gate e Policies. Reforçando que o teste é válido se o usuário estiver logado, caso o contrário sempre será falso. O usuário logado é resolvido automaticamente pelo Laravel.
Agora que conhecemos bastante da área de autorização do Laravel, vamos criar um controle até que simples mas pro nosso app será o suficiente pro momento. Primeiramente vamos adicionar na tabela de usuários(users) a coluna role
com o valor defualt: ROLE_CUSTOMER
.
Utilizarei aqui labels para indicar os papéis puxando a ideia do Symfony Security nestas nomeclaturas.
Vamos gerar nossa migration para esta adição:
1
php artisan make:migration alter_users_table_add_role_column --table=users
Veja os métodos up e down da migration:
1
public
function
up
():
void
2
{
3
Schema
:
:
table
(
'users'
,
function
(
Blueprint
$
table
)
{
4
$
table
->
string
(
'role'
)
->
default
(
'ROLE_CUSTOMER'
);
5
}
);
6
}
7
8
public
function
down
():
void
9
{
10
Schema
:
:
table
(
'users'
,
function
(
Blueprint
$
table
)
{
11
$
table
->
dropColumn
(
'role'
);
12
}
);
13
}
Não esqueça de executar a migração:
1
php artisan migrate
Vamos controlar nosso acesso ao painel admin. Vamos criar o check no model User, o método hasRole
:
1
public
function
hasRole
(
string
$
role
)
:
bool
2
{
3
return
$
this
->
role
===
$
role
;
4
}
Desta forma fica mais maleável, apesar de simples, o controle pelo papél desejado. Lembrando que o método acima está no model User.
Agora podemos checkar via Gate se o usuário têm o papel admin para acessar nosso painel. Adicione a verificação lá no método boot
do AppServiceProvider
:
1
Gate
::
define
(
'can_access_manager'
,
fn
(
\
App
\
Models
\
User
$
user
)
=>
$
user-
>
hasRole
(
'ROL\
2
E_ADMIN'
));
Uma vez definido a permissão podemos utilizar o método middleware no grupo de rotas do nosso admin, para adicionar o check do papél, lá no arquivo web.php:
1
->
middleware
(
'auth'
,
'can:can_access_manager'
)
Veja o grupo do admin completo:
1
Route
::
prefix
(
'admin'
)
-
>
middleware
(
'auth'
,
'can:can_access_manager'
)
-
>
group
(
function
\
2
()
{
3
4
Route
:
:
resource
(
'products'
,
\
App
\
Http
\
Controllers
\
Admin
\
ProductController
::
class
\
5
);
6
Route
:
:
resource
(
'categories'
,
\
App
\
Http
\
Controllers
\
Admin
\
CategoryController
::
cl
\
7
ass
);
8
9
Route
:
:
resource
(
'products/{product}/photos'
,
\
App
\
Http
\
Controllers
\
Admin
\
Product
\
10
PhotoController
::
class
)
11
->
only
(
[
'index'
,
'store'
,
'destroy'
]
);
12
}
);
Como nossos usuários todos estão como clientes(ROLE_CUSTOMER), ao logarmos e tentarmos acessar o gerenciador receberemos um 403 - This action is unauthorized.
:
Teste modificar o papél do usuário autenticado para ROLE_ADMIN
e tente acessar o paínel novamente. Tudo deve fluir corretamente conforme o controle global.
Aqui fizemos um controle simples mas já dá pra você ter uma excelente ideia de como trabalhar com autorização dentro do Laravel.
Num futuro você pode dinamizar mais as coisas associando papéis via tabela a recursos baseados nas rotas do sistema e ligar papéis e recursos em muitos pra muitos para facilitar a criação de papéis com acessos específicos.
Esses recursos, baseados nas rotas podem se torar as permissões e a geração das gates pode ser dinamizado via ServiceProvider. Associando o papél a um usuário ele poderá acessar o que o papél dele têm de abilities.
Aqui foi um rascunho rápido do que pode ser feito, inclusive tenho um curso no portal da Code Experts (codeexeperts.com.br) sobre isso, e caso queira mais facilidade, existe um pacote chamada de Spatie/Permissions que também implementa este pensamento de forma mais simples sem necessidade de criar todo o arcabouço para tal. Recomendo dá uma olhada depois.
Com este controle feito, vamos a mais um conhecimento dentro do framework.
Eventos!
O conceito dentro do sistema de eventos é algo que particularmente é simples de entender. Muita das vezes o que pode confundir é como aplicar e neste ponto iremos realizar as explicações e usar dentro do nosso sistema.
Sistemas de Eventos normalmente são utilizados para um processo desacoplado, onde pontos do sistema podem reagir a execução de outros sem que estejam acoplados e seus contextos sejam diferentes.
Por exemplo, quando o usuário loga no sistema ou se registra emitimos um evento específico daquele processo que pode ser ouvido por outros participantes e assim estes participantes(outros objetos) podem realizar processos baseados neste evento.
Imagine que centralizamos um log, e este log irá registrar quando o usuário logou, se cadastrou, dentre outra coisas. Posso trabalhar estes processos via eventos fazendo com que o log escute eventos específicos para salvar aquela ação do usuário. Sempre teremos em um sistema de eventos o emissor e os escutadores (Emmiters & Listeners).
Emissor é a classe que disparará o evento, e Escutador(Listener) é quem ecustará o evento. Podemos propagar diversos eventos e termos diversos Listeners(gosto mais do termo em eng rs!) que irão reagir mediante o evento propagado.
Irei aplicar eventos aqui para gerenciarmos nosso estoque, onde adicionarei a possibilidade do usuário cancelar o pedido retornando os produtos pro estoque via evento e adicionaremos no momento do checkout a remoção dos itens do estoque também lançando um evento para isso.
Aqui irei focar nos pontos principais de alteração, comentários de adição de link em menu de layout e outros pontos deixo pra você adicionar ou consultar nosso github tudo bem?
Podemos gerar eventos e listeners via artisan com os comandos respectivos: make:event & make:listener.
Vamos entender quais os pontos que iremos gerar para termos o panorama mais de cima e depois vamos as gerações e o restante dos processos. Você deve lembrar também que temos um estoque dentro da tabela de produtos, e por meio destes eventos vamos retirar ou retornar os produtos do estoque.
O cenário de eventos e listeners será o seguinte (colocarei em termos pt_BR mas gerarei os participantes em inglês):
Estes eventos são o suficiente para que possamos entender o processo envolvendo o gerenciamento do estoque também.
Vamos gerar nossos dois eventos:
1
php artisan make:event UserOrderedProductsEvent
2
3
php artisan make:event UserCancelledOrderEvent
Na geração dos listeners, além de escolhermos os nomes iremos associar o listener ao evento que desejamos escutar. Veja os comandos de geração abaixo:
1
php artisan make:listener GetProductsFromStockListener -e UserOrderedProductsEvent
2
3
php artisan make:listener PutProductsBackOnStockListener -e UserCancelledOrderEvent
Perceba que criamos os listeners associados aos eventos anterioremente criados. Um ponto importante de comentar aqui, é como o Laravel mapeia de forma automática estes eventos e listeners.
Se você perceber seu listener, a classe, você perceberá que temos um método handle que recebe a injeção do evento associado. O que o Laravel fará é criar um mapeamento interno onde lerá a assinatura deste método handle e mapeará o evento tipado no parâmetro para o listener em questão.
Não abordarei aqui mas existem formas de mapear manualmente estes eventos e listeners e recomendo que busque mais sobre e se aprofunde nesse cenário. Um ponto importante também é que sempre que você fizer deploy gere cache desses eventos.
Podemos gerar o cache e remover com os comando respectivos: event:cache
& event:clear
via artisan, ou opte pelo optimize
que dentre outros caches cria o cache dos eventos também o clear do optimize é optimize:clear
.
Antes de criarmos os conteúdos dos eventos e listeners vamos criar nosso serviço para o gerenciamento básico do estoque.
Veja o serviço abaixo, criado na pasta app/Services
:
StockManagerService:
1
<?php
2
namespace
App\Services
;
3
4
use
App\Models\Order
;
5
use
App\Models\Product
;
6
7
class
StockManagerService
8
{
9
public
function
__construct
(
private
Order
$order
)
10
{
11
}
12
13
public
function
removeProductFromStock
()
14
{
15
foreach
(
$this
->
order
->
items
as
$item
)
{
16
Product
::
find
(
$item
->
product_id
)
->
decrement
(
'in_stock'
,
$item
->
quantity
);
17
}
18
}
19
20
public
function
addingProductInStock
()
21
{
22
foreach
(
$this
->
order
->
items
as
$item
)
{
23
Product
::
find
(
$item
->
product_id
)
->
increment
(
'in_stock'
,
$item
->
quantity
);
24
}
25
}
26
}
O serviço é bem tranquilo e uma rápida leitura você conseguirá entender, um ponto que saliento é que estamos, em cada método, iterando sobre os itens do pedido passado pro serviço.
Vamos focar no evento que gerará a retirada dos itens do estoque e depois eu apenas coloco o evento e listener do cancelar, uma vez que é bem semelhante as explicações.
Veja nossa classe evento UserOrderedProductsEvent:
1
<?php
2
3
namespace
App\Events
;
4
5
use
App\Models\Order
;
6
use
Illuminate\Broadcasting\Channel
;
7
use
Illuminate\Broadcasting\InteractsWithSockets
;
8
use
Illuminate\Broadcasting\PresenceChannel
;
9
use
Illuminate\Broadcasting\PrivateChannel
;
10
use
Illuminate\Contracts\Broadcasting\ShouldBroadcast
;
11
use
Illuminate\Foundation\Events\Dispatchable
;
12
use
Illuminate\Queue\SerializesModels
;
13
14
class
UserOrderedProductsEvent
15
{
16
use
Dispatchable
,
InteractsWithSockets
,
SerializesModels
;
17
18
/**
19
* Create a new event instance.
20
*/
21
public
function
__construct
(
public
Order
$order
)
22
{
23
//
24
}
25
26
/**
27
* Get the channels the event should broadcast on.
28
*
29
* @return array<int, \Illuminate\Broadcasting\Channel>
30
*/
31
public
function
broadcastOn
()
:
array
32
{
33
return
[
34
new
PrivateChannel
(
'channel-name'
),
35
];
36
}
37
}
A classe evento acima está com a mínima alteração em relação ao que foi gerado, apenas adicionei um parâmetro ao construtor que poderá ser acessado quando o listener for invocado.
Pra te dá um panorama sobre o método broadcastOn
, mesmo que não vamos usar aqui. Ele permite que propaguemos este evento via canal broadcast no Laravel que está associado a conversas em tempo real e mais uma vez recomendo a busca de materiais desta parte do framework, inclusive podemos enfileirar estes eventos via workers.
O listener associado a este evento GetProductsFromStockListener, está abaixo:
1
<?php
2
3
namespace
App\Listeners
;
4
5
use
App\Events\UserOrderedProductsEvent
;
6
use
App\Models\Order
;
7
use
Illuminate\Contracts\Queue\ShouldQueue
;
8
use
Illuminate\Queue\InteractsWithQueue
;
9
use
App\Services\StockManagerService
;
10
11
class
GetProductsFromStockListener
12
{
13
/**
14
* Create the event listener.
15
*/
16
public
function
__construct
()
17
{
18
//
19
}
20
21
/**
22
* Handle the event.
23
*/
24
public
function
handle
(
UserOrderedProductsEvent
$event
)
:
void
25
{
26
(
new
StockManagerService
(
$event
->
order
))
->
removeProductFromStock
();
27
}
28
}
Perceba que, quando o evento UserOrderedProductsEvent for disparado o listener associado GetProductsFromStockListener será acionado e o método, deste listener, handle será executado. É aí que usamos nosso serviço para fazer a retirada do itens do estoque.
Este evento pode ser disparado da seguinte forma:
1
//
via
helper
2
event
(
new
\
App
\
Events
\
UserOrderedProductsEvent
($
order
));
3
4
//
via
método
dispatch
5
\
App
\
Events
\
UserOrderedProductsEvent
::
dispatch
($
order
);
Para utilizarmos dentro do projeto, vamos adicinar a linha do helper lá no CheckoutController no método process. Coloque após a linha do update do código do gateway:
1
event(new \App\Events\UserOrderedProductsEvent($order));
método completo:
1
public
function
process
(
PaymentInterface
$
payment
,
CartService
$
cart
)
2
{
3
if(!$cart->count())
{
4
session()->flash('warning',
'Sem
Itens
para
Processar
Checkout!')
;
5
return
redirect()->route('site.home')
;
6
}
7
8
$
customer
=
auth
()
-
>
user
();
9
10
$
order
=
$
this-
>
modelOrder-
>
create
(
[
11
'code'
=>
Str
::uuid
(),
12
'user_id'
=>
$customer
->
id
13
]
);
14
15
$
saveOrderItems
=
[]
;
16
foreach
($
cart-
>
all
()
as
$
item
)
{
17
$saveOrderItems
[]
=
[
18
'quantity'
=>
$item
[
'quantity'
]
,
19
'product_id'
=>
$item
[
'id'
]
,
20
]
;
21
}
22
$
order-
>
items
()
-
>
createMany
($
saveOrderItems
);
23
24
$
gatewayCode
=
$
payment-
>
doPayment
(
[
25
'our_code'
=>
$order
->
id
,
26
'name'
=>
$customer
->
name
,
27
'email'
=>
$customer
->
email
,
28
'cardToken'
=>
'XYZ'
,
29
'paymentMethod'
=>
'CREDIT_CARD'
30
]
)
[
'gateway_code'
]
;
31
32
33
$
order-
>
update
(
[
'gateway_code'
=>
$gatewayCode
]
);
34
35
event
(
new
\
App
\
Events
\
UserOrderedProductsEvent
($
order
));
36
37
session
()
-
>
forget
(
CartService
::
SESSION_KEY
);
38
39
return
redirect
()
-
>
route
(
'site.checkout.thanks'
,
$
order
);
40
}
Abaixo segue o evento de Cancelamento e seu Listener; onde vamos usar, veremos no próximo bloco:
UserCancelledOrderEvent:
1
<?php
2
3
namespace
App\Events
;
4
5
use
App\Models\Order
;
6
use
Illuminate\Broadcasting\Channel
;
7
use
Illuminate\Broadcasting\InteractsWithSockets
;
8
use
Illuminate\Broadcasting\PresenceChannel
;
9
use
Illuminate\Broadcasting\PrivateChannel
;
10
use
Illuminate\Contracts\Broadcasting\ShouldBroadcast
;
11
use
Illuminate\Foundation\Events\Dispatchable
;
12
use
Illuminate\Queue\SerializesModels
;
13
14
class
UserCancelledOrderEvent
15
{
16
use
Dispatchable
,
InteractsWithSockets
,
SerializesModels
;
17
18
/**
19
* Create a new event instance.
20
*/
21
public
function
__construct
(
public
Order
$order
)
22
{
23
//
24
}
25
26
/**
27
* Get the channels the event should broadcast on.
28
*
29
* @return array<int, \Illuminate\Broadcasting\Channel>
30
*/
31
public
function
broadcastOn
()
:
array
32
{
33
return
[
34
new
PrivateChannel
(
'channel-name'
),
35
];
36
}
37
}
PutProductsBackOnStockListener:
1
<?php
2
3
namespace
App\Listeners
;
4
5
use
App\Events\UserCancelledOrderEvent
;
6
use
Illuminate\Contracts\Queue\ShouldQueue
;
7
use
Illuminate\Queue\InteractsWithQueue
;
8
use
App\Services\StockManagerService
;
9
10
class
PutProductsBackOnStockListener
11
{
12
/**
13
* Create the event listener.
14
*/
15
public
function
__construct
()
16
{
17
//
18
}
19
20
/**
21
* Handle the event.
22
*/
23
public
function
handle
(
UserCancelledOrderEvent
$event
)
:
void
24
{
25
(
new
StockManagerService
(
$event
->
order
))
->
addingProductInStock
();
26
}
27
}
Vamos adicionar uma ação de cancelamento na tela de pedidos do usuário. Desta forma podemos adicionar o uso prático do cancelar e seus eventos.
Vamos adicionar o método que compete a ação do cancelar lá no MyOrdersController:
1
public
function
cancelOrder
(
Order
$
order
)
2
{
3
if
(
$
order
->
status
===
'CUSTOMER_CANCELLED'
)
return
redirect
()
->
back
();
4
5
$
order
->
update
([
'status'
=>
'CUSTOMER_CANCELLED'
]);
6
7
event
(
new
UserCancelledOrderEvent
(
$
order
));
8
9
return
redirect
()
->
route
(
'customers.my-orders'
);
10
}
O método acima está dentro do que já conhecemos até o momento. Um ponto que verifico aqui é: se o pedido já foi cancelado, nós impedimos o processar do cancelamento novamente.
Para controlar a autorização a esta ação, vamos gerar uma policy para controlar que somente o usuário que for dono do pedido pode cancelar seu próprio pedido. Algo que já vimos também e que é bem interessante aplicarmos aqui:
1
php artisan make:policy OrderPolicy
Segue a policy na íntegra e somente com o que precisamos:
1
<?php
2
3
namespace
App\Policies
;
4
5
use
App\Models\Order
;
6
use
App\Models\User
;
7
8
class
OrderPolicy
9
{
10
public
function
update
(
User
$user
,
Order
$order
)
:
bool
11
{
12
return
$user
->
id
===
$order
->
user_id
;
13
}
14
}
Lá no grupo de rotas da área de meus pedidos, adicione no grupo do customer o trecho de rota abaixo:
1
Route
::
put
(
'my-orders/cancel/{order}'
,
[
\
App
\
Http
\
Controllers
\
Customer
\
MyOrdersCont
\
2
roller
::class
,
'cancelOrder'
]
)
3
-
>
can
(
'update'
,
'order'
)
4
-
>
name
(
'my-orders.cancel'
);
A rota já conta com o controle de acesso pela policy de Order pra permissão(ability) update. Desta forma controlamos que somente o dono do pedido é que pode cancelar este pedido.
Por fim coloquei após a linha do total do pedido o trecho do form que irá executar a ação de cancelar, isso na tela my-orders.blade.php:
1
<p
class=
"flex justify-start items-start"
>
2
<form
action=
"
{{
route
(
'customers.my-orders.cancel'
,
$
order
)
}}
"
method=
"POST"
>
3
@method('PUT')
4
@csrf
5
<button
class=
"px-4 py-2 rounded border border-red-900 bg-red-500 text-white\
6
"
>
CANCELAR</button>
7
</form>
8
</p>
Agora o usuário pode cancelar seus pedidos e assim temos um uso prático do processo do evento que retorna os itens pro estoque.
Pra finalizarmos estes pontos vamos gerar um enumerator, vindo no PHP 8 e que você provavelmente já conhece. Apesar de podermos criar livremente um enumerator, temos um comando gerador para eles também dentro do framework.
Pra isso execute o comando abaixo:
1
php
artisan
make
:
enum
Enums
/
OrderStatusEnum
Gerei nosso enumerator na pasta(namespace) Enums
. Segue o conteúdo abaixo na íntegra de como nosso Enum deve ficar:
1
<?php
2
3
namespace
App\Enums
;
4
5
enum
OrderStatusEnum
6
{
7
case
WAITING_PAYMENT
;
8
case
CUSTOMER_CANCELLED
;
9
case
PAYMENT_CONFIRMED
;
10
11
public
function
color
()
:
string
12
{
13
return
match
(
$this
)
14
{
15
self
::
WAITING_PAYMENT
=>
'blue'
,
16
self
::
CUSTOMER_CANCELLED
=>
'red'
,
17
self
::
PAYMENT_CONFIRMED
=>
'green'
18
};
19
}
20
21
public
function
status
()
:
string
22
{
23
return
match
(
$this
)
24
{
25
self
::
WAITING_PAYMENT
=>
'Aguardando Pagamento'
,
26
self
::
CUSTOMER_CANCELLED
=>
'Cancelado pelo Cliente'
,
27
self
::
PAYMENT_CONFIRMED
=>
'Pagamento Confirmado'
28
};
29
}
30
}
Resolvi usar esta estrutura para organizarmos nossos status do pedido e assim termos um ponto central para ser utilizado ao querermos exibir o status, com cor específica e etc.
Dentro do model com Eloquent podemos utilizar um mecanismo chamado de model casts, estes casts nos ajudam a padronizarmos dados baseado nas colunas da tabela que o model gerencia, ao indicarmos a transformação do tipo ou formato do dado desta coluna.
Neste ponto quero apenas fazer o cast da coluna status com nosso enumerator, pra isso adicione o método abaixo no model Order:
1
protected
function
casts
():
array
2
{
3
return
[
4
'status'
=>
OrderStatusEnum
::class
,
5
]
;
6
}
Não esqueça do import:
1
use App\Enums\OrderStatusEnum;
Desta forma ao acessarmos os dados do pedido via model, a coluna status será o enumerator e não mais apenas uma string. Desta forma posso usar o valor real do status via $order->status->name
ou mesmo pegar a cor desejada via $order->status->color()
ou mesmo o nome pt_BR $order->status->status()
.
Vamos alterar e adicionar alguns pontos baseados neste cast por Enum. Primeiro vamos adicionar a exibição do status na listagem de pedidos, coloquei o trecho abaixo logo após a exibição do código do pedido na view my-orders.blade.php:
1
<p>
2
<strong
class=
"text-purple-900"
>
Status: <span
class=
"text-
{{
$
order-
>
status-
>
colo
\
3
r
()
}}
-800"
>
{{
$
order-
>
status-
>
status
()
}}
</span></strong>
4
</p>
Outra alteração nesta tela também é: ocultar a exibição do botão de cancelar se o pedido já foi cancelado. Segue o trecho completo com a condicional:
1
@
if
(
$order
->
status
->
name
!==
'
CUSTOMER_CANCELLED
'
)
2
<
p
class
=
"flex justify-start items-start"
>
3
<
form
action
=
"{{route('customers.my-orders.cancel', $order)}}"
method
=
"POST"
>
4
@
method
(
'
PUT
'
)
5
@
csrf
6
<
button
class
=
"px-4 py-2 rounded border border-red-900 bg-red-500 text-white\
7
"
>
CANCELAR
</
button
>
8
</
form
>
9
</
p
>
10
@
endif
Por fim, como temos um enumerator ao acessarmos o status, vamos só alterar nossa condicional no método de cancelar pedido lá do MyOrdersController
:
O que está assim:
1
$
order
->
status
===
'CUSTOMER_CANCELLED'
fica assim:
1
$
order
->
status
->
name
===
'CUSTOMER_CANCELLED'
método na íntegra:
1
public
function
cancelOrder
(
Order
$
order
)
2
{
3
if
(
$
order
->
status
->
name
===
'CUSTOMER_CANCELLED'
)
return
redirect
()
->
back
();
4
5
$
order
->
update
([
'status'
=>
'CUSTOMER_CANCELLED'
]);
6
7
event
(
new
UserCancelledOrderEvent
(
$
order
));
8
9
return
redirect
()
->
route
(
'customers.my-orders'
);
10
}
Assim concluímos nossa jornada aqui neste projeto!
Espero de coração que você tenha aproveitado bastante e espero ter te dado um excelente visão, desde o ínicio, do trabalho com framework Laravel e também na prática que eu tenha conseguido te passar a visão de negócio baseada no projeto!
Claro que o projeto aqui não é algo pra se descartar mas também está longe de ser uma loja com toda a infra a lá Amazon por exemplo mas o que vimos aqui já te ajudará a entender a lógica central de sistemas como esse e claro, continue sua jornada pois o framework ainda pode te proporcionar muita produtividade em áreas como Filas, Websocket, Cache e muito mais!
Desejo muito sucesso e até mais!!
Continue em contato conosco e se especializando com a Code Experts, temos diversos cursos na área de desenvolvimento web com PHP e frameworks focando totalmente em prática como você pode acompanhar aqui neste livro.
Se você gosta de aprender colocando a mão na massa acesse já codeexperts.com.br.
Mais uma vez obrigado e meu sinceros de desejo de sucesso sempre!
Venha fazer parte dos nossos grupos no Telegram e Facebook:
Obrigado!
Sucesso!