MVC na prática – PHP sem framework – parte 3

Tempo de leitura 11 minutos

Quando começamos a estudar sobre MVC no PHP descobrimos que não é bem preto no branco, existem muitos entendimentos e abordagens sobre o padrão de arquitetura mais conhecido da atualidade.

O que é MVC?

MVC é um padrão de arquitetura, isso quer dizer que ele foca em resolver problemas de… bem… arquitetura. Os architectural patterns (padrão de arquitetura em inglês) fornecem soluções para problemas conhecidos durante a etapa de arquitetura do software, em outras palavras, eles fornecem um padrão de trabalho (ou regras) que ajuda na hora de organizar (o termo correto seria arquitetar) seu projeto.

Um padrão de arquitetura é uma solução geral e reutilizável para um problema que ocorre com frequência em arquitetura de software dentro de um determinado contexto

https://pt.wikipedia.org/wiki/Padr%C3%A3o_de_arquitetura

O MVC, por sua vez, tem foco em separar a camada lógica (dados) da camada de exibição (renderização), note que “camada lógica” é diferente de lógica de programação ou de regras de negócio, afinal você ainda vai precisar de lógica de programação na camada de renderização (ou em qualquer lugar que o código exista).

Um dos focos (se não o principal) do MVC no PHP é eliminar aqueles projetos em que se vê SQL no meio do HTML.

Gostaria de lembrar que você não pode se esquecer de se cadastrar na newsletter para acompanhar o lançamento desta série.

MVC é um acrônimo de Model, View e Controller e cada letra corresponde a uma camada.

M de Model

A primeira camada é responsável pela parte lógica do software e isso quer dizer que ela é responsável pelos dados da aplicação e nada além disso.

Na maioria das implementações que vejo o model lida com o banco de dados, mas também poderia ser responsável por armazenar dados vindos de uma API ou de arquivos no disco local.

Claro que eu prefiro a abordagem do banco de dados e “jogar” leitura de API e arquivos para uma camada separada que grava no banco de dados, mas isso não é uma regra (só uma observação minha).

V de View

A segunda camada é responsável pela renderização dos dados e isso não quer dizer que ele vá fornecer apenas HTML, CSS e Javascript, mas JSON, XML, PDF, XLS, DOC ou qualquer formato que vá ser lido por algo (como em uma API, para outras aplicações) ou alguém (pessoas) é de responsabilidade da view.

C de Controller

A terceira camada é responsável por ditar o fluxo entre as duas camadas anteriores, o próprio nome (controlador) já é bem auto-explicativo.

Normalmente o controller é o que fica mais próximo da camada HTTP no MVC (em um contexto web), ele recebe as informações da requisição e decide quando o model vai buscar os dados e quando repassar para a view, que irá renderizar estas informações.

Resumindo o MVC no PHP

Para ficar claro agora para você e quando ser questionado não acabar confuso, lembre-se:

  • Model: Responsável pelos dados
  • View: Responsável pela camada de renderização
  • Controller: Responsável pela ordem de execução do M e do V

Qualquer coisa além disso deve ser encarado, no máximo, como um reforço ao padrão e nunca como regra absoluta, se reserve no direito de ser inteligente o suficiente para quebrar regras em prol de um aplicativo melhor.

MVC é o suficiente no desenvolvimento web com PHP?

Não! Mas é claro que não, um projeto não pode viver só de MVC e é aqui começam os problemas.

Muitos devs encaram o MVC como o alpha e o ômega de um projeto, tudo começa nele e tudo termina nele, isso é um erro.

Aonde você faz disparos de email? Aonde você faz a requisição para a API do Facebook para uma publicação que deverá ser postada automaticamente em uma página qualquer? Aonde você dispara dados para o Socket.io para ter um app real-time? Aonde você verifica as permissões do usuário? Aonde você verifica se o usuário já aceitou as novas politícas do site antes de renderizar uma página?

Tudo isso são incognitas que fizeram surgir teorias como as que ditam que o MVC vai morrer, quando a verdade é que nada disso é problema dele, o MVC não vai resolver tudo pra você, ele é a porta de entrada, a ponta do iceberg, só isso, lembre-se de seguir o princípio da responsabilidade única.

Responsabilidade única (Single Responsability Principle – SOLID)

