PHP:: PADRÃO DE PROJETO ACTIVE RECORD – ORM

php_logo2

Active Record é um padrão de projeto que trabalha com a técnica ORM (Object Relational Mapper). Este padrão consiste em mapear um objeto a uma tabela do Banco da dados, a fim de tornar o trabalho com os dados persistido em um banco de dados, totalmente orientado a objetos. Alguns explicam isso como sendo relacionar um objeto Modelo (Na sua aplicação) com um objeto relacional (No banco de dados).
Normalmente, uma classe ORM, contém no mínimo as operações de “CRUD”: Criar (Insert), Ler (Select), Atualizar(Update) e Excluir(Delete). Como diz a Wikipedia: é uma abordagem para acesso de dados num banco de dados.

Como funciona? Uma Classe representa uma Tabela no banco de dados e é como se estivesse “embrulhada” nesta classe, ou tecnicamente falando: encapsulada.

Para desenvolvermos uma classe assim, que seja plenamente reutilizável, precisamos começar pela sua interface. Vamos desenvolver como uma classe abstrata com métodos para tratar o seu próprio repositório:

Inciando a estrutura

Crie um arquivo chamado AciveRecord.php e adicione o código:



abstract class ActiveRecord
{

    private $content;
    protected $table = NULL;
    protected $idField = NULL;
    protected $logTimestamp;

    public function __construct()
    {

        if (!is_bool($this->logTimestamp)) {
            $this->logTimestamp = TRUE;
        }

        if ($this->table == NULL) {
            $this->table = strtolower(get_class($this));
        }
        if ($this->idField == NULL) {
            $this->idField = 'id';
        }
    }

    public function __set($parameter, $value)
    {
        $this->content[$parameter] = $value;
    }

    public function __get($parameter)
    {
        return $this->content[$parameter];
    }

    public function __isset($parameter)
    {
        return isset($this->content[$parameter]);
    }

    public function __unset($parameter)
    {
        if (isset($parameter)) {
            unset($this->content[$parameter]);
            return true;
        }
        return false;
    }

    private function __clone()
    {
        if (isset($this->content[$this->idField])) {
            unset($this->content[$this->idField]);
        }
    }

    public function toArray()
    {
        return $this->content;
    }

    public function fromArray(array $array)
    {
        $this->content = $array;
    }

    public function toJson()
    {
        return json_encode($this->content);
    }

    public function fromJson(string $json)
    {
        $this->content = json_decode($json);
    }
}

Até aqui o que foi feito é criar um meio para tornar os atributos da classe totalmente dinâmico. Primeiramente são 4 atributos explícitos na classe. Porém, o atributo content é um array. Utilizamos métodos mágicos __set() e __get() para interagir com esse array e permitir a criação de atributos dinâmicos. Outros métodos mágicos como __isset() e __unset() tornam a experiência melhor ainda, muito semelhante a forma de se trabalhar com objetos standards dos php (standard class). Também adicionamos métodos de conversão dos atributos em content para array e json. Você possivelmente contará com tabelas que contém a chave primária com outro nome diferente de ‘id’. Por isso, o atributo idField lhe dá essa flexibilidade de mudar o padrão. Logo adiante, explicarei com mais detalhes.

Para testar, é necessário criar uma classe para estender ActiveRecord. Note, que não teríamos êxito em aproveitar bem esta classe para muitos projetos, se colocássemos aqui nessa classe que acabamos de desenvolver, todas as regras de negócio. Por isso, utilizando o embasamento da Layer Supertype, criamos uma tabela que seja a abstração das regras de negócios e validações, etc, que herda ActiveRecord. Você ficará convencido rapidamente sobre a eficiência desses padrões. Vale lembrar que o nosso Modelo (Model) será construído com a herança da classe ActiveRecord, ou seja, a fusão das duas classes. Não é atoa que excelentes frameworks de mecado utilizam uma implementação deste padrão.

Assim, crie um arquivo chamado Cliente.php e inclua este código:



class Cliente extends ActiveRecord
{

}

Só isso mesmo! Pois os métodos em Active Record já serão suficientes para o funcionamento da classe. Como ele saberá que estaremos utilizando determinada tabela? Note que a técnica está em ligar o nome da tabela ao nome da classe. Mas é válido dizer que, quando criar a tabela, ela deverá ter nome definido em letras minúsculas. Por enquanto não precisaremos.

Agora em um arquivo de teste, podendo ser mesmo o index.php, insira este código:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';

$cliente = new Cliente;

$cliente->nome = "Cliente 1";
$cliente->endereco = "Rua Principal";
$cliente->telefone = "1199999999";

echo "
<pre>";
var_dump($cliente);
echo "</pre>

";

O resultado irá produzir:

