Skip to main content Link Search Menu Expand Document (external link)
  1. Dia 1
    1. Instalação
    2. MVC
      1. Model
      2. Tinker
      3. Busca
      4. Command
      5. Dusk
    3. Exercício - Importação de Livros
  2. Dia 2
    1. CRUD
      1. Create
      2. Read
      3. Update
      4. Delete
    2. Exercício 2
  3. Dia 3
    1. Migration de Alteração
    2. Validação
      1. Validação no Controller
      2. FormRequest
    3. Mutators
    4. Exercício 3

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 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>

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:

  1. Testar funcionalidades reais do sistema, simulando a interação de um usuário no navegador.
  2. 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 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');

Acessar http://localhost:7900/ com senha secret e assitir.

Rodar o teste:

docker exec -it cursolaravel php artisan dusk tests/Browser/BuscaLivroTest.php

Exercí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 arquivo livros.csv e 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étodo handle().
  • 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 EstatisticaController com um método chamado stats.
  • Defina uma rota livros/stats que aponte para o método stats.
  • No método stats apresente uma tabela com a quantidade de livros por ano.

Exemplo de saída (com dados fictícios):

anoquantidade
19985
200123

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

  1. Criar um CRUD completo para o model frases.
  2. Criar uma classe dusk que testa todas funcionalidades do CRUD frases
  3. Criar um comando, importarfrases, que importa o arquivo csv: frases
  4. Criar uma rota /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.

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.local

No /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=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.

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 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;
}

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 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

  1. 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;
  2. 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;
  3. Faça uma migration de alteração para adicionar o campo pontuação para a frase (entre 0 e 10 - validação com FormRequest);
  4. Faça um mutator converter a virgula, quando existir, para ponto.
  5. Corriga seus formulários para sempre conterem a função old()