Laravel, trait to create additional record during saving? - php

I want to save additional record in database while saving a model in laravel, my model looks like:
class Document extends Model
{
use DocumentSetup;
}
And my trait looks like:
trait DocumentSetup {
protected static function boot()
{
static::saving(function ($model) {
$documentSetup = new DocumentSetup();
$documentSetup->document_id = $model->id;
$documentSetup->is_public = false;
$documentSetup->need_verification = true;
$documentSetup->save();
});
parent::boot();
}
}
If I try that I don't get any error, but document or document setup are not created, does anyone know what i'm doing wrong here?
My idea is to create this additional model while saving...

When using traits for Eloquent lifecycle hooks, you must name the boot method boot[traitName] in your case bootDocumentSetup. You should also remove the parent::boot() call in the trait, as there is no such parent call. If you name it like this it will work.
This is to avoid clashes when you are using one or more traits in a model, that each have its own boot method.

Related

How to pass parameter to a laravel elequent model's event observer

I have a model in laravel and I want to do something after the first time which an object of my model is created. the simplest way is to add a static boot method inside my model's class like the code below:
class modelName extends Model
{
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
});
}
}
so far so good! the problem is: the ONLY parameter that created method accepts is the model object itself(according to the documentation) :
Each of these methods receives the model as their only argument.
https://laravel.com/docs/5.5/eloquent#events
I need more arguments to work with after model creation. how can I do that?
Or is there any other way to do something while it's guaranteed that the model has been created?
laravel version is 5.5.
You're close. What I would probably do would be to dispatch an event right after you actually create the model in your controller. Something like this.
class WhateverController
{
public function create()
{
$model = Whatever::create($request->all());
$anotherModel = Another::findOrFail($request->another_id);
if (!$model) {
// The model was not created.
return response()->json(null, 500);
}
event(new WhateverEvent($model, $anotherModel));
}
}
I solved the issue using static property in eloquent model class:
class modelName extends Model
{
public static $extraArguments;
public function __construct(array $attributes = [],$data = [])
{
parent::__construct($attributes);
self::$extraArguments = $data ;
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
self::$extraArguments; // is available in here
});
}
}
It works! but I don't know if it may cause any other misbehavior in the application.
Using laravel events is also a better and cleaner way to do that in SOME cases.but the problem with event solution is you can't know if the model has been created for sure and it's time to call the event or it's still in creating status ( and not created status).

Traits with PHP and Laravel