Single Responsability Principle é uma das cinco regras do SOLID que te ajudam a ter um código SÓLIDO e mais fácil de ser reutilizado.

Ele diz que uma classe NÃO DEVE ter mais de uma razão de existência, se uma classe consulta os dados do banco, não deve renderizar nada, disparar email ou nada além de lidar com o banco de dados, simples assim.

Sempre se pergunte, pra que serve essa classe. Se tiver mais de uma resposta (ou uma separada por “e”) então sua classe está errada.

E para fechar, algumas afirmações que estão erradas sobre MVC e PHP e o porque estão erradas.

  • Não podemos ter estruturas de controle (if e switch, por exemplo) nas views e controllers, isso porque existe uma confusão com lógica de programação, regras de negócio e lógica de dados. São coisas diferentes!
  • O controller precisa ser menor que o model. Isso não tem nada haver, existem abstrações (como o Eloquent faz) onde é possível criar uma classe de model sem um único método ou atributo, como você faz um controller menor que isso?
  • Validação deve ficar em uma camada separada. Existem possibilidades e diferentes tipos de visões, não quer dizer que uma proposta é melhor ou a única verdadeira, pensar que você achou a ferramenta definitiva é que é ruim. Conheça as ferramentas para saber quando escolher.

E a prática do PHP + MVC?

Este artigo é o terceiro de uma série e vou recomendar que você leia o artigo anterior (link a seguir), nele eu falo como criar o sistema de URLs Amigáveis que vamos usar aqui, mas não é obrigatório, apenas estou recomendando.

URLs amigáveis com PHP e OO – PHP sem framework – parte 2

Se você optou por NÃO LER o artigo, aqui os arquivos para você tomar como base:

https://github.com/erikfig/php-do-zero/tree/63ff2631ad1a5ec5ceb503e805df102b7e3d09b9

O sistema de rotas que construímos no artigo acima trabalha armazenando as possíveis URLs que o usuário pode acessar no sistema e retorna algum valor para fazermos algo com ele, no exemplo do artigo eram funções anônimas que podiamos executar, vamos alterar para que possamos adicionar QUALQUER tipo de dado (neste exemplo eu quero poder retornar nomes de classes).

No arquivo src/Router.php remova a tipagem callable dos métodos.

Tipagem define o tipo de dado que poderemos usar, por exemplo, podemos informar string para qu somente textos possam ser usados, um exemplo:

<?php

function (string $soPodeStringAqui) {
    echo $soPodeStringAqui;
}

O resultado final da classe Router.

<?php

namespace ErikFig\Framework;

class Router
{
    private $routes = [];
    private $method;
    private $path;
    private $params;

    public function __construct($method, $path)
    {
        $this->method = $method;
        $this->path = $path;
    }

    // aqui tinhaum callable, antes do $action
    // remova dos outros métodos também
    public function get(string $route, $action)
    {
        $this->add('GET', $route, $action);
    }

    public function post(string $route, $action)
    {
        $this->add('POST', $route, $action);
    }

    public function add(string $method, string $route, $action)
    {
        $this->routes[$method][$route] = $action;
    }

    public function getParams()
    {
        return $this->params;
    }

    public function handler()
    {
        if (empty($this->routes[$this->method])) {
            return false;
        }

        if (isset($this->routes[$this->method][$this->path])) {
            return $this->routes[$this->method][$this->path];
        }

        foreach ($this->routes[$this->method] as $route => $action) {
            $result = $this->checkUrl($route, $this->path);
            if ($result >= 1) {
                return $action;
            }
        }

        return false;
    }

    private function checkUrl(string $route, $path)
    {
        preg_match_all('/\{([^\}]*)\}/', $route, $variables);

        $regex = str_replace('/', '\/', $route);

        foreach ($variables[0] as $k => $variable) {
            $replacement = '([a-zA-Z0-9\-\_\ ]+)';
            $regex = str_replace($variable, $replacement, $regex);
        }

        $regex = preg_replace('/{([a-zA-Z]+)}/', '([a-zA-Z0-9+])', $regex);
        $result = preg_match('/^' . $regex . '$/', $path, $params);
        $this->params = $params;

        return $result;
    }
}

Note os comentários no arquivo.