Desenvolvendo a Classe Active Record - Primeiro Teste
Desenvolvendo a Classe Active Record – Primeiro Teste

Explicando o que foi feito até agora:

Como você percebeu, iniciamos com a criação de três atributos: $content, $table e $idField. Note que estes atributos têm visibilidade diferente: enquanto $content é private, $table, $idField e $logTimestamp são protected. O motivo disso é que não precisaremos acessar direto o atributo $content de ActiveRecord, mas, apenas os elementos contidos em $content, como se fossem atributos dinâmicos. É por isso que estamos fazendo uso dos métodos mágicos __get() e __set(), que já explicarei adiante. No caso das propriedades $table e $idField, poderemos fazer uso nas classes filhas que estenderão ActiveRecord. Um grande motivo para isso seria: definir manualmente o nome da tabela ou, caso o campo id de nossa tabela não seja identificado como id, mas com outro nome, tal como código. Assim, gostaríamos dessa flexibilidade:

Na classe que estenderá ActiveRecord, por exemplo, a nossa Cliente, defina que tabela será encapsulada por Cliente. Além disso, caso a coluna chave primária tenha outra identificação, defina manualmente, como no nosso exemplo ‘codigo’:



class Cliente extends ActiveRecord
{

    protected $table = 'tb_clientes';
    protected $idField = 'codigo';
    protected $logTimestamp = FALSE;

}

Note o resultado:

Desenvolvendo a Classe Active Record - Primeiro Teste
Desenvolvendo a Classe Active Record – Segundo Teste

Perceba que, os atributos $idField e $table foram sobrescritos pela classe filha, como queríamos. Para ilustrar aquela flexibilidade do protected. O atributo $logTimestamp foi baseado na ideia do Eloquent, utilizado no Laravel. Perceba que este é um atributo que espera valor booleano e mantém o mesmo como sendo TRUE. O que ele faz? Ativa o log de data de criação e data de atualização dos dados, em forma de timestamp, que você verá mais adiante.

Você deve estar se perguntando se é preciso tudo isso! E a resposta é sim se queremos tornar nossa classe mais flexível e reutilizável possível! E isso ficará mais claro quando construirmos o a classe por completo. Mas por hora, entenda que estes atributos são importantes. Aqui fica um incentivo para você conhecer o Eloquent utilizado pelo Laravel. Mas e os métodos definidos até agora?

O método mágico __set($parameter, $value) interceptará todas as vezes que tentarmos adicionar dados ao objeto desta maneira $objeto->nome = “Cliente 1”. Automaticamente ele entende que $parameter significa o ‘nome’, já $value será ‘Cliente 1’. Então podemos pegar estes dados e fazermos o que for necessário com eles. Em nosso método, estamos atribuindo ao array $content: $this->content[$parameter] = $value. Assim criamos um array associativo, pois nesse caso seria o mesmo que atribuir: $this->content[‘nome’] = ‘Cliente 1’. Se você gostou da idéia, pode ler mais a respeito dos métodos mágicos em http://php.net/manual/pt_BR/language.oop5.magic.php.

O método mágico __get($parameter) interceptará ações de se obter um atributo de um objeto e em nosso caso $variavel = $objeto->nome. Esse mecanismo entente que $parameter significa um atributo do objeto, então nesse caso, seria o mesmo que chamar __get(‘nome’). Assim, temos um método para fazer o que for necessário quando for invocado dessa maneira. A nossa classe devolve o elemento no array associativo: return $this->content[$parameter] ou mesmo que solicitar return $this->content[‘nome’]. Este também, assim como outros métodos a seguir, estão descritos no manual do PHP: http://php.net/manual/pt_BR/language.oop5.magic.php.

