Laravel: Override Package Trait Function - laravel

I'm using Compoships package to make Laravel support Multi-key relationship but i want to override a trait function in this package.
Here's the current flow:
the main trait im using in my project uses another one in the package which uses another one (the one i want to override)
public function addConstraints()
{
if (static::$constraints) {
$foreignKey = $this->getForeignKeyName();
$parentKeyValue = $this->getParentKey();
//If the foreign key is an array (multi-column relationship), we adjust the query.
if (is_array($this->foreignKey)) {
foreach ($this->foreignKey as $index => $key) {
/*list(, $key) = explode('.', $key);
$fullKey = $this->getRelated()->getTable().'.'.$key;*/
$keyPortions = explode('.', $key);
if(count($keyPortions) === 2)
$fullKey = $this->getRelated()->getTable() . '.' . $key;
else
$fullKey = $key;
$this->query->where($fullKey, '=', $parentKeyValue[$index]);
$this->query->whereNotNull($fullKey);
}
} else {
$fullKey = $this->getRelated()->getTable().'.'.$foreignKey;
$this->query->where($fullKey, '=', $parentKeyValue);
$this->query->whereNotNull($fullKey);
}
}
}
I was able to override it locally but once i deploy the code (composer update happens) i lose my modifications
so what should i do?

simple just make a public function with the same name in controller where you use it
its automatcally override
for example
we know in auth folder register controller there is a trait RegistersUSers;
this trait contains a function authenticate so if we want to override it. just make a public function with the same name
class RegisterController extends Controller
{
use RegistersUsers;
public function authenticate(){
}

Related

Eloquent's fillable not working with mutators

I have a following model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class PaymentOption extends Model
{
protected $table = 'payment_option';
public $timestamps = false;
protected $fillable = ['payment_option_code', 'payment_option_name'];
public function setCodeAttribute($value)
{
$this->attributes['payment_option_code'] = $value;
}
public function getCodeAttribute()
{
return $this->payment_option_code;
}
public function setNameAttribute($value)
{
$this->attributes['payment_option_name'] = $value;
}
public function getNameAttribute($value)
{
$this->payment_option_name;
}
}
As you can see, I have weird column names. I need mutators cause I will be accepting variables without the prefix payment_option.
Now, I do it in my controller like so:
<?php
namespace App\Http\Controllers;
use App\PaymentOption;
use App\Http\Requests\PaymentOptionRequest;
class PaymentOptionController extends Controller
{
private $paymentOption;
public function __construct(PaymentOption $paymentOption)
{
$this->paymentOption = $paymentOption;
}
public function create(PaymentOptionRequest $request)
{
$paymentOption = $this->paymentOption->fill($request->validated());
dump($paymentOption);
return response()->json([]);
}
}
When I tried to use the fill() it doesn't call the mutators. But when I tried to set it 1 by 1 it worked like so:
$paymentOption->code = $validated['code'];
Why is that so?
Thanks!
fill does loop on the input data that are in your $fillable array
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();
foreach ($this->fillableFromArray($attributes) as $key => $value) {
$key = $this->removeTableFromKey($key);
// The developers may choose to place some attributes in the "fillable" array
// which means only those attributes may be set through mass assignment to
// the model, and all others will just get ignored for security reasons.
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
} elseif ($totallyGuarded) {
throw new MassAssignmentException(sprintf(
'Add [%s] to fillable property to allow mass assignment on [%s].',
$key, get_class($this)
));
}
}
return $this;
}
hence, you need to add 'code' and 'name' to the fillable array to have them assigned by fill
With latest realese of laravel 8 there is a change in implementation of fill. In laravel older version fill method does work for mutator and database column both. But in laravel 8 implementation of fill method is changed now fill also check if the key given in an array to fill method is a database column or not.
For example if you have database column name user_id and mutator name is user it will not work with laravel 8. It does work for the older version of laravel.
If the $guarded array contains at least one column/key, then that Eloquent Model goes into a strict mode where it can only be filled with values for valid database columns. here is the test is written in the official codebase for that(link to test).
To solve the problem use forceFill. That will solve your problem.

lararvel uuid as primary key

