Iremos usar o docker para fazer a instalação. 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 cursolaravel
Dockerfile pronto para usar no contexto USP:
FROM uspdev/uspdev-php-apache:latest
RUN sed -i 's|/var/www/html|/var/www/html/public|' \
/etc/apache2/sites-available/000-default.conf
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 no contexto USP:
services:
cursolaravel:
build: .
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
phpmyadmin:
image: phpmyadmin:latest
container_name: cursolaravel_phpmyadmin
restart: always
depends_on:
- mariadb
ports:
- "8081:80"
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: cursolaravel
networks:
- cursolaravel-network
selenium:
image: selenium/standalone-chrome
container_name: cursolaravel_selenium
ports:
- "7900:7900"
networks:
- cursolaravel-network
shm_size: 2gb
# No /etc/hosts do seu computador colocar a linha `127.0.0.1 auth.local`
# particularidades do protocolo oAuth1 usado na USP
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.local
mailpit:
image: axllent/mailpit:latest
container_name: cursolaravel_mailpit
ports:
- "1025:1025" # SMTP
- "8025:8025" # Interface Web
networks:
- cursolaravel-network
networks:
cursolaravel-network:
volumes:
mariadb_data:
Baixando ambos:
curl -L https://fflch.github.io/assets/laravel/Dockerfile -o Dockerfile
curl -L https://fflch.github.io/assets/laravel/docker-compose.yml -o docker-compose.yml
Criando a imagem e subindo ambiente:
docker compose up --build
Acessar o laravel criado: http://127.0.0.1:8000/
Acessar phpmyadmin criado: http://127.0.0.1:8081/
Acessar servidor de autenticação USP: http://auth.local:3141
Acessar servidor de email: http://localhost:8025/
Acessar http://localhost:7900/ com senha secret para assistir os testes rodando.
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
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>
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.
O comando docker exec -it cursolaravel php artisan tinker nos 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>
Inserindo um campo para busca simples no blade:
<form>
<input type="text" name="search" value="{{ request('search') }}">
<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
]);
}
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, vamos implementar a inserção de um livro via linha de comando:
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 app:importa-livros 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.
Os testes com Laravel Dusk no nosso contexto tem dois propósitos:
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 (e aproveite coloque no .env.example):
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 BuscaLivroTest
Criando 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');
Rodar o teste:
docker exec -it cursolaravel php artisan dusk tests/Browser/BuscaLivroTest.php
1 - Criar um comando para importar os livros do arquivo csv livros no model Livro. Importante:
handle(), implemente a lógica para ler o arquivo livros.csv e para cada livro, fazer a inserção;\App\Models\Livro::truncate() no começo do método handle().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
EstatisticaController com um método chamado stats.livros/stats que aponte para o método stats.stats apresente uma tabela com a quantidade de livros por ano.Exemplo de saída (com dados fictícios):
| ano | quantidade |
|---|---|
| 1998 | 4 |
| 2000 | 7 |
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.
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.
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');
}
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>
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>
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_curso(): 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
/frasedodia e 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.
Instalação do template USP conforme: https://github.com/uspdev/laravel-usp-theme/
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"
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=livros
Nova 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 migrate
No controller, é possível capturar o usuário logado assim: auth()->user()->id.
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',
]);
A função old('titulo') verifica se há input na sessão para o campo titulo, para o create.blade.php:
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')}}">
E no para o edir.blade.php:
Título: <input type="text" name="titulo" value="{{ old('titulo', $livro->titulo) }}">
Autor: <input type="text" name="autor" value="{{ old('autor', $livro->autor) }}">
Ano: <input type="text" name="ano" value="{{ old('ano', $livro->ano) }}">
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:
docker exec -it cursolaravel php artisan make:request LivroRequest
Esse 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;
}
No controler, trocamos as chamadas de Request para LivroRequest:
use App\Http\Requests\LivroRequest;
public function update(LivroRequest $request, Livro $livro){
public function store(LivroRequest $request)
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=livros
E 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)
);
}
Revisão do ambiente com o conteúdo visto até então:
docker run --rm -it \
-v $(pwd):/app \
-u $(id -u):$(id -g) \
composer:latest \
composer create-project uspdev/starter-ng cursolaravel
cd cursolaravel
docker compose up --build
Rodando as migrations:
docker exec -it cursolaravel composer dump-autoload -o
docker exec -it cursolaravel php artisan migrate:fresh
Acessar o laravel criado: http://127.0.0.1:8000/
Gerando o CRUD para livros:
docker exec -it cursolaravel php artisan scg
docker exec -it cursolaravel php artisan migrate
Mude o .env e rode o testes com o dusk:
docker exec -it cursolaravel php artisan dusk:chrome-driver
docker exec -it cursolaravel php artisan dusk tests/Browser/LivroCrudTest.php
Acessar http://localhost:7900/ com senha secret e assistir o teste rodando.
Na tabela livros já inserimos o campo user_id apontando para tabela users. Vamos agora criar as relações (one-to-many) no laravel:
Model Livro:
public function user()
{
return $this->belongsTo(User::class);
}
Nos fornece o poder de acessar o objeto usuário a partir do livro, exemplo no show.blade.php
<p>
Livro cadastrado em {{ $livro->created_at->format('d/m/Y H:i') }} <br>
Última atualização em {{ $livro->updated_at->format('d/m/Y H:i') }} por <b>{{ $livro->user?->name }}</b>
</p>
Model User:
public function livros()
{
return $this->hasMany(Livro::class);
}
Nos fornece o poder de acessar todos os objetos de livros a partir de um usuário, exemplo no show.blade.php:
<div>
@if($livro->user)
Outros livros cadastrados ou editados por {{ $livro->user?->name }}:
<ul>
@foreach($livro->user->livros as $outro_livro)
<li>{{ $outro_livro->titulo }}</li>
@endforeach
</ul>
@endif
</div>
Um Observer é uma classe que escuta e reage a eventos do ciclo de vida de uma Model, como created, updated ou deleted:
docker exec -it cursolaravel php artisan make:observer LivroObserver --model=Livro
Criando um template de email na pasta resources/views/emails/livros com o nome create.blade.php:
mkdir -p resources/views/emails/livros
touch resources/views/emails/livros/create.blade.php
Template:
Novo livro criado: {{ $livro->titulo }}
Criando a rotina de envio de email:
docker exec -it cursolaravel php artisan make:mail LivroCreatedMail
Configurando o email com ShouldQueue para que o Laravel envie o email em segundo plano:
use App\Models\Livro;
class LivroCreatedMail extends Mailable implements ShouldQueue
{
private Livro $livro;
public function __construct(Livro $livro)
{
$this->livro = $livro;
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Novo Livro Cadastrado: ' . $this->livro->titulo,
);
}
public function content(): Content
{
return new Content(
view: 'emails.livros.create',
with: [
'livro' => $this->livro,
],
);
}
Configurando o observer para disparar o email na ação de livro criado:
namespace App\Observers;
use App\Models\Livro;
use App\Mail\LivroCreatedMail;
use Illuminate\Support\Facades\Mail;
class LivroObserver
{
public function created(Livro $livro): void
{
Mail::to('destinatario@email.com')->queue(new LivroCreatedMail($livro));
}
}
Registrando o observer no AppServiceProvider.php:
use App\Models\Livro;
use App\Observers\LivroObserver;
public function boot(): void
{
Livro::observe(LivroObserver::class);
}
}
Agrupe os testes de e-mail do mesmo Model em uma única classe, utilizando o prefixo Livro para identificar os testes da Model Livro, neste caso:
docker exec -it cursolaravel php artisan dusk:make LivroEmailsTest
Implemente o teste Dusk para confirmar se o email está sendo disparado:
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Illuminate\Support\Facades\Http;
use App\Mail\LivroCriadoMail;
use Illuminate\Support\Facades\Mail;
class LivroEmailsTest extends DuskTestCase
{
protected function setUp(): void
{
// Limpa as mensagens do Mailpit antes de cada teste
parent::setUp();
Http::delete('http://mailpit:8025/api/v1/messages');
}
public function test_create_livro(): void
{
Mail::fake();
$this->browse(function (Browser $browser) {
// Login
$browser->visit('/')
->clickLink('Entrar')
->waitFor('#loginUsuario')
->typeSlowly('#loginUsuario', '111111')
->press('Login');
// 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');
});
// Consulta a API do Mailpit para verificar se o e-mail foi entregue
$response = Http::get('http://mailpit:8025/api/v1/messages');
$messages = $response->json('messages');
$latestMail = $messages[0];
// Valida o assunto do e-mail enviado pela Mailable
$this->assertStringContainsString('Novo Livro Cadastrado: ' . '2001: Uma odisséia no espaço', $latestMail['Subject']);
// Valida se o destinatário é o correto
$this->assertEquals('destinatario@email.com', $latestMail['To'][0]['Address']);
}
}
Na USP, o banco de dados corporativo central (Sybase/SQL Server) é replicado para bases de dados locais nas unidades, esse banco local é chamado de Replicado.
docker exec -it cursolaravel composer require uspdev/replicado
Como funciona?
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Uspdev\Replicado\Pessoa;
class IndexController extends Controller
{
public function index(){
if (auth()->check()) {
$curso = Pessoa::retornarCursoPorCodpes(auth()->user()->codpes)['nomcur'];
} else {
$curso = 'usuário não logado';
}
return view('index', ['curso' => $curso]);
}
}
No blade:
Seu curso é: {{ $curso }}
Exemplo de upload para uma imagem por livro. Usaremos uma abordagem mais segura porque impede o acesso público direto via URL. Quando os arquivos ficam na pasta pública, qualquer pessoa que descubra o link pode visualizá-los, ignorando a autenticação do sistema. No diretório privado, a entrega do arquivo obrigatoriamente passa por uma rota e um método no Controller. Isso permite validar se o usuário está logado e aplicar políticas de permissão, garantindo total controle sobre quem pode visualizar ou baixar o conteúdo.
No arquivo App/Http/Requests/LivroRequest.php:
'imagem' => 'nullable|image|mimes:jpeg,jpg|max:2048',
Formulário de criação:
<form method="POST" action="/livros" enctype="multipart/form-data">
...
Capa (JPEG): <input type="file" name="imagem" accept="image/jpeg">
...
</form>
Formulário de edição:
<form method="POST" action="/livros/{{ $livro->id }}" enctype="multipart/form-data">
...
@if($livro->imagem_path)
<img src="/livros/imagem/{{ $livro->id }}" width="200px"> <br>
@endif
Imagem: <input type="file" name="imagem" accept="image/jpeg">
...
</form>
Migration de alteração:
docker exec -it cursolaravel php artisan make:migration add_imagem_column_in_livros --table=livros
Novos campos:
$table->string('imagem_original_name')->nullable();
$table->string('imagem_path')->nullable();
Aplicando
docker exec -it cursolaravel php artisan migrate
No controller, store e update:
if ($request->hasFile('imagem')) {
$livro->imagem_original_name = $request->file('imagem')->getClientOriginalName();
$livro->imagem_path = $request->file('imagem')->store('livros');
}
Rota e método a para ver a imagem, pois por padrão estamos colocando-as numa pasta privada e método para remoção do arquivo:
use Illuminate\Support\Facades\Storage;
# rota: Route::get('/livros/imagem/{livro}', [LivroController::class,'imagem']);
public function imagem(Livro $livro)
{
return Storage::download($livro->imagem_path, $livro->imagem_original_name);
}
# rota: Route::delete('/livros/imagem/{livro}', [LivroController::class,'destroy_imagem']);
public function destroy_imagem(Livro $livro)
{
if ($livro->imagem_path && Storage::exists($livro->imagem_path)) {
Storage::delete($livro->imagem_path);
$livro->imagem_path = null;
$livro->imagem_original_name = null;
$livro->save();
}
return back();
}
# atualizar
public function destroy(Livro $livro)
{
if ($livro->imagem_path && Storage::exists($livro->imagem_path)) {
Storage::delete($livro->imagem_path);
}
$livro->delete();
return redirect('/livros');
}
No show.blade.php:
@if($livro->imagem_path)
<img src="/livros/imagem/{{ $livro->id }}" width="200px"> <br>
<form action="/livros/imagem/{{ $livro->id }} " method="post">
@csrf
@method('delete')
<button type="submit" onclick="return confirm('Tem certeza?');">Deletar Imagem</button>
</form>
@endif
Teste de upload com o dusk:
use Illuminate\Http\UploadedFile;
$this->browse(function (Browser $browser) {
$image1 = UploadedFile::fake()->image('imagem1.jpg', 640, 480);
$image2 = UploadedFile::fake()->image('imagem2.jpg', 640, 480);
# create
$browser->visit('/livros/create')
->attach('imagem', $image1->getPathname())
# update
$browser->clickLink('Editar')
->attach('imagem', $image2->getPathname())
# delete imagem
$browser->press('Deletar Imagem')
->acceptDialog();
Atenção
Quando um livro pode ter múltiplos arquivos associados (como fotos de capa, sumário, anexos ou capítulos em PDF), a melhor prática é criar um Model entidade, como por exemplo, LivroArquivo via relacionamento Um para Muitos (hasMany) ao invés de fazer no model do livro como feito aqui, entretanto, a parte de manipulação do arquivo, continua exatamente a mesma.
Um Gate no Laravel é uma função usada para verificar se um usuário autenticado tem permissão para realizar uma ação específica na aplicação. O senha única socialite fornece acesso para um Gate chamado de admin para aqueles usuários com número USP no .env: SENHAUNICA_ADMINS=111111,782783, no exemplo, somente as pessoas com número USP 111111 e 782783 terão acesso de admim. Para restringir o acesso a controllers somente para admin:
use Illuminate\Support\Facades\Gate;
public function index(){
Gate::authorize('admin');
...
}
Ou no blade:
@can('admin')
{{-- Verificar permission customizada --}}
@endcan
criando algumas permissões a serem utilizadas pela aplicação:
docker exec -it cursolaravel php artisan make:migration seed_permission_table
Arquivo com os Gates:
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
...
public function up()
{
Permission::firstOrCreate(['name' => 'vigilantes']);
Permission::firstOrCreate(['name' => 'limpeza']);
}
docker exec -it cursolaravel php artisan migrate
Importando csv com os livros:
mkdir -p app/Console/Commands
curl -L https://fflch.github.io/assets/laravel/ImportaLivros.php -o app/Console/Commands/ImportaLivros.php
docker exec -it cursolaravel composer require league/csv
docker exec -it cursolaravel php artisan app:importa-livros
docker exec -it cursolaravel composer require phpoffice/phpspreadsheet
Gerando excel
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
# rota: Route::get('/livros/excel', [LivroController::class,'excel']);
public function excel(Request $request)
{
if ($request->has('search')) {
$livros = Livro::where('titulo', 'like', '%' . $request->search . '%')->get();
} else {
$livros = Livro::all();
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Cabeçalho
$sheet->fromArray([
['Título', 'Autor', 'Ano']
]);
// Dados
$linha = 2;
foreach ($livros as $livro) {
$sheet->fromArray([
$livro->titulo,
$livro->autor,
$livro->ano,
], null, "A{$linha}");
$linha++;
}
$writer = new Xlsx($spreadsheet);
return response()->streamDownload(
fn() => $writer->save('php://output'),
'livros.xlsx'
);
}
No blade:
<a href="/livros/excel/?search={{ request('search') }}" class="btn btn-success">
Exportar Excel
</a>
Teste no Dusk simples, somente verificando se a rota entrega um arquivo excel:
$browser->visit('/livros/excel?search=Editado');
$response = $this->get('/livros/excel?search=Editado');
$response->assertStatus(200);
$response->assertHeader('content-disposition', 'attachment; filename=livros.xlsx');
Gerando Pdf no padrão da FFLCH
Instalação
docker exec -it cursolaravel composer require fflch/laravel-fflch-pdf
docker exec -it cursolaravel php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"
docker exec -it cursolaravel php artisan vendor:publish --provider="Fflch\LaravelFflchPdf\LaravelFflchPdfServiceProvider"
No seu .env, configure o cabeçalho do pdf: FFLCHPDF_SETOR='Setor de Graduação'.
use PDF;
# rota: Route::get('/livros/pdf', [LivroController::class,'pdf']);
public function pdf(Request $request)
{
if ($request->filled('search')) {
$livros = Livro::where('titulo', 'like', '%' . $request->search . '%')->get();
} else {
$livros = Livro::all();
}
$pdf = PDF::loadView('livros.pdf', [
'livros' => $livros,
]);
return $pdf->download('livros.pdf');
}
Formatação do pdf em resources/views/livros/pdf.blade.php:
@extends('laravel-fflch-pdf::main')
@section('content')
<h1>Relação de Livros</h1>
<table width="100%" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<th>Título</th>
<th>Autor</th>
<th>Ano</th>
</tr>
</thead>
<tbody>
@foreach($livros as $livro)
<tr>
<td>{{ $livro->titulo }}</td>
<td>{{ $livro->autor }}</td>
<td>{{ $livro->ano }}</td>
</tr>
@endforeach
</tbody>
</table>
@endsection
No blade:
<a href="/livros/pdf/?search={{ request('search') }}" class="btn btn-success">
Exportar Pdf
</a>
Por fim, um teste no dusk simples verificando se o pdf foi gerado:
$browser->visit('/livros/pdf?search=Editado');
$response = $this->get('/livros/pdf?search=Editado');
$response->assertStatus(200);
$response->assertHeader('content-disposition', 'attachment; filename=livros.pdf');
Workflow
Implementando a lógica no model quando o sistema existe etapas, exemplo: pedido, análise e aprovado.
Biblioteca para registro de status:
docker exec -it cursolaravel composer require spatie/laravel-model-status
docker exec -it cursolaravel php artisan vendor:publish --provider="Spatie\ModelStatus\ModelStatusServiceProvider" --tag="migrations"
docker exec -it cursolaravel php artisan migrate
Infomando o Model que ele tem status:
use Spatie\ModelStatus\HasStatuses;
class Livro extends Model
{
use HasStatuses;
}
No controller do Livro colocar status no create e no update respectivamente:
$livro->setStatus('pedido');
$livro->setStatus('aprovado');
Biblioteca da FFLCH para ver os status:
docker exec -it cursolaravel composer require fflch/laravel-fflch-stepper
docker exec -it cursolaravel php artisan vendor:publish --provider="Fflch\LaravelFflchStepper\LaravelFflchStepperServiceProvider" --tag="config"
Seguir a documentação da biblioteca em https://github.com/fflch/laravel-fflch-stepper
1 - Fazer auditoria do model Livro no qual o administrador poderá ver todos usuários que alteraram um determinado livro e quando. Usar owen-it/laravel-auditing. Caso real de uso: https://github.com/uspdev/copaco/blob/master/resources/views/partials/audit/index.blade.php
2 - Criar uma rotina de envio de email quando um livro for cadastrado. Ao invés deixar fixo o texto do email no sistema no arquivo blade, dar a opção do usuário do sistema alterar partes do texto do email, para isso criar uma área de configuração no sistema que poderá ser usada pelos usuários para alterar os emails. Usar spatie/laravel-settings. Caso real de uso: https://github.com/uspdev/estagios/blob/master/resources/views/emails/alteracao.blade.php
1 - Imagem do docker
Para construção da imagem baseada na tag (versão) criar o arquivo .github/workflows/docker.yml:
Arquivo modelo docker.yml
mkdir -p .github/workflows
curl -L https://fflch.github.io/assets/laravel/docker.yml -o .github/workflows/docker.yml
2 - testes no dusk
Para rodar os testes no dusk baseado na configuração do docker-compose.yml, criar o arquivo .github/workflows/dusk.yml:
Arquivo modelo dusk.yml
curl -L https://fflch.github.io/assets/laravel/dusk.yml -o .github/workflows/dusk.yml
É necessário alterar a variável SERVICE_NAME no dusk.yml colocando o nome do sistema, que no caso do curso é cursolaravel.
3 - .env.example
Além disso o .env.example do sistema deve estar preparado para rodar os testes no dusk com o ambiente criado pelo docker-compose.yml, ou seja, o .env.example deve conter minimamente:
# Configurações Mínimas em dev
QUEUE_CONNECTION=sync
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=test@example.com
MAIL_FROM_NAME="Laravel"
DB_CONNECTION=mariadb
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=cursolaravel # TROCAR PARA NOME DO SISTEMA
DB_USERNAME=cursolaravel # TROCAR PARA NOME DO SISTEMA
DB_PASSWORD=cursolaravel # TROCAR PARA NOME DO SISTEMA
APP_URL=http://cursolaravel # TROCAR PARA NOME DO SISTEMA
DUSK_DRIVER_URL='http://selenium:4444/wd/hub'
DUSK_START_MAXIMIZED=true
DUSK_HEADLESS_DISABLED=true
SENHAUNICA_KEY=faker
SENHAUNICA_SECRET=faker
SENHAUNICA_CALLBACK_ID=1
SENHAUNICA_ADMINS=111111
SENHAUNICA_DEV="http://auth.local:3141/wsusuario/oauth"
USP_THEME_SKIN=fflch # ou uspdev
curl -L https://fflch.github.io/assets/laravel/env.example >> .env.example