En cierto proyecto, definí un Trait
("Concern
" en nomenclatura Laraveliana) para encapsular cierta funcionalidad relacionada con modelos de datos (des)activables, Como se puede apreciar, una de las cosas que lleva a cabo el trait es sobreescribir el constructor para llevar a cabo un poco de monkey patching sobre algunos atributos.
trait Activatable
{
protected string $activatableField = 'is_active';
public function __construct()
{
parent::__construct();
$this->fillable[] = $this->activatableField;
$this->casts[] = [
$this->activatableField => 'boolean'
];
}
}
Hasta ahí todo bien. Los modelos que implementaban ese trait, funcionaron correctamente en todos los escenarios requeridos menos en uno: las factorías.
Al ejecutar
DummyModel::factory()->count(50)->make()
Para generar 50 modelos con datos aleatorios (con fines de testing unitario) me devolvía todos los modelos como objetos vacíos:
[
App\Models\DummyModel {#9845},
App\Models\DummyModel {#9846},
App\Models\DummyModel {#9847},
App\Models\DummyModel {#9848},
...
]
Cosa a priori inexplicable. ¿Por qué en todos los demás casos era capaz de construirme un objeto con todos los atributos y justamente en ese escenario en concreto no?
Tras un tiempo de investigación encontré la respuesta: el constructor del trait.
Los métodos definidos en un trait, sobreescriben a los métodos del objeto que lo invoca. Y claro, mi constructor estaba sobreescribiendo al constructor de los modelos.
public function __construct()
{
parent::__construct();
$this->fillable[] = $this->activatableField;
$this->casts[] = [
$this->activatableField => 'boolean'
];
}
Pero... ¿No era esa la idea? (sobreescribir el constructor del modelo para que aceptara las modificaciones en los atributos) Por supuesto. Salvo por una pequeña diferencia: el constructor definido en \Illuminate\Database\Eloquent\Model
se ve así:
public function __construct($attributes = [])
{
$this->attributes = $attributes;
...
}
Al yo omitir el parámetro $attributes
en el constructor del trait, le indico de manera implícita que $this->attributes
siempre sea equivalente a un array vacío (siempre que se asigne desde el constructor).
Y claro, la mayoría de operaciones con eloquent asignan los atributos mediante métodos del objeto, en lugar de desde el constructor, pero... las factorías sí lo hacen. Tiene lógica, ya que internamente se le pasan los parámetros calculados directamente al objeto de destino durante la creación de la instancia.
Entonces... ¿Cuál fue la solución al error? Simplemente, definí el parámetro en el constructor de mi trait y lo proporcioné al constructor padre (el del modelo). De esa forma, la cadena de herencia queda inalterada.
trait Activatable
{
protected string $activatableField = 'is_active';
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->fillable[] = $this->activatableField;
$this->casts[] = [
$this->activatableField => 'boolean'
];
}
}
Y al ejecutar de nuevo
DummyModel::factory()->count(50)->make()
Magia =D
[
App\Models\DummyModel {#3598
id: 1,
name: "Foo 1",
slug: "foo-1",
parent_id: 3
},
App\Models\DummyModel {#3599
id: 2,
name: "Foo 2",
slug: "foo-2",
parent_id: null
},
App\Models\DummyModel {#3600
id: 3,
name: "Foo 3",
slug: "foo-3",
parent_id: null
},
App\Models\DummyModel {#3601
id: 4,
name: "Foo 4",
slug: "foo-4",
parent_id: 5
}
...
]