Skip to content

Commit

Permalink
Allow passing query Expression as column in Many-to-Many relationships (
Browse files Browse the repository at this point in the history
  • Loading branch information
plumthedev committed Mar 30, 2024
1 parent 978ee88 commit 243f807
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 29 deletions.
38 changes: 21 additions & 17 deletions src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
Expand Up @@ -65,7 +65,7 @@ class BelongsToMany extends Relation
/**
* The pivot table columns to retrieve.
*
* @var array
* @var array<string|\Illuminate\Contracts\Database\Query\Expression>
*/
protected $pivotColumns = [];

Expand Down Expand Up @@ -356,7 +356,7 @@ public function as($accessor)
/**
* Set a where clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
Expand All @@ -372,7 +372,7 @@ public function wherePivot($column, $operator = null, $value = null, $boolean =
/**
* Set a "where between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @param string $boolean
* @param bool $not
Expand All @@ -386,7 +386,7 @@ public function wherePivotBetween($column, array $values, $boolean = 'and', $not
/**
* Set a "or where between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @return $this
*/
Expand All @@ -398,7 +398,7 @@ public function orWherePivotBetween($column, array $values)
/**
* Set a "where pivot not between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @param string $boolean
* @return $this
Expand All @@ -411,7 +411,7 @@ public function wherePivotNotBetween($column, array $values, $boolean = 'and')
/**
* Set a "or where not between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @return $this
*/
Expand All @@ -423,7 +423,7 @@ public function orWherePivotNotBetween($column, array $values)
/**
* Set a "where in" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $values
* @param string $boolean
* @param bool $not
Expand All @@ -439,7 +439,7 @@ public function wherePivotIn($column, $values, $boolean = 'and', $not = false)
/**
* Set an "or where" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
Expand All @@ -454,7 +454,7 @@ public function orWherePivot($column, $operator = null, $value = null)
*
* In addition, new pivot records will receive this value.
*
* @param string|array $column
* @param string|\Illuminate\Contracts\Database\Query\Expression|array<string, string> $column
* @param mixed $value
* @return $this
*
Expand Down Expand Up @@ -494,7 +494,7 @@ public function orWherePivotIn($column, $values)
/**
* Set a "where not in" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $values
* @param string $boolean
* @return $this
Expand All @@ -519,7 +519,7 @@ public function orWherePivotNotIn($column, $values)
/**
* Set a "where null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $boolean
* @param bool $not
* @return $this
Expand All @@ -534,7 +534,7 @@ public function wherePivotNull($column, $boolean = 'and', $not = false)
/**
* Set a "where not null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $boolean
* @return $this
*/
Expand All @@ -546,7 +546,7 @@ public function wherePivotNotNull($column, $boolean = 'and')
/**
* Set a "or where null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param bool $not
* @return $this
*/
Expand All @@ -558,7 +558,7 @@ public function orWherePivotNull($column, $not = false)
/**
* Set a "or where not null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @return $this
*/
public function orWherePivotNotNull($column)
Expand All @@ -569,7 +569,7 @@ public function orWherePivotNotNull($column)
/**
* Add an "order by" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $direction
* @return $this
*/
Expand Down Expand Up @@ -1558,11 +1558,15 @@ public function getPivotColumns()
/**
* Qualify the given column name by the pivot table.
*
* @param string $column
* @return string
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @return string|\Illuminate\Contracts\Database\Query\Expression
*/
public function qualifyPivotColumn($column)
{
if ($this->getGrammar()->isExpression($column)) {
return $column;
}

return str_contains($column, '.')
? $column
: $this->table.'.'.$column;
Expand Down
152 changes: 152 additions & 0 deletions tests/Database/DatabaseEloquentBelongsToManyExpressionTest.php
@@ -0,0 +1,152 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use PHPUnit\Framework\TestCase;

class DatabaseEloquentBelongsToManyExpressionTest extends TestCase
{
protected function setUp(): void
{
$db = new DB;

$db->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
]);

$db->bootEloquent();
$db->setAsGlobal();

$this->createSchema();
}

public function testAmbiguousColumnsExpression(): void
{
$this->seedData();

$tags = DatabaseEloquentBelongsToManyExpressionTestTestPost::findOrFail(1)
->tags()
->wherePivotNotIn(new Expression("tag_id || '_' || type"), ['1_t1'])
->get();

$this->assertCount(1, $tags);
$this->assertEquals(2, $tags->first()->getKey());
}

public function testQualifiedColumnExpression(): void
{
$this->seedData();

$tags = DatabaseEloquentBelongsToManyExpressionTestTestPost::findOrFail(2)
->tags()
->wherePivotNotIn(new Expression("taggables.tag_id || '_' || taggables.type"), ['2_t2'])
->get();

$this->assertCount(1, $tags);
$this->assertEquals(3, $tags->first()->getKey());
}

/**
* Setup the database schema.
*
* @return void
*/
public function createSchema()
{
$this->schema()->create('posts', fn (Blueprint $t) => $t->id());
$this->schema()->create('tags', fn (Blueprint $t) => $t->id());
$this->schema()->create('taggables', function (Blueprint $t) {
$t->unsignedBigInteger('tag_id');
$t->unsignedBigInteger('taggable_id');
$t->string('type', 10);
$t->string('taggable_type');
}
);
}

/**
* Tear down the database schema.
*
* @return void
*/
protected function tearDown(): void
{
$this->schema()->drop('posts');
$this->schema()->drop('tags');
$this->schema()->drop('taggables');
}

/**
* Helpers...
*/
protected function seedData(): void
{
$p1 = DatabaseEloquentBelongsToManyExpressionTestTestPost::query()->create();
$p2 = DatabaseEloquentBelongsToManyExpressionTestTestPost::query()->create();
$t1 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();
$t2 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();
$t3 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();

$p1->tags()->sync([
$t1->getKey() => ['type' => 't1'],
$t2->getKey() => ['type' => 't2'],
]);
$p2->tags()->sync([
$t2->getKey() => ['type' => 't2'],
$t3->getKey() => ['type' => 't3'],
]);
}

/**
* Get a database connection instance.
*
* @return \Illuminate\Database\ConnectionInterface
*/
protected function connection()
{
return Eloquent::getConnectionResolver()->connection();
}

/**
* Get a schema builder instance.
*
* @return \Illuminate\Database\Schema\Builder
*/
protected function schema()
{
return $this->connection()->getSchemaBuilder();
}
}

