Eloquent relationships for not-existing model class - laravel

I would like to have in my applications many models/modules but some of them would be removed for some clients.
Now I have such relation:
public function people()
{
return $this->hasMany('People', 'model_id');
}
and when I run $model = Model::with('people')->get(); it is working fine
But what if the People model doesn't exist?
At the moment I'm getting:
1/1 ErrorException in ClassLoader.php line 386: include(...): failed
to open stream: No such file or directory
I tried with
public function people()
{
try {
return $this->hasMany('People', 'model_id');
}
catch (FatalErrorException $e) {
return null;
}
}
or with:
public function people()
{
return null; // here I could add checking if there is a Model class and if not return null
}
but when using such method $model = Model::with('people')->get(); doesn't work.
I will have a dozens of relations and I cannot have list of them to use in with. The best method for that would be using some empty relation (returning null) just to make Eloquent not to do anything but in this case Eloquent still tries to make it work and I will get:
Whoops, looks like something went wrong.
1/1 FatalErrorException in Builder.php line 430: Call to a member function
addEagerConstraints() on null
Is there any simple solution for that?

The only solution I could come up with is creating your own Eloquent\Builder class.
I've called it MyBuilder. Let's first make sure it gets actually used. In your model (preferably a Base Model) add this newEloquentBuilder method:
public function newEloquentBuilder($query)
{
return new MyBuilder($query);
}
In the custom Builder class we will override the loadRelation method and add an if null check right before addEagerConstraints is called on the relation (or in your case on null)
class MyBuilder extends \Illuminate\Database\Eloquent\Builder {
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation == null){
return $models;
}
$relation->addEagerConstraints($models);
call_user_func($constraints, $relation);
$models = $relation->initRelation($models, $name);
$results = $relation->getEager();
return $relation->match($models, $results, $name);
}
}
The rest of the function is basically the identical code from the original builder (Illuminate\Database\Eloquent\Builder)
Now simply add something like this in your relation function and it should all work:
public function people()
{
if(!class_exist('People')){
return null;
}
return $this->hasMany('People', 'model_id');
}
Update: Use it like a relationship
If you want to use it like you can with a relationship it gets a bit more tricky.
You have to override the getRelationshipFromMethod function in Eloquent\Model. So let's create a Base Model (Your model obviously needs to extend it then...)
class BaseModel extends \Illuminate\Database\Eloquent\Model {
protected function getRelationshipFromMethod($key, $camelKey)
{
$relations = $this->$camelKey();
if ( $relations instanceof \Illuminate\Database\Eloquent\Collection){
// "fake" relationship
return $this->relations[$key] = $relations;
}
if ( ! $relations instanceof Relation)
{
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation');
}
return $this->relations[$key] = $relations->getResults();
}
}
Now we need to modify the relation to return an empty collection
public function people()
{
if(!class_exist('People')){
return new \Illuminate\Database\Eloquent\Collection();
}
return $this->hasMany('People', 'model_id');
}
And change the loadRelation function in MyBuilder to check for the type collection instead of null
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation instanceof \Illuminate\Database\Eloquent\Collection){
return $models;
}
// ...
}

Related

Laravel trait crud controller with request validation and resources

I'm trying to refactor my code to be more reusable.
I created a trait CrudControllerTrait to implement the index,show,store,update,destroy methods.
But I found 2 problems:
BrandController.php
public function store(BrandNewRequest $request)
{
$requestData = $request->validated();
return new BrandResource($this->brands->store($requestData));
}
ProductController.php
public function store(ProductNewRequest $request)
{
$requestData = $request->validated();
return new ProductResource($this->products->store($requestData));
}
The trait method would be:
public function store(xxxxx $request)
{
$requestData = $request->validated();
return new xxxxxResource($this->repository()->store($requestData));
}
Problem1: The hint type. How can I abstract them? If I remove it shows that errror:
"message": "Too few arguments to function App\\Http\\Controllers\\BrandController::store(), 0 passed and exactly 1 expected"
Problem2: Return the resource. How can create the new resource? On the collection I can solve it doing this:
public function index()
{
$models = $this->repository()->index();
return $this->resource()::collection($models);
}
The resource is on the controller who uses the trait:
public function resource()
{
return BrandResource::class;
}
But with single resource didn't know how to do it...
The idea is, that I have so much controllers using the same pattern: BrandController, ProductController, etc. I'd love to reuse these 5 crud methods on the same trait...
The only way I found is creating an abstract method.
trait CrudRepositoryTrait
{
abstract function model();
public function index()
{
return $this->model()::with($this->with())->get();
}
public function find($id)
{
return $this->model()::findOrFail($id);
}
public function store($data)
{
$request = $this->dtoRequest($data);
return $this->model()::create($request);
}
(...)
}
And then, an example how to use this treat:
class ProductRepository implements ProductRepositoryContract
{
use CrudRepositoryTrait;
function model()
{
return Product::class;
}
(...)
}
By this way I could reuse a lot of code.

How to filter a polymorphic relationship to only return a specific model?

I'm having an issue with defining a function to filter a polymorphic relationship and only return a specific model. I'll try to explain below:
Say I have three models: A, B, and C. Model A can belong to either of the other two models. Say we're using the polymorphism field name of recipient, so on our model A database schema we have recipient_type and recipient_id.
On model A, I have a the default function called recipient, defined like so:
public function recipient()
{
return $this->morphTo();
}
However, I want a function called b() which will return a relationship so that it can be used with a query builder using the with() function. The idea being I can call $a->b and it will either return an instance of B or null, depending on whether the instance of A belongs to and instance of B...
Sorry this has been a bit of a mouthful..
Appreciate all the help I can get with this one!
Cheers!
You can define it like this
Model A (define accessor)
public function recipient()
{
return $this->morphTo();
}
public function bRelation()
{
return $this->belongsTo(B::class, 'recipient_id', 'id');
}
public function cRelation()
{
return $this->belongsTo(C::class, 'recipient_id', 'id');
}
public function getBAttribute(){ //define accessor
if($this->recipient_type == 'App\B') return $this->bRelation;
return null;
}
public function getCAttribute(){ //define accessor
if($this->recipient_type == 'App\C') return $this->cRelation;
return null;
}
Now use it with eager loading
$records = A::with('bRelation', 'cRelation')->get();
foreach($records as $a){
dd($a->b); //it will return you either instance of `B` or `null`
}
Works well with accessors:
public function getIssueAttribute()
{
if($this->commentable_type == 'issues') return $this->commentable;
return null;
}
public function getProjectAttribute()
{
if($this->commentable_type == 'projects') return $this->commentable;
return null;
}
public function commentable()
{
return $this->morphTo('commentable');
}

Custom Eloquent relation which sometimes returns $this Model

I have a Model Text which has a 1-to-many-relation called pretext(), returning a 1-to-many-Relationshop to Text, like so:
class Text extends Model
{
public function pretext(){
return $this->belongsTo('App\Models\Text', 'pretext_id');
}
public function derivates(){
return $this->hasMany('App\Models\Text', 'pretext_id');
}
}
If a $text does not have any pretext (which, in my scenario, means $text['pretext_id'] == 0) $text->pretext() shall return the $text itself. When I try
public function pretext(){
if ( $this->belongsTo('App\Models\Text', 'pretext_id') ) {
return $this->belongsTo('App\Models\Text', 'pretext_id');
}
else {
return $this;
}
}
I get the error
local.ERROR: LogicException: Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
when the else-part is executed. So my questions are:
How do I turn $this into an object of type Relation? or alternatively:
How can I achieve my goal on a different way?
Try dynamic props
class Text extends Model
{
protected $appends = ['content'];
public function pretext(){
return $this->belongsTo('App\Models\Text', 'pretext_id');
}
public function getContentAttribute(){
$pretext = $this->pretext()->get();
if ($pretext->count()) {
return $pretext;
}
return $this;
}
}
Then in controller or view if you have the instance
(consider optimizing it if you have N+1 issues)
$obj = Text::find(1);
dd($obj->content);
I think you can create another method that calling pretext() and check the returned value.
public function getPretext() {
$value = pretext();
return ($value)? $value : $this;
}

Illuminate Eloquent return null relation on a relation returning different objects

I have an object in Illuminate that has a relation that can return different objets depending on a property of the main object
public function relation(){
switch($this->type){
case "type_1":
return $this->belongsTo('\Models\Type1', 'idElement');
break;
case "type_2":
return $this->belongsTo('\Models\Type2', 'idElement');
break;
default:
return null;
}
}
This produces an error of "Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation" when the "default" section is executed.
Also I can't instantiate a new Illuminate\Database\Eloquent\Relations\Relation() as it is an abstract class.
I could create an empty table and return a relation to this empty table that will always return an empty value, but this is not a solution.
How can I return an empty relation in the default option?
UPDATE:
I have changed it to use polymorphic relations, but now the problem is: how to set a polymorphic relation as an optional relation?
Relation::morphMap([
'type_1' => \App\Models\Type1::class,
'type_2' => \App\Models\Type2::class
]);
....
public function relation(){
return $this->morphTo(null, 'type', 'idElement');
}
I suggest creating a stub relationship class and use it when you need to return always empty (or fixed) collection.
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
/**
* Stub relationship
*/
class EmptyRelation extends Relation
{
public function __construct() {}
public function addConstraints() {}
public function addEagerConstraints(array $models) {}
public function initRelation(array $models, $relation) {}
public function match(array $models, Collection $results, $relation)
{
return $models;
}
public function getResults()
{
return new Collection();
}
}
Then in your default section you do:
return new \App\EmptyRelation();
Well..this is probably not the best option but works
public function relation(){
switch($this->type){
case "type_1":
return $this->belongsTo('\Models\Type1', 'idType1');
break;
case "type_2":
return $this->belongsTo('\Models\Type2', 'idType2');
break;
default:
return $this->hasOne('\Models\ThisModel', 'id','id')->where('id','<',0 );//Assuming you have an id autoincrement field that starts in 1 in your table,
}
}
This will return an empty collection, I'm not sure if this is what you are looking for

Laravel relationships avoid query where foreign key is null

When eager loading, is it possible to avoid doing an extra query when the foreign key in a relationship is null and therefore does not match any related record? In my example I have a Product and User.
A Product is owned by a User but can also be optionally edited by a User. So my model looks like this:
class Product extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function editor()
{
return $this->belongsTo(User::class, 'editor_id');
}
}
When a product has not been edited, it's editor_id attribute is NULL.
If I hadn't been eager loading I know I could do something like the following:
$product = Product::find(1);
if (!is_null($product->editor_id)) {
// Load the relation
}
However, this isn't an option for me and I would ideally like to avoid an extra, unnecessary query being run when eager loading:
Query: select * from `users` where `users`.`id` in (?)
Bindings: [0]
I was wondering if something similar to the following would be possible?
public function editor()
{
if (!is_null($this->editor_id)) {
return $this->belongsTo(User::class, 'editor_id');
}
}
When doing the above I get this error:
Call to a member function addEagerConstraints() on a non-object
I'm guessing that this is because it's not guaranteed that this method returns a Relation object.
Thanks
I solved this by creating a new Relation subclass that implements the required methods but simply returns null when actually obtaining results:
namespace My\App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class NullRelation extends Relation {
public function __construct() {}
public function addConstraints() {}
public function addEagerConstraints(array $models) {}
public function initRelation(array $models, $relation) {}
public function match(array $models, Collection $results, $relation) {
return [];
}
public function getResults() {
return null;
}
}
Then inside your relation method(s) you can check for null and return an instance of NullRelation instead:
public function editor() {
if ($this->editor_id === null) {
return new NullRelation();
} else {
return $this->belongsTo(User::class, 'editor_id');
}
}
It's a bit ugly and you'd be repeating youself a lot, so if I were using this in more than one place I'd probably create a subclass of the Model, then create versions of the belongsTo, hasOne methods and perform the check there:
public function belongsToIfNotNull($related, $foreignKey = null, $otherKey = null, $relation = null) {
if ($this->$foreignKey === null) {
return new NullRelation();
} else {
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
}
Finally in your modal that inherits the new subclass, your relation method simply becomes
public function editor() {
return $this->belongsToIfNotNull(User::class, 'editor_id');
}
Laravel Docs
Querying Relations When Selecting
When accessing the records for a model, you may wish to limit your results based on the existence of a relationship.
$posts= Post::has('editor')->get();
You may also specify an operator and a count:
$posts = Post::has('editor', '>=', 3)->get();

Resources