Para usar este sistema de rotas apenas criamos uma instância da classe ErikFig\Framework\Router informando a url acessada e o método HTTP (GET, POST…).

// defino o método http e a url amigável
$method = $_SERVER['REQUEST_METHOD'];
$path = $_SERVER['PATH_INFO'] ?? '/';

// instancio o Router
$router = new ErikFig\Framework\Router($method, $path);

Em seguida registramos as rotas e o que acontece quando elas forem acessadas:

$router->get('/', function () {
    return 'Olá mundo';
});

E por fim executamos:

// encontra a rota
$result = $router->handler();

// executa a função e imprime o resultado
echo $result();

O exemplo imprime um Olá mundo no navegador.

Aqui tem um exemplo completo:

https://github.com/erikfig/php-do-zero/blob/63ff2631ad1a5ec5ceb503e805df102b7e3d09b9/bootstrap.php

Para testar você acessa a url do servidor seguido de index.php/, sendo que / é a URL registrada no segundo parâmetro $router->get('/AQUI', function (), em um servidor corretamente configurado você pode omitir o index.php.

Com isso entendido, vamos configurar o sistema para ler uma classe de controller.

Se não entendeu, por favor, volte no link que indiquei acima, para ver um artigo completo sobre este sistema de rotas.

Controller

Vamos pegar o exemplo completo e que eu já citei acima e partir daquele ponto:

https://github.com/erikfig/php-do-zero/tree/63ff2631ad1a5ec5ceb503e805df102b7e3d09b9

Com as alterações que fizemos no Router, eu preciso:

  • Alterar também o arquivo bootstrap.php que está executando apenas closure.
  • Acrescentar as novas classes de controller

Como disse acima, eu quero ser capaz de informar classes e métodos de forma prática, de acordo com o exemplo abaixo que substitui as linhas 17 até 19 para:

$router->get('/ola-{nome}', 'App\Controllers\HomeController::hello');

Vamos precisar registrar um novo vendor name de namespace chamado App no Composer, é bem simples, apenas adicione o novo ao composer.json aonde eu marquei com <- aqui:

{
    "name": "erikfig/php-do-zero",
    "authors": [
        {
            "name": "Erik Figueiredo",
            "email": "erik.figueiredo@gmail.com"
        }
    ],
    "autoload": {
        "psr-4": {
            "App\\": "app", <- aqui
            "ErikFig\\Framework\\": "src"
        }
    },
    "require": {
        "phpunit/phpunit": "^8.3"
    }
}

Por favor, não esqueça de remover o <- aqui, e rode composer dump para atualizar.

Crie um diretório app no mesmo nível do composer.json, dentro dele um Controllers e dentro deste último um arquivo chamado HomeController.php com o seguinte conteúdo:

<?php

namespace App\Controllers;

class HomeController
{
    public function hello($params)
    {
        return "Olá {$params[1]}";
    }
}

Agora podemos organizar nossas rotas “por assunto”.

Imagine um UsuariosController para incluir, ver, lista, atualizar e remover (CRUD) usuários, fica tudo organizado em um mesmo local.

Agora no bootstrap.php vamos fazer algumas alterações para que essa classe seja executada, eu já disse acima sobre a linha 17 até a 19, se você não fez, faça a edição.

Também altere o final, aonde está:

// imprimo a página atual
echo $result($router->getParams());

Altera para:

// verifico se é uma função anônima
if ($result instanceof Closure) {
    // imprimo a página atual
    echo $result($router->getParams());

// se não for uma função anônima e for uma string
} elseif (is_string($result)) {
    // eu quebro a string nos dois-pontos, dois::pontos
    // transformando em array
    $result = explode('::', $result);

    // instancio o controller
    $controller = new $result[0];
    // guardo o método a ser executado (em um controller ele se chama action)
    $action = $result[1];

    // finalmente executo o método da classe
    echo $controller->$action($router->getParams());
}

Viu como foi simples a camada de controller.

Model

A camada de model também vai ser simples, na verdade eu vou me aproveitar de um recurso que já existe chamado Eloquent.

Para instalar o Eloquent no projeto rode o seguinte comando na raiz do projeto:

composer require illuminate/database

Na sequência, crie um arquivo chamado database.php, eu até poderia fazer isso direto no bootstrap.php, mas aqui vai ficar mais organizado.

Carregue o arquivo database.php no bootstrap.php após a linha require __DIR__ . '/vendor/autoload.php';

require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/database.php';

Agora adicione o seguinte conteúdo ao novo arquivo:

<?php

use Illuminate\Database\Capsule\Manager;

$capsule = new Manager;

$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'blog_php_sem_framework',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
]);

