Clone an Eloquent object including all relationships? - laravel

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);
}
}

Related

Laravel Relation Query

I have two tables course and module where each course belongs to a single module.
What I need is to eager load the module with course
my codes is :
$courses = Course::all();
$module = Coursemdule::all();
You need to create relation between both the table
In the course model
/**
* Get the user that owns the phone.
*/
public function module()
{
return $this->belongsTo('App\Coursemdule');
}
In the Coursemdule model
/**
* Get the Course record associated with the Coursemdule.
*/
public function course()
{
return $this->hasOne('App\Course');
}
To fetch this value
$phone = Course::find(1)->module;
OR
$phone = Course::with('module')->all();
I hope this will help you.

Eloquent ORM: Define allowed model attributes

In laravel's eloquent ORM, is there a way to define a model's allowed attributes?
By default I can put any attributes into the model's constructor - but then I only get notified about the erroneous attribute names when I actually try to save the model to database.
Example code:
// this works although there is a typo in "lastname"
$user = new \App\User(['firstname' => 'foo', 'lastnam' => 'bar']);
// this errors out with an SQL error
$user->save();
So, is there a way to let Laravel automatically check if there are invalid keys in the request's input data?
If you would like to prevent not only filling not allowed attributes using fill() method but also directly setting them, like $model->foo = 'bar', then you got to override Model::setAttribute() method.
Best to do it in a custom base Model that extends Eloquent. So in app/Model.php:
namespace App;
use Exception;
use Illuminate\Database\Eloquent\Model as Eloquent;
class Model extends Eloquent
{
// this should be actually defined in each sub-model
protected $allowed = ['firstname', 'lastname'];
public function setAttribute($key, $value)
{
// this way we can allow some attributes by default
$allowed = array_merge($this->allowed, ['id']);
if (! in_array($key, $allowed)) {
throw new Exception("Not allowed attribute '$key'.");
}
return parent::setAttribute($key, $value);
}
}
Then in the models that should not allow invalid attributes you can extend this base model:
use App\Model;
class User extends Model
I don't believe this can be done natively. I think Laravel is intentionally permissive in that sense, and I personally don't mind having a SQL error instead of an Eloquent one if I make a mistake setting attributes somewhere.
That being said, it's not hard to customize your Models to fail when non-existing attributes are set:
// User.php
protected $fillable = [
'firstname',
'lastname',
];
public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if (!in_array($key, $this->getFillable())) {
throw new \Exception("Attribute [{$key}] is not fillable.");
}
}
return parent::fill($attributes);
}
When you're adding attributes like this, Laravel uses the fill() method which is part of mass assignment feature:
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
} elseif ($totallyGuarded) {
throw new MassAssignmentException($key);
}
So, to make it work add all allowed values you want to be saved to $fillable array :
$fillable = ['firstname', 'lastname'];
You could override the model constructor and validate there:
use Illuminate\Support\Facades\Schema;
//...
public function __construct(array $attributes = [])
{
$columns = Schema::getColumnListing($this->table);
foreach ($attributes as $attribute => $value) {
if (! in_array($attribute, $columns)) {
// not allowed
}
}
parent::__construct($attributes);
}
You can use laravel exists:column validation rule for each input.
Please check the documentation https://laravel.com/docs/5.3/validation#rule-exists
OR
You can make helper for this purpose
$table is table name
function validateInputColumns($table, array $inputs)
{
$unknownCols = null;
$i = 0;
foreach ($inputs as $key => $val) {
if (! Schema::hasColumn($table, $key)) {
$unknownCols[$i] = $key;
$i++;
}
}
return is_null($unknownCols) ? true : $unknownCols;
}
It will return the unknown column list in array.
If I understand you correctly, Eloquent Events might be of help to you.
You could then compare the input array to the fillable array.

Laravel Eloquent get relationship with keyBy

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');

Saving related records in laravel

