Ce qui se passe
Laravel 13.33 introduit l'attribut #[Refreshes], proposé par Caleb White
(PR #61523). Il corrige un
piège ancien d'Eloquent : les colonnes calculées par la base ne sont pas relues
après une écriture.
Le problème
Prenons une facture dont le total TTC est calculé par la base :
$table->decimal('total_ttc', 10, 2)->storedAs('total_ht * (1 + vat_rate)');Après Invoice::create([...]), $invoice->total_ttc vaut null. Après un
update(), il garde l'ancienne valeur. La parade habituelle, refresh(),
relit toute la ligne et recharge toutes les relations déjà chargées.
La solution
use Illuminate\Database\Eloquent\Attributes\Refreshes;
use Illuminate\Database\Eloquent\Model;
#[Refreshes('total_ttc', 'reference')]
class Invoice extends Model
{
protected $fillable = ['total_ht', 'vat_rate'];
}Plusieurs colonnes se passent en tableau ou en arguments successifs. Sans
attribut, la propriété protected array $refreshes fait la même chose ; si les
deux sont présents, la propriété l'emporte. Un modèle qui n'a ni l'un ni
l'autre ne déclenche aucune requête en plus.
Après chaque INSERT ou UPDATE, Eloquent lit ces seules colonnes par la clé
primaire et les recopie sur le modèle. Sont concernés create(), save(),
saveQuietly(), saveOrIgnore(), update(), ainsi que increment(),
decrement(), incrementEach() et decrementEach().
Le détail qui compte : l'ordre
La relecture a lieu avant les événements created et updated. Un
observer qui envoie la facture lit donc le bon total, et
wasChanged('total_ttc') répond true alors que votre code n'a jamais écrit
cette colonne : elle figure dans getChanges().
Les limites
- Chaque écriture coûte un
SELECTde plus : ne lister que les colonnes relues juste après. - La requête passe par la connexion d'écriture, une réplique pouvant ne pas avoir encore la ligne.
- Les scopes globaux sont ignorés : un scope de tenant ou de suppression douce ne peut pas masquer la ligne qu'on vient d'écrire.
- La relecture utilise
firstOrFail(): si la ligne a disparu entre-temps, la sauvegarde lève uneModelNotFoundException.
Comment s'y prendre
Cherchez les refresh() qui suivent un create() ou un update() : ce sont
les premiers candidats. L'attribut vaut aussi pour les colonnes remplies par un
trigger ou par une valeur par défaut que pose la base. Pour une écriture
isolée, gardez refresh() : l'attribut ajouterait une requête à toutes les
autres. Détails dans la
documentation Eloquent.
Source : Eloquent Refreshes: Load Generated Columns After Save, Laravel News, 24 septembre 2026.