class DatabaseEloquentBelongsToManyExpressionTestTestPost extends Eloquent
{
protected $table = 'posts';
protected $fillable = ['id'];
public $timestamps = false;

public function tags(): MorphToMany
{
return $this->morphToMany(
DatabaseEloquentBelongsToManyExpressionTestTestTag::class,
'taggable',
'taggables',
'taggable_id',
'tag_id',
'id',
'id',
);
}
}

class DatabaseEloquentBelongsToManyExpressionTestTestTag extends Eloquent
{
protected $table = 'tags';
protected $fillable = ['id'];
public $timestamps = false;
}
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -61,6 +62,9 @@ protected function getRelation()
$builder->shouldReceive('getModel')->andReturn($related);
$related->shouldReceive('qualifyColumn');
$builder->shouldReceive('join', 'where');
$builder->shouldReceive('getGrammar')->andReturn(
m::mock(Grammar::class, ['isExpression' => false])
);

return new BelongsToMany(
$builder,
Expand Down
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use stdClass;
Expand Down Expand Up @@ -55,6 +56,7 @@ public function getRelationArguments()
$builder->shouldReceive('join')->once()->with('club_user', 'users.id', '=', 'club_user.user_id');
$builder->shouldReceive('where')->once()->with('club_user.club_id', '=', 1);
$builder->shouldReceive('where')->once()->with('club_user.is_admin', '=', 1, 'and');
$builder->shouldReceive('getGrammar')->andReturn(m::mock(Grammar::class, ['isExpression' => false]));

return [$builder, $parent, 'club_user', 'club_id', 'user_id', 'id', 'id', null, false];
}
Expand Down
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

Expand All @@ -31,6 +32,7 @@ public function testItWillNotTouchRelatedModelsWhenUpdatingChild(): void
$parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
$builder->shouldReceive('getModel')->andReturn($related);
$builder->shouldReceive('where');
$builder->shouldReceive('getGrammar')->andReturn(m::mock(Grammar::class, ['isExpression' => false]));
$relation = new BelongsToMany($builder, $parent, 'article_users', 'user_id', 'article_id', 'id', 'id');
$builder->shouldReceive('update')->never();

Expand Down
2 changes: 2 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Expand Up @@ -2860,6 +2860,7 @@ protected function addMockConnection($model)
$resolver->shouldReceive('connection')->andReturn($connection = m::mock(Connection::class));
$connection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class));
$grammar->shouldReceive('getBitwiseOperators')->andReturn([]);
$grammar->shouldReceive('isExpression')->andReturnFalse();
$connection->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class));
$connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) {
return new BaseBuilder($connection, $grammar, $processor);
Expand Down Expand Up @@ -3214,6 +3215,7 @@ public function getConnection()
$mock = m::mock(Connection::class);
$mock->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class));
$grammar->shouldReceive('getBitwiseOperators')->andReturn([]);
$grammar->shouldReceive('isExpression')->andReturnFalse();
$mock->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class));
$mock->shouldReceive('getName')->andReturn('name');
$mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor) {
Expand Down

0 comments on commit 243f807

Please sign in to comment.