Introdução
Fala pessoal como andam as coisas?, espero que estejam todos bem, por aqui estou recuperando de um burnout mas a cada dia melhor.
Faz uns meses que eu queria criar esse artigo de hoje sobre um assunto que é muito importante no trabalho como pessoa desenvolvedora, e esse assunto é a criação de um histórico de ações do usuário e do sistema visando trazer mais rastreabilidade e segurança para nós e nossa aplicação.
Entendendo a importância de se ter uma boa rastreabilidade nas aplicações
Imagine o seguinte cenário, você trabalha em uma empresa que possui um SaaS focado na venda de produtos, na aplicação tem uma feature de desconto onde o usuário pode escolher um determinado produto de uma categoria e adicionar um desconto sobre ele, então em um belo dia na Black Friday o gerente de uma loja cliente pede para o funcionário responsável por administrar o sistema para adicionar um desconto de 5% sobre um determinado produto, mas , por falta de atenção infelizmente, o funcionário adiciona um desconto de 50%.
Algumas horas se passam e então ele percebe a falha que cometeu quando as vendas daquele produto começam a disparar de forma inacreditável, então quando ele vai checar os detalhes do produto no sistema acaba percebendo que adicionou a porcentagem de desconto errada, tenso não?
Sendo assim, o funcionário com medo da bronca que iria levar do chefe resolveu colocar a culpa no sistema, e adivinha ?, o gerente acreditou e ligou para o suporte da sua empresa.
Agora imagine só, como provaríamos que o erro foi do funcionário ao aplicar o desconto e não da aplicação que adicionou o desconto com valor errado?, precisaríamos de provas correto?, e é aí que entra a importância de se ter um bom sistema de histórico (por isso eu disse no inicio que é para nossa segurança também , porque tendo um sistema de histórico conseguimos comprovar quem foi o autor da falha).
Sei que para muitos esse assunto soa como sendo bem básico, e realmente é, mas acredite, muitos sistemas hoje em dia não contemplam essa feature.
Nesse artigo irei utilizar de exemplo a linguagem PHP juntamente com o Laravel, e mais especificamente, um projeto que estou desenvolvendo. Mas por se tratar de orientação a objetos conseguimos replicar em outras linguagens que suportem o paradigma.
Sem mais delongas bora botar a mão na massa!
Primeiramente, onde salvar o histórico?
Bom , nesse exemplo irei utilizar um banco de dados relacional, mas nada nos impede de salvar essas informações em um banco não relacional, ou até mesmo em um arquivo de log, o importante é de alguma forma conseguir salvar todas as ações necessárias para se ter uma boa rastreabilidade. Sendo assim fique a vontade para escolher onde irá salvar essas informações.
Quais ações mapear no histórico?
Vamos então começar partindo da seguinte pergunta, quais ações serão salvas em nosso histórico?
Pois bem, o mais importante de fato é salvar ações que são criticas no sistema e que também sejam passíveis de erros que possam acarretar em grandes problemas.
Seguindo esse nosso exemplo poderia ser a ação de adicionar um desconto, mas poderíamos ter outras ações, como por exemplo a ação de se realizar uma venda, alterar algum dado do produto, deletar um produto, e assim por diante.
Mas também nada impede de salvar todas as ações do usuário/sistema, eu particularmente prefiro salvar as ações criticas e que podem trazer alguma consequência (mas pense bem, pois a tabela de históricos ficaria lotada bem rapidinho, dificultando o “debug”).
É bom lembrar que quando eu digo usuário estou me referindo também ao sistema (talvez atores soaria melhor que usuários? lembrou daquela aula chatinha de UML da facul nééé?).
Montando nosso sistema de histórico
Bom , agora que enxergamos a importância de se ter um sistema de histórico, e quais ações devemos mapear, está na hora de botar a mão na massa e criar o nosso módulo de histórico.
Entidade/Modelo
Para a nossa entidade/modelo é importante termos o tipo da entidade (enum), que nesse caso seria o módulo em que foi registrado aquele histórico (product, sales, financial), uma coluna para salvar quem foi o responsável por aquela mudança (eu gosto de chamar de changed_by), e a ação que foi feita (enum). Também é indispensável ter colunas timestamp como created_at e updated_at para fins de rastreabilidade.
Estrutura do modelo
Seguindo a entidade da aplicação que irei utilizar como exemplo, temos as seguintes colunas:
Podemos utilizar as informações acima para criar uma migration para a tabela histories.
Ações que serão rastreadas
Dentro de nosso modelo teremos as seguintes ações que serão rastreadas:
Nessa aplicação que estou utilizando como exemplo até então as ações que criam histórico são relacionadas a produtos, pois são as mais críticas do sistema, por isso só tenho elas dentro do model de History. Mas no futuro irei adicionar ações relacionadas a autenticação, edição de permissões e pagamentos.
No PHP, antes da versão 8 onde foi introduzido o tipo enum nós costumávamos utilizar constantes como uma forma de representar um enum, mas na versão 8 > já é possível criar os enums como criamos em C#, por exemplo.
Então se você estiver utilizando o PHP 8 >, pode substituir as constantes por enums 🙂 , não substituí ainda por falta de tempo (ou vergonha na cara, ainda não me decidi rs…).
Segue o código da Model:
Acima podemos observar também os enums mapeando as entidades que utilizarão o histórico (Dê uma olhada nas constantes terminadas em ENTITY), para podermos saber qual módulo é dono de qual registro.
Relacionamentos e função para criação do histórico
Por fim, temos os relacionamentos de History com User e Company, também temos uma função para criação do histórico chamada createChange, onde recebemos como parâmetro o id da ação ocorrida, e um array com os dados para serem salvos na tabela.
Mais para frente utilizaremos essa função para criar os registros de histórico através da nossa classe de Serviços.
Código da entidade History:
<?php
namespace App\Models;
use App\Models\Scopes\FilterTenant;
use App\Traits\UsesLoggedEntityId;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Collection;
/**
* @property int $entity_id
* @property string $entity_type
* @property string $metadata
* @property int $changed_by_id
* @property int $company_id
* @property int $action_id
*/
class History extends Model
{
/* Para esse exemplo , ignore essas 2 traits.*/
use HasFactory;
use UsesLoggedEntityId;
const PRODUCT_ENTITY = 'product';
const BRAND_ENTITY = 'brand';
const CATEGORY_ENTITY = 'category';
const PRODUCT_CREATED = 1;
const PRODUCT_UPDATED = 2;
const PRODUCT_DELETED = 3;
const PRODUCT_SOLD = 4;
const ADDED_QUANTITY = 5;
protected $fillable = [
'entity_id',
'entity_type',
'company_id',
'action_id',
'metadata',
'changed_by_id',
];
/* Um scope global que eu utilizo para sempre filtrar as queries por tenantId, pode ignorar tb... */
protected static function booted(): void
{
static::addGlobalScope(new FilterTenant());
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function createChange(int $actionId, array $data): void
{
$this->entity_id = $data['entityId'];
$this->entity_type = $data['entityType'];
$this->metadata = $data['metadata'];
$this->changed_by_id = $data['changedById'];
$this->company_id = self::getLoggedCompanyId();
$this->action_id = $actionId;
$this->save();
}
}
Agora que temos nosso Modelo criado está na hora de criarmos a classe de serviço para podermos utilizá-la nos módulos que salvarão no histórico.
Classe de serviço e sua utilização nos módulos
Para quem nunca ouviu falar de classes de serviços vou explicar bem brevemente, trata de uma classe que é utilizada para fazer a separação da regra de negócio na nossa aplicação, assim ao invés de termos as regras de negócio em um controller por exemplo, delegamos essa função para a classe de serviços, e o controlador fica com o papel de ser um orquestrador, lidando somente com requisições HTTP e delegando processamento relacionado a regras de negócios ao serviço. Caso tenha interesse fiz um artigo explicando um pouco sobre.
Criando o HistoryService
Muito bem, para começarmos podemos criar uma pasta chamada Services dentro de App, e logo após criamos uma pasta para a entidade History.
Dentro da pasta Services/History criamos uma classe PHP chamada HistoryService, e então criamos uma função chamada createHistory, recebendo como parâmetro o id da ação e um array de parâmetros.
Dentro da função createHistory faremos o seguinte:
- Criaremos uma nova instância da model History.
- Na instância criada chamaremos a função createChange passando como parâmetro o actionId e o array de parâmetros.
- Chamaremos o método save do History para persistir no banco.
O código ficará assim:
<?php
namespace App\Services\History;
use App\Models\History;
class HistoryService
{
public function createHistory(int $actionId, array $params): void
{
$history = new History();
$history->createChange($actionId, $params);
$history->save();
}
}
Utilizando o HistoryService nos módulos da nossa aplicação
Enfim chegamos a parte final desse processo, utilizar a classe de serviço dentro dos módulos da nossa aplicação, criando assim os registros no nosso histórico. Nesse exemplo utilizarei a classe de serviço responsável por importar produtos de uma planilha para o nosso sistema.
Deixei somente as funções que mostrarão o uso do History , e o resto só comentei oque cada uma faz para vocês não ficarem voando:
/**
* @throws FailedToImportProducts|AttachmentInvalid
*/
public function importProducts(UploadedFile $importedFile): void
{
$this->setImportedProductsFile($importedFile);
$this->validateFile();
try {
DB::beginTransaction();
$this->createProductsBasedOnImport();
DB::commit();
}
catch(CustomException $e) {
DB::rollBack();
throw $e;
}
catch(\Throwable $e) {
DB::rollBack();
throw new FailedToImportProducts($e);
}
}
Nessa função fazemos o seguinte:
- Seto o arquivo importado em uma propriedade, para ser utilizado em outras funções de processamento.
- Valido se o tipo do arquivo enviado é uma planilha CSV ou XLSX, e também o tamanho do arquivo.
- Chamamos a função createProductsBasedOnImport para começar o processo de criar os produtos da planilha importada.
private function createProductsBasedOnImport(): void
{
$products = $this->convertSpreadsheetToCollection();
$importedProducts = $products->map(function (array $product) {
return ImportedProduct::create()->fromArray($product);
});
$this->storeImportedProducts($importedProducts);
}
Agora, na função de criar os produtos fazemos o seguinte:
- Criamos uma Collection apartir da planilha, facilitando as operações que serão feitas mais pra frente.
- Pegamos cada produto da Collection e criamos um objeto usando Factory, para facilitar mais uma vez as operações a seguir.
- Chamamos a função storeImportedProducts, que é responsável por salvar no banco de dados os produtos importados passando como parâmetro uma coleção dos objetos que criamos com a Factory.
Então finalmente chegamos na cereja do bolo, onde faremos o seguinte:
private function storeImportedProducts(Collection $products): void
{
$products->each(function (ImportedProduct $product) {
$createdProduct = Product::create()->fromRequest($product->toCollection());
$this->createImportedProductHistory($createdProduct);
});
}
- Para cada produto da coleção, criamos um produto na nossa tabela Products.
- Para cada produto salvo na tabela products, chamamos a função que criará o histórico de produto importado.
private function createImportedProductHistory(Product &$product): void
{
$historyService = new HistoryService();
$params = [
'entityId' => $product->getId(),
'entityType' => History::PRODUCT_ENTITY,
'changedById' => self::getChangedBy(),
'metadata' => $this->createHistoryMetaData($product)
];
$historyService->createHistory(History::PRODUCT_UPDATED, $params);
}
- Criamos uma instância do HistoryService para ser utilizado.
- Criamos um Array com os parâmetros necessários no histórico, e no metadata chamamos uma função que retornara todos os dados que precisamos do produto para ser utilizado futuramente se necessário.
- Chamamos a função createHistory, passando como parâmetro a ação e o array de parâmetros.
private function createHistoryMetaData(Product &$product): string
{
return collect([
'entityId' => $product->id,
'productName' => $product->name,
'initialQuantity' => $product->quantity,
'categoryId' => $product->category_id ?? null,
'brandId' => $product->brand_id ?? null,
'minimumQuantity' => $product->minimum_quantity
])->toJson();
}
E dessa forma acabamos de utilizar o nosso sistema de histórico 🙂.
Bom, espero que de alguma forma esse artigo tenha sido útil para você, se gostou compartilhe com aquele amigo que ainda não sabe da importância de se ter um histórico, e críticas construtivas são bem vindas!
Tem um jeito melhor de resolver tal problema?, comenta aí embaixo, vamos compartilhar conhecimento! 😀, é isso aí pessoal uma ótima semana a todos, e até a proxima 👋🏿.
Top comments (1)
Muito importante o tema, além dos exemplos citados existem sempre várias outras formas produtivas de usar históricos, como logs de desempenho por exemplo, entender onde seu usuário mais esta tendo dificuldade ou lentidão, etc. Muito valioso!