$capsule->setAsGlobal();
$capsule->bootEloquent();

Pronto, conectado ao banco de dados, não esqueça de alterar host, username e password para acessar o seu banco de dados.

Claro que vamos precisar de um banco de dados, aqui tem um SQL que cria um, adiciona uma tabela e preenche com informações falsas.

CREATE DATABASE `blog_php_sem_framework`;

USE `blog_php_sem_framework`;

CREATE TABLE IF NOT EXISTS `users` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(250) NOT NULL,
  `email` VARCHAR(250) NOT NULL,
  `password` VARCHAR(100) NOT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB;

INSERT INTO `users` (`name`, `email`, `password`) VALUES
  ("Erik", "erik@erik.com", "secret"),
  ("Erik 2", "erik2@erik.com", "12345678"),
  ("Erik 3", "erik3@erik.com", "654321"),
  ("Erik 4", "erik4@erik.com", "qwe123");

Agora vamos criar nosso primeiro model.

Dentro de app crie um diretório Models e dentro um arquivo chamado User.php, com o seguinte conteúdo:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    //
}

Por convenção o Eloquent vai entender que a classe User se refere a tabela users do banco, mas você sempre pode informar o nome da tabela usando a propriedade $table.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $table = "nome_da_tabela";
}

Mas não é o nosso caso, no SQL que eu informi a tabela já se chama users.

Agora vamos criar uma nova action no controller HomeController para listar os usuários:

<?php

namespace App\Controllers;

use App\Models\User;

class HomeController
{
    public function hello($params)
    {
        return "Olá {$params[1]}";
    }

    // aqui
    public function listUsers()
    {
        return User::all();
    }
}

E vamos registrar uma rota para isso:

$router->get('/users', 'App\Controllers\HomeController::listUsers');

Quando você acessar a url http://endereco-do-servidor/index.php/users vai ver os dados do banco de dados. Bem simples e eficiente.

O Eloquent já converte os dados para JSON quando você imprime eles como string sem a necessidade de uma camada de view. Como eu disse, existem muitas abordagens para o MVC, essa é apenas uma delas.

Se quiser aprender mais sobre o Eloquent a documentação é uma excelente fonte (embora esteja em inglês):

https://laravel.com/docs/6.x/eloquent

Viu? Nada de reinventar a roda, vamos usar algo que funciona e tem MUITOS recursos disponíveis.

View

A última camada que vamos trabalhar, agora vamos renderizar HTML utilizando uma biblioteca simples e poderosa, o Twig.

Comece instalando a biblioteca com o comando composer require "twig/twig:^3.0" e criando um arquivo renderer.php na raiz do projeto com o seguinte conteúdo:

<?php

// aqui o local em que ficarão os templates no caso,
// um diretório chamado templates, no mesmo nível do renderer.php
$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
// eu comentei a linha sobre o cache, se qusier usar,
// basta criar o diretório cache
// recomendo que use
$twig = new \Twig\Environment($loader, [
    // 'cache' => __DIR__ . '/cache',
]);

return $twig;

E no bootstrap.php, logo após o bloco que verifica se encontrou algo:

if (!$result) {
    http_response_code(404);
    echo 'Página não encontrada!';
    die();
}

Carregue o arquivo armazenando o valor do return em uma variável:


if (!$result) {
    http_response_code(404);
    echo 'Página não encontrada!';
    die();
}

// a nova linha
$twig = require(__DIR__ . '/renderer.php');

Além disso, como bem observado pelo Renan Medeiros nos comentários, eu esqueci de alterar o bootstrap.php, na linha a seguir:

$controller = new $result[0];

Alterar para:

$controller = new $result[0]($twig);

Desta forma fazemos a injeção do Twig para ser usado no controller.

Obrigado Renan!

No HomeController.php, adicione uma variável de instância e um construtor injetando o twig, assim:

<?php

namespace App\Controllers;

use App\Models\User;
use Twig\Environment;