I am using Laravel 5.1 and would like to access an array on the Model from the Trait when the Model before the model uses the appends array.
I would like to add certain items to the appends array if it exists from my trait. I don't want to edit the model in order to achieve this. Are traits actually usable in this scenario or should I use inheritance?
array_push($this->appends, 'saucedByCurrentUser');
Here is how my current setup works.
Trait
<?php namespace App;
trait AwesomeSauceTrait {
/**
* Collection of the sauce on this record
*/
public function awesomeSauced()
{
return $this->morphMany('App\AwesomeSauce', 'sauceable')->latest();
}
public function getSaucedByCurrentUserAttribute()
{
if(\Auth::guest()){
return false;
}
$i = $this->awesomeSauced()->whereUserId(\Auth::user()->id)->count();
if ($i > 0){
return true;
}
return false;
}
}
Model
<?php namespace App;
use App\AwesomeSauceTrait;
use Illuminate\Database\Eloquent\Model;
class FairlyBlandModel extends Model {
use AwesomeSauceTrait;
protected $appends = array('age','saucedByCurrentUser');
}
What I would like to do is something to achieve the same effect as extending a class. I have a few similar traits, so using inheritance gets somewhat ugly.
trait AwesomeSauceTrait {
function __construct() {
parent::__construct();
array_push($this->appends, 'saucedByCurrentUser');
}
}
I have seen some workarounds for this, but none of them seem better/cleaner than just adding the item to the array manually. Any ideas are appreciated.
Update
I discovered this way of accomplishing what I need for one trait, but it only works for one trait and I don't see an advantage of using this over inheritance.
trait
protected $awesomeSauceAppends = ['sauced_by_current_user'];
protected function getArrayableAppends()
{
array_merge($this->appends, $this->awesomeSauceAppends);
parent::getArrayableAppends();
}
How I am currently handling my Model, for what it is worth.
model
public function __construct()
{
array_merge($this->appends, $this->awesomeSauceAppends);
}
Traits are sometimes described as "compiler-assisted copy-and-paste"; the result of using a Trait can always be written out as a valid class in its own right. There is therefore no notion of parent in a Trait, because once the Trait has been applied, its methods are indistinguishable from those defined in the class itself, or imported from other Traits at the same time.
Similarly, as the PHP docs say:
If two Traits insert a method with the same name, a fatal error is produced, if the conflict is not explicitly resolved.
As such, they are not very suitable for situations where you want to mix in multiple variants of the same piece of behaviour, because there is no way for base functionality and mixed in functionality to talk to each other in a generic way.
In my understanding the problem you're actually trying to solve is this:
add custom Accessors and Mutators to an Eloquent model class
add additional items to the protected $appends array matching these methods
One approach would be to continue to use Traits, and use Reflection to dynamically discover which methods have been added. However, beware that Reflection has a reputation for being rather slow.
To do this, we first implement a constructor with a loop which we can hook into just by naming a method in a particular way. This can be placed into a Trait of its own (alternatively, you could sub-class the Eloquent Model class with your own enhanced version):
trait AppendingGlue {
public function __construct() {
// parent refers not to the class being mixed into, but its parent
parent::__construct();
// Find and execute all methods beginning 'extraConstruct'
$mirror = new ReflectionClass($this);
foreach ( $mirror->getMethods() as $method ) {
if ( strpos($method->getName(), 'extraConstruct') === 0 ) {
$method->invoke($this);
}
}
}
}
Then any number of Traits implementing differently named extraConstruct methods:
trait AwesomeSauce {
public function extraConstructAwesomeSauce() {
$this->appends[] = 'awesome_sauce';
}
public function doAwesomeSauceStuff() {
}
}
trait ChocolateSprinkles {
public function extraConstructChocolateSprinkles() {
$this->appends[] = 'chocolate_sprinkles';
}
public function doChocolateSprinklesStuff() {
}
}
Finally, we mix in all the traits into a plain model, and check the result:
class BaseModel {
protected $appends = array('base');
public function __construct() {
echo "Base constructor run OK.\n";
}
public function getAppends() {
return $this->appends;
}
}
class DecoratedModel extends BaseModel {
use AppendingGlue, AwesomeSauce, ChocolateSprinkles;
}
$dm = new DecoratedModel;
print_r($dm->getAppends());
We can set the initial content of $appends inside the decorated model itself, and it will replace the BaseModel definition, but not interrupt the other Traits:
class ReDecoratedModel extends BaseModel {
use AppendingGlue, AwesomeSauce, ChocolateSprinkles;
protected $appends = ['switched_base'];
}
However, if you over-ride the constructor at the same time as mixing in the AppendingGlue, you do need to do a bit of extra work, as discussed in this previous answer. It's similar to calling parent::__construct in an inheritance situation, but you have to alias the trait's constructor in order to access it:
class ReConstructedModel extends BaseModel {
use AppendingGlue { __construct as private appendingGlueConstructor; }
use AwesomeSauce, ChocolateSprinkles;
public function __construct() {
// Call the mixed-in constructor explicitly, like you would the parent
// Note that it will call the real parent as well, as though it was a grand-parent
$this->appendingGlueConstructor();
echo "New constructor executed!\n";
}
}
This can be avoided by inheriting from a class which either exists instead of the AppendingGlue trait, or already uses it:
class GluedModel extends BaseModel {
use AppendingGlue;
}
class ReConstructedGluedModel extends GluedModel {
use AwesomeSauce, ChocolateSprinkles;
public function __construct() {
// Standard call to the parent constructor
parent::__construct();
echo "New constructor executed!\n";
}
}
Here's a live demo of all of that put together.
I thought I'd add an update for 2019 since this was one of the first discussions that popped up when trying to do a similar thing. I'm using Laravel 5.7 and nowadays Laravel will do the reflection that IMSoP mentioned.
After the trait has been booted, Laravel will then call initializeTraitName() on the constructed object (where TraitName is the full name of the trait).
To add extra items to $appends from a trait, you could simply do this...
trait AwesomeSauceTrait {
public function initializeAwesomeSauceTrait()
{
$this->appends[] = 'sauced_by_current_user';
}
public function getSaucedByCurrentUserAttribute()
{
return 'whatever';
}
}
KISS:
I don't see any reason why you should use trait when your are simply appending attributes.
I would only recommend using trait without a constructor like you were doing, only if you model is getting pretty bulky and you wish to slim down things.
Please also note this not the correct way of appending attribute
protected $appends = array('age','saucedByCurrentUser');
You could do this:
protected $appends = array('age','sauced_by_current_user');
Appends attribute names should the snake_case of its method Name
Edited:
The idea behind appends is to dynamically add fields that doesn't exist in your database table to your model so after you can do like:
$model = FairlyBlandModel ::find(1);
dd($model->sauced_by_current_user);

