Set up rápido do PHP, PHPUnit e Docker

Read in English

Neste texto eu vou lhe mostrar alguns snippets do meu setup básico pra iniciar aplicações PHP.

Meu maior objetivo aqui é que você marque este post nos seus favoritos para que possa voltar, copiar e colar as coisas daqui sempre que precisar criar ou alterar suas aplicações php. 😉

A coisa legal de usar este setup é que você pode facilmente trocar as versões das imagens sem precisar configurar um montante de coisas de uma vez.

Então...

Antes de começar: tenha certeza de que você possui docker e docker-compose instalados.

O resultado final

Se você seguir este tutorial, será capaz de executar diferentes serviços através do comando docker-compose.

Você encontra o resultado final no repositório público.

A maior ideia é que cada serviço pode ou não se tornar um comando. E o formato se parece com o seguinte:

$ docker-compose run <comando> [--args]

Rodar uma suíte de testes, por exemplo, poderia se parecer com isso:

$ docker-compose run tests

Pra tornar a digitação mais simples, podemos também adicionar um alias para o comando docker-compose run. Vou chamar de dcr aqui:

$ alias dcr='docker-compose run'
$ dcr lol
ERROR: Can't find a suitable
configuration file in this
directory or any parent.
Are you in the right directory?

Supported filenames:
docker-compose.yml,
docker-compose.yaml

Alias criado! O programa ainda vai reclamar porque não existe um arquivo docker-compose ainda. Bora criar então!

Um docker-compose básico

Então a gente vai criar um projeto do zero, huh? Bora lá! Comece criando a pastsa do projeto e mais tarde criando o arquivo docker-compose.yml:

$ mkdir meu-projeto
$ cd meu-projeto
$ touch docker-compose.yml

Eu vou criar as pastas comuns que normalmente minhas aplicações têm. Vai incluir pastas como source, testes e binários.

Apenas execute o seguinte:

$ mkdir -p src/ tests/ bin/ \
  .conf/nginx/ var/

Agora podemos começar a trabalhar com o nosso docker-compose.yml. Ele deverá conter todas dependências que o nosso projeto teria.

O conteúdo inicial no nosso docker-compose será bem simples. Apenas escreva o seguinte:

# docker-compose.yml
version: '3'
services:

A gente vai escrever os serviços já agora! O mais essencial de todos, como deveria ser, é o composer.

Adicionando composer no docker-compose

Provavelmente usaremos o php de dentro do container. Então não faz sentido rodar o composer fora de um container, já que as versões do php podem divergir.

Vamos então adicionar um serviço composer ao nosso arquivo:

# docker-compose.yml
version: '3'
services:
  composer:
    image: composer:1.9.3
    environment:
      - COMPOSER_CACHE_DIR=/app/var/cache/composer
    volumes:
      - .:/app
    restart: never

O snippet acima vai criar um serviço composer, que mapeia o diretório atual para /app dentro do container.

Definir a variável de ambiente COMPOSER_CACHE_DIR com o valor /app/var/cache/composer fará com que o composer escreva o cache na máquina local em vez de somente dentro do container. Isto irá previnir que o composer baixe todas dependências a cada execução.

Então é bom tomar conta de que a pasta var/ nunca vá parar no seu GIT, hein!

Só pra não esquecermos, vamos ignorar os arquivos relacionados ao composer já agora. Apenas rode os seguintes comandos pra evitar commitar esses caras:

$ echo 'vendor/' >> .gitignore
$ echo 'var/' >> .gitignore

Perfeito! Agora com o composer em mãos nós estamos preparados para instalar a dependência mais importante de todo proejto!

Preparando o PHPUnit

A dependência mais importante deste skeleton app é o motor de testes, é claro!

Vamos instalar o phpunit a partir do nosso serviço composer:

$ dcr composer require --dev \
  phpunit/phpunit

Não precisa adicionar a barra invertida. Eu só coloquei alí para que fique legível em telas pequenas 😬

As dependência devem estar sendo baixadas, e os arquivos composer.json e composer.lock devem ter aparecido no seu diretório local. Ah, e tem uma pasta vendor/ também.

Parece que rolou...

Bora então cirar um serviço php simplão pra rodar coisa de cli. A gente vai usar a imagem oficial do php para cli pra isso. E quanto mais chique melhor, vamo fazer com o php 7.4! 🔥

