php - hasone - Patrón de objeto nulo con relaciones Eloquent
que es belongsto laravel (3)
Puedes lograrlo usando fábricas modelo.
Defina una fábrica de autor dentro de su ModelFactory.php
$factory->define(App/Author::class, function (Faker/Generator $faker) {
return [
''name'' => $faker->firstName, //or null
''avatar'' => $faker->imageUrl() //or null
];
});
agregue valores para todos los atributos necesarios. Estoy usando valores ficticios de Faker, pero puede usar cualquier cosa que desee.
Luego, dentro de su modelo de libro puede devolver una instancia de autor como esta:
public function getAuthorAttribute($author)
{
return $author ?: factory(App/Author::class)->make();
}
A menudo ocurre que una determinada relación de modelo elocuente no está establecida (es decir, en una tabla de libros, author_id
es nulo) y, por lo tanto, llama a algo como $ model-> relation returns null.
Por ejemplo, un modelo de libro tiene una relación de autor () (hasOne) que podría querer hacer
$author = Book::find(1)->author->name;
Si el Libro 1 no tiene un autor establecido arrojará un error "tratando de obtener la propiedad de un objeto no". ¿Hay alguna forma de evitar esto y de forma predeterminada con un Author
blanco, así que siempre podré invocar el name
independientemente de si la relación se ha establecido para el modelo específico?
Básicamente, quiero evitar condicionales para comprobar si $book->author
es un Author
real antes de llamar a otros métodos / propiedades en él. Debería establecerse por defecto a una nueva instancia de Autor si la relación no está establecida.
Intenté algo como:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
sin embargo, esto no funciona; $author
se pasa como nulo, incluso si está configurado en el modelo. Presumiblemente porque es una relación más que una propiedad directa de un libro. Necesitaría algo como
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
que parece muy poco elegante y parece que anularía cualquier carga ansiosa que produjera un rendimiento deficiente.
Tuve el mismo problema en mi proyecto. En mi opinión, hay algunas filas que acceden a las propiedades dinámicas de las relaciones nulas, pero en lugar de devolver un campo vacío, la aplicación fue trepidante y una excepción.
Acabo de agregar un bucle foreach en mi controlador como una solución temporal que verifica en cada valor de la colección si la relación es nula. Si este caso es verdadero, asigna una nueva instancia del modelo de deseo a ese valor.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
De esta forma, cuando tengo acceso a $this->productivity->something
en mi opinión cuando la relación no está establecida, obtengo un valor vacío en lugar de una excepción sin poner ninguna lógica en mis vistas ni reemplazar los métodos.
Esperando una mejor solución para hacer esto automáticamente.
Actualizar
A partir de Laravel 5.3.23, ahora hay una forma integrada para lograr esto (al menos para HasOne
relaciones HasOne
). Se withDefault()
un método withDefault()
a la relación HasOne
. En el caso de su ejemplo de Book
/ Author
, su código se vería así:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
Esta relación ahora devolverá un modelo de Author
bastante vacío (se establecen las claves) si no se encuentra ningún registro en la base de datos. Además, puede pasar una serie de atributos si desea rellenar su modelo vacío con algunos datos adicionales, o puede pasar un Cierre que devuelve lo que le gustaría tener su configuración predeterminada (no tiene ser un modelo de Author
).
Hasta que esto llegue a la documentación un día, para obtener más información, puede consultar las solicitudes de extracción relacionadas con el cambio: 16198 y 16382 .
En el momento de escribir esto, esto solo se ha implementado para la relación HasOne
. Eventualmente puede migrar a las relaciones BelongsTo
, MorphOne
y MorphTo
, pero no puedo asegurarlo.
Original
No hay una forma de construir que conozca para hacer esto, pero hay un par de soluciones.
Usando un accesorio
El problema con el uso de un descriptor de acceso, como descubriste, es que el $value
pasa al descriptor de acceso siempre será null
, ya que se completa desde la matriz de atributos en el modelo. Esta matriz de atributos no incluye las relaciones, ya estén cargadas o no.
Si desea intentar resolver esto con un descriptor de acceso, simplemente ignorará el valor que se transfiere y comprobará la relación usted mismo.
public function getAuthorAttribute($value)
{
$key = ''author'';
/**
* If the relationship is already loaded, get the value. Otherwise, attempt
* to load the value from the relationship method. This will also set the
* key in $this->relations so that subsequent calls will find the key.
*/
if (array_key_exists($key, $this->relations)) {
$value = $this->relations[$key];
} elseif (method_exists($this, $key)) {
$value = $this->getRelationshipFromMethod($key);
}
$value = $value ?: new Author();
/**
* This line is optional. Do you want to set the relationship value to be
* the new Author, or do you want to keep it null? Think of what you''d
* want in your toArray/toJson output...
*/
$this->setRelation($key, $value);
return $value;
}
Ahora, el problema al hacer esto en el acceso es que necesita definir un acceso para cada relación hasOne / belongsTo en cada modelo.
Un segundo problema, más pequeño, es que el descriptor de acceso solo se usa al acceder al atributo. Entonces, por ejemplo, si tuvieras que cargar la relación, y luego dd()
o toArray
/ toJson
el modelo, aún mostraría null
para el relatioinship, en lugar de un Autor vacío.
Reemplazar los métodos del modelo
Una segunda opción, en lugar de usar acceso de atributos, sería anular algunos métodos en el Model
. Esto resuelve los dos problemas al usar un acceso de atributo.
Puede crear su propia clase de Model
base que amplíe el Model
Laravel y anule estos métodos, y luego todos sus otros modelos extenderán su clase de Model
base, en lugar de la clase de Model
de Laravel.
Para manejar relaciones cargadas ansiosas, necesitaría anular el método setRelation()
. Si usa Laravel> = 5.2.30, esto también manejará relaciones cargadas perezosas. Si usa Laravel <5.2.30, también tendrá que anular el método getRelationshipFromMethod()
para las relaciones de carga getRelationshipFromMethod()
.
MyModel.php
class MyModel extends Model
{
/**
* Handle eager loaded relationships. Call chain:
* Model::with() => Builder::with(): sets builder eager loads
* Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
* =>Relation::initRelation() => Model::setRelation()
* =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
*/
public function setRelation($relation, $value)
{
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When attempting
* to set to null, override with a new instance of the expected model.
*/
if (is_null($value)) {
// set the value to a new instance of the related model
$value = $this->$relation()->getRelated()->newInstance();
}
$this->relations[$relation] = $value;
return $this;
}
/**
* This override is only needed in Laravel < 5.2.30. In Laravel
* >= 5.2.30, this method calls the setRelation method, which
* is already overridden and contains our logic above.
*
* Handle lazy loaded relationships. Call chain:
* Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
*/
protected function getRelationshipFromMethod($method)
{
$results = parent::getRelationshipFromMethod($method);
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When the
* result is null, override with a new instance of the related model.
*/
if (is_null($results)) {
$results = $this->$method()->getRelated()->newInstance();
}
return $this->relations[$method] = $results;
}
}
Book.php
class Book extends MyModel
{
//
}