I'm using Laravel 4, and have 2 models:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product');
}
}
class Product extends \Eloquent {
public function assets() {
return $this->belongsToMany('Asset');
}
}
Product has the standard timestamps on it (created_at, updated_at) and I'd like to update the updated_at field of the Product when I attach/detach an Asset.
I tried this on the Asset model:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product')->withTimestamps();
}
}
...but that did nothing at all (apparently). Edit: apparently this is for updating timestamps on the pivot table, not for updating them on the relation's own table (ie. updates assets_products.updated_at, not products.updated_at).
I then tried this on the Asset model:
class Asset extends \Eloquent {
protected $touches = [ 'products' ];
public function products() {
return $this->belongsToMany('Product');
}
}
...which works, but then breaks my seed which calls Asset::create([ ... ]); because apparently Laravel tries to call ->touchOwners() on the relation without checking if it's null:
PHP Fatal error: Call to undefined method Illuminate\Database\Eloquent\Collection::touchOwners() in /projectdir/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 1583
The code I'm using to add/remove Assets is this:
Product::find( $validId )->assets()->attach( $anotherValidId );
Product::find( $validId )->assets()->detach( $anotherValidId );
Where am I going wrong?
You can do it manually using touch method:
$product = Product::find($validId);
$product->assets()->attach($anotherValidId);
$product->touch();
But if you don't want to do it manually each time you can simplify this creating method in your Product model this way:
public function attachAsset($id)
{
$this->assets()->attach($id);
$this->touch();
}
And now you can use it this way:
Product::find($validId)->attachAsset($anotherValidId);
The same you can of course do for detach action.
And I noticed you have one relation belongsToMany and the other hasMany - it should be rather belongsToMany in both because it's many to many relationship
EDIT
If you would like to use it in many models, you could create trait or create another base class that extends Eloquent with the following method:
public function attach($id, $relationship = null)
{
$relationship = $relationship ?: $this->relationship;
$this->{$relationship}()->attach($id);
$this->touch();
}
Now, if you need this functionality you just need to extend from another base class (or use trait), and now you can add to your Product class one extra property:
private $relationship = 'assets';
Now you could use:
Product::find($validId)->attach($anotherValidId);
or
Product::find($validId)->attach($anotherValidId, 'assets');
if you need to attach data with updating updated_at field. The same of course you need to repeat for detaching.
From the code source, you need to set $touch to false when creating a new instance of the related model:
Asset::create(array(),array(),false);
or use:
$asset = new Asset;
// ...
$asset->setTouchedRelations([]);
$asset->save();
Solution:
Create a BaseModel that extends Eloquent, making a simple adjustment to the create method:
BaseModel.php:
class BaseModel extends Eloquent {
/**
* Save a new model and return the instance, passing along the
* $options array to specify the behavior of 'timestamps' and 'touch'
*
* #param array $attributes
* #param array $options
* #return static
*/
public static function create(array $attributes, array $options = array())
{
$model = new static($attributes);
$model->save($options);
return $model;
}
}
Have your Asset and Product models (and others, if desired) extend BaseModel rather than Eloquent, and set the $touches attribute:
Asset.php (and other models):
class Asset extends BaseModel {
protected $touches = [ 'products' ];
...
In your seeders, set the 2nd parameter of create to an array which specifies 'touch' as false:
Asset::create([...],['touch' => false])
Explanation:
Eloquent's save() method accepts an (optional) array of options, in which you can specify two flags: 'timestamps' and 'touch'. If touch is set to false, then Eloquent will do no touching of related models, regardless of any $touches attributes you've specified on your models. This is all built-in behavior for Eloquent's save() method.
The problem is that Eloquent's create() method doesn't accept any options to pass along to save(). By extending Eloquent (with a BaseModel) to accept the $options array as the 2nd attribute, and pass it along to save(), you can now use those two options when you call create() on all your models which extend BaseModel.
Note that the $options array is optional, so doing this won't break any other calls to create() you might have in your code.
Related
What i know:
$this->$parent->childs(); //we get childs data
what i want to know how:
$this->child->find($id)->parent(); //how to get childs parent without including model in controller | by just using eloquent
heres my sample code of employee and employeeDependent model:
trait EmployeeRelationships{
public function dependents(){
return $this->hasMany(\App\DB\EmployeeDependent\EmployeeDependent::class);
}
}
trait EmployeeDependentRelationships{
/**
* #return mixed
*/
public function employee(){
return $this->belongsTo(\App\DB\Employee\Employee::class, 'employee_id');
}
}
If you want to get the reverse of a BelongsTo relationship you need to specify the inverse of the relationship on the corresponding model. For example:
Employee Class
class Employee extends Model
{
public dependents()
{
return $this->hasMany(Dependant::class);
}
}
Dependent Class
class Dependent extends Model
{
public employee()
{
return $this->belongsTo(Employee::class, 'employee_id');
}
}
With these relationships defined you can then access the relevant models by calling the appropriate methods like so:
$dependents = Employee::first()->dependents; // Returns an eloquent collection
$employee = Dependent::first()->employee; // Returns a model of type Employee
Note that in this example using the first() method to grab a model, you can can do this with any object of the correct type.
Using a REST approach I want to be able to save more than one model in a single action.
class MyController extends ActiveController {
public $modelClass = 'models\MyModel';
}
class MyModel extends ActiveRecord {
...
}
That automagically creates actions for a REST api. The problem is that I want to save more than one model, using only that code in a POST will result in a new record just for MyModel. What if I need to save AnotherModel?
Thanks for any suggestion.
ActiveController implements a common set of basic actions for supporting RESTful access to ActiveRecord. For more advanced use you will need to override them or just merge to them your own custom actions where you will be implementing your own code & logic.
Check in your app the /vendor/yiisoft/yii2/rest/ folder to see how ActiveController is structured and what is doing each of its actions.
Now to start by overriding an ActiveController's action by a custom one, you can do it within your controller. Here is a first example where i'm overriding the createAction:
1-
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions()
{
$actions = parent::actions();
unset($actions['create']);
return $actions;
}
public function actionCreate(){
// your code
}
}
2-
Or you can follow the ActiveController's structure which you can see in /vendor/yiisoft/yii2/rest/ActiveController.php by placing your custom actions in separate files. Here is an example where I'm overriding the updateAction by a custom one where i'm initializing its parameters from myController class :
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions() {
$actions = parent::actions();
$custom_actions = [
'update' => [
'class' => 'app\controllers\actions\WhateverAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
'params' => \Yii::$app->request->bodyParams,
],
];
return array_merge($actions, $custom_actions);
}
}
Now let's say as example that in my new action file app\controllers\actions\WhateverAction.php I'm expecting the Post Request (which i'm storing in $params) to have a subModels attribute storing a list of child models to which I'm going to apply some extra code like relating them with their parent model if they already exists in first place :
namespace app\controllers\actions;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use yii\web\ServerErrorHttpException;
use yii\rest\Action;
use app\models\YourSubModel;
class WhateverAction extends Action
{
public $scenario = Model::SCENARIO_DEFAULT;
public $params;
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
$model->load($this->params, '');
foreach ($this->params["subModels"] as $subModel) {
/**
* your code related to each of your model's posted child
* for example those lines will relate each child model
* to the parent model by saving that to database as their
* relationship has been defined in their respective models (many_to_many or one_to_many)
*
**/
$subModel = YourSubModel::findOne($subModel['id']);
if (!$subModel) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
$subModel->link('myParentModelName', $model);
//...
}
// ...
return $model;
}
}
So if I understand you wish to add a new database entry not only for the model you are querying, but for another model.
The best place to do this would be in the AfterSave() or BeforeSave() functions of the first model class. Which one would depend on the data you are saving.
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.
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.
I'm trying to get an array of all of my model's associations. I have the following model:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public function author()
{
return $this->belongsTo('Author');
}
public function category()
{
return $this->belongsTo('Category');
}
}
From this model, I'm trying to get the following array of its relations:
array(
'author',
'category'
)
I'm looking for a way to pull this array out from the model automatically.
I've found this definition of a relationsToArray method on an Eloquent model, which appears to return an array of the model's relations. It seems to use the $this->relations attribute of the Eloquent model. However, this method returns an empty array, and the relations attribute is an empty array, despite having my relations set up correctly.
What is $this->relations used for if not to store model relations? Is there any way that I can get an array of my model's relations automatically?
It's not possible because relationships are loaded only when requested either by using with (for eager loading) or using relationship public method defined in the model, for example, if a Author model is created with following relationship
public function articles() {
return $this->hasMany('Article');
}
When you call this method like:
$author = Author::find(1);
$author->articles; // <-- this will load related article models as a collection
Also, as I said with, when you use something like this:
$article = Article::with('author')->get(1);
In this case, the first article (with id 1) will be loaded with it's related model Author and you can use
$article->author->name; // to access the name field from related/loaded author model
So, it's not possible to get the relations magically without using appropriate method for loading of relationships but once you load the relationship (related models) then you may use something like this to get the relations:
$article = Article::with(['category', 'author'])->first();
$article->getRelations(); // get all the related models
$article->getRelation('author'); // to get only related author model
To convert them to an array you may use toArray() method like:
dd($article->getRelations()->toArray()); // dump and die as array
The relationsToArray() method works on a model which is loaded with it's related models. This method converts related models to array form where toArray() method converts all the data of a model (with relationship) to array, here is the source code:
public function toArray()
{
$attributes = $this->attributesToArray();
return array_merge($attributes, $this->relationsToArray());
}
It merges model attributes and it's related model's attributes after converting to array then returns it.
use this:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public $relationships = array('Author', 'Category');
public function author() {
return $this->belongsTo('Author');
}
public function category() {
return $this->belongsTo('Category');
}
}
So outside the class you can do something like this:
public function articleWithAllRelationships()
{
$article = new Article;
$relationships = $article->relationships;
$article = $article->with($relationships)->first();
}
GruBhub, thank you very much for your comments. I have corrected the typo that you mentioned.
You are right, it is dangerous to run unknown methods, hence I added a rollback after such execution.
Many thanks also to phildawson from laracasts, https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships
You can use the following trait:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Relations\Relation;
trait EloquentRelationshipTrait
{
/**
* Get eloquent relationships
*
* #return array
*/
public static function getRelationships()
{
$instance = new static;
// Get public methods declared without parameters and non inherited
$class = get_class($instance);
$allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC);
$methods = array_filter(
$allMethods,
function ($method) use ($class) {
return $method->class === $class
&& !$method->getParameters() // relationships have no parameters
&& $method->getName() !== 'getRelationships'; // prevent infinite recursion
}
);
\DB::beginTransaction();
$relations = [];
foreach ($methods as $method) {
try {
$methodName = $method->getName();
$methodReturn = $instance->$methodName();
if (!$methodReturn instanceof Relation) {
continue;
}
} catch (\Throwable $th) {
continue;
}
$type = (new \ReflectionClass($methodReturn))->getShortName();
$model = get_class($methodReturn->getRelated());
$relations[$methodName] = [$type, $model];
}
\DB::rollBack();
return $relations;
}
}
Then you can implement it in any model.
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use App\Traits\EloquentRelationshipTrait;
class User extends Authenticatable
{
use Notifiable, HasApiTokens, EloquentRelationshipTrait;
Finally with (new User)->getRelationships() or User::getRelationships() you will get:
[
"notifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"readNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"unreadNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"clients" => [
"HasMany",
"Laravel\Passport\Client",
],
"tokens" => [
"HasMany",
"Laravel\Passport\Token",
],
]
I have published a package in order to get all eloquent relationships from a model. Such package contains the helper "rel" to do so.
Just run (Composer 2.x is required!):
require pablo-merener/eloquent-relationships
If you are on laravel 9, you are able to run artisan command model:show