Preparando uma cli PHP

Vamos usar a imagem php:7.4-cli pra isso.

Vamos também mapear os volumes da mesma forma que fizemos com o composer. Pode ser útil no futuro.

# docker-compose.yml
version: '3'
services:
  composer:
    image: composer:1.9.3
    environment:
      - COMPOSER_CACHE_DIR=/app/.cache/composer
    volumes:
      - .:/app
    restart: never
  # NOVO AQUI
  php:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app

Aqui a gente também colocou o working dir com o valor /app. Então sempre que rodarmos dcr php ele irá executar como se /app fosse o caminho inicial de execução.

Tá pensando como vamos rodar os testes, certo?

Siligaaqui!

Rodando PHPUnit dentro do container

Rodar PHPUnit deveria ser tão simples quanto rodar um comando de cli. Já que ele é um comando de cli...

O seguinte, portanto, funciona bem:

$ dcr php vendor/bin/phpunit

Você pode usar <TAB> para auto completar normalmente 😉

Parece bem chatão escrever tudo isso aí cada vez mais. Dá pra simplificar?

Sim!

Vamos adicionar um serviço phpunit para o nosso docker-compose.yml:

# docker-compose.yml
version: '3'
services:
  composer:
    image: composer:1.9.3
    environment:
      - COMPOSER_CACHE_DIR=/app/.cache/composer
    volumes:
      - .:/app
    restart: never
  php:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
  # NOVO AQUI
  phpunit:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
    entrypoint: vendor/bin/phpunit

O truque aqui tá no entrypoint! Agora em seu terminal você pode executar o seguinte:

$ dcr phpunit --version
PHPUnit 9.0.1 by Sebastian
Bergmann and contributors.

Aooo! Que lindeza!

A gente, aliás, gerar o nosso phpunit.xml antes de pular pro próximo passo.

Assim ó:

$ dcr phpunit \
  --generate-configuration

Esse comando vai te perguntar algumas coisas. Apenas pressione enter pra tudo e tá de boa...

Criando um teste simples

Só pra ter certeza de que as coisas tão rodando né.

$ touch tests/MyTest.php

E dentro de tests/MyTest.php adicione o seguinte:

# tests/MyTest.php
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
  public function testMyTest(): void
  {
    self::assertTrue(false);
  }
}

Funciona perfeitamente! E o teste também está falhando... Tu pode consertar depois, relaxe!

Agora que conseguimos rodar os nossos testes, podemos pensar em construir a aplicação em si.

Provavelmente você quer criar uma aplicação web, sim? Então vamos fazer algo com o nginx e php-fpm!!

Configurando o Web Server

Para configurar o php fpm, precisaremos de dois serviços diferentes. Um será o servidor HTTP e o outro será a instância FPM.

Como estes são processos de longa execução, a gente não vai usar o docker-compose run com eles. Em vez disso, usemos o up -d.

O comando final vai parecer com o seguinte:

$ docker-compose up -d fpm nginx

Vamos adicionar o PHP-FPM na bagaça:

# docker-compose.yml
version: '3'
services:
  composer:
    image: composer:1.9.3
    environment:
      - COMPOSER_CACHE_DIR=/app/.cache/composer
    volumes:
      - .:/app
    restart: never
  php:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
  phpunit:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
    entrypoint: vendor/bin/phpunit
  # NOVO AQUI
  fpm:
    image: php:7.4-fpm
    restart: always
    volumes:
      - .:/app

Simplasso! Ao rodar docker-compose up -d fpm ele deveria rodar e ficar no background já.

Agora vamos configurar a parte do nginx que vai expor a porta 8080 e tratar as requests ao php envinando para a porta 9000 do fpm.

O arquivo docker-compose.yml vai ficar assim:

# docker-compose.yml
version: '3'
services:
  composer:
    image: composer:1.9.3
    environment:
      - COMPOSER_CACHE_DIR=/app/.cache/composer
    volumes:
      - .:/app
    restart: never
  php:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
  phpunit:
    image: php:7.4-cli
    restart: never
    volumes:
      - .:/app
    working_dir: /app
    entrypoint: vendor/bin/phpunit
  fpm:
    image: php:7.4-fpm
    restart: always
    volumes:
      - .:/app
  # NOVO AQUI
  nginx:
    image: nginx:1.17.8-alpine
    ports:
      - 8080:80
    volumes:
      - .:/app
      - ./var/log/nginx:/var/log/nginx
      - .conf/nginx/site.conf:/etc/nginx/conf.d/default.conf

