PHP para Iniciantes - Classes e Objetos - Autoloading

Se eu pudesse resumir esse artigo em poucas palavras, diria: uma das melhores coisas já desenvolvidas no PHP. Isso para quem segue as boas práticas de programação orientada a objeto.

Como assim, Kiko?

Se você programa seguindo a POO, você certamente vai transcrever várias abstrações de domínio da sua aplicação em classes. Por exemplo:

<?php

class Evento {
    public function __construct(
        public int $idEvento,
        public string $titulo,
        public string $detalhes,
        public string $dataInicio,
        public string $dataFim,
    ) { }
}
<?php

class Ingresso {
    public function __construct(
        public int $idIngresso,
        public int $idEvento,
        public int $idUsuario,
        public int $valorCentavos,
        public bool $foiPago,
    ) { }
}

Considerando essas duas classes hipotéticas, não faz sentido escrever as duas no mesmo arquivo, certo? E até aqui, nós chamamos as duas em um terceiro arquivo usando as operações de inclusão via include(), include_once(), require() ou require_once(), confere?

<?php

include_once __DIR__ . "/Evento.php";
include_once __DIR__ . "/Ingresso.php";

// faz o que quiser

Não tem nenhum problema em construir um sistema inteiro dessa forma. Você só vai precisar criar um arquivo exclusivo para inicialização, comumente chamado de bootstrap, onde você centralizaria toda essa chamada de includes e requires.

Isso só tem um grande problema: performance.

Você estará incluindo em todas as requisições do seu servidor, por exemplo, todas as classes que você já desenvolveu, incluindo aquelas que não irá utilizar. Isso faz sentido pra você?

Ah, Kiko, então ao invés de centralizar os includes e requires no bootstrap, eu tenho de fazer onde for precisar?

Também poderia fazer isso, mas é bem mais difícil de controlar as dependências de cada fluxo. Começar estruturando o projeto dessa forma é o jeito ideal de destruir esperanças de refatoração. Hahahah

Ah, Kiko, e se eu fizesse uma classe para tratar importações dinâmicas?

Até poderia dar certo. Eu cheguei a fazer isso no framework da primeira agência onde trabalhei. A ideia inicial era de modularizar a aplicação (que nem classes tinha antes) de forma a reduzir as importações. Assim, você usava a classe para importar módulos inteiros, não somente classes.

Funcionou bem, mas o problema resolvido não era exatamente o mesmo do proposto por aqui. Naquela época eu não conhecia o autoloading do PHP, caso contrário, não teria perdido meu tempo fazendo isso, hahaha.

Tá bom, Kiko, aceitei o fato de que tem alguma coisa melhor. Mostra isso logo!

OK, ok! O que você precisa é de apenas uma função!!

spl_autoload_register()

Com essa função, é possível ensinar ao PHP "o que fazer quando uma classe ou interface não-definida for acionada?". E é basicamente isso. Então você pode centralizar as suas classes em uma pasta e simplesmente ensinar o PHP a importar as classes a partir daquele diretório.

Por exemplo, digamos que você tenha um sistema de arquivos assim:

  • /index.php: seu arquivo que é sempre executado em todas as rotas (seja via .htaccess, configuração no NGINX, no Apache, sei lá, mas você manda tudo passar por lá);
  • /bootstrap.php: seu arquivo de inicialização, para centralizar coisas obrigatórias e onde iremos colocar o spl_autoload_register();
  • /classes/Evento.php: aquela classe de evento que mencionei lá em cima;
  • /classes/Ingresso.php: aquela outra classe (...);
  • etc (outros arquivos irrelevantes no momento).

Note que já temos duas classes dentro da pasta /classes e que o arquivo de inicialização (que chama o spl_autoload_register) está logo acima dela. Portanto, a chamada pode ficar assim:

