Doing ->save to multiple relations - laravel

I am using Laravel 5.5.13.
I have a model called Thumb. This thumb is related to two things in a many-to-one relationship: Player and Comment.
I currently do things like this:
public function store(Request $request, Entity $entity, Player $player)
{
$thumb = new Thumb($request->all());
$thumb->player_id = $player->id;
$entity->thumbs()->save($thumb);
return response()->json($thumb, 201);
}
We see how I have to set $thumb->player_id AND I don't have to set the entity_id because I am doing $entity->thumbs()->save
Is there a way to do $entityAndPlayer->thumbs()->save? Or is the way I did it above the recommend way?

You cannot use relationships to set 2 foreign columns so they way you showed is the correct one. However you can make it a bit cleaner in my opinion.
Into Thumb model you could add:
public function setPlayer(Player $player)
{
$this->player_id = $player->id;
}
and then instead of:
$thumb->player_id = $player->id;
you could use:
$thumb->setPlayer($player);
Or you could add create setPlayerAttribute method and finally instead of:
$thumb = new Thumb($request->all());
$thumb->player_id = $player->id;
use just:
$thumb = new Thumb($request->all() + ['player' => $player]);

You can't save multiple relationships at once but, for many to one associations you can use the method associate() (Laravel docs) to save using the belongs to part of the relationship, for example:
public function store(Request $request, Entity $entity, Player $player)
{
$thumb = new Thumb($request->all());
$thumb = $thumb->save();
$thumb->player()->associate($player);
$thumb->entity()->associate($entity);
return response()->json($thumb, 201);
}

Related

How to programmatically get relationship data without having a model instance?

I have the following working code that gives me a collection of a model type that each have any of the given relationship values (like a tag with id 1, 2 or 3):
<?php
public function getEntitiesWithRelationValues($entityType, $relations = []) {
$related = new EloquentCollection();
$locale = App::getLocale();
$entityType = new $entityType(); // bad?
// $entityType = new ReflectionClass($entityType); // not working
foreach ($relations as $relation => $modelKeys) {
if ($entityType->{$relation}()->exists()) {
$relatedClass = get_class($entityType->{$relation}()->getRelated());
$relationPrimaryKeyName = ($instance = new $relatedClass)->getQualifiedKeyName();
$relationEntities = $entityType::where('published->' . $locale, true)
->whereHas($relation, function (Builder $query) use($modelKeys, $relationPrimaryKeyName) {
$query->whereIn($relationPrimaryKeyName, $modelKeys);
})
->get()
->sortKeysDesc()
->take(10)
;
$related = $related->concat($relationEntities->except($related->modelKeys()));
}
}
return $related;
}
I feel $entityType = new $entityType(); is bad code because I dont want to create a new model. The reflection class throws the error "ReflectionClass undefined method {$relation}". How can I get the relationship data of a model type without actually loading/ instantiating a model?
A few weeks ago I asked something similar here but in that case I did have a model loaded.
You could consider the following solution:
Actually use a Model instance as the input to your getEntitiesWithRelationValues() function, since one way or another you are going to retrieve the relationships for a specific instance right?
Create a property for your model:
public static $relationNames = ['tags', 'posts', 'comments'];
Retrieve that property in your code using MyModel::$relationNames and call whichever functions you need.
Alternative solution using reflection
The snippet below uses reflection to find all public methods of User::class, then filter return types based on if it is a subclass of \Illuminate\Database\Eloquent\Relations\Relation. Note that you need to annotate the return types specifically using the new language struct like public function myMethod() : MyReturnType { }.
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\Relation;
$relationshipMethods = collect(
(new ReflectionClass(User::class))
->getMethods(\ReflectionMethod::IS_PUBLIC)
)
->filter(function(\ReflectionMethod $method) {
// The `?` makes the code work for methods without an explicit return type.
return is_subclass_of($method->getReturnType()?->getName(), Relation::class);
})->values();
// $relationshipMethods now is a collection with \ReflectionMethod objects.

Laravel 5.6 - Using model functions in ModelFactory

