PHP

Artigo 52 — Projeto Final Parte 3: Testes, Deploy e Encerramento Já leu

11 min de leitura

Artigo 52 — Projeto Final Parte 3: Testes, Deploy e Encerramento
Chegamos à última semana. O sistema está implementado. Agora fazemos o que distingue software amador de software profissional: cobrimos com testes,

Artigo 52 — Projeto Final Parte 3: Testes, Deploy e Encerramento

Módulo 11 · Semana 52 · Nível: Avançado


Introdução

Chegamos à última semana. O sistema está implementado. Agora fazemos o que distingue software amador de software profissional: cobrimos com testes, configuramos o deploy automatizado, e entregamos com confiança.

Esta semana é também um momento de reflexão. Um ano de estudo. Cinquenta e dois artigos. Doze módulos. Você chegou aqui.


Suite de testes completa

<?php
// tests/Unit/Domain/DinheiroTest.php

use App\Domain\Produto\Dinheiro;

describe('Dinheiro', function () {

    it('cria valor em centavos', function () {
        $d = new Dinheiro(2990);
        expect($d->centavos())->toBe(2990);
        expect($d->reais())->toBe(29.90);
        expect($d->formatar())->toBe('R$ 29,90');
    });

    it('cria a partir de reais', function () {
        $d = Dinheiro::dereais(19.90);
        expect($d->centavos())->toBe(1990);
    });

    it('soma corretamente', function () {
        $r = (new Dinheiro(1000))->somar(new Dinheiro(990));
        expect($r->centavos())->toBe(1990);
    });

    it('subtrai corretamente', function () {
        $r = (new Dinheiro(2000))->subtrair(new Dinheiro(500));
        expect($r->centavos())->toBe(1500);
    });

    it('lança exceção em subtração negativa', function () {
        expect(fn() => (new Dinheiro(100))->subtrair(new Dinheiro(200)))
            ->toThrow(\DomainException::class);
    });

    it('aplica desconto percentual', function () {
        $r = (new Dinheiro(10000))->aplicarDesconto(10.0);
        expect($r->centavos())->toBe(9000);
    });

    it('é imutável', function () {
        $original = new Dinheiro(1000);
        $original->somar(new Dinheiro(500));
        expect($original->centavos())->toBe(1000);
    });

})->group('domain');
<?php
// tests/Unit/Domain/CarrinhoTest.php

use App\Domain\Carrinho\{Carrinho, ItemDoCarrinho};
use App\Domain\Produto\Dinheiro;

describe('Carrinho', function () {

    it('começa vazio', function () {
        $carrinho = new Carrinho();
        expect($carrinho->estaVazio())->toBeTrue();
        expect($carrinho->total()->centavos())->toBe(0);
    });

    it('adiciona item novo', function () {
        $carrinho = new Carrinho();
        $carrinho->adicionar(item(produtoId: 1, preco: 1000, qtd: 2));

        expect($carrinho->estaVazio())->toBeFalse();
        expect($carrinho->total()->centavos())->toBe(2000);
    });

    it('incrementa quantidade ao adicionar item existente', function () {
        $carrinho = new Carrinho();
        $carrinho->adicionar(item(produtoId: 1, preco: 1000, qtd: 1));
        $carrinho->adicionar(item(produtoId: 1, preco: 1000, qtd: 2));

        expect($carrinho->itens()[1]->quantidade())->toBe(3);
        expect($carrinho->total()->centavos())->toBe(3000);
    });

    it('remove item ao atualizar com quantidade zero', function () {
        $carrinho = new Carrinho();
        $carrinho->adicionar(item(produtoId: 1, preco: 1000, qtd: 2));
        $carrinho->atualizar(1, 0);

        expect($carrinho->estaVazio())->toBeTrue();
    });

    it('calcula total com múltiplos itens', function () {
        $carrinho = new Carrinho();
        $carrinho->adicionar(item(produtoId: 1, preco: 1000, qtd: 2)); // R$ 20,00
        $carrinho->adicionar(item(produtoId: 2, preco: 2500, qtd: 1)); // R$ 25,00
        $carrinho->adicionar(item(produtoId: 3, preco: 500, qtd: 3));  // R$ 15,00

        expect($carrinho->total()->centavos())->toBe(6000); // R$ 60,00
    });

})->group('domain');