I have users, and users belong to a dealership.
Upon user registration, I'm trying to save a new user, and a new dealership.
User database has a dealership_id column, which I want to be populated with the ID of the newly created dealership.
This is my current code in the UserController store method.
public function store()
{
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$dealership = new Dealership();
$dealership->name = Input::get('dealership_name');
$user->push();
return "User Saved";
}
Trying to use $user->push(); User data gets updated, but dealership is not created or updated.
Eloquent's push() saves the model and its relationships, but first you have to tell what you want to be involved in the relationsship.
Since your user-model/table holds the id of the dealership, I assume that a user can belong to only one dealership, so the relationship should look like this:
User Model:
public function dealership()
{
return $this->belongsTo('Dealership');
}
Dealership Model:
public function users()
{
return $this->hasMany('User');
}
To save a User from the Dealership perspective, you do this:
$dealership->users()->save($user);
To associate a dealership with a user, you do this:
$user->dealership()->associate($dealership);
$user->save();
Please check this answer to see the difference of push() and save()
You will need to define correctly your models relationships as per documentation
If this is done correctly, it should work .
This is what push() does :
/**
* Save the model and all of its relationships.
*
* #return bool
*/
public function push()
{
if ( ! $this->save()) return false;
// To sync all of the relationships to the database, we will simply spin through
// the relationships and save each model via this "push" method, which allows
// us to recurse into all of these nested relations for the model instance.
foreach ($this->relations as $models)
{
foreach (Collection::make($models) as $model)
{
if ( ! $model->push()) return false;
}
}
return true;
}
In your case, you have a one (dealership) belongs to many (users)
In your Users model :
class Users extends Eloquent {
public function dealership()
{
return $this->belongsTo('Dealership');
}
}
In the example above, Eloquent will look for a dealership_id column on the users table.
In your Dealership Model :
class Dealership extends Eloquent {
public function users()
{
return $this->hasMany('User');
}
}
In your store function :
public function store()
{
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$user->dealership = new Dealership();
$user->dealership->name = Input::get('dealership_name');
$user->push();
return "User Saved";
}
Learn here more about eloquent relationships
Also please take a look at my answer here
By using push on the User model, Laravel is basically recursively calling save on all the related models (which, in this case, is none, since you haven't associated any other models to it yet).
Therefore, in order to accomplish what you're trying to do, you can do first create the user then associate the dealership with it by doing the following:
$user = new User();
$user->email = Input::get('email');
$user->password = Input::get('password');
$user->save();
$dealership = new Dealership();
$dealership->name = Input::get('dealership_name');
$user->dealerships()->save($dealership);
return "User Saved";
However, prior to doing this, you must ensure your User and Dealership models have their relationships set up correctly:
User Model:
public function dealership()
{
return $this->belongsTo('Dealership');
}
Dealership Model:
public function users()
{
return $this->hasMany('User');
}
This is how I manage to do it.
In your controller: (Laravel 5)
use Auth;
$dealership->user = Auth::user()->ref_user->staff_id;

How to filter a response based on an extra foreign key in a many-to-many relationship in Laravel?

I'm having a difficult time with this many-to-many relationship in Laravel. I have a many-to-many relationship, projects and persons. I also have a third table, roles (with 2 columns: id, name), which contains roles a person can have on a project (in this case, "actor", "director", etc). The pivot table, person_project, has the columns "person_id", "project_id", and "role_id". I had success getting all the persons associated with a project using
$project = Project::find($id);
$project->persons;
But how can I just get persons with a specific role on a project? And how would I save such a relationship?
Models:
// Project.php
class Project extends Eloquent {
protected $table = 'projects';
public $timestamps = false;
public function persons() {
return $this->belongsToMany('Person');
}
}
// Person.php
class Person extends Eloquent {
protected $table = 'persons';
public $timestamps = false;
public function projects() {
return $this->belongsToMany('Project');
}
}
This article was helpful in figuring out the retrieving of the relationship. Eloquent's withPivot() and join() methods were key in getting it to work.
// In the Project model
public function persons() {
return $this->belongsToMany('Person')
->withPivot('role_id')
->join('roles', 'role_id', '=', 'roles.id');
}
I figured out the insertion part from Laravel's docs: http://laravel.com/docs/eloquent#inserting-related-models
In this example, Input::get('directors') is an array of person_ids selected to be connected to the role of "director". The same deal for Input::get('actors').
// Within the update method of the Projects controller
foreach (Input::get('directors') as $directorId) {
$project->persons()->attach($directorId, array('role_id' => 1)); // roles.id 1 = "director"
}
foreach (Input::get('actors') as $actorId) {
$project->persons()->attach($actorId, array('role_id' => 2)); // roles.id 2 = "actor"
}
Try one of the following:
If you are using Laravel 4.1:
$project = Project::whereHas('persons', function($q)
{
$q->where('role_id', 1);
})->get();
Laravel 4 and 4.1:
$project = Project::with(array('persons' => function($query)
{
$query->where('role_id', 1);
}))->get();

Resources