I have a pivot table that connects users to workspaces. On the pivot table, I also have a column for role, which defines the users role for that workspace. Can I provide Accessor (Getter) & Mutator (Setter) methods on the role inside the pivot table? I have been trying to look all over, but details on pivot tables in eloquent are pretty sparse.
I am not sure if I have to setup a custom pivot model? If I do, an example would be awesome as the documentation on pivot models is very basic.
Thanks.
If all you need to do is access additional fields on the pivot table, you just need to use the withPivot() method on the relationship definition:
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
}
Now your role field will be available on the pivot table:
$user = User::first();
// get data
foreach($user->workspaces as $workspace) {
var_dump($workspace->pivot->role);
}
// set data
$workspaceId = $user->workspaces->first()->id;
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
If you really need to create accessors/mutators for your pivot table, you will need to create a custom pivot table class. I have not done this before, so I don't know if this will actually work, but it looks like you would do this:
Create a new pivot class that contains your accessors/mutators. This class should extend the default Pivot class. This new class is the class that is going to get instantiated when User or Workspace creates a Pivot model instance.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspacePivot extends Pivot {
getRoleAttribute() {
...
}
setRoleAttribute() {
...
}
}
Now, update your User and Workspace models to create this new pivot table class, instead of the default one. This is done by overriding the newPivot() method provided by the Model class. You want to override this method so that you create an instance of your new UserWorkspacePivot class, instead of the default Pivot class.
class User extends Model {
// normal many-to-many relationship to workspaces
public function workspaces() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
class Workspace extends Model {
// normal many-to-many relationship to users
public function users() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
I figured out how to use Accessors and Mutators on the Pivot table (I'm using Laravel 5.8)
You must use using() on your belongsToMany relationships, for example:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Workspace')->using('App\UserWorkspace');
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\User')->using('App\UserWorkspace');
}
}
So, use your Pivot model:
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspace extends Pivot {
public function getRoleAttribute() {
// your code to getter here
}
public function setRoleAttribute($value) {
// your code to setter here
}
}
This is a difficult question. The solutions I can think of are smelly and may cause some problems later on.
I am going to extend on Patricus's answer to make it work.
I was going to comment on Patricus's answer but there is simply too much to explain. To make his solution work with attach and sync we must do some ugly things.
The Problem
First let's identify the problem with his solution. His getters and setters do work but the belongsToMany relationship doesn't use the Pivot model when running sync, attach, or detach. This means every time we call one of these with the $attributes parameter the non-mutated data will be put into the database column.
// This will skip the mutator on our extended Pivot class
$user->workspaces()->attach($workspace, ['role' => 'new role value']);
We could just try to remember that every time we call one of these we can't use the second parameter to attach the mutated data and just call updateExistingPivot with the data that must be mutated. So an attach would be what Patricus stated:
$user->workspaces()->attach($workspace);
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
and we could never use the correct way of passing the pivot attributes as the attach methods second parameter shown in the first example. This will result in more database statements and code rot because you must always remember not to do the normal way. You could run into serious problems later on if you assume every developer, or even yourself, will just know not to use the attach method with the second parameter as it was intended.
The Solution (untested and imperfect)
To be able to call attach with the mutator on the pivot columns you must do some crazy extending. I haven't tested this but it may get you on the right path if you feel like giving it a try. We must first create our own relationship class that extends BelongsToMany and implements our custom attach method:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class UserWorkspaceBelongsToMany extends BelongsToMany {
public function attach($id, array $attributes = [], $touch = true)
{
$role = $attributes['role'];
unset($attributes['role']);
parent::attach($id, $attributes, $touch);
$this->updateExistingPivot($id, ['role' => $role], $touch);
}
// You will need sync here too
}
Now we have to make each Model::belongsToMany use our new UserWorkspaceBelongsToMany class instead of the normal BelongsToMany. We do this by mocking the belongsToMany in our User and Workspace class:
// put this in the User and Workspace Class
public function userWorkspaceBelongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new UserWorkspaceBelongsToMany($query, $this, $table, $foreignKey, $otherKey, $relation);
}
As you can see, we are still calling the database more but we don't have to worry about someone calling attach with the pivot attributes and them not getting mutated.
Now use that inside your models instead of the normal belongsToMany:
class User extends Model {
public function workspaces() {
return $this->userWorkspaceBelongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->userWorkspaceBelongsToMany('App\Models\User')->withPivot('role');
}
}
Its impossible to use setters, will not affect pivot table... make the change in the controller instead.
Related
I have a model called RealEstate, this model has a relation with another model called TokenPrice, I needed to access the oldest records of token_prices table using by a simple hasOne relation, So I did it and now my relation method is like following:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function firstTokenPrice(): HasOne
{
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
By far it's fine and no complexity. But now, I need to involve another relation into firstTokenPrice.
Let me explain a bit more:
As my project grown, the more complexity was added it, like changing firstTokenPrice using by a third table called opening_prices, so I added a new relation to RealEstate called lastOpeningPrice:
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
So the deal with simplicity of firstTokenPrice relation is now off the table, I want to do something like following every time a RealEstate object calls for its firstTokenPrice:
Check for lastOpeningPrice, if it was exists, then firstTokenPrice must returns a different record of token_price table, otherwise the firstTokenPrice must returns oldestOfMany of TokenPrice model.
I did something like following but it's not working:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
I have checked the $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany() using by ->toSql() method and it returns something unusual.
I need to return a HasOne object inside of firstTokenPrice method.
You can use ofMany builder for that purpose:
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->ofMany([
'id' => 'min',
], function ($query) use ($lop) {
$query->where('amount', $lop->amount);
});
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
I used ->oldest() with a custom scope called amounted in TokenPrice model:
class TokenPrice extends Model
{
public function scopeAmounted(Builder $query, OpeningPrice $openingPrice): Builder
{
return $query->where('amount', $openingPrice->amount);
}
/....
}
And then changed my firstTokenPrice
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount);
return $this->hasOne(TokenPrice::class)->amounted($lop)->oldest();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
It's working, but I don't know if it's the best answer or not
Thought I'd ask this as Laravel is the most elegant Framework I've come across and wondered if there was a "prettier way" of doing this.
I have a system which records books such that:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
}
In the system there are number of other models which extend from "Book" such as "Novel", "Biography" etc. Is there a way for Eloquent to provide me with a correctly cast object given the right info (i.e. a namespaced class)? Currently, I am obtaining the book and the casting it using the function at https://gist.github.com/borzilleri/960035 which works but doesn't feel very "tidy".
I can see a few different options here. One would be to write your class like this:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
public function biography()
{
return $this->belongsTo('\App\Biography')->where('type', 'biography');
}
public function novel()
{
return $this->belongsTo('\App\Novel')->where('type', 'novel');
}
}
You'd then need to know ahead of time which type of book it is though. Another would be to do something like this:
class Chapter extends Model
{
protected function parent_book()
{
return $this->belongsTo('\App\Book');
}
public function getBookAttribute()
{
$book = $this->parent_book;
if (!$book) return $book; // No related book.
if ($book->type == 'novel') return (Novel)$book;
if ($book->type == 'biography') return (Biography)$book;
return $book;
}
}
You still have to do all of the casting yourself, but at least it's all in one place and transparent to the rest of the app, as it can still just reference $chapter->book For this second solution, if you ever set $chapter->book = new Book(), you'd also need to make sure to make a setBookAttribute() function.
One more complicated possibility would be to create your own custom relationship type by extending the BelongsTo class and overriding getResults() to to the casting before returning the result. This would be pretty transparent from the outside and would let you still call $chapter->book() and treat it as a relationship.
This should be attributed to Joshua Dwire as he set me on the path to this solution. I was intrigued by his reference to extending the standard BelongsTo class and make it work for me. Ideally I want to be able to call a custom relationship:
$this->belongsToBook('\App\Book');
And for that function to return a correctly cast object.
Routing through the code I found that it was the trait HasRelationship used by Model which was responsible for returning the relationship. By changing that relationship we can change the implementation and therefore the returned object.
I also wanted to replicate the same methodology that Laravel employs so have mimiced it in my own app.
With all that in mind the first step is to create a new trait HasBookRelationship which can be used in a model to handle the call to $this->belongsToBook('\App\Book'):
trait HasBookRelationship
{
public function belongsToBook($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}
$instance = $this->newRelatedInstance($related);
if (is_null($foreignKey)) {
$foreignKey = \Str::snake($relation).'_'.$instance->getKeyName();
}
$ownerKey = $ownerKey ?: $instance->getKeyName();
//We change the return relationship here
**return new BelongsToBook(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);**
}
}
This is simply copied from the existing belongsTo method in the HasRelationships trait. The key thing here is that we are going to return a custom relationship BelongsToBook and use that to override what is returned. The last line of the method is changed to return our desired relationship class.
The class we use is extended from BelongsTo but we change the get method to cast the object before returning it.
class BelongsToBook extends BelongsTo
{
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
{
parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName);
}
public function get($columns = ['*'])
{
$objs = $this->query->get($columns);
//iterate over the collated objects...
$objs->transform(function($item)
{
//..and return a cast object with whatever method you want
return castTheCorrectObject($item);
});
return $objs;
}
}
castTheCorrectObject can be any casting function you like perhaps set up as a helper or another method in the relationship.
Once these are set up, we can empoy it in our own Model:
class Author extends Model
{
use HasBookRelationship;
public function books()
{
return $this->belongsToBook('\App\Book');
}
}
This will return a collection of correctly cast objects and maintains the relationship.
One thing did puzzle me though. The method I overrode in my BelongsToBook class was get() and not getResults() as suggested by Joshua. get() is defined in Relation and is inherited by BelongsTo where as getResults() is defined in BelongsTo. I'm not sure what the difference between getResults() and get() is nor why I had to override get() rather than getResults(). If anyone can shed any light , it would be appreciated.
I've got a observer that has a update method:
ObserverServiceProvider.php
public function boot()
{
Relation::observe(RelationObserver::class);
}
RelationObserver.php
public function updated(Relation $relation)
{
$this->cache->tags(Relation::class)->flush();
}
So when I update a relation in my controller:
public function update(Request $request, Relation $relation)
{
$relation->update($request->all()));
return back();
}
Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.
So now my controller method looks like this:
public function update(Request $request, Relation $relation)
{
if(empty($request->products)) {
$relation->products()->detach();
} else {
$relation->products()->sync(collect($request->products)->pluck('id'));
}
$relation->update($request->all());
return back();
}
The problem is that the observer is not triggered anymore if I only add or remove products.
How can I trigger the observer when the pivot table updates aswel?
Thanks
As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.
1 - To add some extra functionality to the sync() method:
If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:
public function products() {
// Product::class is by default the 1. argument in ->belongsToMany calll
$instance = $this->newRelatedInstance(Product::class);
return new BelongsToManySpecial(
$instance->newQuery(),
$this,
$this->joiningTable(Product::class), // By default the 2. argument
$this->getForeignKey(), // By default the 3. argument
$instance->getForeignKey(), // By default the 4. argument
null // By default the 5. argument
);
}
Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.
Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.
public function sync($ids, $detaching = true) {
// Call the parent class for default functionality
$changes = parent::sync($ids, $detaching);
// $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
// Add your functionality
// Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.
// Return the original response
return $changes
}
Alternatively override the detach and attachNew functions for similar results.
protected function attachNew(array $records, array $current, $touch = true) {
$result = parent::attachNew($records, $current, $touch);
// Your functionality
return $result;
}
public function detach($ids = null, $touch = true)
$result = parent::detach($ids, $touch);
// Your functionality
return $result;
}
If you want to dig deeper and want to understand what's going on under the hood then analyze the Illuminate\Database\Eloquent\Concerns\HasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.
2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class
trait BelongsToManySyncEvents {
public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
return new BelongsToManyWithSyncEvents(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
);
}
}
Create the BelongsToManyWithSyncEvents class:
class BelongsToManyWithSyncEvents extends BelongsToMany {
public function sync($ids, $detaching = true) {
$changes = parent::sync($ids, $detaching);
// Do your own magic. For example using these variables if needed:
// $this->get() - returns an array of objects given with the sync method
// $this->parent - Object they got attached to
// Maybe call some function on the parent if it exists?
return $changes;
}
}
Now add the trait to your class.
3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...
$functionName = 'on' . $this->foreignKey . 'Sync';
if(method_exists($this->parent), $functionName) {
$this->parent->$functionName($changes);
}
4 - Create a service
Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality
As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.
When I search about this topic, it came as the first result.
However, for newer Laravel version you can just make a "Pivot" model class for that.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tag';
public $timestamps = null;
}
For the related model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->using(PostTag::class);
}
and you have to put your declare your observer in EventServiceProvider
as stated in Laravel Docs
PostTag::observe(PostTagObserver::class);
Reference: Observe pivot tables in Laravel
Just add:
public $afterCommit = true;
at the beginning of the observer class.. It will wait until the transactions are done, then performs your sync which should then work fine..
Please check Laravel's documentation for that.
It seems this solutions was just added in Laravel 8.
I have the following Eloquent Models with relationships:
class Lead extends Model
{
public function contacts()
{
return $this->belongsToMany('App\Contact')
->withPivot('is_primary');
}
}
class Contact extends Model
{
public function leads()
{
return $this->belongsToMany('App\Lead')
->withPivot('is_primary');
}
}
The pivot table contains an additional param (is_primary) that marks a relationship as the primary. Currently, I see returns like this when I query for a contact:
{
"id": 565,
"leads": [
{
"id": 349,
"pivot": {
"contact_id": "565",
"lead_id": "349",
"is_primary": "0"
}
}
]
}
Is there a way to cast the is_primary in that to a boolean? I've tried adding it to the $casts array of both models but that did not change anything.
In Laravel 5.4.14 this issue has been resolved. You are able to define a custom pivot model and tell your relationships to use this custom model when they are defined. See the documentation, under the heading Defining Custom Intermediate Table Models.
To do this you need to create a class to represent your pivot table and have it extend the Illuminate\Database\Eloquent\Relations\Pivot class. On this class you may define your $casts property.
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class CustomPivot extends Pivot
{
protected $casts = [
'is_primary' => 'boolean'
];
}
You can then use the using method on the BelongsToMany relationship to tell Laravel that you want your pivot to use the specified custom pivot model.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Lead extends Model
{
public function contacts()
{
return $this->belongsToMany('App\Contact')->using('App\CustomPivot');
}
}
Now, whenever you access your pivot by using ->pivot, you should find that it is an instance of your custom pivot class and the $casts property should be honoured.
Update 1st June 2017
The issue raised in the comments by #cdwyer regarding updating the pivot table using the usual sync/attach/save methods is expected to be fixed in Laravel 5.5 which is due to be released next month (July 2017).
See Taylor's comment at the bottom of this bug report and his commit, fixing the issue here.
Since this is an attribute on the pivot table, using the $casts attribute won't work on either the Lead or Contact model.
One thing you can try, however, is to use a custom Pivot model with the $casts attribute defined. Documentation on custom pivot models is here. Basically, you create a new Pivot model with your customizations, and then update the Lead and the Contact models to use this custom Pivot model instead of the base one.
First, create your custom Pivot model which extends the base Pivot model:
<?php namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PrimaryPivot extends Pivot {
protected $casts = ['is_primary' => 'boolean'];
}
Now, override the newPivot() method on the Lead and the Contact models:
class Lead extends Model {
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new \App\PrimaryPivot($parent, $attributes, $table, $exists);
}
}
class Contact extends Model {
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new \App\PrimaryPivot($parent, $attributes, $table, $exists);
}
}
Good news! Tylor already fixed this bug:
https://github.com/laravel/framework/issues/10533
In Laravel 5.1 or higher you can use dot notation for pivot casts:
protected $casts = [
'id' => 'integer',
'courses.pivot.course_id' => 'integer',
'courses.pivot.active' => 'boolean'
]
The answer provided by #patricus above is absolutely correct, however, if like me you're looking to also benefit from casting out from JSON-encoded strings inside a pivot table then read on.
The Problem
I believe that there's a bug in Laravel at this stage. The problem is that when you instantiate a pivot model, it uses the native Illuminate-Model setAttributes method to "copy" the values of the pivot record table over to the pivot model.
This is fine for most attributes, but gets sticky when it sees the $casts array contains a JSON-style cast - it actually double-encodes the data.
A Solution
The way I overcame this is as follows:
1. Set up your own Pivot base class from which to extend your pivot subclasses (more on this in a bit)
2. In your new Pivot base class, redefine the setAttribute method, commenting out the lines that handle JSON-castable attributes
class MyPivot extends Pivot {
public function setAttribute($key, $value)
{
if ($this->hasSetMutator($key))
{
$method = 'set'.studly_case($key).'Attribute';
return $this->{$method}($value);
}
elseif (in_array($key, $this->getDates()) && $value)
{
$value = $this->fromDateTime($value);
}
/*
if ($this->isJsonCastable($key))
{
$value = json_encode($value);
}
*/
$this->attributes[$key] = $value;
}
}
This highlights the removal of the isJsonCastable method call, which will return true for any attributes you have casted as json, array, object or collection in your whizzy pivot subclasses.
3. Create your pivot subclasses using some sort of useful naming convention (I do {PivotTable}Pivot e.g. FeatureProductPivot)
4. In your base model class, change/create your newPivot method override to something a little more useful
Mine looks like this:
public function newPivot(Model $parent, array $attributes, $table, $exists)
{
$class = 'App\Models\\' . studly_case($table) . 'Pivot';
if ( class_exists( $class ) )
{
return new $class($parent, $attributes, $table, $exists);
}
else
{
return parent::newPivot($parent, $attributes, $table, $exists);
}
}
Then just make sure you Models extend from your base model and you create your pivot-table "models" to suit your naming convention and voilĂ you will have working JSON casts on pivot table columns on the way out of the DB!
NB: This hasn't been thoroughly tested and may have problems saving back to the DB.
I had to add some extra checks to have the save and load functions working properly in Laravel 5.
class BasePivot extends Pivot
{
private $loading = false;
public function __construct(Model $parent, array $attributes, $table, $exists)
{
$this->loading = true;
parent::__construct($parent, $attributes, $table, $exists);
$this->loading = false;
}
public function setAttribute($key, $value)
{
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key)) {
$method = 'set'.Str::studly($key).'Attribute';
return $this->{$method}($value);
}
// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
$value = $this->fromDateTime($value);
}
/**
* #bug
* BUG, double casting
*/
if (!$this->loading && $this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->asJson($value);
}
$this->attributes[$key] = $value;
return $this;
}
}
I look at many search results with this trouble but i can`t get it to work.
The User Model:
<?php namespace Module\Core\Models;
class User extends Model {
(...)
protected function Person() {
return $this->belongsTo( 'Module\Core\Models\Person', 'person_id' );
}
(...)
And the Person Model:
<?php namespace Module\Core\Models;
class Person extends Model {
(...)
protected function User(){
return $this->hasOne('Module\Core\Models\User', 'person_id');
}
(...)
Now, if i use User::find(1)->Person->first_name its work. I can get the Persons relations from the User Model.
But.. User::with('Person')->get() fails with a Call to undefined method Illuminate\Database\Query\Builder::Person()
What im doing wrong? i need a collection of all the users with their Person information.
You have to declare the relationship methods as public.
Why is that? Let's take a look at the with() method:
public static function with($relations)
{
if (is_string($relations)) $relations = func_get_args();
$instance = new static;
return $instance->newQuery()->with($relations);
}
Since the method is called from a static context it can't just call $this->Person(). Instead it creates a new instance of the model and creates a query builder instance and calls with on that and so on. In the end the relationship method has to be accessible from outside the model. That's why the visibility needs to be public.