// Helper para criar itens nos testes
function item(int $produtoId, int $preco, int $qtd): ItemDoCarrinho
{
    return new ItemDoCarrinho($produtoId, 'Produto Teste', new Dinheiro($preco), $qtd);
}
<?php
// tests/Unit/Domain/StatusDoPedidoTest.php

use App\Domain\Pedido\StatusDoPedido;

describe('StatusDoPedido — transições', function () {

    it('pendente pode ir para confirmado ou cancelado', function () {
        $s = StatusDoPedido::Pendente;
        expect($s->podeTransicionarPara(StatusDoPedido::Confirmado))->toBeTrue();
        expect($s->podeTransicionarPara(StatusDoPedido::Cancelado))->toBeTrue();
        expect($s->podeTransicionarPara(StatusDoPedido::Enviado))->toBeFalse();
    });

    it('entregue é estado final', function () {
        $s = StatusDoPedido::Entregue;
        foreach (StatusDoPedido::cases() as $outro) {
            expect($s->podeTransicionarPara($outro))->toBeFalse();
        }
    });

    it('cancelado é estado final', function () {
        $s = StatusDoPedido::Cancelado;
        foreach (StatusDoPedido::cases() as $outro) {
            expect($s->podeTransicionarPara($outro))->toBeFalse();
        }
    });

})->group('domain');
<?php
// tests/Feature/CheckoutTest.php

use App\Models\{User, Produto, Categoria, Endereco};
use App\Domain\Pedido\StatusDoPedido;

describe('Checkout', function () {

    beforeEach(function () {
        $this->usuario  = User::factory()->create();
        $categoria = Categoria::factory()->create();
        $this->produto  = Produto::factory()->create([
            'categoria_id' => $categoria->id,
            'preco'        => 5000, // R$ 50,00
            'estoque'      => 10,
            'ativo'        => true,
        ]);
        $this->endereco = Endereco::factory()->create(['usuario_id' => $this->usuario->id]);
    });

    it('cria pedido a partir do carrinho', function () {
        // Adiciona item ao carrinho via sessão
        $this->actingAs($this->usuario)
            ->post(route('carrinho.adicionar'), [
                'produto_id' => $this->produto->id,
                'quantidade' => 2,
            ]);

        // Realiza checkout
        $response = $this->post(route('checkout.store'), [
            'endereco_id' => $this->endereco->id,
        ]);

        $response->assertRedirect();

        $this->assertDatabaseHas('pedidos', [
            'usuario_id' => $this->usuario->id,
            'status'     => StatusDoPedido::Pendente->value,
        ]);

        $this->assertDatabaseHas('pedido_itens', [
            'produto_id' => $this->produto->id,
            'quantidade' => 2,
        ]);

        // Estoque foi decrementado
        expect($this->produto->fresh()->estoque)->toBe(8);
    });

    it('rejeita checkout com carrinho vazio', function () {
        $response = $this->actingAs($this->usuario)
            ->post(route('checkout.store'), [
                'endereco_id' => $this->endereco->id,
            ]);

        $response->assertRedirect()->assertSessionHas('erro');
        $this->assertDatabaseCount('pedidos', 0);
    });

    it('rejeita checkout com estoque insuficiente', function () {
        $this->produto->update(['estoque' => 1]);

        $this->actingAs($this->usuario)
            ->post(route('carrinho.adicionar'), [
                'produto_id' => $this->produto->id,
                'quantidade' => 5,
            ]);

        $response = $this->actingAs($this->usuario)
            ->post(route('checkout.store'), ['endereco_id' => $this->endereco->id]);

        $response->assertRedirect()->assertSessionHas('erro');
        expect($this->produto->fresh()->estoque)->toBe(1); // não mudou
    });

})->group('feature');

Deploy final — configuração de produção

#!/bin/bash
# deploy-producao.sh

set -euo pipefail

RELEASES_DIR="/var/www/mercatto/releases"
SHARED_DIR="/var/www/mercatto/shared"
CURRENT_LINK="/var/www/mercatto/current"
REPO="git@github.com:empresa/mercatto.git"
BRANCH="${1:-main}"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
RELEASE_DIR="${RELEASES_DIR}/${TIMESTAMP}"

echo "🚀 Deploy Mercatto — release: $TIMESTAMP"