I'm trying to set an uuid as primary key in a Laravel Model. I've done it setting a boot method in my model as stablished here so I don't have to manually create it everytime I want to create and save the model. I have a controller that just creates the model and saves it in database.
It is saved correctly in database but when controller returns the value of the id is always returned with 0. How can I make it to actually return the value that it is creating in database?
Model
class UserPersona extends Model
{
protected $guarded = [];
protected $casts = [
'id' => 'string'
];
/**
* Setup model event hooks
*/
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$uuid = Uuid::uuid4();
$model->id = $uuid->toString();
});
}
}
Controller
class UserPersonaController extends Controller
{
public function new(Request $request)
{
return UserPersona::create();
}
}
You need to change the keyType to string and incrementing to false. Since it's not incrementing.
public $incrementing = false;
protected $keyType = 'string';
Additionally I have an trait which I simply add to those models which have UUID keys. Which is pretty flexible. This comes originally from https://garrettstjohn.com/articles/using-uuid-laravel-eloquent-orm/ and I added some small adjustments to it for issues which I have discovered while using it intensively.
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
/**
* Class Uuid.
* Manages the usage of creating UUID values for primary keys. Drop into your models as
* per normal to use this functionality. Works right out of the box.
* Taken from: http://garrettstjohn.com/entry/using-uuids-laravel-eloquent-orm/
*/
trait UuidForKey
{
/**
* The "booting" method of the model.
*/
public static function bootUuidForKey()
{
static::retrieved(function (Model $model) {
$model->incrementing = false; // this is used after instance is loaded from DB
});
static::creating(function (Model $model) {
$model->incrementing = false; // this is used for new instances
if (empty($model->{$model->getKeyName()})) { // if it's not empty, then we want to use a specific id
$model->{$model->getKeyName()} = (string)Uuid::uuid4();
}
});
}
public function initializeUuidForKey()
{
$this->keyType = 'string';
}
}
Hope this helps.
Accepted answer not worked for me on Laravel 9, but this way worked perfect, you can try it:
1- Create new Trait Class in project path app/Traits/IdAsUuidTrait.php (if you not found Traits folder create it, this is full code of this Class:
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait IdAsUuidTrait
{
public function initializeIdAsUuidTrait(): void
{
$this->keyType = 'string';
$this->id = Str::orderedUuid()->toString();
}
}
2- In any model you want to make id as UUID just call trait like this:
use App\Traits\IdAsUuidTrait;
class YourModelName extends Model
{
use IdAsUuidTrait;
...
That is it, now try to create, select, update any row in database by this model...

Laravel Mystery - Two Similar Item Types Producing 2 Different Query Strings in Same Use Case

Ok, this is weird... You ready?
I have an item type on my site, lets call it SomeItem
It can have tags associated with it via a one-to-many relationship.
The sorts of queries that Laravel builds when dealing with tags for SomeItem are like this, for instance in response to route api/someitem/10:
select `tags`.*, `someitem_tag`.`someitem_id` as `pivot_someitem_id`, `someitem_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `someitem_tag` on `tags`.`id` = `someitem_tag`.`tag_id` where `someitem_tag`.`someitem_id` in (10)
When I create a second Item with identical settings - let's call it AnotherItems - it treats the database query for extracting tags in a different manner, using a different syntax in the queries. Extremely weird.
(and yes, I have an s at the end of the model name...)
For instance, this route api/anotheritems/1
produces this error:
Base table or view not found: 1146 Table 'mysite.tag_anotheritems' doesn't exist (SQL: select `tags`.*, `tag_anotheritems`.`anotheritems_id` as `pivot_anotheritems_id`, `tag_anotheritems`.`tag_id` as `pivot_tag_id` from `tags` inner join `tag_anotheritems` on `tags`.`id` = `tag_anotheritems`.`tag_id` where `tag_anotheritems`.`anotheritems_id` in (1))
See what is happening? Of course I am getting this error - in the database this tag table for AnotherItems is created as anotheritems_tag. That is analogous to SomeItem.
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item??? WTF?
First let me show you how SomeItem is set up.
Here is the database structure related to Tags:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSomeItemTagTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('someitem_tag', function (Blueprint $table) {
$table->integer('tag_id')->unsigned();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->integer('someitem_id')->unsigned();
$table->foreign('someitem_id')->references('id')->on('someitems')->onDelete('cascade');
$table->primary(array('tag_id', 'someitem_id'));
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('someitem_tag');
}
}
There is a Tags model/class that has this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name'];
protected $hidden = [];
public $timestamps = false;
public function someitems()
{
return $this->belongsToMany(SomeItem::class);
}
}
And here is some relevant lines for SomeItem model/class:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Presenters\Presentable;
use Illuminate\Notifications\Notifiable;
use Auth;
class Exercise extends Model
implements Presentable
{
use Traits\SerializesUniversalDate;
use Traits\Presents;
use Notifiable;
protected $presenter = 'App\Presenters\SomeItemPresenter';
protected $fillable = ['title', etc];
protected $hidden = [];
public function parentitem()
{
return $this->belongsTo(ParentItem::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* Update lesson tag array.
*
* #param array \App\Tag $tags
* #return void
*/
public function updateTags($tagsArray)
{
foreach ($tagsArray as &$value)
{
$tag = Tag::where('name', $value['name'])->first();
if (is_null($tag))
{
$tag = new Tag([
'name' => $value['name']
]);
$tag->save();
}
if (!$this->tags->contains($tag->id))
{
$this->tags()->attach($tag->id);
}
}
foreach($this->tags as &$existingTag)
{
if (!self::arrayContains($tagsArray, 'name', $existingTag->name))
{
$this->tags()->detach($existingTag->id);
}
}
$this->load('tags');
}
private static function arrayContains($array, $key, $value)
{
foreach ($array as $item)
{
if($item[$key] == $value) return true;
}
return false;
}
}
And here is some relevant code for SomeItem API controller:
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Input;
class SomeItemController extends Controller
{
public function index(Request $request)
{
$query = \App\SomeItem::query();
return $query->get()->load('parentitem')->load('tags');
}
//show item for editing
public function show($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->load('parentitem')->load('tags');
$someitem->attachKindToFiles();
return $someitem;
}
//store new entry to db
public function store()
{
$someitem = \App\SomeItem::create(Input::all());
isset(Input::all()['tags']) ? $someitem->updateTags(Input::all()['tags']) : '';
return $someitem;
}
//update/save
public function update($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->update(Input::all());
$someitem->updateTags(Input::all()['tags']);
$someitem->load('tags');
return $someitem;
}
There is also a SomeItem presenter and composer but they don't do anything with tags.
With AnotherItems, I literally I duplicated everything from SomeItem and just changed names as needed.
So in the Tag model there is
public function anotheritems()
{
return $this->belongsToMany(AnotherItems::class);
}
In AnotherItems model there is this, for instance
public function tags()
{
return $this->belongsToMany(Tag::class);
}
In the AnotherItems API controller there is this, for instance (which is for route api/anotheritems/1):
public function index(Request $request)
{
$query = \App\AnotherItems::query();
if ($request->has('id')) {
$query->where('id', $request['id']);
}
return $query->get()->load('parentitem')->load('tags');
}
So, this is a total mystery. I have been trying to figure this out for 2 days now. And I continue asking myself
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item???
I upgraded from laravel 5.2 to 5.3 and it is after the upgrade that I added this AnotherItems. But I can't figure out how that could possibly alter things in terms of these database queries.
I have tried a ton of artisan commands for clearing everything imaginable, but somewhere in the framework it wants to handle SomeItem and AnotherItems differently when building these join queries to extract/save tags.
Thoughts?
thanks,
Brian
Decided to step through code in debugger. Seems things are breaking down in Str.php in various snake related function, and I also noticed a snakeCache call, whatever the heck that is. Not sure why such a strange methodology to determine table names... Also in these functions there is some pluralizing related checks, so maybe this is related to me using an s at the end of my item name. Pretty messed up stuff if an s at the end of a model name can cause two different logic branches...

Event fails when using serialization in laravel 5.4

I have different models, which describe different tables in database.
I use this trait for these classes
trait ApplicationModelTrait
{
protected static $_currentModel = null;
protected $currentModel;
public static function setCurrentModel(ApplicationModel $model) {
static::$_currentModel = $model;
}
public function __construct(array $attributes = [], ApplicationModel $model = null) {
parent::__construct($attributes);
$this->currentModel = $model !== null ? $model : static::$_currentModel;
if($this->currentModel === null) throw new \InvalidArgumentException('No model passed');
}
}
All models are similar. The only difference is what fields exist in database for every model, so I describe these fields in separate config and main model class ApplicationModel has some methods to work with different tables. For example
public function getInstanceTable() {
return 'application_model_instances_' . $this->name;
}
public function getCommentsTable() {
return 'application_model_instance_' . $this->name . '_comments';
}
Where application_model_instances_{name} contains (obviously) instances of this model and application_model_instance_{name}_comments contains comments for instances of this model.
Everything works fine except events.
When I add comment to model instance, I pass as argument current model
$comment = new ApplicationModelInstanceComment([], $this->currentModel);
$comment->text = $request->input('comment');
// etc.
After comment has been saved I want it to be instantly delivered to user browser via websocket
event(new CommentCreated($comment, $this)); // this represents model instance class
And, finally, the event
namespace App\Events;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Models\Application\ApplicationModelInstanceComment;
use App\Models\Application\ApplicationModelInstance;
class CommentCreated implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $comment;
public $instance;
public function __construct(ApplicationModelInstanceComment $comment, ApplicationModelInstance $instance)
{
$this->comment = $comment;
$this->instance = $instance;
}
public function broadcastOn() {
$notifiableUsers = $this->instance->getNotifiableUsers(); // this method is used to fetch list of users who must get this comment in their browser
$channels = [];
foreach($notifiableUsers as $user) {
$channels[] = new PrivateChannel('user.' . $user->id);
}
return $channels;
}
}
But when I add comment Laravel my code throws an exception (this is what I see in network tab in Google Chrome because request is done via ajax:
Whoops, looks like something went wrong.
1/1 InvalidArgumentException in ApplicationModelTrait.php line 20: No model passed
in ApplicationModelTrait.php line 20
at ApplicationModelInstanceComment->__construct() in SerializesAndRestoresModelIdentifiers.php line 45
at CommentCreated->getRestoredPropertyValue(object(ModelIdentifier)) in SerializesModels.php line 41
at CommentCreated->__wakeup()
at unserialize('O:38:"Illuminate\\Broadcasting\\BroadcastEvent":4:{s:5:"event";O:25:"App\\Events\\CommentCreated":3:{s:7:"comment";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:54:"App\\Models\\Application\\ApplicationModelInstanceComment";s:2:"id";i:68;}s:8:"instance";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:47:"App\\Models\\Application\\ApplicationModelInstance";s:2:"id";i:11;}s:6:"socket";N;}s:10:"connection";N;s:5:"queue";N;s:5:"delay";N;}') in CallQueuedHandler.php line 95
at CallQueuedHandler->failed(array('commandName' => 'Illuminate\\Broadcasting\\BroadcastEvent', 'command' => 'O:38:"Illuminate\\Broadcasting\\BroadcastEvent":4:{s:5:"event";O:25:"App\\Events\\CommentCreated":3:{s:7:"comment";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:54:"App\\Models\\Application\\ApplicationModelInstanceComment";s:2:"id";i:68;}s:8:"instance";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":2:{s:5:"class";s:47:"App\\Models\\Application\\ApplicationModelInstance";s:2:"id";i:11;}s:6:"socket";N;}s:10:"connection";N;s:5:"queue";N;s:5:"delay";N;}'), object(InvalidArgumentException)) in Job.php line 158
at Job->failed(object(InvalidArgumentException)) in FailingJob.php line 33
I've passed model into ApplicationModelInstanceComment upon creation of comment, so everything is fine here. There is some problem with deserialization, but I do not know how to deal with this problem.

Global filtering - how to use global scope in Laravel Eloquent

I have a published filter that I use for my articles. Guests can only view published articles, logged in users can view and apply filter (?published=0/1):
public function scopePublishedFilter($query)
{
if(!Auth::check()) $query->where('published', '=', 1);
else
{
$published = Input::get('published');
if (isset($published)) $query->where('published', '=', $published);
}
return $query;
}
I apply this in my ArticlesController:
public function index()
{
return View::make('articles.index', [
'articles' => Article::with('owner')
->with('category')
->with('tags')
->publishedFilter()
->get()
]);
}
And on the article relationships:
public function articles()
{
return $this->hasMany('Article')->publishedFilter();
}
But ideally I would like to only define it in the Article model itself, since it's easy to forget to include this filter when implementing new features or views.
How can I make sure that all returned articles from the Article model are run through this filter before returned?
UPDATE: Just use this: https://github.com/jarektkaczyk/laravel-global-scope for global scopes in L5+
Better way is a bit too long to paste it and works like SoftDeleting thing in the core.
Read this if you want it http://softonsofa.com/laravel-how-to-define-and-use-eloquent-global-scopes/
Short way: you need global scope for this. And here's how you do it in 2 steps (squashed a bit):
1 Create a class PublishedScope that implements ScopeInterface
class PublishedScope implements ScopeInterface {
public function apply(Builder $builder)
{
$table = $builder->getModel()->getTable();
$builder->where($table.'.published', '=', 1);
$this->addWithDrafts($builder);
}
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$column = $builder->getModel()->getTable().'.published';
$bindingKey = 0;
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isPublishedConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
$this->removeBinding($query, $bindingKey);
}
// Check if where is either NULL or NOT NULL type,
// if that's the case, don't increment the key
// since there is no binding for these types
if ( ! in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++;
}
}
protected function removeBinding(Builder $query, $key)
{
$bindings = $query->getRawBindings()['where'];
unset($bindings[$key]);
$query->setBindings($bindings);
}
protected function addWithDrafts(Builder $builder)
{
$builder->macro('withDrafts', function(Builder $builder)
{
$this->remove($builder);
return $builder;
});
}
2 Boot that class in your Eloquent model by calling static::addGlobalScope(new AbcScope)
// the model
public static function boot()
{
parent::boot();
static::addGlobalScope(new PublishedScope);
}
If I were you I would use published_at column and check it for null instead of = 1, but that's up to you.
edit remove method updated - thanks to #Leon for pointing out unexpected behaviour, when using this scope together with SoftDeletingTrait. The problem is a bit deeper:
when you use this one with SoftDeletingScope or another one, that utilizes NULL or NOT NULL constraint and this scope is not the first one used (yes, order of use statements matters here), remove method will not work as expected. It will not remove any binding or not the one, that it should.
you can use trait and add your method or filter thing in booting method check the following
http://laravel.com/docs/4.2/eloquent#global-scopes

Resources