Dia 1
Instalação
Biblioteca mínimas para instalação no debian 12:
sudo apt-get install php php-common php-cli php-gd php-curl php-xml php-mbstring php-zip php-sybase php-mysql php-sqlite3
sudo apt-get install mariadb-server sqlite3 git
Instalação do php 7.4 para subir drupal da FFLCH (temporário):
sudo apt install apt-transport-https lsb-release ca-certificates curl -y
sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
sudo sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
sudo apt update
Instalação das dependências em php7.4:
sudo apt-get install php7.4 php7.4-common php7.4-cli php7.4-gd php7.4-curl php7.4-xml php7.4-mbstring php7.4-zip php7.4-sybase php7.4-sqlite3 php7.4-mysql
Trocar versão do php para 7.4:
sudo update-alternatives --set php /usr/bin/php7.4
sudo update-alternatives --set phar /usr/bin/phar7.4
sudo update-alternatives --set phar.phar /usr/bin/phar.phar7.4
Instalação do composer:
curl -s https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
Criação de usuário admin com senha admin e criação de uma banco chamado drupal:
sudo mariadb
GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' IDENTIFIED BY 'admin' WITH GRANT OPTION;
create database treinamento;
quit
Criando um projeto laravel:
composer create-project laravel/laravel treinamento
cd treinamento
php artisan serve
MVC
Criando a primeira rota:
Route::get('/exercises', function () {
echo "Uma rota sem controller, not good!";
});
Criando o controller ExerciseController:
php artisan make:controller ExerciseController
Agora sim uma rota com controller:
use App\Http\Controllers\ExerciseController;
Route::get('/exercises', [ExerciseController::class,'index']);
Criando um arquivo para a view:
mkdir resources/views/exercises
touch resources/views/exercises/index.blade.php
echo "Ola" > resources/views/exercises/index.blade.php
Como retornar uma view no controller:
return view('exercises/index.blade.php');
#ou
return view('exercises.index');
Conteúdo mínimo de index.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>treinamento</title>
</head>
<body>
Uma view melhor...
</body>
</html>
Retornando uma view passando variáveis para o template:
return view('exercises.index', [
'variavel1' => 'qualquer'
]);
php artisan make:model Exercise -m
Colocar duas colunas:
$table->string('diet');
$table->integer('pulse');
Tinker
Criando um registro:
php artisan tinker
$exercise = new App\Models\Exercise;
$exercise->diet = "low fat";
$exercise->pulse = 95;
$exercise->save();
quit
Listando registro no index:
public function index(){
$exercises = App\Models\Exercise:all();
return view('exercises.index',[
'exercises' => $exercises
]);
}
No blade:
@forelse ($exercises as $exercise)
<li>{{ $exercise->diet }}</li>
<li>{{ $exercise->pulse }}</li>
@empty
Não há livros cadastrados
@endforelse
Command
Criando um comando no laravel:
php artisan make:command ImportaExerciseCsv
Que gera o arquivo: app/Console/Commands/ImportaExerciseCsv.php, no qual devemos definir o comando em si:
protected $signature = 'ImportaExerciseCsv';
No método handle() definimos nossa lógica.
Exercício 1
1) Criar uma rota e um método novo no controler chamado stats e mostrar a quantidade de linhas do tipo rest, walking e running. Também mostar a média da coluna pulse nos três casos rest, walking e running, tudo referente ao arquivo:
https://raw.githubusercontent.com/mwaskom/seaborn-data/master/exercise.csv
Exemplo de saída:
exercise.csv | rest | walking | running |
---|---|---|---|
Qtde linhas | XX | XX | XXX |
Média Pulse | XX | XX | XXX |
2) Criar uma rota e um método novo no controler que importa cada linha do csv no banco de dados, assim como fizemos com o tinker.
Dia 2
CRUD
Operações básicas sob o Model: Create (Criação), Read (Consulta), Update (Atualização) e Delete (Destruição)
No dia 1 criamos o Model Exercise e dois campos: diet e pulse. Nesta parte vamos criar um novo model Chamado Livro e correspondente migration, com os campos:
$table->string('titulo');
$table->string('autor')->nullable();
$table->string('isbn');
Create
Rotas para mostrar o formulário de cadastro de Livro:
use App\Http\Controllers\LivroController;
Route::get('/livros/create', [LivroController::class,'create']);
Route::post('/livros', [LivroController::class,'store']);
Método create para mostrar o formulário html:
public function create()
{
return view('livros.create');
}
Formulário html:
<form method="POST" action="/livros">
@csrf
Título: <input type="text" name="titulo">
Autor: <input type="text" name="autor">
ISBN: <input type="text" name="isbn">
<button type="submit">Enviar</button>
</form>
Read
Temos dois acessos aos dados. O acesso a um livro específico e uma listagem.
use App\Http\Controllers\LivroController;
Route::get('/exercises', [LivroController::class,'index']);
Route::get('/exercises/{livro}', [LivroController::class,'show']);
Controllers:
public function index()
{
$livros = Livro::all();
return view('livros.index',[
'livros' => $livros
]);
}
public function show(Livro $livro)
{
return view('livros.show',[
'livro' => $livro
]);
}
html para index:
@forelse ($livros as $livro)
<ul>
<li><a href="/livros/{{$livro->id}}">{{ $livro->titulo }}</a></li>
<li>{{ $livro->autor }}</li>
<li>{{ $livro->isbn }}</li>
</ul>
@empty
Não há livros cadastrados
@endforelse
Update
Novamente temos que mostrar o fomulário de edição e um método para salvar:
use App\Http\Controllers\LivroController;
Route::get('/exercises/{livro}', [LivroController::class,'edit']);
Route::post('/exercises/{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->isbn = $request->isbn;
$livro->save();
return redirect("/livros/{$livro->id}");
}
Html para edição:
<form method="POST" action="/livros">
@csrf
Título: <input type="text" name="titulo" value="{{ $livro->titulo }}">
Autor: <input type="text" name="autor" value="{{ $livro->autor }}">
ISBN: <input type="text" name="isbn" value="{{ $livro->isbn }}">
<button type="submit">Enviar</button>
</form>
Delete
Rota para delete:
Route::delete('/exercises/{livro}', [LivroController::class,'update']);
Controller para delete:
public function destroy(Livro $livro)
{
$livro->delete();
return redirect('/livros');
}
Botão html para delete:
<li>
<form action="/livros/{{ $livro->id }} " method="post">
@csrf
@method('delete')
<button type="submit" onclick="return confirm('Tem certeza?');">Apagar</button>
</form>
</li>
Exercício 2
- Criar um model Book com migration e um controller BookController
- Na migration criar os campos https://github.com/zygmuntz/goodbooks-10k/blob/master/samples/books.csv
- Criar uma rotina de importação com php artisan make:command ImportBookCsv e importa o csv https://raw.githubusercontent.com/zygmuntz/goodbooks-10k/master/books.csv (esse é o completo!)
- Implementar todos médodos do CRUD, de forma que pela inteface poderemos criar, editar, ver e apagar books
Criar rota, controler e view que vai mostrar:
- uma tabela com a quantidade de livros por ano
- uma tabela com a quantidade de livros por autor
- uma tabela com a quantidade de livros por idioma
Dia 3
Instalação do template USP conforme:
https://github.com/uspdev/laravel-usp-theme/blob/master/docs/configuracao.md
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',
'isbn' => 'required|integer',
]);
Podemos usar a função old('titulo')
nos formulários, que verifica se há inputs na sessão e em caso negativo usa o segundo parâmetro:
Título: <input type="text" name="titulo" value="{{old('titulo')}}">
Autor: <input type="text" name="autor" value="{{old('autor')}}">
ISBN: <input type="text" name="isbn" value="{{old('isbn')}}">
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 LivroRequest
Esse comando gerou o arquivo app/Http/Requests/LivroRequest.php
. Como ainda não falamos de autenticação e autorização, retorne true
no método authorize()
. As validações podem ser implementada em rules()
. Perceba que o isbn pode ser digitado com traços ou ponto, mas eu só quero validar a parte numérica do campo e ignorar o resto, para isso usei o método prepareForValidation
:
public function rules(){
$rules = [
'titulo' => 'required',
'autor' => 'required',
'isbn' => 'required|integer',
];
return $rules;
}
protected function prepareForValidation()
{
$this->merge([
'isbn' => preg_replace('/[^0-9]/', '', $this->isbn),
]);
}
Não queremos livros cadastrados com o mesmo isbn. Há uma validação chamada unique
que pode ser invocada na criação de um livro como unique:TABELA:CAMPO
, mas na edição, temos que ignorar o próprio livro assim unique:TABELA:CAMPO:ID_IGNORADO
. Dependendo do seu projeto, talvez seja melhor fazer um formRequest para criação e outro para edição. Aqui usaremos o mesmo FormRequest para ambos. As mensagens de erros podem ser customizadas com o método messages()
:
public function rules(){
$rules = [
'titulo' => 'required',
'autor' => 'required',
'isbn' => ['required','integer'],
];
if ($this->method() == 'PATCH' || $this->method() == 'PUT'){
array_push($rules['isbn'], 'unique:livros,isbn,' .$this->livro->id);
}
else{
array_push($rules['isbn'], 'unique:livros');
}
return $rules;
}
protected function prepareForValidation()
{
$this->merge([
'isbn' => str_replace('-','',$this->isbn)
]);
}
public function messages()
{
return [
'isbn.unique' => 'Este isbn está cadastrado para outro livro',
];
}
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 o campo isbn guarde apenas números, faremos:
composer require doctrine/dbal
php artisan make:migration change_isbn_column_in_livros --table=livros
Para usar migration de alteração devemos incluir o pacote doctrine/dbal
e na sequência criar a migration que alterará a tabela existente.
Alterando a coluna isbn
de string para integer na migration acima:
$table->integer('isbn')->change();
Aplique a mudança no banco de dados:
php artisan migrate
Campos do tipo select
Vamos supor que queremos um campo adicional na tabela de livros chamado tipo
. Já sabemos como criar uma migration de alteração para alterar a tabela livros:
php artisan make:migration add_tipo_column_in_livros --table=livros
E adicionamos na nova coluna:
$table->string('tipo');
Vamos trabalhar com apenas dois tipos: nacional e internacional. A lista de tipos poderia vir de qualquer fonte: outro model, api, csv etc. No nosso caso vamos fixar esse dois tipos em um array e usar em todo o sistema. No model do livro vamos adicionar um método estático que retorna os tipos, pois assim, fica fácil mudar caso a fonte seja alterada no futuro:
public static function tipos(){
return [
'Nacional',
'Internacional'
];
}
No form.blade.php
podemos inserir o tipo com um campo select desta forma:
<select name="tipo">
<option value="" selected=""> - Selecione -</option>
@foreach ($livro::tipos() as $tipo)
<option value="{{$tipo}}" {{ ( $livro->tipo == $tipo) ? 'selected' : ''}}>
{{$tipo}}
</option>
@endforeach
</select>
Se quisermos contemplar o old
para casos de erros de validação:
<select name="tipo">
<option value="" selected=""> - Selecione -</option>
@foreach ($livro::tipos() as $tipo)
{{-- 1. Situação em que não houve tentativa de submissão --}}
@if (old('tipo') == '')
<option value="{{$tipo}}" {{ ( $livro->tipo == $tipo) ? 'selected' : ''}}>
{{$tipo}}
</option>
{{-- 2. Situação em que houve tentativa de submissão, o valor de old prevalece --}}
@else
<option value="{{$tipo}}" {{ ( old('tipo') == $tipo) ? 'selected' : ''}}>
{{$tipo}}
</option>
@endif
@endforeach
</select>
Por fim, temos que validar o campo tipo para que só entrem os valores do nosso array. No LivroRequest.php:
use Illuminate\Validation\Rule;
...
'tipo' => ['required', Rule::in(\App\Models\Livro::tipos())],
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=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
diretamente no model do livro:
public function setPrecoAttribute($value){
$this->attributes['preco'] = str_replace(',','.',$value);
}
public function getPrecoAttribute($value){
return number_format($value, 2, ',', '');
}
Exercício 3
Esse exercíco é referente ao arquivo: https://github.com/owid/covid-19-data/blob/master/public/data/vaccinations/country_data/Brazil.csv
- Criar model, migration, CRUD, command de importação do csv. Na migration defina os campos total_vaccinations e people_vaccinated como string. Importe o csv.
- Faça uma migration de alteração para alterar total_vaccinations e people_vaccinated para inteiro
- Faça um mutator para mostrar o campo date com o formato brasileiro dd/mm/yyyy e outro mutator para salvá-lo commo yyyy-mm-dd (lembre-se que o formulário deve receber dd/mm/yyyy)
- Implemente o FormRequest garantindo que seja digitado dd/mm/yyyy, além implementar as outras validações
- Corriga seus formulários para sempre conterem a função old()
Dia 4
Testes automatizados com Dusk
Dusk é uma ferramenta de teste automatizado que permite escrever testes de navegador.
Execute o seguinte comando para adicionar o Laravel Dusk como uma dependência de desenvolvimento:
composer require laravel/dusk --dev
php artisan dusk:install
O diretório tests/Browser será criado automaticamente e um arquivo de configuração para o Dusk.
Você terá um arquivo .env.dusk.local com as configurações específicas para o ambiente de teste Dusk. Copie o arquivo .env para .env.dusk.local e ajuste conforme necessário.
Você pode criar um novo teste de navegador usando o Artisan:
php artisan dusk:make LivroCrudTest
Isso criará um novo arquivo de teste em tests/Browser/LivroCrudTest.php
Visita a página de criação de livros, preenche o formulário e verifica se o livro foi criado corretamente:
public function testCreateLivro()
{
$this->browse(function (Browser $browser) {
$browser->visit('/livros/create')
->type('title', 'Meu Novo Post')
->type('content', 'Este é o conteúdo do meu novo post.')
->press('Salvar')
->assertPathIs('/posts')
->assertSee('Meu Novo Post')
->assertSee('Este é o conteúdo do meu novo post.');
});
}
Para executar seus testes, use o seguinte comando:
php artisan dusk
Cria um livro diretamente no banco de dados, visita a página de detalhes do livro e verifica se as informações estão corretas:
public function testReadPost()
{
$post = Post::create([
'title' => 'Post Existente',
'content' => 'Este é um post existente.',
]);
$this->browse(function (Browser $browser) use ($post) {
$browser->visit('/posts/' . $post->id)
->assertSee('Post Existente')
->assertSee('Este é um post existente.');
});
}
Cria um livro, visita a página de edição, atualiza os dados e verifica se as mudanças foram salvas:
public function testUpdatePost()
{
$post = Post::create([
'title' => 'Post Atualizável',
'content' => 'Este é um post que será atualizado.',
]);
$this->browse(function (Browser $browser) use ($post) {
$browser->visit('/posts/' . $post->id . '/edit')
->type('title', 'Post Atualizado')
->type('content', 'Este é o conteúdo atualizado do post.')
->press('Salvar')
->assertPathIs('/posts/' . $post->id)
->assertSee('Post Atualizado')
->assertSee('Este é o conteúdo atualizado do post.');
});
}
Cria um livro, executa a ação de deleção e verifica se o livro foi removido da lista.
public function testDeletePost()
{
$post = Post::create([
'title' => 'Post Deletável',
'content' => 'Este é um post que será deletado.',
]);
$this->browse(function (Browser $browser) use ($post) {
$browser->visit('/posts')
->assertSee('Post Deletável')
->press('#delete-post-' . $post->id) // Supondo que existe um botão de deleção com este ID
->assertPathIs('/posts')
->assertDontSee('Post Deletável');
});
}
Dia 5 (Em construção)
Relações
One (User) To Many (Livros)
Primeiramente vamos implementar esse relação no nível do banco de dados. Na migration dos livros insira:
$table->unsignedBigInteger('user_id')->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');;
No model Livro podemos criar um método que carregará o objeto user
automaticamente ou no model User
podemos carregar todos livros do usuário:
class Livro extends Model
{
public function user(){
return $this->belongsTo(\App\Models\User::class);
}
}
class User extends Model
{
public function livros()
{
return $this->hasMany(App\Models\Livro::class);
}
}
Assim no fields.blade.php
faremos referência direta a esse usuário:
<li>Cadastrado por {{ $livro->user->name ?? '' }}</li>
Extra
Configuração do .env para conexão com banco de dados:
DB_DATABASE=treinamento
DB_USERNAME=admin
DB_PASSWORD=admin
Criando model e tabela no banco de dados:
biblioteca Audit
O módulo Audit é uma forma de verificar e manter registros das modificações feitas por usuários no sistema.
- Instalação
- O pacote é instalado via Composer. Para o obter é necessário executar este comando dentro da pasta do seu projeto:
composer require owen-it/laravel-auditing
- Configuração
- Após isso, use o comando a seguir para publicar as configurações feitas e criar o arquivo config/audit.php:
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="config"
- Migration
- A seguir, crie a tabela audits com o seguinte comando:
php artisan vendor:publish --provider "OwenIt\Auditing\AuditingServiceProvider" --tag="migrations"
php artisan migrate
- Model
- Para implementar o audit no model desejado, é necessário adicionar as linhas:
use OwenIt\Auditing\Contracts\Auditable;
class Instance extends Model implements Auditable
{
use HasFactory;
use \OwenIt\Auditing\Auditable;
/ ...
}
- Implementação
Agora vamos implementar o módulo para ser exibido nas views.
- Primeiro, para facilitar a leitura dos usuários, é necessário mapear os campos da migration do Model em que o audit será utilizado. Então, em app/Utils vamos criar Mapeamento.php para o Model Livro, como no exemplo abaixo:
<?php
namespace App\Utils;
use App\Models\Livro;
class Mapeamento
{
public static function map($chave){
$mapeamento = [
'id' => 'ID',
'autores' => 'Autores',
'titulo' => 'Título',
'editora' => 'Editora',
'ano' => 'Ano',
'isbn' => 'ISBN',
];
return $mapeamento[$chave];
}
}
- Agora vamos chamar este mapeamento dentro do Model correspondente:
app/Models/Livro.php
use App\Utils\Mapeamento;
class Livro extends Model implements Auditable
{
/ ...
public function mapeamento($chave) {
return MapRecords::map($chave);
}
}
- Após isso, é preciso adicionar a exibição do módulo audit nas views, criando uma tabela com as alterações que foram feitas, a data da alteração e o usuário que a realizou. Dentro da pasta resources/views/livros/partials vamos criar audit.blade.php, já adicionando o mapeamento feito anteriormente na tabela: partials/audit.blade.php
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Data</th>
<th scope="col">Usuário(a)</th>
<th scope="col">Campos alterados</th>
<th scope="col">Alterações</th>
</tr>
</thead>
<tbody>
@foreach($model->audits as $field => $audit)
<tr>
<td> {{ \Carbon\Carbon::parse($audit->getMetadata()['audit_created_at'])->setTimezone('America/Sao_Paulo')->format('d/m/Y H:i') }} </td>
<td> {{ $audit->getMetadata()['user_name'] }}</td>
<td>
@foreach($audit->getModified() as $field=>$modified)
@if($field)
<b>{{ $livro->mapeamento($field) }}: {{ $modified['old'] }}<br>
@endif
@endforeach
</td>
<td>
@foreach($audit->getModified() as $field2=>$modified)
@if($field)
<b>{{ $record->mapeamento($field2) }}: {{ $modified['new'] }}<br>
@endif
@endforeach
</td>
</tr>
@endforeach
</tbody>
</table>
- Agora vamos incluir a tabela na view show.blade.php de livros:
@include('livros.partials.audit', ['model'=>$livro])
- Para que a página não fique muito poluída, optamos por inserir a tabela dentro de uma tag details usando um alerta bootstrap, desta forma:
<div class="alert alert-info" role="alert">
<details>
<summary>Visualizar histórico de alterações</summary>
<br>
@include('livros.partials.audit', ['model'=>$livro])
</details>
</div>
Para mais informações visite: Laravel Auditing — Escrito por Isabela