Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.8] Pivot models provided by using() should use model methods for write operations #27571

Merged
merged 8 commits into from
Feb 26, 2019
35 changes: 26 additions & 9 deletions src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public static function fromAttributes(Model $parent, $attributes, $table, $exist
{
$instance = new static;

// if this factory was presented valid timestamp columns, set the $timestamps
// property accordingly
$instance->timestamps = $instance->hasTimestampAttributes($attributes);

// The pivot model is a "dynamic" model since we will set the tables dynamically
// for the instance. This allows it work for any intermediate tables for the
// many to many relationship that are defined by this developer's classes.
Expand All @@ -57,8 +61,6 @@ public static function fromAttributes(Model $parent, $attributes, $table, $exist

$instance->exists = $exists;

$instance->timestamps = $instance->hasTimestampAttributes();

return $instance;
}

Expand All @@ -75,9 +77,11 @@ public static function fromRawAttributes(Model $parent, $attributes, $table, $ex
{
$instance = static::fromAttributes($parent, [], $table, $exists);

$instance->setRawAttributes($attributes, true);
// if this factory was presented valid timestamp columns, set the $timestamps
// property accordingly
$instance->timestamps = $instance->hasTimestampAttributes($attributes);

$instance->timestamps = $instance->hasTimestampAttributes();
$instance->setRawAttributes($attributes, true);

return $instance;
}
Expand Down Expand Up @@ -110,11 +114,22 @@ protected function setKeysForSaveQuery(Builder $query)
*/
public function delete()
{
// support for pivot classes that container a non-composite primary key
if (isset($this->attributes[$this->getKeyName()])) {
return parent::delete();
return (int) parent::delete();
}

return $this->getDeleteQuery()->delete();
if ($this->fireModelEvent('deleting') === false) {
return 0;
}

$this->touchOwners();

$affectedRows = $this->getDeleteQuery()->delete();

$this->fireModelEvent('deleted', false);

return $affectedRows;
}

/**
Expand Down Expand Up @@ -193,13 +208,15 @@ public function setPivotKeys($foreignKey, $relatedKey)
}

/**
* Determine if the pivot model has timestamp attributes.
* Determine if the pivot model has timestamp attributes in either a provided
* array of attributes or the currently tracked attributes inside the model.
*
* @param $attributes array|null Options attributes to check instead of properties
* @return bool
*/
public function hasTimestampAttributes()
public function hasTimestampAttributes($attributes = null)
{
return array_key_exists($this->getCreatedAtColumn(), $this->attributes);
return array_key_exists($this->getCreatedAtColumn(), $attributes ?? $this->attributes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,19 @@ protected function attachNew(array $records, array $current, $touch = true)
*/
public function updateExistingPivot($id, array $attributes, $touch = true)
{
if ($this->using) {
$updated = $this->newPivot([
$this->foreignPivotKey => $this->parent->getKey(),
$this->relatedPivotKey => $this->parseId($id),
], true)->fill($attributes)->save();

if ($touch) {
$this->touchIfTouching();
}

return (int) $updated;
}

if (in_array($this->updatedAt(), $this->pivotColumns)) {
$attributes = $this->addTimestampsToAttachment($attributes, true);
}
Expand All @@ -209,12 +222,21 @@ public function updateExistingPivot($id, array $attributes, $touch = true)
*/
public function attach($id, array $attributes = [], $touch = true)
{
// Here we will insert the attachment records into the pivot table. Once we have
// inserted the records, we will touch the relationships if necessary and the
// function will return. We can parse the IDs before inserting the records.
$this->newPivotStatement()->insert($this->formatAttachRecords(
$this->parseIds($id), $attributes
));
if ($this->using) {
$records = $this->formatAttachRecords(
$this->parseIds($id), $attributes
);
foreach ($records as $record) {
$this->newPivot($record, false)->save();
}
} else {
// Here we will insert the attachment records into the pivot table. Once we have
// inserted the records, we will touch the relationships if necessary and the
// function will return. We can parse the IDs before inserting the records.
$this->newPivotStatement()->insert($this->formatAttachRecords(
$this->parseIds($id), $attributes
));
}

if ($touch) {
$this->touchIfTouching();
Expand Down Expand Up @@ -355,26 +377,36 @@ protected function hasPivotColumn($column)
*/
public function detach($ids = null, $touch = true)
{
$query = $this->newPivotQuery();
if ($this->using) {
$results = 0;
foreach ($this->parseIds($ids) as $id) {
$results += $this->newPivot([
$this->foreignPivotKey => $this->parent->getKey(),
$this->relatedPivotKey => $id,
], true)->delete();
}
} else {
$query = $this->newPivotQuery();

// If associated IDs were passed to the method we will only delete those
// associations, otherwise all of the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
if (! is_null($ids)) {
$ids = $this->parseIds($ids);

// If associated IDs were passed to the method we will only delete those
// associations, otherwise all of the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
if (! is_null($ids)) {
$ids = $this->parseIds($ids);
if (empty($ids)) {
return 0;
}

if (empty($ids)) {
return 0;
$query->whereIn($this->relatedPivotKey, (array) $ids);
}

$query->whereIn($this->relatedPivotKey, (array) $ids);
// Once we have all of the conditions set on the statement, we are ready
// to run the delete on the pivot table. Then, if the touch parameter
// is true, we will go ahead and touch all related models to sync.
$results = $query->delete();
}

// Once we have all of the conditions set on the statement, we are ready
// to run the delete on the pivot table. Then, if the touch parameter
// is true, we will go ahead and touch all related models to sync.
$results = $query->delete();

if ($touch) {
$this->touchIfTouching();
}
Expand Down
2 changes: 2 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/Pivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Pivot extends Model
{
use AsPivot;

public $incrementing = false;

/**
* The attributes that aren't mass assignable.
*
Expand Down
3 changes: 2 additions & 1 deletion tests/Database/DatabaseEloquentPivotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ public function testDeleteMethodDeletesModelByKeys()
$query->shouldReceive('delete')->once()->andReturn(true);
$pivot->expects($this->once())->method('newQueryWithoutRelationships')->will($this->returnValue($query));

$this->assertTrue($pivot->delete());
$rowsAffected = $pivot->delete();
$this->assertEquals(1, $rowsAffected);
}

public function testPivotModelTableNameIsSingular()
Expand Down
115 changes: 115 additions & 0 deletions tests/Integration/Database/EloquentPivotEventsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Pivot;

/**
* @group integration
*/
class EloquentPivotEventsTest extends DatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('users', function ($table) {
$table->increments('id');
$table->string('email');
$table->timestamps();
});

Schema::create('projects', function ($table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});

Schema::create('project_users', function ($table) {
$table->integer('user_id');
$table->integer('project_id');
$table->string('role')->nullable();
});
}

public function test_pivot_will_trigger_events_to_be_fired()
{
$user = PivotEventsTestUser::forceCreate(['email' => '[email protected]']);
$user2 = PivotEventsTestUser::forceCreate(['email' => '[email protected]']);
$project = PivotEventsTestProject::forceCreate(['name' => 'Test Project']);

$project->collaborators()->attach($user);
$this->assertEquals(['saving', 'creating', 'created', 'saved'], PivotEventsTestCollaborator::$eventsCalled);

PivotEventsTestCollaborator::$eventsCalled = [];
$project->collaborators()->sync([$user2->id]);
$this->assertEquals(['deleting', 'deleted', 'saving', 'creating', 'created', 'saved'], PivotEventsTestCollaborator::$eventsCalled);

PivotEventsTestCollaborator::$eventsCalled = [];
$project->collaborators()->sync([$user->id => ['role' => 'owner'], $user2->id => ['role' => 'contributor']]);
$this->assertEquals(['saving', 'creating', 'created', 'saved', 'saving', 'updating', 'updated', 'saved'], PivotEventsTestCollaborator::$eventsCalled);
}
}

class PivotEventsTestUser extends Model
{
public $table = 'users';
}

class PivotEventsTestProject extends Model
{
public $table = 'projects';

public function collaborators()
{
return $this->belongsToMany(
PivotEventsTestUser::class, 'project_users', 'project_id', 'user_id'
)->using(PivotEventsTestCollaborator::class);
}
}

class PivotEventsTestCollaborator extends Pivot
{
public $table = 'project_users';

public static $eventsCalled = [];

public static function boot()
{
parent::boot();

static::creating(function ($model) {
static::$eventsCalled[] = 'creating';
});

static::created(function ($model) {
static::$eventsCalled[] = 'created';
});

static::updating(function ($model) {
static::$eventsCalled[] = 'updating';
});

static::updated(function ($model) {
static::$eventsCalled[] = 'updated';
});

static::saving(function ($model) {
static::$eventsCalled[] = 'saving';
});

static::saved(function ($model) {
static::$eventsCalled[] = 'saved';
});

static::deleting(function ($model) {
static::$eventsCalled[] = 'deleting';
});

static::deleted(function ($model) {
static::$eventsCalled[] = 'deleted';
});
}
}