Dia 1
Instalação
Iremos usar o docker para fazer a instalação;
mkdir cursolaravel
cd cursolaravel
O Composer é um gerenciador de dependências para PHP. Ele permite instalar, atualizar e gerenciar bibliotecas e pacotes de forma simples, garantindo que um projeto tenha todas as dependências necessárias. No Laravel, o Composer é usado para instalar o framework e suas bibliotecas.
docker run --rm -it \
-v $(pwd):/app \
-u $(id -u):$(id -g) \
composer:latest \
composer create-project laravel/laravel .
Dockerfile pronto para usar no contexto USP:
FROM php:8.5-apache
# packages
RUN sed -i 's|main|main non-free|' /etc/apt/sources.list.d/debian.sources && apt-get update && apt-get install -y \
unixodbc \
unixodbc-dev \
freetds-bin \
freetds-dev \
libicu-dev \
git \
unzip \
libzip-dev \
libpng-dev \
libonig-dev \
libxml2-dev \
libjpeg-dev \
libpng-dev \
libfreetype6-dev \
curl
# cleanup
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# php libs
RUN docker-php-ext-install \
intl \
pdo_mysql \
soap \
zip \
mbstring \
bcmath \
pdo_dblib
# gd
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
docker-php-ext-install gd
# php memory
ENV PHP_MEMORY_LIMIT=512M
ENV PHP_UPLOAD_LIMIT=512M
RUN { \
echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \
echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \
echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \
} > "${PHP_INI_DIR}/conf.d/upload.ini"
# apache
RUN a2enmod rewrite
RUN sed -i 's|/var/www/html|/var/www/html/public|' /etc/apache2/sites-available/000-default.conf
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# composer
USER www-data
COPY --chown=www-data . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
CMD ["apache2-foreground"]
docker-compose.yml pronto para usar o dusk:
services:
cursolaravel:
image: cursolaravel:latest
container_name: cursolaravel
ports:
- "8000:80"
depends_on:
- mariadb
networks:
- cursolaravel-network
volumes:
- ./:/var/www/html
environment:
HOME: /tmp
user: "${UID:-1000}:${GID:-1000}"
mariadb:
image: mariadb:11
container_name: cursolaravel_mariadb
restart: always
environment:
MYSQL_DATABASE: cursolaravel
MYSQL_USER: cursolaravel
MYSQL_PASSWORD: cursolaravel
MYSQL_ROOT_PASSWORD: cursolaravel
volumes:
- mariadb_data:/var/lib/mysql
networks:
- cursolaravel-network
selenium:
image: selenium/standalone-chrome
container_name: cursolaravel_selenium
ports:
- "7900:7900" # VNC (pra ver o browser rodando)
networks:
- cursolaravel-network
shm_size: 2gb
networks:
cursolaravel-network:
volumes:
mariadb_data:
Criando a imagem e subindo ambiente:
docker build --no-cache -t cursolaravel .
docker compose up
Acessar pelo navegador: http://127.0.0.1:8000/
No arquivo .env vamos trocar para mariadb:
DB_CONNECTION=mariadb
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=cursolaravel
DB_USERNAME=cursolaravel
DB_PASSWORD=cursolaravel
Recriando tabelas no banco de dados:
docker exec -it cursolaravel php artisan migrate
MVC
Uma rota é a forma como o framework define e gerencia URLs para acessar diferentes partes da aplicação. As rotas são configuradas no arquivo routes/web.php (para páginas web) ou routes/api.php (para APIs) e determinam qual código será executado quando um usuário acessa uma URL específica. Exemplo:
Route::get('/rota-sem-controller', function () {
echo "Uma rota sem controller, not good!";
});
O controller é uma classe responsável por organizar a lógica da aplicação, separando as regras de negócio das rotas. Em vez de definir toda a lógica diretamente nas rotas, os controllers agrupam funcionalidades relacionadas, tornando o código mais limpo e modular. A convenção de nomenclatura para controllers segue o padrão PascalCase, onde o nome deve ser descritivo, no singular e sempre terminar com “Controller”, como ProdutoController ou UsuarioController.
docker exec -it cursolaravel php artisan make:controller MeuPrimeiroController
Método do controller:
public function index(){
return 'Uma rota com controller, Great!';
}
Mesma ideia, mas com controller:
use App\Http\Controllers\MeuPrimeiroController;
Route::get('/rota-com-controller', [MeuPrimeiroController::class,'index']);
Com essa ideia, vamos criar um sistema de cadastro de livros:
docker exec -it cursolaravel php artisan make:controller LivroController
A seguir criamos a rota livros e a apontamos para o controller LivroController, importando anteriormente o namespace App\Http\Controllers\LivroController.
use App\Http\Controllers\LivroController;
Route::get('/livros', [LivroController::class,'index']);
A camada View é responsável por exibir a interface da aplicação, separando a lógica de apresentação da lógica de negócio (controller). Ela utiliza o Blade, uma linguagem de templates que permite criar páginas dinâmicas de forma eficiente. As views ficam armazenadas na pasta resources/views e podem ser retornadas a partir de um controller usando return view('nome_da_view').
mkdir resources/views/livros
touch resources/views/livros/index.blade.php
No controller:
public function index(){
return view('livros.index');
}
Conteúdo mínimo de index.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>Livros</title>
</head>
<body>
Memórias de um Sargento de Milícias<br>
O Primo Basílio<br>
Memórias Póstumas de Brás Cubas<br>
A Hora da Estrela<br>
</body>
</html>
Model
O Model é uma representação de uma tabela no banco de dados e é responsável pela interação com os dados dessa tabela.
Criando o model chamado Livro:
docker exec -it cursolaravel php artisan make:model Livro -m
As migrations são uma forma de versionar e gerenciar o esquema do banco de dados, permitindo criar, alterar e remover tabelas de forma controlada e rastreável.
Cada migration é uma classe PHP que define as operações a serem realizadas no banco de dados. As migrations são armazenadas na pasta database/migrations. Vamos colocar três colunas para o model Livro: titulo, autor e ano.
$table->string('titulo');
$table->string('autor');
$table->integer('ano');
Depois da modificação na migration, aplicá-la no banco de dados: docker exec -it cursolaravel php artisan migrate.
Tinker
tinker
O comando
docker exec -it cursolaravel php artisan tinkernos permite digitar comandos PHP e ver imediatamente o resultado, como se estivesse dentro da sua aplicação Laravel, ou seja, executamos comandos PHP diretamente dentro do contexto da aplicação, de forma prática e rápida.
Usando o tinker, vamos cadastrar dois livros:
$livro = new \App\Models\Livro;
$livro->titulo = "Memórias de um Sargento de Milícias";
$livro->autor = "Manuel Antônio de Almeida";
$livro->ano = 1853;
$livro->save();
$livro = new \App\Models\Livro;
$livro->titulo = "O Primo Basílio";
$livro->autor = "Eça de Queiroz";
$livro->ano = 1878;
$livro->save();
Na view da index podemos listar os livros cadastrados:
use App\Models\Livro;
public function index(){
return view('livros.index',[
'livros' => Livro::all()
]);
}
No blade index.blade.php, listamos os livros:
<h1>Listagem de Livros</h1>
<ul>
@foreach($livros as $livro)
<li>{{ $livro->titulo }}, por <i>{{ $livro->autor }}</i> em {{ $livro->ano }}</li>
@endforeach
</ul>
Busca
No blade, podemos inserir um campo para busca:
<form>
<input type="text" name="search" value="">
<button type="submit">Pesquisar</button>
</form>
E no controller temos que tratar a busca:
public function index(Request $request){
if($request->has('search')){
$livros = Livro::where('titulo','like','%'.$request->search.'%')->get();
} else {
$livros = Livro::all();
}
return view('livros.index',[
'livros' => $livros
]);
}
Command
Por fim, podemos criar um comando no artisan que automatiza o cadastro de livros a partir de alguma lógica que podemos desenvolver.
docker exec -it cursolaravel php artisan make:command ImportaLivros
O comando acima criará o arquivo app/Console/Commands/ImportaLivros.php, o qual temos que mudar o $signature e implementar a lógica do comando:
#[Signature('livros:importar')]
public function handle()
{
$livro = new \App\Models\Livro;
$livro->titulo = "A Hora da Estrela";
$livro->autor = "Clarice Lispector";
$livro->ano = 1977;
$livro->save();
}
Ao rodarmos no terminal o comando docker exec -it cursolaravel php artisan livros:importar o livro da Clarice será cadastrado. Essa é um implementação simples (e inútil, pois o mesmo livro é sempre cadastrado repetidamente), mas a ideia é que qualquer lógica pode ser implementada no handle() para cadastro de muitos livros a partir de uma fonte externa, como, por exemplo, uma lista de livros oriunda de um arquivo csv.
Dusk
Os testes com Laravel Dusk no nosso contexto tem dois propósitos:
- Testar funcionalidades reais do sistema, simulando a interação de um usuário no navegador.
- Servir como documentação funcional, demonstrando como as principais funcionalidades do sistema devem se comportar.
docker exec -it cursolaravel composer require --dev laravel/dusk
docker exec -it cursolaravel php artisan dusk:install
docker exec -it cursolaravel php artisan dusk:chrome-driver
Para rodar os testes, configure no .env:
APP_URL=http://cursolaravel
DUSK_DRIVER_URL='http://selenium:4444/wd/hub'
DUSK_START_MAXIMIZED=true
DUSK_HEADLESS_DISABLED=true
Criando uma classe do Dusk para inserirmos nosso teste:
docker exec -it cursolaravel php artisan dusk:make BuscaLivroTestCriando um teste que verifica se na rota /livros existe a frase “Listagem de Livros”:
$browser->visit('/livros')
->pause(2000)
->typeSlowly('search', 'primo', 300)
->pause(2000)
->press('Pesquisar')
->pause(2000)
->assertSee('O Primo Basílio');Acessar http://localhost:7900/ com senha secret e assitir.
Rodar o teste:
docker exec -it cursolaravel php artisan dusk tests/Browser/BuscaLivroTest.phpExercício - Importação de Livros
1 - Criar um comando para importar os livros do arquivo csv livros no model Livro. Importante:
- No método
handle(), implemente a lógica para ler o arquivolivros.csve para cada livro, fazer a inserção; - Dica 1: Para zerar os registros a cada importação, pode-se usar o comando
\App\Models\Livro::truncate()no começo do métodohandle(). - Dica 2: Você pode usar a classe
League\Csv\Reader(disponível via Composer) para facilitar a leitura do CSV.
2 - Criar teste Dusk para buscar a string “processo” e deverá ter um assert para ver Franz Kafka e um assert not para José de Alencar;
3 - Criar estatísticas básicas sobre os dados importados
- Criar o controller
EstatisticaControllercom um método chamadostats. - Defina uma rota
livros/statsque aponte para o métodostats. - No método
statsapresente uma tabela com a quantidade de livros por ano.
Exemplo de saída (com dados fictícios):
| ano | quantidade |
|---|---|
| 1998 | 5 |
| 2001 | 23 |
4 - No método stats apresente uma segunda tabela com a quantidade de livros por autor.
Na próxima reunião, cada membro do grupo (estagiários e funcionários) deve apresentar na TV rapidamente e solução do exercício.
Dia 2
CRUD
CRUD é um acrônimo para as quatro operações básicas utilizadas na manipulação de dados em sistemas web: Create (Criar), Read (Ler), Update (Atualizar) e Delete (Excluir). Essas operações interagem com bancos de dados, permitindo, por exemplo, que usuários possam cadastrar novas informações, visualizar registros existentes, modificar dados já salvos e remover registros.
Create
São geralmente necessárias duas rotas para salvar um registro em uma operação CRUD porque o processo é dividido em duas etapas: exibir o formulário e processar os dados enviados. A rota GET serve para exibir o formulário de criação e a rota POST serve para processar os dados enviados pelo formulário no controller:
Route::get('/livros/create', [LivroController::class,'create']);
Route::post('/livros', [LivroController::class,'store']);
Para mostrar o formulário html usamos o método create :
public function create(){
return view('livros.create');
}
Formulário html resources/views/livros/create.blade.php com o seguinte conteúdo:
<form method="POST" action="/livros">
@csrf
Título: <input type="text" name="titulo">
Autor: <input type="text" name="autor">
Ano: <input type="text" name="ano">
<button type="submit">Enviar</button>
</form>
Por fim o método store, que salva no banco de dados o cadastro do livro:
public function store(Request $request){
$livro = new Livro;
$livro->titulo = $request->titulo;
$livro->autor = $request->autor;
$livro->ano = $request->ano;
$livro->save();
return redirect('/livros');
}
Read
Já implementamos uma forma de acessar os livros em forma de listagem com o método index, podemos implementar outra forma de acesso individual para cada livro. Rota para acesso ao registro de um livro específico:
Route::get('/livros/{livro}', [LivroController::class,'show']);
Respectivo controller:
public function show(Livro $livro){
return view('livros.show',[
'livro' => $livro
]);
}
Criamos um blade para a rota show resources/views/livros/show.blade.php com o seguinte conteúdo:
Título: {{ $livro->titulo }} <br>
Autor: <i>{{ $livro->autor }}</i> <br>
Ano de publicação: {{ $livro->ano }} <br>
<a href="/livros">Voltar</a>
No index.blade.php podemos criar um link para o show de cada livro:
<a href="/livros/{{ $livro->id}}">{{ $livro->titulo }}</a>
Update
Novamente precisamos de duas rotas para atualizar um registro, uma para exibir o formulário e outra para processar os dados enviados.
Route::get('/livros/{livro}/edit', [LivroController::class,'edit']);
Route::patch('/livros/{livro}', [LivroController::class,'update']);
Implementação no controller:
public function edit(Livro $livro){
return view('livros.edit',[
'livro' => $livro
]);
}
public function update(Request $request, Livro $livro){
$livro->titulo = $request->titulo;
$livro->autor = $request->autor;
$livro->ano = $request->ano;
$livro->save();
return redirect("/livros/{$livro->id}");
}
Criando o blade para edição:
touch resources/views/livros/edit.blade.php
Html para edição no edit.blade.php:
<form method="POST" action="/livros/{{ $livro->id }}">
@csrf
@method('PATCH')
Título: <input type="text" name="titulo" value="{{ $livro->titulo }}">
Autor: <input type="text" name="autor" value="{{ $livro->autor }}">
Ano: <input type="text" name="ano" value="{{ $livro->ano }}">
<button type="submit">Enviar</button>
</form>
Vamos colocar o botão para edição no blade show.blade.php:
<a href="/livros/{{ $livro->id }}/edit">Editar</a> <br>
Delete
Rota para delete:
Route::delete('/livros/{livro}', [LivroController::class,'destroy']);
Controller para delete:
public function destroy(Livro $livro)
{
$livro->delete();
return redirect('/livros');
}
Botão html para delete que podemos colocar no blade do show.blade.php:
<form action="/livros/{{ $livro->id }} " method="post">
@csrf
@method('delete')
<button type="submit" onclick="return confirm('Tem certeza?');">Apagar</button>
</form>
Implementação do dusk para testar as operações de CRUD:
php artisan dusk:make LivroCrudTest
Vamos testar sequencialmente as operações:
use App\Models\Livro;
public function test_crud_livros(): void
{
$this->browse(function (Browser $browser) {
// Create
$browser->visit('/livros/create')
->typeSlowly('titulo', '2001: Uma odisséia no espaço')
->typeSlowly('autor', 'Arthur C. Clarke')
->typeSlowly('ano', '1968')
->press('Enviar')
->assertPathIs('/livros')
->assertSee('2001: Uma odisséia no espaço');
// Read
$browser->clickLink('2001: Uma odisséia no espaço')
->assertSee('Arthur C. Clarke')
->assertSee('1968');
// Update
$browser->clickLink('Editar')
->typeSlowly('titulo', '2001: Uma odisséia no espaço - Edição Revisada')
->press('Enviar')
->assertSee('2001: Uma odisséia no espaço - Edição Revisada');
// Delete
$browser->press('Apagar')
->acceptDialog()
->assertPathIs('/livros')
->assertDontSee('2001: Uma odisséia no espaço - Edição Revisada');
});
}
Rodando o teste:
docker exec -it cursolaravel php artisan dusk tests/Browser/LivroCrudTest.php
Exercício 2
- Criar um CRUD completo para o model frases.
- Criar uma classe dusk que testa todas funcionalidades do CRUD frases
- Criar um comando, importarfrases, que importa o arquivo csv: frases
- Criar uma rota
/frasedodiae o método correspondente que ao ser acessada mostra uma frase aleatória, porém correspondente ao dia da semana.
Na próxima reunião, cada membro do grupo (estagiários e funcionários) deve apresentar a implementação na TV.
Dia 3
Adaptação do Dockerfile conforme: https://github.com/uspdev/dockerfiles/blob/master/php-apache/readme.md
Instalação do template USP conforme: https://github.com/uspdev/laravel-usp-theme/
No docker-compose.yml inserir imagem do serviço de senha única faker:
senhaunica-faker:
image: uspdev/senhaunica-faker
container_name: cursolaravel_senhaunica-faker
ports:
- "3141:3141"
environment:
- APP_URL=http://auth.local:3141
networks:
cursolaravel-network:
aliases:
- auth.localNo /etc/hosts do seu computador colocar a linha 127.0.0.1 auth.local.
Instalação do senhaunica-socialite conforme: https://github.com/uspdev/senhaunica-socialite
Configurações para usar o faker:
APP_URL=http://localhost:8000
SENHAUNICA_KEY=faker
SENHAUNICA_SECRET=faker
SENHAUNICA_CALLBACK_ID=1
SENHAUNICA_ADMINS=111111
SENHAUNICA_DEV="http://auth.local:3141/wsusuario/oauth"
Migration de Alteração
Quando o sistema está produção, você nunca deve alterar uma migration que já foi para o ar, mas sim criar uma migration que altera uma anterior. Por exemplo, se quisermos que adicionar o campo user_id na tabela livros:
php artisan make:migration add_user_id_to_livros_table --table=livrosNova coluna user_id:
$table->unsignedBigInteger('user_id')->nullable();
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();Aplique a mudança no banco de dados:
php artisan migrateNo controller, é possível capturar o usuário logado assim: auth()->user()->id.
Validação
Validação no Controller
Quando estamos dentro de um método do controller, a forma mais rápida de validação é usando $request->validate, que validará os campos com as condições que passarmos e caso falhe a validação, automaticamente o usuário é retornado para página de origem com todos inputs que foram enviados na requisição, além da mensagens de erro:
$request->validate([
'titulo' => 'required',
'autor' => 'required',
'ano' => 'required|integer',
]);Podemos usar a função old('titulo') nos formulários, que nesse caso verifica se há input na sessão para o campo titulo:
Título: <input type="text" name="titulo" value="{{old('titulo')}}">
Autor: <input type="text" name="autor" value="{{old('autor')}}">
Ano: <input type="text" name="ano" value="{{old('ano')}}">FormRequest
A validação, que muitas vezes será idêntica no store e no update, pode ser delegada para um FormRequest. Crie um FormRequest com o artisan:
php artisan make:request LivroRequestEsse comando gerou o arquivo app/Http/Requests/LivroRequest.php. Como ainda não falamos de permissões, retorne true no método authorize(). As validações podem ser implementada em rules().
public function rules(){
$rules = [
'titulo' => 'required',
'autor' => 'required',
'ano' => 'required|integer',
];
return $rules;
}Mutators
Há situações em que queremos fazer um leve processamento antes de salvar um valor no banco de dados e logo após recuperarmos um valor. Vamos adicionar um campo para preço. Já sabemos como criar uma migration de alteração para alterar a tabela livros:
php artisan make:migration add_preco_column_in_livros --table=livrosE adicionamos na nova coluna:
$table->float('preco')->nullable();No LivroRequest também deixaremos esse campo como opcional: 'preco' => 'nullable'.
Queremos que o usuário digite, por exemplo, 12,50, mas guardaremos 12.5. Quando quisermos mostrar o valor, vamos fazer a operação inversa. Poderíamos fazer esse tratamento diretamente no controller, mas também podemos usar mutators através no model do livro:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function preco(): Attribute
{
return Attribute::make(
get: fn($value) => number_format($value, 2, ',', ''),
set: fn($value) => str_replace(',','.',$value)
);
}Exercício 3
- No exercício anterior, inserir o usuário no model de frases como nullable, e restringir o cadastro somente para usuários cadastrados, guardando o id do respectivo usuário que está realizando o cadastro;
- Alterar o Dusk do exercício anterior (frases) para realizar todos os testes com um usuário logado, para isso será necessário criar o usuário durante o teste;
- Faça uma migration de alteração para adicionar o campo pontuação para a frase (entre 0 e 10 - validação com FormRequest);
- Faça um mutator converter a virgula, quando existir, para ponto.
- Corriga seus formulários para sempre conterem a função old()