A propriedade $casts do Laravel é uma daquelas conveniências silenciosas que moldam como você lida com dados sem chamar muita atenção. Adicione um tipo nativo como boolean ou datetime ao array, e o Eloquent converterá automaticamente strings brutas do banco de dados em algo mais amigável antes mesmo que o valor chegue aos seus controllers ou views. Isso mantém seu código limpo. Mas no momento em que você tenta chamar sua própria função auxiliar diretamente dentro desse array, o framework reage. Escrever algo como 'username' => 'encrypt_data()' não funcionará. O Laravel espera uma palavra-chave de cast nativa ou uma classe que implemente a interface CastsAttributes. Esse requisito existe porque a camada de casting roda profundamente dentro do ciclo de hidratação e serialização do Eloquent; ela precisa de um objeto previsível com um contrato claro, em vez de uma string de função arbitrária que pode não existir em tempo de execução.

Por que funções auxiliares falham no array $casts

Quando o Eloquent carrega um modelo, ele percorre o array $casts para decidir como transformar cada coluna. O framework reconhece primitivos específicos — string, integer, array, encrypted, e assim por diante — e reconhece nomes de classes totalmente qualificados que implementam CastsAttributes. Ele não avalia a string como código. Portanto, 'encrypt_data()' é tratado como um tipo de cast desconhecido, o que gera um erro. Mesmo que o Laravel fizesse o parse, não haveria uma maneira confiável de passar a instância do modelo, a chave do atributo, o valor atual e os atributos circundantes para sua função na ordem correta. A interface existe precisamente para padronizar essa interação entre sua lógica personalizada e os internos do Eloquent.

Você tem duas maneiras sólidas de preencher essa lacuna. Uma favorece o reuso a longo prazo. A outra favorece a velocidade quando você precisa apenas de um ajuste rápido dentro de um único modelo.

Método 1: Escreva uma Classe de Cast Customizada

Se a mesma transformação se aplica a múltiplos campos ou se espalha por vários modelos, uma classe de cast dedicada é a escolha mais limpa. Ela vive em seu próprio arquivo, pode ser testada unitariamente de forma isolada e mantém seus modelos livres de boilerplate repetitivo.

Comece criando app/Casts/CustomEncrypt.php. O namespace deve corresponder à sua configuração de autoloading, normalmente App\Casts. A classe deve implementar Illuminate\Contracts\Database\Eloquent\CastsAttributes, o que obriga você a definir dois métodos: get e set.

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class CustomEncrypt implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return decrypt_data($value);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return encrypt_data($value);
    }
}

As assinaturas importam. O Eloquent passa quatro argumentos para cada método. $model é a instância que está sendo populada ou salva, o que permite inspecionar outros campos caso sua lógica dependa deles. $key é o nome da coluna que está sendo convertida no momento. $value é a string bruta ou null vinda do banco de dados em um get, ou o valor fornecido pelo usuário em um set. $attributes é o array bruto completo de colunas para aquela linha. Você não precisa usar os quatro todas as vezes, mas a interface exige que eles estejam lá.

No método get acima, decrypt_data($value) é executado após o Eloquent buscar a linha no banco de dados e antes que o valor chegue ao seu modelo. No método set, encrypt_data($value) é executado antes do INSERT ou UPDATE, garantindo que o banco de dados nunca veja o texto em plaintext.

Para configurar, faça referência à classe dentro do array $casts do seu modelo:

protected $casts = [
    'username' => CustomEncrypt::class,
    'password' => CustomEncrypt::class,
];

Como você está usando a constante da classe, o autoloader cuida do resto. Se sua aplicação precisar trocar o esquema de criptografia mais tarde, você edita apenas um arquivo e todos os campos mapeados mudam de comportamento instantaneamente. Essa centralização é difícil de superar quando você está gerenciando dados sensíveis em muitas tabelas.

Método 2: Utilize um Accessor e um Mutator