# 1. Cria diretório do release
mkdir -p "$RELEASE_DIR"
git clone --depth=1 --branch="$BRANCH" "$REPO" "$RELEASE_DIR"
echo "✅ Código clonado"

# 2. Symlinks para diretórios compartilhados entre releases
ln -sfn "$SHARED_DIR/.env"     "$RELEASE_DIR/.env"
ln -sfn "$SHARED_DIR/storage"  "$RELEASE_DIR/storage"
echo "✅ Symlinks de shared configurados"

# 3. Dependências
cd "$RELEASE_DIR"
composer install --no-dev --optimize-autoloader --no-interaction
echo "✅ Dependências instaladas"

# 4. Ativa manutenção no release atual (se existir)
if [ -L "$CURRENT_LINK" ]; then
    php "$CURRENT_LINK/artisan" down --secret="bypass-manutencao" 2>/dev/null || true
fi

# 5. Migrations
php artisan migrate --force
echo "✅ Migrations executadas"

# 6. Cache e otimizações
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
echo "✅ Cache otimizado"

# 7. Ativa o novo release
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
echo "✅ Release $TIMESTAMP ativado"

# 8. Reinicia serviços
php artisan queue:restart
sudo systemctl reload php8.3-fpm
echo "✅ Serviços reiniciados"

# 9. Desativa manutenção
php artisan up
echo "✅ Aplicação online"