A __construct on an Eloquent Laravel Model

I have a custom setter that I'm running in a __construct method on my model.
This is the property I'm wanting to set.
protected $directory;
My Constructor
public function __construct()
{
$this->directory = $this->setDirectory();
}
The setter:
public function setDirectory()
{
if(!is_null($this->student_id)){
return $this->student_id;
}else{
return 'applicant_' . $this->applicant_id;
}
}
My problem is that inside my setter the, $this->student_id (which is an attribute of the model being pulled from the database) is returning null.
When I dd($this) from inside my setter, I notice that my #attributes:[] is an empty array. So, a model's attributes aren't set until after __construct() is fired. How can I set my $directory attribute in my construct method?
You need to change your constructor to:
public function __construct(array $attributes = array())
{
parent::__construct($attributes);
$this->directory = $this->setDirectory();
}
The first line (parent::__construct()) will run the Eloquent Model's own construct method before your code runs, which will set up all the attributes for you. Also the change to the constructor's method signature is to continue supporting the usage that Laravel expects: $model = new Post(['id' => 5, 'title' => 'My Post']);
The rule of thumb really is to always remember, when extending a class, to check that you're not overriding an existing method so that it no longer runs (this is especially important with the magic __construct, __get, etc. methods). You can check the source of the original file to see if it includes the method you're defining.
I wouldn't ever use a constructor in eloquent. Eloquent has ways to accomplished what you want. I would used a boot method with an event listener. It would look something like this.
protected static function boot()
{
parent::boot();
static::retrieved(function($model){
$model->directory = $model->student_id ?? 'applicant_' . $model->applicant_id;
});
}
Here are all the model events you can use: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleted, restoring, restored, and replicating.

Accessors (Getter) & Mutators (Setter) On a Pivot Table in Laravel

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.

Laravel update or create an Item

I have this question about Laravel:
I have a my model and my RestfulAPI controller.
Into the store() method I would check if I have an element that already has the field 'myField' (myField id different from 'id') equal to what I have to create. If it already exist then I would like to update, otherwise I would simply create (save())..
Have I to use find() method?
From my experience, you'll have to traverse table and check for uniqueness.
You can create your helper function and use something like array_unique function. Maybe it is worth checking how Validator class is checking that users entry is unique.
Currently we have firstOrCreate or firstOrNew, but I don't think they really fit your needs. For instance, firstOrCreate will try to locate a row by all attributes, not just some, so an update in this case wouldn't make sense. So I think you really would have to find it, but you can create a BaseModel and create a createOrUpdate method that could look like this:
This is untested code
class BaseModel extends Eloquent {
public function createOrUpdate($attributes, $keysToCheck = null)
{
// If no attributes are passed, find using all
$keysToCheck = $keysToCheck ?: $attributes;
if ($model = static::firstByAttributes(array_only($keysToCheck, $attributes))
{
$model->attributes = $attributes;
$model->save();
}
else
{
$model = static::create($attributes);
}
return $model;
}
}
This is an implementation of it:
class Post extends BaseModel {
public function store()
{
$model = $this->createOrUpdate(Input::all(), ['full_name']);
return View::make('post.created', ['model' => $model]);
}
}

Categories