Às vezes, você não quer um novo arquivo para uma transformação que só importa em um lugar. A classe Attribute do Laravel permite definir a lógica de get e set diretamente no modelo usando a sintaxe de closure do PHP 8+.

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function username(): Attribute
{
    return Attribute::make(
        get: fn ($value) => decrypt_data($value),
        set: fn ($value) => encrypt_data($value),
    );
}

Aqui, o nome do método deve corresponder à coluna ou atributo que você está visando. Quando o Eloquent lê username do banco de dados, ele passa o valor bruto pela closure get. Quando você atribui um novo valor à propriedade username do modelo, a closure set o criptografa antes que o Eloquent construa a query.

Essa abordagem brilha em protótipos ou modelos legados onde um diretório completo de classes de cast parece exagero. A desvantagem é a repetição. Se mais tarde você decidir que email, phone e backup_code precisam do mesmo tratamento, você acabará copiando essas closures para múltiplos métodos ou modelos. Esse ruído se acumula. Isso também impede que você reutilize a lógica em um teste unitário sem inicializar o modelo inteiro.

Escolhendo entre as duas opções

Use uma classe de cast customizada quando você se preocupa com o reuso, testes e em manter os models enxutos. Isso sinaliza para outros desenvolvedores que essa transformação é um conceito de primeira classe em sua aplicação, e não um hack pontual.

Use um accessor quando a lógica for verdadeiramente localizada, experimental ou improvável de sobreviver à sprint atual. Isso permite que você avance rápido sem espalhar pequenos arquivos pelo código-fonte. Apenas esteja pronto para refatorar para uma classe de cast assim que o mesmo padrão aparecer pela segunda ou terceira vez.

Detalhes práticos para manter em mente

Casts customizados são poderosos, mas introduzem comportamentos que podem surpreendê-lo se você não estiver atento. Primeiro, lembre-se de que o get recebe o que quer que o banco de dados tenha retornado, incluindo null. Se o decrypt_data não tolerar uma entrada nula, proteja-se contra isso:

public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
    return is_null($value) ? null : decrypt_data($value);
}

Segundo, os casts são executados durante a serialização de array e JSON. Quando você retorna um model como um recurso de API ou chama toArray(), a lógica de get do cast ainda se aplica. Geralmente é isso que você deseja, mas vale a pena lembrar se você estiver expondo valores descriptografados e precisar adicionar camadas extras de controle de visibilidade.

Terceiro, e o mais importante, o casting acontece após a recuperação. Você não pode realizar consultas baseadas no valor transformado. Uma consulta como User::where('username', 'john_doe')->first() envia a string john_doe diretamente para o banco de dados. Ela nunca passa pela sua lógica de descriptografia. Se a coluna estiver criptografada em repouso, essa consulta não encontrará nada, a menos que você pesquise contra o próprio texto cifrado. Planeje seus padrões de acesso ao banco de dados de acordo, pois casts não são um substituto para funções nativas de banco de dados ou colunas de texto simples indexadas.

Classes de cast customizadas também são ótimos lugares para pequenos trechos de configuração. Se você precisar de argumentos no construtor — talvez passando um modo de cifra ou uma string de formato — o Laravel os suporta através do array $casts usando uma expressão como 'field' => CustomEncrypt::class . ':arg', embora isso seja um passo além da configuração básica descrita aqui.

A principal conclusão

A restrição contra o uso de funções auxiliares diretamente no $casts não é uma burocracia arbitrária. Ela o direciona para um código que é explícito, testável e reutilizável. Classes de cast customizadas transformam lógicas inline espalhadas em componentes confiáveis que você pode compartilhar entre models sem duplicação. Accessors mantêm a porta aberta para correções rápidas e localizadas quando um arquivo separado parece um exagero. Domine ambos, escolha com base no escopo do problema, e sua camada Eloquent permanecerá legível muito depois que a aplicação crescer.