# 10. Mantém apenas os últimos 5 releases
ls -dt "$RELEASES_DIR"/* | tail -n +6 | xargs rm -rf 2>/dev/null || true
echo "✅ Releases antigos removidos"

echo ""
echo "🎉 Deploy concluído — release: $TIMESTAMP"

Checklist de go-live

Antes de apontar o DNS para o servidor de produção, verifique cada item:

Segurança: - .env com APP_ENV=production, APP_DEBUG=false, e APP_KEY gerada - HTTPS configurado com certificado válido (Let's Encrypt) - Headers de segurança aplicados (CSP, HSTS, X-Frame-Options) - composer audit sem vulnerabilidades - Diretório storage/ e bootstrap/cache/ com permissões corretas (755, dono www-data) - Arquivo .env inacessível pelo servidor web

Performance: - OPcache ativado com validate_timestamps=0 - Redis configurado para sessões e cache - Workers de fila rodando com Supervisor - php artisan route:cache, config:cache, view:cache executados

Observabilidade: - Logs configurados (diário + Slack para críticos) - Monitoramento de uptime (UptimeRobot, Betterstack) - Alertas de erro (Sentry ou similar)

Backup: - Backup automático do banco configurado (diário, 7 dias de retenção) - Backup testado — restore funciona?

Testes: - Suite de testes passando (CI verde) - Smoke test manual no ambiente de staging


Reflexão final — o que você construiu

Cinquenta e dois artigos atrás, você aprendeu o que é o PHP e como exibir "Olá, Mundo!" no navegador. Hoje você sabe:

Fundamentos sólidos. Tipos, funções, arrays, strings, manipulação de datas, expressões regulares. A base que não muda independentemente do framework ou da versão da linguagem.

Orientação a objetos de verdade. Classes, herança, interfaces, traits, polimorfismo, design patterns. Não apenas a sintaxe — a filosofia de modelar o mundo em objetos com responsabilidades claras.

Bancos de dados. MySQL, PostgreSQL, SQLite e MongoDB. PDO com prepared statements, query builders, ORM com Eloquent, migrations, relacionamentos, índices e performance.

Desenvolvimento web completo. Formulários, sessões, cookies, upload de arquivos, autenticação, autorização. Os mecanismos que fazem a web funcionar.

PHP moderno. Composer, namespaces, PSR, boas práticas, tipagem estrita, análise estática com PHPStan.

Laravel profissional. Roteamento, controllers, Eloquent, Blade, autenticação, APIs com Sanctum, filas, eventos.

Segurança. SQL injection, XSS, CSRF, criptografia, headers HTTP, tokens seguros, LGPD.

Testes automatizados. PHPUnit e Pest, testes unitários, de integração e de feature, mocks, data providers, cobertura de código.

Deploy e produção. NGINX, PHP-FPM, OPcache, Docker, GitHub Actions, scripts de deploy com zero downtime, monitoramento, logs.

E o Mercatto — um sistema real, completo, com domínio modelado, arquitetura em camadas, testes, e pipeline de deploy.


O que vem depois

Dominar PHP em um ano é uma conquista real. Mas é também um começo. O ecossistema continua evoluindo, e há muito mais para explorar:

Aprofundar no Laravel. Livewire para interfaces reativas sem JavaScript pesado. Laravel Octane para performance extrema com Swoole. Laravel Telescope para debugging em desenvolvimento.

Explorar arquiteturas. Event Sourcing, CQRS, microsserviços. Quando aplicar cada padrão e quando não aplicar.

Outras linguagens. PHP é excelente, mas um desenvolvedor completo conhece mais de uma linguagem. TypeScript para o frontend, Go para serviços de alta performance, Python para análise de dados.

Contribuir com open source. O ecossistema PHP que você usou durante este ano foi construído por pessoas que dedicaram tempo. Reportar bugs, melhorar documentação, enviar pull requests — essas contribuições constroem reputação e aprofundam o aprendizado.


Última mensagem

Programar é uma habilidade acumulativa. Cada problema resolvido torna o próximo mais fácil. Cada sistema construído revela padrões que se repetem. Cada bug encontrado ensina algo que nenhuma aula ensinaria.

Você tem agora as ferramentas. O que você vai construir com elas?


Exercício final

Não há exercício técnico nesta semana. O exercício é este:

Implemente o Mercatto completo. Não apenas os trechos deste artigo — o sistema inteiro, funcionando, testado, deployado em um servidor real (mesmo que gratuito — Railway, Fly.io, ou um VPS básico). Com CI/CD configurado. Com HTTPS. Com monitoramento.

Quando estiver no ar, compartilhe o link e o repositório. O melhor portfólio é um sistema em produção.


Referências finais da série

  • PHP The Right Way: https://phptherightway.com/
  • Laravel Documentation: https://laravel.com/docs
  • PHP Manual (pt-BR): https://www.php.net/manual/pt_BR/
  • OWASP Top 10: https://owasp.org/www-project-top-ten/
  • Refactoring Guru: https://refactoring.guru/
  • Clean Code — Robert C. Martin (livro)
  • Domain-Driven Design — Eric Evans (livro)
  • Designing Data-Intensive Applications — Martin Kleppmann (livro)

🎓 Série completa

Dominando o PHP em 1 Ano — Prof. Ricardo Matos, Mestre em Programação

52 artigos · 11 módulos · 1 ano de estudo estruturado

Do "Olá, Mundo!" ao sistema em produção.


Fim da série. Agora é hora de gerar os HTMLs.

Quando quiser, me diga quais artigos quer converter primeiro — podemos fazer em lotes ou do 30 ao 52 em sequência.


you asked

Sim


claude response

Vamos começar pelos pendentes. O artigo 30 ficou sem HTML na sessão anterior, e temos os artigos 31 a 52 novos. Vou fazer em lotes para não cortar.

Qual ordem prefere?

Perfeito — começamos pelo artigo 30, um por vez. Deixa eu resgatar o conteúdo completo do artigo 30 do transcript antes de gerar o HTML.


claude response

O HTML do artigo 30 estava sendo gerado mas foi cortado. Encontrei o conteúdo completo no transcript. Vou agora gerar o HTML completo do artigo 30 e salvar corretamente.

Artigo 30 entregue — completo e sem cortes. Próximo: Artigo 31 — PostgreSQL com PHP. Continuo?


you asked

Continue


claude response

Buscando o conteúdo do artigo 32 no transcript antes de gerar o HTML.


you asked

Continue


you asked

Continue


claude response

Agora vou ler o artigo 32 do transcript para gerar o HTML do artigo 32.

Artigo 31 HTML já existe. Próximo é o Artigo 32 — SQLite com PHP. O texto completo está no transcript. Gerando o HTML agora.

Artigo 32 — SQLite com PHP ✅

Comentários

Mais em PHP

Artigo 49 — Performance e Otimização
Artigo 49 — Performance e Otimização

Performance não é premature optimization — é entender onde estão os gargalos...

Artigo 41 — Introdução ao Laravel
Artigo 41 — Introdução ao Laravel

Laravel é o framework PHP mais popular do mundo, e por boas razões. Ele não a...

ORM com Eloquent
ORM com Eloquent

ORM (Object-Relational Mapping) é a camada que traduz automaticamente entre o...