I am working with Laravel 5.6 and found myself a weird problem while extending the functionality of my project.
Right now i need to create two new models: order and item. It was quite easy to fill the items table with dummy data using Faker and Laravel Factories/Seeders. The biggest problem is while working with the order model.
This little fellow is related to a company with a foreign key named company_id and user with a foreign key named seller_id. The company field is okay, the trouble is behind my seller_id
This seller needs a role related to the company my factory will randomly pick for it because the user is not related to the company (directly) and i can't just look for it with a company_id.
In order to get all the users "related" to my company, i've created the next function on my Company model:
public function users()
{
$roles = $this->roles;
$users = [];
foreach ($roles as $role) {
foreach ($role->users as $user) {
$user->makeHidden(['pivot']);
array_push($users, $user);
}
}
$users = array_unique_objects($users);
return $users;
}
btw: I'm using laravel-permissions, a library made by Spatie.
What this functions does is get every role from a company and then it pushes it to an array of users.
This custom helper: array_unique_objects tracks any repeated user on my array and removes them.
That function works find because i've tested on a couple of controllers so i know there is no problem with it. Either way, my OrderFactory.php looks like this:
<?php
use Faker\Generator as Faker;
use App\Models\User;
use App\Models\Company;
$factory->define(App\Models\Order::class, function (Faker $faker) {
$company = Company::get()->random(1);
$users = $company->users();
$user = array_random($users);
return [
'company_id' => $company,
'seller_id' => $user->id,
'code' => strtoupper(str_random(10)),
'description' => $faker->sentence($nbWords = rand(2, 4), $variableNbWords = true),
'created_at' => $faker->dateTimeBetween($startDate = '-1 year', $endDate = 'now', $timezone = null)
];
});
But when i run the php artisan db:seed command, it throws the next error in console:
BadMethodCallException : Method Illuminate\Database\Eloquent\Collection::users does not exist.
at >/home/ironman/Documentos/Sandbox/Proventas/Backend/vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php:99
95| */
96| public function __call($method, $parameters)
97| {
98| if (! static::hasMacro($method)) {
99| throw new BadMethodCallException(sprintf(
100| 'Method %s::%s does not exist.', static::class, $method
101| ));
102| }
103|
Exception trace:
1 Illuminate\Support\Collection::__call("users", [])
/home/ironman/Documentos/Sandbox/Proventas/Backend/database/factories/OrderFactory.php:10
2 Illuminate\Database\Eloquent\Factory::{closure}(Object(Faker\Generator), [])
/home/ironman/Documentos/Sandbox/Proventas/Backend/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:274
Please use the argument -v to see more details.
Is there anything I can do to fix this problem? I know that using Laravel Relationships will fix my problem but the specifications of this project says that i have to keep things just as the are.
Your call to
$company = Company::get()->random(1);
does not return a single company. It returns a Collection, which does not have a users dynamic function. Try
$company = Company::get()->random(1)->first();

One To Many associate

I have a model Shop and a model Comment.
on the Comment model:
public function shop()
{
return $this->belongsTo('App\Shop', 'commentable_id');
}
The Shop model is:
public function comments() {
return $this->hasMany('App\Comment', 'commentable_id');
}
So the relationship is One To Many
I want to copy all the comments of a Shop and attach them to another Shop:
$shop = Shop::find(1);
$comments = $shop->comments()->pluck('id')->toArray(); // it works, i have all the ids of the comments
$othershop = Shop::find(2);
$othershop->comments()->associate($comments); // doesn't work
$othershop->comments()->attach($comments); // doesn't work
Anyone knows how to proceed with a One to Many situation?
You can utilize createMany method of relationship:
$othershop->comments()->createMany($shop->comments->toArray());
You can also utilize chunking if one shop can have many comments (split it into more queries to make it more memory efficient):
$shop->comments()->chunk(500, function ($comments) use ($othershop) {
$othershop->comments()->createMany($comments->toArray());
});
edit: changed my answer based on comments.
To copy comments, you first need to replicate them. Then you can associate them with their owning model:
$shop = Shop::find(1);
$otherShop = Shop::find(2);
$comments = $shop->comments()->get();
foreach ($comments as $comment) {
$tmp = $comment->replicate();
$tmp->shop()->associate($otherShop);
$tmp->save();
}

Set custom date field when new record created in Laravel

I want to be able to save a "followup date" for a student when a new student is created. This followup date needs to be 2 weeks in the future.
I have managed to do this in the controller as follows:
public function store(StudentRequest $request)
{
$data = $request->all();
$data['followup'] = Carbon::now()->addWeeks(2);
$student = Student::create($data);
// ....
}
However, this feels like a very long-winded way of doing things. I feel as though there ought to be a way to do it automatically on the Model with less code. I thought about using a Mutator, but that wouldn't work because the 'followup' field isn't actually being set from anywhere.
You can extend the save function in your model like this:
...
public function save(array $options = []) {
if(!$this->exists){ # only before creating
$this->attributes['followup'] = Carbon::now()->addWeeks(2);
}
return parent::save($options);
}
...

How to update field when delete a row in laravel

Let I have a table named customer where customer table has a field named deleted_by.
I implement softDelete in customer model. Now I want to update deleted_by when row delete. So that I can trace who delete this row.
I do search on google about it But I don't found anything.
I use laravel 4.2.8 & Eloquent
You may update the field using something like this:
$customer = Customer::find(1); // Assume 1 is the customer id
if($customer->delete()) { // If softdeleted
DB::table('customer')->where('id', $customer->id)
->update(array('deleted_by' => 'SomeNameOrUserID'));
}
Also, you may do it in one query:
// Assumed you have passed the id to the method in $id
$ts = Carbon\Carbon::now()->toDateTimeString();
$data = array('deleted_at' => $ts, 'deleted_by' => Auth::user()->id);
DB::table('customer')->where('id', $id)->update($data);
Both is done within one query, softDelete and recorded deleted_by as well.
Something like this is the way to go:
// override soft deleting trait method on the model, base model
// or new trait - whatever suits you
protected function runSoftDelete()
{
$query = $this->newQuery()->where($this->getKeyName(), $this->getKey());
$this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp();
$deleted_by = (Auth::id()) ?: null;
$query->update(array(
$this->getDeletedAtColumn() => $this->fromDateTime($time),
'deleted_by' => $deleted_by
));
}
Then all you need is:
$someModel->delete();
and it's done.
I would rather use a Model Event for this.
<?php
class Customer extends \Eloquent {
...
public static function boot() {
parent::boot();
// We set the deleted_by attribute before deleted event so we doesn't get an error if Customer was deleted by force (without soft delete).
static::deleting(function($model){
$model->deleted_by = Auth::user()->id;
$model->save();
});
}
...
}
Then you just delete it like you would normally do.
Customer::find(1)->delete();
I know this is an old question, but what you could do (in the customer model) is the following....
public function delete()
{
$this->deleted_by = auth()->user()->getKey();
$this->save();
return parent::delete();
}
That would still allow the soft delete while setting another value just before it deletes.

Resources