Em algum momento talvez seja necessário validar se um atributo foi definido com a função isset(), dessa maneira: if(isset($objeto->nome)) { …
Então entra em cena o método mágico __isset($paramter) que fará esse trabalho para nós. Acredito que nesse momento está começando a ficar meio óbivo, assim como no caso de __unset(). Se em nosso código cliente, invocarmos a operação unset($objeto->nome), trataremos esse processo no contexto do método __unset(), que nesse caso é eliminar o elemento da array $content.
Dos métodos mágicos, o que você deve então ter voltado logo sua atenção é __clone(). Este método é invocado quanto se tenta clonar o objeto. Porque utilizaríamos esse método? Porque caso estejamos clonando um registro, não gostaríamos que o id fosse clonado junto, senão, não teria efeito nenhum. Por isso tratamos isso dentro do contexto do método __clone(), eliminar o elemento id da array $content.

Note que nossa classe está adquirindo certo nível de inteligência para tratar dos dados contidos na array $content, e acaba até funcionando como atributos dinâmicos. Isso é bom e ruim, pois poderá permitir atribuição de qualquer atributo indesejado e causar falhas de execução na dinâmica no momento em que formos persistir os dados na tabela. É por isso que há um atributo $recordable, que nos obriga a definir estes campos que poderão ser persistidos no banco.

Faltam explicar os métodos fromArray(array $array) e toArray(). Estes métodos permitem obter e extrair o conteúdo da array $content. Em algumas situações isso é necessário, pois você talvez necessitasse converter um objeto em array. Porém, se tentar fazer isso com ActiveRecord, não terá o efeito desejado. Assim, fromArray() e toArray() cobrem essa necessidade. Os métodos fromJson(string $json) e toJson(), trabalham de forma semelhante aos metodos anteriores, mas ao invés de receberem ou devolverem uma array, será um json.

Se desejar, pode testar dois destes métodos, com o código abaixo:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';

$cliente = new Cliente;

$cliente->nome = "Cliente 1";
$cliente->endereco = "Rua Principal";
$cliente->telefone = "1199999999";

/*
echo "<pre>";
var_dump($cliente);
echo "</pre>";
 */

echo $cliente->toJson();

echo '<pre>';
print_r($cliente->toArray());
echo '</pre>';

O resultado deverá ser semelhante:

Desenvolvendo a Classe Active Record - Terceiro Teste
Desenvolvendo a Classe Active Record – Terceiro Teste

Já que temos os nossos campos para tratar dos atributos, agora vamos entrar na parte de persistência, as operações de CRUD no Active Record.

Um problema que precisamos estar cientes, é que dados podem ser informados pelo código cliente de forma incorreta para serem trabalhados pelas queries SQL. Por isso, antes de criar os métodos de operações CRUD, precisamos adicionar alguns métodos de suporte. Se são métodos de suporte a classe ActiveRecord então não precisam ser acessíveis, portanto serão “private”. Então, voltando a atenção para a classe ActiveRecord, adicione o método format:

private function format($value)
{
    if (is_string($value) && !empty($value)) {
        return "'" . addslashes($value) . "'";
    } else if (is_bool($value)) {
        return $value ? 'TRUE' : 'FALSE';
    } else if ($value !== '') {
        return $value;
    } else {
        return "NULL";
    }
}

Este método format($value) impede por exemplo, quando um atributo possuir valor nulo, no momento de ser concatenar a string para gerar a query, fazer algo como $concatenar = “string” . NULL, ou seja, concatenar uma string com um valor nulo . O que vai dar isso? Ou concaternar uma string com um valor booleano, vai dar tudo erro!

Agora precisamos adicionar um método que ajusta o array para se converter em uma query. O que? Calma você vai entender! Adicione o método convertContent():

private function convertContent()
{
    $newContent = array();
    foreach ($this->content as $key => $value) {
        if (is_scalar($value)) {
            $newContent[$key] = $this->format($value);
        }
    }
    return $newContent;
}

Note que este método vai percorrer o atributo $content e verificar com a função is_scalar() se o valor contido para cada elemento é válido, contendo um dos modos primitivos integer, float, string ou boolean. Se possuir os tipos não escalares (array, object e resource) ignora. Sendo dados escalares, este método irá consumir o método format() para ajustar ao padrão que pode ser concatenado em uma string. Tudo isso será devolvido na nova array $newContent. E saiba quão importante são estes dois métodos! Sem eles pode-se dizer que nossa classe não funcionaria como esperado.

Agora sim! Vamos as operações CRUD!

Vamos começar pelo método save(). Este método é o responsável pela persistência dos dados, é ele quem vai fazer um insert ou update na tabela. Vamos por partes para você entender a lógica de funcionamento dele, então, primeiro adicione o método save() desta maneira:

public function save()
{
    $newContent = $this->convertContent();

    if (isset($this->content[$this->idField])) {
        $sets = array();
        foreach ($newContent as $key => $value) {
            if ($key === $this->idField)
                continue;
            $sets[] = "{$key} = {$value}";
        }
        $sql = "UPDATE {$this->table} SET " . implode(', ', $sets) . " WHERE {$this->id} = {$this->content[$this->idField]};";
    } else {
        $sql = "INSERT INTO {$this->table} (" . implode(', ', array_keys($newContent)) . ') VALUES (' . implode(',', array_values($newContent)) . ');';
    }
    echo $sql;
}

Antes de explicar, vamos testar, pois talvez você já irá captar a ideia! Então, volte lá no arquivo Cliente.php e deixe a classe vazia desta forma:



class Cliente extends ActiveRecord
{

}

Então no arquivo index.php, que você está utilizando para teste, altere para ficar assim:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';

$cliente = new Cliente;

$cliente->nome = "Cliente 1";
$cliente->endereco = "Rua Principal";
$cliente->telefone = "1199999999";

$cliente->save();

O resultado será este:

Desenvolvendo a Classe Active Record - Quarto Teste
Desenvolvendo a Classe Active Record – Quarto Teste

Certo, mas antes de avançar, volte lá no index e adicione um id da seguinte forma:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';

$cliente = new Cliente;

$cliente->nome = "Cliente 1";
$cliente->endereco = "Rua Principal";
$cliente->telefone = "1199999999";
$cliente->id = 1;

$cliente->save();

Agora o resultado quando se executa o código é:

Desenvolvendo a Classe Active Record - Quinto Teste
Desenvolvendo a Classe Active Record – Quinto Teste

Ei sei que saiu um sorriso enorme da sua boca agora! 😀
Então vamos entender logo como o método save() funciona. Você deve ter percebido que ele começa consumindo o método convertContent(), que irá percorrer o atributo $content e reconstruir um novo array $newContent com valores formatados pelo método format(). Graças a estes dois métodos será possível concatenar os elementos de um array sem erros de execução causado por valores não escalares que poderiam quebrar o código.
Tendo o array $newContent no formato que precisamos, devermos saber se a operação é de atualização ou inserção. Para saber isso, é fácil! Testamos se há um id entre os elementos de $content e se houver é porque os dados foram recuperados do banco. Exceto se o seu código cliente estourar contra todo sentido da coisa e inserir um código manualmente, como fizemos apenas para teste!

Mas falta persistir isso no banco! Se você ainda não leu meu artigo sobre conexão com banco de dados Singleton, essa é uma boa hora!
O motivo para isso é a necessidade de uma conexão com banco de dados, e faremos uso da classe tratada neste artigo. Se você já possui uma classe de conexão que devolve um objeto PDO e preferir utilizá-lo, sem problemas mas tenho certeza que gostará também desta classe! Você poderá obter os códigos dela no Github e alterar o arquivo de configuração conforme a seguir:

; Arquivo de configuração de conexão com o banco de dados
; Driver de conexão
sgdb      = mysql
; Dados para conexão
banco     = activerecord
servidor = localhost
porta     = 3306
; Credenciais de usuário
usuario  = root
senha    = 123456

Observação: Claro, refletindo as configurações do seu ambiente! Por exemplo, minha senha aí é 123456, mas a sua poder ser outra ou nem ter senha!

Então crie um banco de dados no mysql com o nome activerecord. O procedimento para se criar um banco de dados e as tabelas não faz parte deste artigo, mas você é livre para utilizar um aplicativo de sua preferencia ou mesmo o myphpadmin. Todavia se estiver utilizando um aplicativo que permita rodar script, tal como MySQL Workbench, pode utilizar este:

create database activerecord default character set utf8 default collate utf8_general_ci;

use activerecord;

Então crie a tabela, e da mesma forma, utilize o seu jeito de costume ou se estiver utilizando um aplicativo que permita rodar script, tal como MySQL Workbench, pode utilizar este:

create table cliente (
    id int auto_increment primary key,
    nome varchar(80) not null,
    endereco text
);

Feito isso podemos adicionar as linhas em nosso método save() que fará todo trabalho com o banco, então no lugar de echo $sql, substitua por:

 if ($connection = Connection::getInstance('./configdb.ini')) {
    return $connection->exec($sql);
} else {
    throw new Exception("Não há conexão com Banco de dados!");
}

Feito isso, vamos fazer o teste da seguinte maneira, primeiro altere o arquivo que você está usando como código cliente, provavelmente index.php dessa forma:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

$cliente = new Cliente;

$cliente->nome = "Cliente 1";
$cliente->endereco = "Rua Principal";


if ($cliente->save()) {
    echo "Registro salvo!";
} else {
    echo "Registro <b>NÃO FOI</b> salvo!";
}

Note que para simplificar, até mesmo no momento da criação da tabela no banco de dados, eliminamos o atributo telefone. Além disso, foi necessário importar a classe de conexão com o comando include_one ‘Connection.php’. Logo abaixo, foi adicionado um teste para nos informar o que aconteceu e, se deu tudo certo, no browser deverá aparecer apenas:

Desenvolvendo a Classe Active Record - Sexto Teste
Desenvolvendo a Classe Active Record – Sexto Teste

Faça um teste no banco de dados e você deverá encontrar o registro salvo:

Desenvolvendo a Classe Active Record - Conferindo no banco de dados
Desenvolvendo a Classe Active Record – Conferindo no banco de dados

Muito bem, parece que concluímos nosso método save() e você deve estar tentado a fazer o teste de Update! mas vamos dar uma pausa no save() e partir para o método find. O método find é estático e aceitará um inteiro para recuperar um registro através do seu id. Porque estático? Porque, não faz muito sentido termos de instanciar um objeto ActiveRecord para depois popular seus atributos! Então adicione o método find() da seguinte maneira:

public static function find($parameter)
{
    $class = get_called_class();
    $idField = (new $class())->idField;
    $table = (new $class())->table;

    $sql = 'SELECT * FROM ' . (is_null($table) ? strtolower($class) : $table);
    $sql .= ' WHERE ' . (is_null($idField) ? 'id' : $idField);
    $sql .= " = {$parameter} ;";

    if ($connection = Connection::getInstance('./configdb.ini')) {
        $result = $connection->query($sql);

        if ($result) {

            $newObject = $result->fetchObject(get_called_class());
        }

        return $newObject;
    } else {
        throw new Exception("Não há conexão com Banco de dados!");
    }
}

Veja, que como estamos trabalhando com um método estático, não podemos nos referenciar simplesmente aos atributos da classe com $this, porém como temos a flexibilidade de sobrescrever atributos pela classe que estende ActiveRecord, precisamos de alguma forma recuperar estes dados. O método get_called_class() devolve o nome da classe corrente, usamos o resultado para gerar instancias somente em tempo de execução, a fim de obter os parâmetros. Tendo isso, montamos a query.

Para fazer o teste, você precisa alterar o arquivo index.php para:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

$cliente = Cliente::find(1);

echo "<pre>";
var_dump($cliente);
echo "</pre>";

Se você executar o código, o find irá localizar o registro e devolver um objeto ActiveRecord para $cliente. Na tela você terá o seguinte resultado:

Desenvolvendo a Classe Active Record - Sétimo teste
Desenvolvendo a Classe Active Record – Sétimo teste

Até aqui temos as 3 operações do CRUD. Você deve estar tentado a testar o Update! Pode fazer da seguinte maneira, alterando o index.php para:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

$cliente = Cliente::find(1);

echo "<pre>";
var_dump($cliente);
echo "</pre>";

$cliente->nome = "Fulano de Tal";
$cliente->endereco = "Endereco modificado para fulano de tal";
$cliente->save();
unset($cliente);

$cliente = Cliente::find(1);
echo "<hr>";
echo "<pre>";
var_dump($cliente);
echo "</pre>";

O resultado desse código:

Desenvolvendo a Classe Active Record - Oitávo teste
Desenvolvendo a Classe Active Record – Oitávo teste

Note que na segunda representação o find() já trouxe o registro atualizado!
Portanto vamos ao delete, ultima operação do CRUD. Adicione o seguinte método:

public function delete()
{
    if (isset($this->content[$this->idField])) {

        $sql = "DELETE FROM {$this->table} WHERE {$this->idField} = {$this->content[$this->idField]};";

        if ($connection = Connection::getInstance('./configdb.ini')) {
            return $connection->exec($sql);
        } else {
            throw new Exception("Não há conexão com Banco de dados!");
        }
    }
}

Para efetuar o teste desta operação, altere o index.php da seguinte forma:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

$cliente = Cliente::find(1);
$cliente->delete();

$cliente = Cliente::find(1);
echo "<hr>";
echo "<pre>";
var_dump($cliente);
echo "</pre>";

O resultado na tela demonstrará que a operação foi executada com êxito:

Desenvolvendo a Classe Active Record - Nono teste
Desenvolvendo a Classe Active Record – Nono teste

Com as quatro operações básicas de nossa classe concluída, faltarão agora para concluir apenas dois métodos muito usuados: all() e findFirst().

O método all, embora possa parecer difícil, é simples. Este é o código do método all:

public static function all(string $filter = '', int $limit = 0, int $offset = 0)
{
    $class = get_called_class();
    $table = (new $class())->table;
    $sql = 'SELECT * FROM ' . (is_null($table) ? strtolower($class) : $table);
    $sql .= ($filter !== '') ? " WHERE {$filter}" : "";
    $sql .= ($limit > 0) ? " LIMIT {$limit}" : "";
    $sql .= ($offset > 0) ? " OFFSET {$offset}" : "";
    $sql .= ';';

    if ($connection = Connection::getInstance('./configdb.ini')) {
        $result = $connection->query($sql);
        return $result->fetchAll(PDO::FETCH_CLASS, get_called_class());
    } else {
        throw new Exception("Não há conexão com Banco de dados!");
    }
}

Para testar, termos de inserir alguns registros e então, invocar all. Para isso, altere o index.php da seguinte forma:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';


for ($c = 1; $c <= 10; $c++) { 
    $cliente = new Cliente; $cliente->nome = "Cliente {$c}";
    $cliente->endereco = "Rua do Cliente {{$c}}";


    if ($cliente->save()) {
        echo "Registro {$c} salvo!";
    } else {
        echo "Registro <b>NÃO FOI</b> salvo!";
    }
}

$todos = Cliente::all();

echo "
<pre>";
var_dump($todos);
echo "</pre>

";

Como você pode perceber, seria possível definir um filtro e deixamos este parâmetro opcional. Além disso, deixamos mais dois parâmetros muito úteis quando geramos uma query do tipo select, o Limit e Offset. Mas o resultado do código acima, já te dará uma visão boa da funcionalidade do método e será exatamente: primeiro algumas strings acima informando as iterações do Loop For quando foi adicionando os registros e, depois, em seguida uma lista com todos os registros listados:

Desenvolvendo a Classe Active Record - Décimo teste
Desenvolvendo a Classe Active Record – Décimo teste

Por fim, o método findFirst(). O método findFirst() espera um filtro como argumento, por exemplo “id = 1” ou “nome = ‘Cliente 2’”. Na verdade, parece até ser desnecessário mas, muito útil quanto a clareza. Este método faz uso do método all, indormando o parâmetro limit para trazer apenas o primeiro registro encontrado.

public static function findFisrt(string $filter = '')
{
    return self::all($filter, 1);
}

Para testar, faça da mesma forma como fez para find(), mas desta vez com um filtro string ao invés de um valor inteiro como $id:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

$cliente = Cliente::findFisrt("nome = 'Cliente 4'");

echo "
<pre>";
var_dump($cliente);
echo "</pre>

";

Este padrão de projeto é ótimo e muito usual hoje em muitos frameworks tais como Laravel. A implementação do Active Record usado no Laravel é o Eloquent. Mas o Cake é um outro exemplo de framework que utiliza Active Record.

Parece que terminamos a classe, mas só que não! Precisamos fazer alguns refinamentos: talvez você se lembre da propriedade no início $logTimestamp. Precisamos alterar o método save() para adicionar este recurso.

public function save()
{

    $newContent = $this->convertContent();

    if (isset($this->content[$this->idField])) {

        $sets = array();
        foreach ($newContent as $key => $value) {
            if ($key === $this->idField || $key == 'created_at' || $key == 'updated_at')
                continue;
            $sets[] = "{$key} = {$value}";
        }
        if ($this->logTimestamp === TRUE) {

            $sets[] = "updated_at = '" . date('Y-m-d H:i:s') . "'";
        }
        $sql = "UPDATE {$this->table} SET " . implode(', ', $sets) . " WHERE {$this->idField} = {$this->content[$this->idField]};";
    } else {
        if ($this->logTimestamp === TRUE) {
            $newContent['created_at'] = "'" . date('Y-m-d H:i:s') . "'";
            $newContent['updated_at'] = "'" . date('Y-m-d H:i:s') . "'";
        }
        $sql = "INSERT INTO {$this->table} (" . implode(', ', array_keys($newContent)) . ') VALUES (' . implode(',', array_values($newContent)) . ');';
    }
    if ($connection = Connection::getInstance('./configdb.ini')) {
        return $connection->exec($sql);
    } else {
        throw new Exception("Não há conexão com Banco de dados!");
    }
}

Agora para fazer o teste deste método com este novo recurso. Primeiro altere a classe Cliente para:



class Cliente extends ActiveRecord
{

    protected $logTimestamp = TRUE;

}

Então adicione as duas novas colunas na tabela cliente como datetime: created_at e updated_at. Você poderá executar este script:

alter table cliente add column created_at datetime after endereco;
alter table cliente add column updated_at datetime after created_at;

Então, adicione mais alguns registros e faça um update para verificar. Altere o index.php para ficar desta maneira:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';


for ($c = 11; $c <= 20; $c++) { $cliente = new Cliente; $cliente->nome = "Cliente {$c}";
    $cliente->endereco = "Rua do Cliente {$c}";


    if ($cliente->save()) {
        echo "Registro {$c} salvo!
";
    } else {
        echo "Registro <b>NÃO FOI</b> salvo!
";
    }
}

$cliente12 = Cliente::find(12);

echo "
<pre>";
var_dump($cliente12);
echo "</pre>

";
echo "
<hr>

";

sleep(2);
$cliente12->nome = "Cliente 12 Atualizado";
$cliente12->save();

$cliente12upd = Cliente::find(12);
echo "
<pre>";
var_dump($cliente12upd);
echo "</pre>

";

Então você poderá comparar, além de fazer seus próprios testes com Update.

Desenvolvendo a Classe Active Record - Próprios testes
Desenvolvendo a Classe Active Record – Próprios testes

Mas, isso ainda não é tudo, na verdade há um pequeno ajuste a se fazer nesta classe, para diminuir a dependência da classe Connection. Caso contrário, se você não desejar usar a classe Connection ficará obrigado a isso para usar a classe ActiveRecord. E como podemos fazer então?
Primeiro, adicione mais um atributo estático privado chamado connection:

private static $connection;

Então, substitua todas as variáveis nos métodos $connection por self::$connection. Se você estiver utilizando um editor de textos ou IDE, isso será mamata! Mas preste atenção pois se utilizar o recurso, “substituir todos”, poderá ter esse resultado indesejado no atributo estático que acabou de criar:

private static self::$connection;

Então, você deverá corrigir isso, para não ter problemas. Mas, falta substituir também nos métodos esta declaração que normalmente está dentro dos ifs:

$connection = Connection::getInstance('./configdb.ini')

por apenas:

self::$connection

Por fim adicione este método estático para adicionar a conexão ao nível de classe:

public static function setConnection(PDO $connection)
{
    self::$connection = $connection;
}

Pronto! Se você chegou a se preocupar com isso … já era!
Você poderá utilizar a classe Connection:

Cliente::setConnection(Connection::getInstance('./configdb.ini'));

Ou fazer da sua maneira. É possível melhorar esta classe ainda mais, adicionando recursos para gerar log, e de maneira facílima! Vou deixar a dica embora, fica para outro post: Veja nos métodos que antes de executar as queries, sempre elas são criadas e armazenadas em uma variável $sql. Isso pode te inspirar!
Você ainda pode adicionar outros métodos que seriam genéricos e uteis, como por exemplo o count:

public static function count(string $fieldName = '*', string $filter = '')
{
    $class = get_called_class();
    $table = (new $class())->table;
    $sql = "SELECT count($fieldName) as t FROM " . (is_null($table) ? strtolower($class) : $table);
    $sql .= ($filter !== '') ? " WHERE {$filter}" : "";
    $sql .= ';';
    if (self::$connection) {
        $q = self::$connection->prepare($sql);
        $q->execute();
        $a = $q->fetch(PDO::FETCH_ASSOC);
        return (int) $a['t'];
    } else {
        throw new Exception("Não há conexão com Banco de dados!");
    }
}

Mas, embora o artigo este chegando ao seu final, (depois de um longo percurso!) note que não temos muito atenção a classe Cliente. Essa classe é onde será todo trabalho de regras e validações. A classe que estende ActiveRecord junto a classe ActiveRecord constrói um Modelo. Veja se quiséssemos fazer alguma coisa em nossa classe cliente que fosse específico para cliente:


class Cliente extends ActiveRecord
{
    protected $logTimestamp = TRUE;
    public static function listarRecentes(int $dias = 10)
    {
        return self::all("created_at >= '" . date('Y-m-d h:m:i', strtotime("-{$dias} days")) . "'");
    }

    public static function numTotal()
    {
        return self::count();
    }
}

Então para chamar no código cliente:


include_once 'ActiveRecord.php';
include_once 'Cliente.php';
include_once 'Connection.php';

Cliente::setConnection(Connection::getInstance('./configdb.ini'));

echo "
<pre>";
var_dump(Cliente::listarRecentes(5));
echo "</pre>

";

echo Cliente::numTotal();

Conclusão
Com esta classe você, você tem a representação de um Recordset ou registro do banco de dados, como uma entidade e poderá fazer com essa entidade o que for preciso. Mas note que la não poderá por si só fazer nada diretamente, sem o apoio de sua classe filha e até por isso ela foi definido como abstrada. Na verdade, ela é uma abstração das operações do banco. Estendendo esta classe, ou seja, herdando para elaborar trabalhar com as regras de negócio, como fizemos por criar a classe Cliente, você terá exatamente o conceito de definição de responsabilidades muito bem definido pelo padrão de projetos MVC. Lembrando, que existem ótimas soluções prontas para esta finalidade, mas é sempre bom criarmos as nossas próprias classes para entendermos mais porfundamente o funcionamento, ou seja, o que está por detrás das cortinas nos Frameworks. Espero que este conteúdo possa ser muito útil aos seus estudos, ele até mesmo será base para outros artigos aqui no blog! Se desejar poderá acessar os códigos desevolvidos neste artigo no Github por clicar AQUI.
[]’s

21 comentários

  1. Boa noite! Achei o artigo super interessante e intuitivo. Gostaria de saber como a partir daí criar um formulario para coletar os registros e como listá-los em tabela.

    1. Boa noite José Antonio. Que bom, este é um design muito usado em muitos Frameworks, muito comum. Sobre a captação, não muda, você faz da mesma forma como faria utilizando PDO, mas invés de captar a variável e passar para uma query em PDO, você manda as variáveis diretos para o model. Por exemplo: se no seu formulário há um input text para nome de uma pessoa, você teria uma chave “nome” em post, assim $_POST[‘nome’], certo? Então você passaria esse valor para uma variável $nome = $_POST[‘nome]’. Até aí você já deve estar bem acostumado. Então, suponhamos que a Model se refira a uma tabela clientes, aonde você tenha uma coluna ‘nome’, ok? E você instanciou assim: $cliente = new Cliente; pronto, você têm uma referência para tratar um novo registro de cliente, então $cliente->nome = $nome; Simples certo? Daí se você já passou todos os dados que recebeu do formulário, basta salvar: $cliente->save(); Só isso mesmo!

      1. Boa noite Alexandre!
        Você possui algum curso com esse exemplo completo. Ou alguma documentação com um exemplo de negocio simples empregando este modelo.

      2. Olá José Anônio! Obrigado por prestigiar o material! Ainda não há um curso, mas estou trabalhando para isso. O que recomendo é tentar aplicar o conhecimento obtido neste artigo, com o de MVC. Experimente substituir a classe Model simples de lá, por adaptar esta. Sistemas utilizando arquitetura MVC utilizam muito uma forma orientada a objetos para tratar tabelas do banco de dados: Entity, Repository, DAO, ORM. Estes são diversos padrões com aplicações diferentes, mas voltados para Banco de dados. ORM é muito utilizado, inclusive a forma como o Eloquent do Laravel trabalha. Se você ainda não conhece outros artigos, lhe convido a este: https://alexandrebbarbosa.wordpress.com/2018/06/28/phpcrud-com-mvc/
        Também, se estiver gostando do material, não deixe de publicar aos seus amigos e colegas. Isso poderá contribuir para que eu consiga colocar online o mais rápido possível, o material que estou trabalhando. Abraço!

  2. Muito legal. Criei outra tabela chamada customers (renomeei depois para customer), que contém campos com nomes totalmente diferentes e então criei a classe Customer similar a Cliente e consigo inserir registros normalmente. Muito legal.

  3. Muito bom, parabéns.

    Mas eu me perdi nessa parte do artigo

    Primeiro, adicione mais um atributo estático privado chamado connection:

    1
    private static $connection;
    Então, substitua todas as variáveis nos métodos $connection por self::$connection. Se você estiver utilizando um editor de textos ou IDE, isso será mamata! Mas preste atenção pois se utilizar o recurso, “substituir todos”, poderá ter esse resultado indesejado no atributo estático que acabou de criar:

    1
    private static self::$connection;

    não ficou claro pra mim o que é pra fazer. rs

    1. Olá Heverton, seja bem vindo! Para que eu possa compreender melhor sua dúvida: você não entendeu o funcionamento ou a substituição? Se utilizar uma vez opção de substituir todas as representações encontradas utilizando uma função “substituir todas”, alguns locais que fazem referência ao nome e que não deveriam ser alterados, também serão, causando quebra do código.

      1. Fala Alex, eu não tinha entendido a substituição. Depois de le e reler entendi a parada. rss e funcionou.

  4. Oi gostei muito desse seu artigo, bastante didático, parabéns!
    Uma dúvida que me ocorreu ao chegar ao fim dessa postagem, como poderia utilizar alguns recuros como joins, case, except e union?

    1. Olá Marcelo, está classe não foi preparada para fazer relacionamento e alguns recursos. Eu até gostaria de ter feito mas o artigo iria ficar muito mais longo e muito cansativo. Por isso estou pensando em criar um sobre query builder. Este atenderá sua dúvida. Mas assim como o antigo sobre rotas, terá que ser produzido em uma série de 3 ou 4 artigos.

  5. Muito interessante o uso do PDO::fetchObject(nome_da_classe), não o conhecia!
    Já sei o eloquent no laravel porém entender melhor este funcionamento é essencial para trabalhar com projetos com php puro que é uma realidade ainda presente em muitos trabalhos no mercado!
    Valeu mesmo!

  6. Boa tarde Alexandre, queria agradecer pelo seus artigos, tenho 25 anos sou estudante de sistemas de informação e estou estudando em busca do primeiro emprego na área, e posso dizer que seus artigos são os melhores que encontrei, parabéns pela didática, você ensina muito bem.
    Dito isso, na parte do método save da classe activerecord, mais especificamente na query de update você deixou o campo de busca ‘WHERE’ para receber o valor de busca como this->id, o que acaba gerando um erro quando a pessoa usa uma tabela com outro nome para coluna index, creio que o correto seja usar this->idField para que seja usado o valor da coluna index passada na classe que herda o activerecord. Me corrija se estiver errado.
    No mais, parabéns mais uma vez pela dedicação e muito obrigado.

    1. Muito obrigado Felipe por prestigiar o material! Sim, a sua observação é muito válida, você estressou mais os testes e acredito que na direção certa. Estou tentando arrumar um tempo em alguma folga para lançar novos artigos, e esse artigo poderá receber um super update! Muito obrigado pelas observações. Abraço!

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.