spl_autoload_register(function (string $className) {
    $slash = DIRECTORY_SEPARATOR; // isso aqui é a barra "/" de forma amigável para todos os sistemas operacionais
    $basePath = "{$slash}classes{$slash}";
    include_once __DIR__ . "{$basePath}{$className}.php";
});

Assim, quando o você chamar new Evento() em alguma parte do fluxo, o interpretador vai:

  1. verificar se Evento é uma classe ou interface definida;
  2. não sendo, irá verificar se nós definimos alguma instrução para tentar incluir uma classe indefinida;
  3. como nós definimos, irá acionar a função que passamos como instrução, introduzindo nos argumentos o nome do que estamos tentando acionar: "Evento";
  4. dentro da função, irá chamar include_once __DIR__ "/classes/Evento.php", que irá importar aquele arquivo que define a classe Evento;
  5. por fim, irá tentar executar o new Evento(), com sucesso.

Nas próximas vezes que alguma coisa chamar a classe no mesmo fluxo, não precisará importar novamente pois a classe já está definida (saltando do passo 1 ao 5).

Uaaaauuuuuu

E isso não é tudo!

Se você usar namespaces, poderá deixar tudo ainda mais organizado, colocando o caminho do namespace em forma de pastas. Tudo o que precisaria mudar é colocar uma tratativa de barras, pois o namespace vem todo bugado para dentro da instrução.

No caso, ficaria algo assim:

spl_autoload_register(function (string $className) {
    $slash = DIRECTORY_SEPARATOR; // isso aqui é a barra "/" de forma amigável para todos os sistemas operacionais
    $basePath = "{$slash}classes{$slash}";
    $className = str_replace("\\", $slash, $className);
    include_once __DIR__ . "{$basePath}{$className}.php";
});

Então, se você armazena a classe Evento na pasta classes/Entidades e atribuindo o devido namespace (namespace Entidades;) antes da declaração da classe, ao chamar new Entidades\Evento(), o PHP iria rodar a instrução include_once __DIR__ . "/classes/Entidades/Evento.php" no passo 4.

Ok, Kiko, e qual é o real benefício disso?

Primeiramente, você ensina ao PHP sistematicamente onde encontrar classes indefinidas no seu sistema de arquivos. Isso automaticamente retirará a obrigação de sair importando todas as classes, mesmo onde não precisa. Deixa que o PHP encontre-as!

E, consequentemente, seus fluxos só importarão as classes que precisam. Maravilhoso, não é mesmo? E você pode ir muito além, criando uma função mais complexa para estruturar tudo o que for necessário.

É desse jeito que funciona o Composer. Exceto que você, como desenvolvedor, só precisa configurar o composer.json para mapear namespaces fictícios.

Por exemplo, no Laravel, todas as classes que são colocadas dentro da pasta app/ são acionáveis pelo namespace App. Se você olhar no composer.json do Laravel, vai encontrar isso aqui:

    // ...
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        }
    },
    // ...

Esse trecho das configurações diz, basicamente, o seguinte:

  1. Se a classe começar com o namespace App\\, retire esse namespace e procure o resto da instrução dentro da pasta app/ (a partir da raiz do projeto);
  2. Se a classe começar com o namespace Database\\Factories\\ (...) dentro da pasta database/factories/;
  3. Se a classe (...) Database\\Seeders\\ (...) database/seeders/.

E assim, ao compilar as dependências usando o composer, um arquivo grande de autoload será gerado dentro da pasta vendor/. Se você abrir, encontrará uma implementação bem genérica do spl_autoload_register, que é o que faz a mágica acontecer para gente.

Mindblow

Você sabia disso?? Hehehe, na época que descobri eu fiquei super fascinado por esse rolê, espero que tenha transmitido o mesmo sentimento pra você! E se curtiu, lembra de curtir e compartilhar, hein?! No próximo artigo voltaremos falando sobre Construtores e Destrutores, onde falaremos sobre o ciclo de vida dos objetos e é bem relacionado a outros assuntos como Graceful Shutdown. Não perca, hein?

Inté!!