Com isto nós expomos a porta 8080 como sendo a porta 80 do container (porta padrão do http).

Também ligamos o nosso diretório atual para /app. Normalmente as pessoas fazem /var/www, mas eu gosto de deixar as coisas consistentes em comparação com os outros serviços.

O diretório local var/log/nginx foi conectado ao /var/log/nginx do container. Desta forma a gente não fica cego quando precisar checar os logs de acesso ou erros.

Por último, mas não menos importante, o site.conf foi introduzido ao container com o nome default.conf. Esta é só uma maneira rápida de fazer com que o nginx aceite a nossa configuração.

A gente precisa criar o nosso arquivo de configuração. Façamos então!

$ touch .conf/nginx/site.conf

Escreva o seguinte arquivo de configuração no caminho .conf/nginx/site.conf:

# .conf/nginx/site.conf
server {
  listen 80;
  listen [::]:80;

  root /app/public;
  index index.php;

  location / {
      try_files $uri $uri/ /index.php$is_args$args;
  }

  location ~ .php$ {
      try_files $uri =404;
      fastcgi_split_path_info ^(.+.php)(/.+)$;
      fastcgi_pass fpm:9000;
      fastcgi_index index.php;
      include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
  }

  access_log /var/log/nginx/myapp_access.log;
  error_log /var/log/nginx/myapp_error.log;
}

Repare como root está apontando para /app/public. Isto fará com que a pasta public/ seja o ponto de início para toda requisição que o nginx gerenciar.

Repare também no fastcgi_pass e veja que está apontando para fpm:9000. Esta é a nossa imagem fpm. Se você a deu um nome diferente, ajuste essa linha aqui também!

Pra testar isso tudo, vamos criar um simples index.php dentro da pasta /public. Este arquivo vai servir como o ponto de partida da nossa aplicação.

Apenas adicione uma chamada ao phpinfo neste arquivo:

# public/index.php
<?php

phpinfo();

Agora vamos levantar o servidor nginx:

$ docker-compose up -d nginx

A partir deste momento você poderá acessar http://localhost:8080/ a partir do seu navegador normalmente.

Não esqueça o autoloader

Nós instalamos o composer corretamente, mas usar as nossas classes ainda não está perfeito.

Vamos ajustar o nosso composer.json para que o composer possa saber de onde carregar nossas classes:

# composer.json
{
  "require-dev": {
    "phpunit/phpunit": "^9.0"
  },
  "autoload": {
    "psr-4": {
      "ThePHPWebsite\\": "src/"
    }
  }
}

Agora rode um composer dump:

$ dcr composer -- dump
Generated autoload files
containing 646 classes

Este -- antes do comando apenas faz com que o docker-compose não pense que dump é um serviço em vez de parâmetro ao nosso comando do composer.

Pra testar este pedacinho, criemos um arquivo chamado App.php dentro de src/:

# src/App.php
<?php

declare(strict_types=1);

namespace ThePHPWebsite;

class App
{
  public function sayHello(): void
  {
    echo 'Hello!';
  }
}

E agora apenas modifique o arquivo public/index.php para usar a nossa classe:

<?php

require_once __DIR__
  . '/../vendor/autoload.php';

use ThePHPWebsite\App;

$app = new App();

$app->sayHello();

Recarregue a página no navegador e XABLAU! "Hello!" tá lá, chapa!


Buum! Isso é tudo! Um guia massa de como montar um ambiente local de desenvolvimento pra aplicações php usando docker compose e podendo rodar testes com o phpunit.

Você provavelmente gostará também de adicionar outros serviços como bancos de dados, servidores de filas, um servidor Solr...

Vai na fé e adiciona eles aí, agora tu não precisa mais bagunçar seu ambiente todo pra mexer com diferentes serviços.

Se em algum momento você entender que precisa de alguma coisa MUITO específica, como uma extensão do php ou coisa do gênero, basta criar um Dockerfile customizado e referenciá-lo no docker-compose.yml.

Não se esqueça de compartilhar com seus(uas) amigos(as) preguiçosos(as) sempre que começarem a reclamar do processo de criar um projeto base com PHP.

E também sinta-se livre pra me dar um alô se você teve algum problema durante este tutorial.

Valeus!