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

Tempo de leitura 8 minutos

O artigo MVC na prática (este artigo) é um artigo MUITO importante pra mim! Ao tempo que continua a série PHP sem frameworks, também complementa o primeiro artigo que escrevi aqui no blog, o PHP e MVC – Tudo o que você precisa saber (vale a pena você ler ele também).

Erik Figueiredo

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.

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

Tentarei ser mais prático neste artigo e mostrar como eu gosto de abordar o MVC na prática, claro, teremos um pouco de teoria, mas não é o foco aqui, para teoria, leia o outro artigo indicado acima.

O que é MVC?

Se você não leu o artigo que indiquei, bom, vou tentar resumir.

MVC é um padrão de arquitetura, isso quer dizer que ele foca em resolver problemas de… bem… arquitetura. Para ser mais direto, o foco de um padrão de arquitetura é resolver “problemas na organização” de um sistema e a relação dos “componentes” presentes nele (o sistema).

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, afinal você ainda vai precisar de lógica na camada de renderização.

O padrão MVC é separado em 3 camadas:

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

E o que mais?

Eu vou reforçar, para saber mais sobre MVC, por favor, leia este artigo:

PHP e MVC – Tudo o que você precisa saber.

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/

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.

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.

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