class HomeController
{
    private $twig;

    public function __construct(Environment $twig)
    {
        $this->twig = $twig;
    }

// ... restante do arquivo

E finalmente o Twig está disponível, basta você informar o local do template (no diretório templates que configuramos no renderer.php) e um array com os valores que ficarão disponívels para renderização (a lista de usuários, por exemplo).

Essa ideia de informar quais valores poderão ser usados no template é muito importante, não queremos disponibilizar qualquer coisa e acabar cometendo um erro (de segurança ou outros), por exemplo.

Aqui o novo action listUsers:

    public function listUsers()
    {
        return $this->twig->render('users/index.html', ['users' => User::all()]);
    }

Agora crie um arquivo a partir da raiz do projeto, em templates/users/index.html com o seguinte conteúdo:

<h1>Usuários</h1>

<ul>
{% for user in users %}
    <li>{{ user.name }}</li>
{% endfor %}
</ul>

O Twig é um poderoso template engine, MUITO completo e simples, se quiser saber mais sobre ele:

https://twig.symfony.com/

PHP além do MVC

Não posso dizer que minha forma de trabalhar é a melhor do mundo, mas funciona muito bem pra mim, quem sabe ajude você.

Além do MVC eu gosto de trabalhar com:

  • Middlewares
  • Events e Listeners
  • Modularização

Cada uma destas camadas são responsáveis por alguma coisa, claro que cada um deles é assunto para outro dia, mas vou deixar um pequeno resumo aqui.

Middlewares são responsáveis por interceptar as requisições e empilhar processos, a própria rota (que define, na maioria das vezes, qual controller será usado) é um middleware por definição.

É no middleware que eu, normalmente, dito o que deverá acontecer antes ou depois do controller ser executado, por exemplo, se o usuário não está autenticado eu devo parar a execução no middleware ANTES dele chegar no controller ou até posso gerar um log sobre o que aconteceu durante a exeução da aplicação após a execução do MVC.

Events e listeners são um show a parte, de longe a camada mais importante depois do MVC, normalmente os eventos são disparados de dentro do MVC e em cada um eu posso anexar ou desanexar QUANTOS listeners eu quiser e estes listeners são classes isoladas que fazem qualquer coisa, como enviar emails, consultar APIS (lembra da postagem no Facebook que eu disse acima, faria aqui), fazer pagamentos de cartão.

Com um bom sistema de events e listeners eu poderia até jogar as tarefas numa fila em background e o usuário nem precisa esperar isso acontecer, em outras palavras, aplicações mais organizadas e performáticas.

Modularização é onde eu agrupo responsabilidades específicas para poder reaproveitar, um sistema de autenticação com lembrar de mim, login social, oauth2 e crud de usuários com relatórios seria um módulo pronto e separado, facil de instalar (poucos passos) e de mantém (manutenção centralizada com versionamento semântico).

Conclusão

Trabalhar com MVC é muito prático e facilita a organização, se engana quem pensa que é complicado, existe uma estrutura e regras a serem seguidas que facilitam MUITO o processo todo, além disso, novas camadas e padrões são SEMPRE bem vindos, desde que somem qualidade ao projeto.

NUNCA ESTEJA SATISFEITO COM SEU CÓDIGO, sempre busque melhorar.

Aqui os arquivos finais deste artigo:

https://github.com/erikfig/php-do-zero/tree/01e9e086ce202eb4007923a8f224af61a8b0f23b

Espero ter ajudado.

Mais artigos desta série

Artigo anterior

Próximo artigo

Todos os artigos

Autor: Erik Figueiredo

Músico, gamer amador, tutor de programação, desenvolvedor freelancer full cycle, com foco em PHP (Laravel e CakePHP), Javascript (Front e Node.js), Dart (Front e Flutter) e infra.

5 comentários em “MVC na prática – PHP sem framework – parte 3”

  1. Olá Erik!!

    Primeiramente agradeço o conteúdo compartilhado!! Estou achando muito legal essa sua série de artigos.
    Só uma questão que eu acho que faltou no artigo e eu acabei resolvendo pegando diretamente do seu github:
    Faltou informar que na linha 46 do arquivo bootstrap.php tem que alterar “$controller = new $result[0];” para “$controller = new $result[0]($twig);”

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *