accessor considered as foreign key defined in OneToMany relations - php

My User model have these fields :
user_id
username
name
family
supervisor
And in that model I defined an accesssor that same name as supervisor attribute like this (because I want to return supervisor user as an User object and not a simple id):
public function getSupervisorAttribute($value)
{
return is_null($value) ? null : User::select('user_id', 'name', 'family')->find($value);
}
In the other hand there is a OneToMany relationship like this:
public function child()
{
return $this->hasMany(self::class, 'supervisor', 'user_id');
}
Now each time I call child() relation it return Illegal offset type error. seems that supervisor field does not recognized in second argument of hasMany method.
There is any way to solve this problem Without having to change accessor name.

I think the problem comes when you try to retrieve the relationship child, why? Because you have an accessor on your supervisor which is a foreign key inside of child relationship, so what happens is when you ask for that relationship, Laravel will try to use your supervisor property, since it has an accessor, it will trigger and instead of getting a desired property (which i guess is an integer), you will either get NULL or a User. I hope this clarifies it for you.
One workaround for this is to add appends attribute to your Model and then put mutators and accessors on that attribute.

If a User has children then it's one to many(he/she can have many children or none)
Anyway,
Lets assume you have a table named Children make sure you change the table name in the model(laravel assumes it's childrens in the DB).
If public function child() {} is in the User model then,
/*
* children since he/she can have many children
* hasMany means this model has many of the other model by self::class
* it's as if you're saying this model has many of this model so change it
*/
public function children()
{
/* you're building a relationship between User('user_id' as local primary key)
* and Children('parent_id' as foreign key)
* means children table has foreign key parent_id(unsignedInt)
* it returns an array of all the children objects of this User row
*/
return $this->hasMany('Children', 'parent_id', 'user_id');
}
On the other hand the Children Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Children extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'children';
public function parent()
{
// belongsTo means 'parent_id' in this model(Children) relates to 'user_id' on 'User' model
// it returns the User object which is the parent of this child row
return $this->belongsTo('User', 'user_id', 'parent_id');
}
}
This solution is for creating another table however it seems you want it with the same table(it's not very clear edit your post).
// this function makes no sense, it takes an integer and finds the parameter to was given
$userWithIdOne = $user->getSupervisorAttribute(1);
Give us the migrations of the table, show us the relationships.

Related

how to require attaching related resources upon creation of resources - Laravel Nova

I have a model called Tree that is supposed to be associated to 1..n Things. Things can be associated to 0..n things. In other words this is a many-to-many relationship, and a Thing must be chosen when a Tree is being created. My thing_tree migration looks like this (there's also a thing_thing pivot table but that's irrelevant):
public function up()
{
Schema::create('thing_tree', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->unsignedBigInteger('tree_id')->nullable();
$table->unsignedBigInteger('thing_id')->nullable();
$table->unique(['tree_id', 'thing_id']);
$table->foreign('tree_id')->references('id')->on('trees')->onDelete('cascade');
$table->foreign('thing_id')->references('id')->on('things')->onDelete('cascade');
});
}
My Tree model looks like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tree extends Model
{
use HasFactory;
protected $guarded = [];
public function path(){
$path = '/trees/' . $this->id;
return $path;
}
public function associatedThings () {
return $this->belongsToMany(Thing::class);
}
}
The Thing model looks like this:
public function trees()
{
return $this->belongsToMany(Tree::class);
}
public function parentOf (){
return $this->belongsToMany(Thing::class, 'thing_thing', 'parent_id', 'child_id');
}
public function childOf(){
return $this->belongsToMany(Thing::class, 'thing_thing', 'child_id', 'parent_id');
}
Finally, the Tree Nova resource has these fields:
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
Text::make('name'),
ID::make('user_id')->hideWhenUpdating()->hideWhenCreating(),
Boolean::make('public'),
BelongsToMany::make('Things', 'associatedThings')
];
}
It should not be possible to create a Tree without an attached Thing, but the creation screen looks like this:
How do I require this in Nova?
This is not possible through nova's default features. Here is how I would go about it with the least effort (you Might want to create a custom field for that yourself) - or at least how I solved a similar issue in the past:
1. Add the nova checkboxes field to your project
2. Add the field to your nova ressource :
// create an array( id => name) of things
$options = Things::all()->groupBy('id')->map(fn($e) => $e->name)->toArray();
// ...
// add checkboxes to your $fields
Checkboxes::make('Things', 'things_checkboxes')->options($options)
3. Add a validator that requires the things_checkboxes to be not empty
4. Add an observer php artisan make:observer CheckboxObserver that will sync the model's relations with the given id-array through the checkboxes and then remove the checkboxes field from the object (as it will throw a column not found otherwise), so something like this:
public function saving($tree)
{
// Note: In my case I would use the checkbox_relations method of the HasCheckboxes trait and loop over all checkbox relations to perform the following and get the respective array keys and relation names
$available_ids = array_unique($tree['things_checkboxes']);
// Attach new ones, remove old ones (Relation name in my case comes from the HasCheckboxes Trait)
$tree->things->sync($available_ids);
// Unset Checkboxes as the Key doesn't exist as column in the Table
unset($tree['things_checkboxes']);
return true;
}
5. Add the same thing in reverse for the retreived method in your observer if you want to keep using the checkboxes to handle relations. Otherwise, add ->hideWhenUpdating() to your checkbox field
I added a trait for that to easily attach the relations through checkboxes to a model:
trait HasCheckboxRelations
{
/**
* Boot the trait
*
* #return void
*/
public static function bootHasCheckboxRelations()
{
static::observe(CheckboxObserver::class);
}
/**
* Defines which relations should be display as checkboxes instead of
* #return CheckboxRelation[]
*/
public static function checkbox_relations()
{
return [];
}
}
And checkbox_relations holds an array of instances of class CheckboxRelation which again holds informations about the key name, the relation name and so on.
public function __construct(string $relationName, string $relatedClass, string $fieldName, bool $hasOverrides = false, string $relationType = null, array $_fields = [])
Also, I added a method attachCheckboxRelationFields to the default nova resource which will be called on the $fields when the model uses the trait.
Now, I only have to add HasCheckboxRelations to a model, add the array of checkbox_relations and thats it - I have a belongsToMany relation on the nova resource through checkboxes. Of course you don't have the option to manage pivot fields anymore if you go for it this way - which might be why it was not done by the nova devs - but for simple belongsToMany relations I really like to work with the checkbox solution instead of the default attach-table. And for data with pivot fields you can still use the default way.
Also note that parts of the code where written on the fly so it might not work out of the box, but the overall idea should be delivered.
Hope it helped!
alternative
https://github.com/Benjacho/belongs-to-many-field-nova
BelongsToManyField::make('Role Label', 'roles', 'App\Nova\Role'),

How to create relationship between 3 models in laravel?

SQL scheme:
bulletins
id increment
deals
id increment
seller_id
buyer_id
deals_items - items = bulletins
id increment
title
desc
bulletin_id
deal_id
How can I get deal row by bulletin id? In raw SQL it looks like:
select `deals`.* from `deals` inner join `deals_items` on `deals_items`.`deal_id` = `deals`.`id` where `deals_items`.`bulletin_id` = 10572
I tried:
public function deals()
{
return $this->hasManyThrough(DealItem::class,Deal::class, 'bulletin_id','dealid','id');
}
But it seems a wrong way. Can't find right way in laravel doc about relation.
#HCK shows right way.
but when I doing $bulletin->deals() in blade template I got empty collection of deals.
When just $bulletin->deal - all is fine, we have collection of deals.
I using protected $with = ['deals'] in my bulletin model, but what is different call method or property? Why with method empty result?
#Amarnasan was close, but the order of the foreign keys was wrong. Try this:
Deal.php
public function bulletins()
{
return $this
->belongsToMany(Bulletin::class, 'deals_items', 'deal_id', 'bulletin_id')
->withPivot('title','desc');
}
Bulletin.php
public function deals()
{
return $this
->belongsToMany(Deal::class, 'deals_items', 'bulletin_id', 'deal_id')
->withPivot('title','desc');
}
From the docs:
As mentioned previously, to determine the table name of the
relationship's joining table, Eloquent will join the two related model
names in alphabetical order. However, you are free to override this
convention. You may do so by passing a second argument to the
belongsToMany method:
return $this->belongsToMany('App\Role', 'role_user');
In addition to customizing the name of the joining table, you may also
customize the column names of the keys on the table by passing
additional arguments to the belongsToMany method. The third argument
is the foreign key name of the model on which you are defining the
relationship, while the fourth argument is the foreign key name of the
model that you are joining to:
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
Update
When you access the relationship as a method: $bulletin->deals() you are accessing the relationship itself. This will return an instance of \Illuminate\Database\Eloquent\Relations\BelongsToMany (in your case). Here the query is not executed yet, so you could keep adding constrains to your query, for example:
$bulletin
->deals()
->where('seller_id', 45) // <---
->skip(5) // <---
-> ... (And so on)
When you access it as a dynamic property, you are already executing the query, so this will return a Collection instance. Is the same as calling the relationship as a method and then attach the ->get() at the end, so this two are equivalent:
$bulletin->deals()->get()
// equals to:
$bulletin->deals
Check this other answer, it answers your question.
DealClass:
public function bulletins()
return $this->belongsToMany('App\Bulletin', 'deals_items ', 'bulletin_id', 'deal_id')->withPivot('title','desc');
}
BulletinClass:
public function deals()
return $this->belongsToMany('App\Deal', 'deals_items ', 'deal_id', 'bulletin_id')->withPivot('title','desc');
}
deals model -
public function bulletins()
return $this->belongsToMany(Bulletin::class, 'deals_items ', 'bulletin_id', 'deal_id');
}
bulletin model:-
public function deals()
{
return $this
->belongsToMany(Deal::class, 'deals_items', 'deal_id', 'bulletin_id',);
}

Laravel 5.6 OneToOne relation not working

I have a BuildingImage model with a OneToOne relation to BuildingType:
BuildImage Model:
/**
* Get the source type of the Building Image.
*/
public function type()
{
return $this->hasOne('App\BuildingType');
}
BuildingType Model:
/**
* Get the Building Image that owns the building type.
*/
public function buildingImage()
{
return $this->belongsTo('App\BuildingImage');
}
My tables:
building_images table -> source is the building type id
building_types table
When I try to do this in my controller just to test:
(an ImageRequest has one or more Builings and a Building has one BuildingType)
$imageRequest = ImageRequest::findOrFail($id);
$buildings = $imageRequest->buildingImages;
foreach ($buildings as $building) {
dd($building->type);
}
I get this error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column
'building_types.building_image_id' in 'where clause' (SQL: select *
from building_types where building_types.building_image_id = 45
and building_types.building_image_id is not null limit 1)
What am I doing wrong here?
That's because by default laravel will look for a primary key named {model}_id, and given that you are using a different column name (source), you need to specify when defining the relationship:
As the documentation states:
Eloquent determines the foreign key of the relationship based on the model name. In this case, the Phone model is automatically assumed to have a user_id foreign key. If you wish to override this convention, you may pass a second argument to the hasOne method:
return $this->hasOne('App\Phone', 'foreign_key');
Additionally, Eloquent assumes that the foreign key should have a value matching the id (or the custom $primaryKey) column of the parent. In other words, Eloquent will look for the value of the user's id column in the user_id column of the Phone record. If you would like the relationship to use a value other than id, you may pass a third argument to the hasOne method specifying your custom key:
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
Now that that is clear. Let's talk about the relationship itself.
You are defining that a BuildImage has one BuildingType. But with that logic, the foreign key should be stored in the building_types table, and not the other way around (source column appears in the building_images table). And -I'm just assuming that- many BuildImage can belongs to an specific BuildingType. So, if this assumption is correct:
a BuildImage belongs to a specific BuildingType.
a BuildinType can be specify in many BuildImages
So, you should define your relationship like this:
BuildImage.php
public function type()
{
return $this->belongsTo('App\BuildingType', 'source');
}
BuildingType.php
public function images()
{
return $this->hasMany(BuildingImage::class, 'source');
}
Your BuildImage model should be
/**
* Get the source type of the Building Image.
*/
public function type() {
return $this->hasOne('App\BuildingType',"id","source");
}
And BuildingType Model should be
/**
* Get the Building Image that owns the building type.
*/
public function buildingImage()
{
return $this->belongsTo('App\BuildingImage',"source","id");
}
This should work.
For more info have a look
https://laravel.com/docs/5.7/eloquent-relationships#one-to-one
Have you tried to indicate the index ID like this?
public function buildingImage()
{
return $this->belongsTo('App\BuildingImage', 'image_request_id');
}
Eloquent determines the foreign key of the relationship based on the
model name. In this case, the Phone model is automatically assumed to
have a user_id foreign key. If you wish to override this convention,
you may pass the second argument to the hasOne method:
return $this->hasOne('App\Phone', 'foreign_key');
https://laravel.com/docs/5.7/eloquent-relationships#one-to-one

use relationship in model accessor in laravel

Suppose I have a Course model like this :
class Course extends Model
{
public $primaryKey = 'course_id';
protected $appends = ['teacher_name'];
public function getTeacherNameAttribute ()
{
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
}
public function teacher ()
{
return $this->belongsTo('App\User', 'teacher', 'user_id');
}
}
And in the other hand there is a User model like this :
class User extends Authenticatable
{
public $primaryKey = 'user_id';
protected $appends = ['full_name'];
public function getFullNameAttribute ()
{
return $this->name . ' ' . $this->family;
}
public function course ()
{
return $this->hasMany('App\Course', 'teacher', 'user_id');
}
}
As you can see there is a hasMany relationship between those.
There is an full_name accessor in User model.
Now I want to add a teacher_name accessor to Course model that uses it's teacher relations and gets full_name of teacher and appends to Course always.
In fact I want whenever call a Course model, it's related teacher name included like other properties.
But every time , when call a Course model , I got this error :
exception 'ErrorException' with message 'Trying to get property of non-object' in D:\wamp\www\lms-api\app\Course.php:166
That refers to this line of Course model :
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
I do not know how can I solve that and what is problem exactly.
Yikes some interesting answers here.
FYI to those coming after me- getFooAttribute() should return the data, and not modify the internal attributes array.
If you set a new value in the attributes array (that doesnt exist in this model's db schema) and then attempt to save the model, you'll hit a query exception.
It's worth reading up the laravel docs on attribute accessors/mutators for more info.
Furthermore, if you need to access a related object from within the model (like in an accessor) you ought to call $related = $this->getRelation('foo'); - note that if the relation isnt loaded (e.g., if you didnt fetch this object/collection with eager loaded relations) then $this->getRelation() could return null, but crucially if it is loaded, it won't run the same query(ies) to fetch the data again. So couple that with if (!$this->relationLoaded('foo')) { $this->loadRelation('foo'); }. You can then interact with the related object/collection as normal.
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
Should be
$this->attributes['teacher_name'] = $this->teacher->full_name;
First thing is that you want to reference the relationship, so loose the brackets (), and because the relationship is belongsTo, you will have one user / teacher returned. So you don't need the first().
We haven't seen your fields but probably you will have to change:
return $this->belongsTo('App\User', 'teacher', 'user_id');
to
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
where foreign_key and other_key are the primary keys that you need to make the join on.
Check this link from the documentation for reference:
https://laravel.com/docs/5.4/eloquent-relationships#one-to-many-inverse
the right way to do this is:
COURSE
public function setTeacherNameAttribute ()
{
$this->attributes['teacher_name'] = $this->teacher->full_name;
}
100% working for me.
I have one to one relationship between Order and Shipment. I have to add the accessor of shipments table column from orders table.
function getOrderNoAttribute()
{
$appendText = "OR100";
if($this->orderShipment()->first()) {
$appendText = $this->orderShipment()->first()->is_shipping === 1 ? "ORE100" : "OR100";
}
return $appendText . $this->attributes['id'];
}
This error is only object data to array use or array data to object data use.
example::
$var->feild insted of $var[feild]
$var[feild] insted of $var->feild
You should use return for accessors . something like this :
public function getTeacherNameAttribute ()
{
return $this->teacher()->first()->full_name ?? '';
}
maybe a course hasn't teacher.

Null object pattern with Eloquent relations

There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.
E.g. say a Book model has an author() (hasOne) relation I might want to do
$author = Book::find(1)->author->name;
If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?
Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.
I tried something like:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.
Update
As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).
Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.
At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.
Original
There's no built in way that I know of to do this, but there are a couple workarounds.
Using an Accessor
The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.
If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.
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;
}
Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.
A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.
Overriding Model Methods
A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.
You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.
To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.
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
{
//
}
I had the same problem in my project. In my views there's some rows that are accesing to dinamics properties from null relationships, but instead of returning an empty field, the app was thrwoing and exception.
I just added a foreach loop in my controller as a temporal solution that verifies in every value of the collection if the relationship is null. If this case is true, it assigns a new instance of the desire model to that value.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
This way when I access to $this->productivity->something in my view when the relationship is unset, I get a empty value instead of an exception without putting any logic in my views nor overriding methods.
Waiting for a better solution to do this automatically.
You can achieve this using model factories.
Define an author factory inside your ModelFactory.php
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName, //or null
'avatar' => $faker->imageUrl() //or null
];
});
add values for all the needed attributes I am using dummy values from Faker but you can use anything you want.
Then inside your book model you can return an instance of Author like this:
public function getAuthorAttribute($author)
{
return $author ?: factory(App\Author::class)->make();
}

Categories