Laravel Eloquent get relationship with keyBy - laravel

I have a Product model with a hasMany relationship
public function pricing()
{
return $this->hasMany('App\ProductPrice', 'prod_id', 'id');
}
I then get the relationship
Product::with('pricing')->all();
How can I retrieve the pricing relationship with the id as the key. I know I can do it on a Collection with keyBy('id) but it doesn't work on a query.
I want to acheive the same results as below but I want to get it from the Product relationship.
ProductPrice::keyBy('id')

You have to create your own relationship:
<?php
namespace App\Helpers\Classes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HasManyKeyBy extends HasMany
{
private $keyBy;
public function __construct($keyBy, Builder $query, Model $parent, string $foreignKey, string $localKey)
{
$this->keyBy = $keyBy;
parent::__construct($query, $parent, $foreignKey, $localKey);
}
public function getResults()
{
return parent::getResults()->keyBy($this->keyBy);
}
protected function getRelationValue(array $dictionary, $key, $type)
{
return parent::getRelationValue($dictionary, $key, $type)->keyBy($this->keyBy);
}
}
For the sake of simplicity I also recommend you to create a trait:
<?php
namespace App\Helpers\Traits;
use Illuminate\Database\Eloquent\Relations\HasMany;
trait HasManyKeyBy
{
/**
* #param $keyBy
* #param $related
* #param null $foreignKey
* #param null $localKey
* #return HasMany
*/
protected function hasManyKeyBy($keyBy, $related, $foreignKey = null, $localKey = null)
{
// copied from \Illuminate\Database\Eloquent\Concerns\HasRelationships::hasMany
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return new \App\Helpers\Classes\HasManyKeyBy($keyBy, $instance->newQuery(),
$this, $instance->getTable().'.'.$foreignKey, $localKey);
}
}
Now, you can include this trait into your model, and use $this->hasManyKeyBy protected method:
[...]
class Product extends Model
{
use HasManyKeyBy;
public function pricing()
{
return $this->hasManyKeyBy('id', ProductPrice::class, 'prod_id', 'id');
}
[...]
}

A quick workaround is to replace the current relation in you array using setRelation method. In your case:
$product = Product::with('pricing')->all();
$product->setRelation('pricing', $product->pricing->keyBy('id'));

What you could also do is define an accessor:
/**
* #return Collection
*/
public function getPricingByIdAttribute() : Collection
{
return $this->pricing->keyBy('id');
}
Then on each product returned in the Collection you can get the pricing by the id using:
$pricing = $product->pricing_by_id;
Make sure to still eager load the pricing if necessary:
$products = Product::query()->with('pricing')->get();
Also, when returning the Products in e.g. an API using json you could use appending to json:
$products->each->append('pricing_by_id');

The problem is that you can't 'keyBy' an existing relationship.
However, you can create a 'fake' attribute that you return which can be keyed. So instead:
$products = Product::with('pricing') -> all();
$products -> keyedPricing = $products -> pricing -> keyBy('id');
$products -> addVisible('keyedPricing');

Related

trying to get property 'sector' of non-object

guys. I'm trying to pass the 'officeToSector' id from employee table to get the 'sector' table values using the id. But it doesn't work, it show me 'trying to get property 'sector' of non-object'. However, if I entered a number like 1 or 2, I can access the property. I had checked the type of the return query, it is an object not an array...
Can you guys pls help me to find out what is the problem. Thank you very much.
$employees = $this->employeeRepository->listEmployees();
return Datatables::of($employees)
->addColumn('department', function($employee){
// if I assign 2 to $id, I got the return value,
// but it I use the emp->dept_id, I get the non-object error
$id = (int)$employee->department_id;
$sector = OfficeToSector::find($id);
return $sector->sector->name;
)}
->rawColumns(['department'])
->make(true);
officeToSector model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
// use Illuminate\Database\Eloquent\Relations\Pivot;
class OfficeToSector extends Model
{
protected $table = 'office_to_sectors';
/**
* #var array
*/
protected $fillable = ['office_id','sector_id'];
public function office()
{
return $this->belongsTo(Office::class, 'office_id', 'id');
}
public function sector()
{
return $this->belongsTo(Sector::class, 'sector_id');
}
public function employee()
{
return $this->hasMany(Employee::class, 'employee_id', 'id');
}
public function registeredDepartment()
{
return $this->hasOne(DepartmentContent::class, 'department_id');
}
}
sector model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Sector extends Model
{
/**
* #var array
*/
protected $fillable = ['name','status'];
/**
* #var array
*/
protected $casts = [
'status' => 'boolean',
];
public function offices()
{
return $this->belongsToMany('App\Models\Office', 'office_to_sectors')->withPivot('sector_id', 'office_id')->withTimestamps();
}
}
I found the solution. It is because one of the sector value in the officeToSector table is null. So, this is why Yajra Datatable cannot display the data because the table has null value.

Laravel Eloquent How Can I Select Using Condition "where" for pivot table

I have three database tables called user(id,name), group(id,name) and user_group(user_id, group_id,valid_before) with relations many to many.
class User extends Model
{
protected $table = 'user';
public function groups()
{
return $this->belongsToMany(Group::class, 'user_group')
->withPivot('valid_before');
}
}
class Group extends Model
{
protected $table = 'group';
public $timestamps = false;
public function user()
{
return $this->belongsToMany(User::class, 'user_group');
}
}
How can I select all users (using Eloquent) who have
valid_before < $some_date
?
There are many ways to achieve this goal. I'll show you an example using query scopes.
In your User class you have to make a little update:
class User extends Model
{
protected $table = 'user';
public function groups()
{
return $this->belongsToMany(Group::class, 'user_group')
//->withPivot('valid_before'); <-- Remove this
}
}
and create a scope in your Group model:
class Group extends Model
{
protected $table = 'group';
public $timestamps = false;
public function user()
{
return $this->belongsToMany(User::class, 'user_group');
}
/**
* This scope gets as input the date you want to query and returns the users collection
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param string $date
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeUsersValidBefore($query, $date)
{
return $query->users()->wherePivot('valid_before', '<', $date);
}
}
Now, I imagine you have a GroupController that somewhere creates a query to retrieve the valid before users. Something like:
// [...]
$users = Group::usersValidBefore($yourDate)->get();
// [...]
If you want to create the query from the other side, I mean you want to use the User model and list all the Users that has a pivot relation with valid_before populated, than the right approach is creating a UserGroup intermediate model that can be easily used to create a query.
If you are using Laravel 8.x.x
It's much easier with Inline Relationship Existence Queries
If you would like to query for a relationship's existence with a single, simple where condition attached to the relationship query, you may find it more convenient to use the whereRelation and whereMorphRelation methods. For example, we may query for all posts that have unapproved comments:
use App\Models\Post;
$posts = Post::whereRelation('comments', 'is_approved', false)->get();
Of course, like calls to the query builder's where method, you may also specify an operator:
$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->subHour()
)->get();

Undefined property in laravel framework accessor calling belongsTo

I need your help.
I'm working with Laravel Framework and I have a trouble with a belongsTo relationship.
My project have to tables, address book and delivery types, the columns in tables are:
address_book
id
name
mail
delivery_1
deliverytype_id_1
delivery_2
deliverytype_id_2
...
delivery_types
id
name
...
The code of delivery types model is this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class DeliveryType extends Model
{
protected $table = 'delivery_types';
protected $guarded = ['id'];
}
This is the address book model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AddressBook extends Model
{
protected $table = 'address_book'; // table
protected $guarded = ['id']; // primary key
protected $appends = ['delivery', 'actions']; // accessors
protected $delivery = '';
public function del1() {
return $this->belongsTo('App\DeliveryType', 'deliverytype_id_1', 'id')->withDefault();
}
public function del2() {
return $this->belongsTo('App\DeliveryType', 'deliverytype_id_2', 'id');
}
/**
* Accessor: get the actions column information.
*
* #return string
*/
public function getActionsAttribute() {
$actions = '<a href='. route('admin.addressbook.show', $this->id) .'>'.
'show<i class="livicon" data-name="info" data-size="18" data-loop="true" data-c="#428BCA" data-hc="#428BCA" title="view contact"></i></a>';
return $actions;
}
/**
* Accessor: get the deliveries information.
*
* #return string
*/
public function getDeliveryAttribute () {
$deliveries = [
['val' => $this->delivery_1, 'type' => $this->del1()->name], //row error
['val' => $this->delivery_2, 'type' => $this->del2()->name]
];
foreach($deliveries as $delivery) {
$this->delivery = (strlen($delivery['val']) > 0) ?
$this->appendString($this->delivery, '<strong>'.$delivery['type'].'</strong> '.$delivery['val']) :
$this->delivery;
}
return $this->delivery;
}
protected function appendString(string $str, string $val) {
return (strlen($str) > 0) ? $str.'<br>'.$val : $val;
}
In the html page the data is loaded through ajax call to the controller function. This is the code of function:
public function data(Request $request) {
// Init data
$this->addressbooks = AddressBook::get(
['address_book.id',
'address_book.name',
'address_book.email',
'address_book.delivery_1',
'address_book.delivery_2');
// Return json array
header("Content-Type: application/json");
return $this->addressbooks;
}
When the page call the function through ajax, the framework return the error "Undefined property" in the accessor getDeliveryAttribute, where I try to call relationship belongs to about delivery type ID and its reference in the delivery types table.
What I'm doing wrong? Thanks in advance to those who can help me.
Here is how I would write the AddressBook model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
//use Illuminate\Support\Facades\Log;
class AddressBook extends Model
{
protected $table = 'address_book'; // table
//protected $guarded = ['id']; // primary key
// always load these accessors
protected $appends = [
'delivery',
'actions',
];
protected $mail = '';
// No need for $delInfo, use $this->delivery (same data)
// protected $delInfo = '';
public function category() {
/*
You can use `AddressBookCategory::class` instead of `'App\DeliveryType'`
http://php.net/manual/en/migration55.new-features.php#migration55.new-features.class-name
Because models are in the same napespace, we don't need to write \App\AddressBookCategory
*/
return $this->belongsTo(AddressBookCategory::class, 'address_book_category_id', 'id');
}
public function deliveryType1() {
return $this->belongsTo(DeliveryType::class, 'deliverytype_id_1', 'id')
/*
Specify empty string for the name, so that when we access
$this->deliveryType1->name it's equal ''
*/
->withDefault(['name' => '']);
}
public function deliveryType2() {
return $this->belongsTo(DeliveryType::class, 'deliverytype_id_2', 'id')
->withDefault(['name' => '']);
}
/**
* Accessor: get the actions column information.
*
* Access by using: $this->action
*
* #return string
*/
public function getActionsAttribute() {
// moved this into multi line, easier to read
return '<a href='. route('admin.addressbook.show', $this->id) .'>'
.'show'
.'<i class="livicon" data-name="info" data-size="18" data-loop="true" data-c="#428BCA" data-hc="#428BCA" title="view contact"></i>'
.'</a>';
}
/**
* Accessor: get the deliveries information
*
* Access by using: $this->delivery
*
* #return string
*/
public function getDeliveryAttribute () {
// I've updated logic here, it should be easier to read...
$delivery = [];
if ( ! empty($this->delivery_1) ) {
$delivery[] = '<strong>'.$this->deliveryType1->name.'</strong> '.$this->delivery_1;
}
if ( ! empty($this->delivery_2) ) {
$delivery[] = '<strong>'.$this->deliveryType2->name.'</strong> '.$this->delivery_2;
}
// join array elements with a string `<br>`
return implode('<br>', $delivery);
}
}

Comment/Post system in Laravel

I can't seem to get relationships concrete in my head with Laravel. Having tried to follow the docs for eloquent orm, I still can't get my foreign keys to mean something (I update them manually). Right now I am trying to get a bulletin board system to work. A user can create a bulletin post, and here it is working in my controller:
public function editPost($id)
{
$user = User::find($id);
$user->bulletin = new Bulletin;//new post
$user->bulletin->creator_id = $id;//why doesn't it automatically update given the relationship?
$user->bulletin->type = Input::get('type');
$user->bulletin->title = Input::get('title');
$user->bulletin->content = Input::get('bulletinEdit');
$user->bulletin->save();
if(Input::hasFile('bulletinImage')){
$extension = Input::file('bulletinImage')->getClientOriginalExtension();
$fileName = str_random(9).'.'.$extension;
$user->bulletin->photo = new Photo;
$user->bulletin->photo->user_id = $id;
$user->bulletin->photo->type = Input::get('type');
$user->bulletin->photo->filename = $fileName;
$user->bulletin->photo->touch();
$user->bulletin->photo->save();
Input::file('bulletinImage')->move('public/images/bulletin/',$fileName);
}
return Redirect::to('bulletin');
}
If I have the relationship set up properly, shouldn't the creator_id be updated automatically? Here is what I have in my models:
Bulletin
<?php
class Bulletin extends Eloquent {
public function creator()
{
return $this->belongsTo('User');
}
public function comments()
{
return $this->hasMany('Comment');
}
public function type()
{
//if 1 then, etc
}
public function photos(){
return $this->hasMany('Photo');
}
}
User
<?php
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
/**
* The database table used by the model.
*
* #var string
*/
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password', 'remember_token');
public function tags()
{
//TO REMOVE RECORD
//User::find(1)->tags()->detach();
return $this->belongsToMany('Tag');
}
public function createUser()
{
$password = Hash::make('secret');
}
public function bulletin()
{
return $this->hasMany('Bulletin','creator_id');
}
public function profile()
{
return $this->hasOne('Profile');
}
}
Could anybody give me some tips on tightening this up?
The way you are doing it should work, you are just using more code and Eloquent has some methods to help you attach relationships, so I would try something like this:
public function editPost($id)
{
$user = User::find($id);
// Create a new bulletin, passing the necesssary data
$bulletin = new Bulletin(Input::only(['type', 'title', 'bulletinEdit']));
// Attach the bulletin model to your user, Laravel should set the creator_id itself
$bulletin = $user->bulletin()->save($bulletin);
...
return Redirect::to('bulletin');
}
In your model, you'll have to:
class User extends Eloquent implements UserInterface, RemindableInterface {
protected $fillable = ['type', 'title', 'bulletinEdit'];
...
}
So Laravel doesn't give you a MassAssignmentException.

Clone an Eloquent object including all relationships?

Is there any way to easily clone an Eloquent object, including all of its relationships?
For example, if I had these tables:
users ( id, name, email )
roles ( id, name )
user_roles ( user_id, role_id )
In addition to creating a new row in the users table, with all columns being the same except id, it should also create a new row in the user_roles table, assigning the same role to the new user.
Something like this:
$user = User::find(1);
$new_user = $user->clone();
Where the User model has
class User extends Eloquent {
public function roles() {
return $this->hasMany('Role', 'user_roles');
}
}
tested in laravel 4.2 for belongsToMany relationships
if you're in the model:
//copy attributes
$new = $this->replicate();
//save model before you recreate relations (so it has an id)
$new->push();
//reset relations on EXISTING MODEL (this way you can control which ones will be loaded
$this->relations = [];
//load relations on EXISTING MODEL
$this->load('relation1','relation2');
//re-sync everything
foreach ($this->relations as $relationName => $values){
$new->{$relationName}()->sync($values);
}
You may also try the replicate function provided by eloquent:
http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Model.html#method_replicate
$user = User::find(1);
$new_user = $user->replicate();
$new_user->push();
For Laravel 5. Tested with hasMany relation.
$model = User::find($id);
$model->load('invoices');
$newModel = $model->replicate();
$newModel->push();
foreach($model->getRelations() as $relation => $items){
foreach($items as $item){
unset($item->id);
$newModel->{$relation}()->create($item->toArray());
}
}
You may try this (Object Cloning):
$user = User::find(1);
$new_user = clone $user;
Since clone doesn't deep copy so child objects won't be copied if there is any child object available and in this case you need to copy the child object using clone manually. For example:
$user = User::with('role')->find(1);
$new_user = clone $user; // copy the $user
$new_user->role = clone $user->role; // copy the $user->role
In your case roles will be a collection of Role objects so each Role object in the collection needs to be copied manually using clone.
Also, you need to be aware of that, if you don't load the roles using with then those will be not loaded or won't be available in the $user and when you'll call $user->roles then those objects will be loaded at run time after that call of $user->roles and until this, those roles are not loaded.
Update:
This answer was for Larave-4 and now Laravel offers replicate() method, for example:
$user = User::find(1);
$newUser = $user->replicate();
// ...
Here is an updated version of the solution from #sabrina-gelbart that will clone all hasMany relationships instead of just the belongsToMany as she posted:
//copy attributes from original model
$newRecord = $original->replicate();
// Reset any fields needed to connect to another parent, etc
$newRecord->some_id = $otherParent->id;
//save model before you recreate relations (so it has an id)
$newRecord->push();
//reset relations on EXISTING MODEL (this way you can control which ones will be loaded
$original->relations = [];
//load relations on EXISTING MODEL
$original->load('somerelationship', 'anotherrelationship');
//re-sync the child relationships
$relations = $original->getRelations();
foreach ($relations as $relation) {
foreach ($relation as $relationRecord) {
$newRelationship = $relationRecord->replicate();
$newRelationship->some_parent_id = $newRecord->id;
$newRelationship->push();
}
}
This is in laravel 5.8, havent tried in older version
//# this will clone $eloquent and asign all $eloquent->$withoutProperties = null
$cloned = $eloquent->cloneWithout(Array $withoutProperties)
edit, just today 7 April 2019 laravel 5.8.10 launched
can use replicate now
$post = Post::find(1);
$newPost = $post->replicate();
$newPost->save();
When you fetch an object by any relation you want, and replicate after that, all relations you retrieved are also replicated. for example:
$oldUser = User::with('roles')->find(1);
$newUser = $oldUser->replicate();
If you have a collection named $user, using the code bellow, it creates a new Collection identical from the old one, including all the relations:
$new_user = new \Illuminate\Database\Eloquent\Collection ( $user->all() );
this code is for laravel 5.
Here is a trait that will recursively duplicate all the loaded relationships on an object. You could easily expand this for other relationship types like Sabrina's example for belongsToMany.
trait DuplicateRelations
{
public static function duplicateRelations($from, $to)
{
foreach ($from->relations as $relationName => $object){
if($object !== null) {
if ($object instanceof Collection) {
foreach ($object as $relation) {
self::replication($relationName, $relation, $to);
}
} else {
self::replication($relationName, $object, $to);
}
}
}
}
private static function replication($name, $relation, $to)
{
$newRelation = $relation->replicate();
$to->{$name}()->create($newRelation->toArray());
if($relation->relations !== null) {
self::duplicateRelations($relation, $to->{$name});
}
}
}
Usage:
//copy attributes
$new = $this->replicate();
//save model before you recreate relations (so it has an id)
$new->push();
//reset relations on EXISTING MODEL (this way you can control which ones will be loaded
$this->relations = [];
//load relations on EXISTING MODEL
$this->load('relation1','relation2.nested_relation');
// duplication all LOADED relations including nested.
self::duplicateRelations($this, $new);
I added this function in BaseModel to duplicate data with relations. It works in Laravel 9.
public function replicateWithRelationsAttributes(): static
{
$model = clone $this->replicate();
foreach ($this->getRelations() as $key => $relation) {
$model->setAttribute($key, clone $relation);
}
return $model;
}
Here's another way to do it if the other solutions don't appease you:
<?php
/** #var \App\Models\Booking $booking */
$booking = Booking::query()->with('segments.stops','billingItems','invoiceItems.applyTo')->findOrFail($id);
$booking->id = null;
$booking->exists = false;
$booking->number = null;
$booking->confirmed_date_utc = null;
$booking->save();
$now = CarbonDate::now($booking->company->timezone);
foreach($booking->segments as $seg) {
$seg->id = null;
$seg->exists = false;
$seg->booking_id = $booking->id;
$seg->save();
foreach($seg->stops as $stop) {
$stop->id = null;
$stop->exists = false;
$stop->segment_id = $seg->id;
$stop->save();
}
}
foreach($booking->billingItems as $bi) {
$bi->id = null;
$bi->exists = false;
$bi->booking_id = $booking->id;
$bi->save();
}
$iiMap = [];
foreach($booking->invoiceItems as $ii) {
$oldId = $ii->id;
$ii->id = null;
$ii->exists = false;
$ii->booking_id = $booking->id;
$ii->save();
$iiMap[$oldId] = $ii->id;
}
foreach($booking->invoiceItems as $ii) {
$newIds = [];
foreach($ii->applyTo as $at) {
$newIds[] = $iiMap[$at->id];
}
$ii->applyTo()->sync($newIds);
}
The trick is to wipe the id and exists properties so that Laravel will create a new record.
Cloning self-relationships is a little tricky but I've included an example. You just have to create a mapping of old ids to new ids and then re-sync.
In Laravel v5.8.10+ (Currently Laravel v9.x) If you need to work with Laravel replicate() model with relationships this could be a solution. Let's see two simple example.
app/Models/Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* #var array<string>
*/
protected $fillable = [
'name', 'price', 'slug', 'category_id'
];
}
app/Models/Category.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
/**
* Get all the products for the Category.
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function products()
{
return $this->hasMany(Product::class);
}
/**
* Clone the model into a new, non-existing instance with all the products.
*
* #return \App\Models\Category
*/
public function replicateRow()
{
$clon = $this->replicate();
$clon->push();
$this->products->each(
fn ($product) => $clon->products()->create($product->toArray())
);
return $clon;
}
}
Controller Code
<?php
namespace App\Http\Controllers;
use App\Models\Category;
class ReplicateController extends Controller
{
/**
* Handle the incoming request.
*
* #param \App\Models\Category $category
* #return void
*/
public function index(Category $category)
{
$newCategory = $category->replicateRow();
dd($newCategory);
}
}

Resources