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

Fix Type Mismatch in Polymorphic Relationships When Using PostgreSQL #54414

Draft
wants to merge 9 commits into
base: 11.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

/**
Expand Down Expand Up @@ -53,16 +55,29 @@ public function __construct(Builder $query, Model $parent, $type, $id, $localKey
*
* @return void
*/
#[\Override]
public function addConstraints()
{
if (static::$constraints) {
$this->getRelationQuery()->where($this->morphType, $this->morphClass);

parent::addConstraints();
if (is_null(SchemaBuilder::$defaultMorphKeyType)) {
parent::addConstraints();
} else {
$query = $this->getRelationQuery();

$query->where($this->foreignKey, '=', transform($this->getParentKey(), fn ($key) => match (SchemaBuilder::$defaultMorphKeyType) {
'uuid', 'ulid', 'string' => (string) $key,
default => $key,
}));

$query->whereNotNull($this->foreignKey);
}
}
}

/** @inheritDoc */
#[\Override]
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
Expand Down Expand Up @@ -113,6 +128,7 @@ protected function setForeignAttributesForCreate(Model $model)
* @param array|null $update
* @return int
*/
#[\Override]
public function upsert(array $values, $uniqueBy, $update = null)
{
if (! empty($values) && ! is_array(reset($values))) {
Expand All @@ -127,6 +143,7 @@ public function upsert(array $values, $uniqueBy, $update = null)
}

/** @inheritDoc */
#[\Override]
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(
Expand Down Expand Up @@ -176,4 +193,26 @@ protected function getPossibleInverseRelations(): array
...parent::getPossibleInverseRelations(),
]);
}

/** @inheritDoc */
#[\Override]
protected function getKeys(array $models, $key = null)
{
$castKeyToString = in_array(SchemaBuilder::$defaultMorphKeyType, ['uuid', 'ulid', 'string']);

return (new Collection(parent::getKeys($models, $key)))
->transform(fn ($key) => $castKeyToString === true ? (string) $key : $key)
->all();
}

/** @inheritDoc */
#[\Override]
protected function whereInMethod(Model $model, $key)
{
if (! in_array(SchemaBuilder::$defaultMorphKeyType, ['uuid', 'ulid', 'string'])) {
return parent::whereInMethod($model, $key);
}

return 'whereIn';
}
}
36 changes: 36 additions & 0 deletions src/Illuminate/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,8 @@ public function morphs($name, $indexName = null)
$this->uuidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'ulid') {
$this->ulidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'string') {
$this->stringableMorphs($name, $indexName);
} else {
$this->numericMorphs($name, $indexName);
}
Expand All @@ -1504,11 +1506,45 @@ public function nullableMorphs($name, $indexName = null)
$this->nullableUuidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'ulid') {
$this->nullableUlidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'string') {
$this->nullableStringableMorphs($name, $indexName);
} else {
$this->nullableNumericMorphs($name, $indexName);
}
}

/**
* Add the proper columns for a polymorphic table using string as IDs (mixed of UUID/ULID & incremental integer).
*
* @param string $name
* @param string|null $indexName
* @return void
*/
public function stringableMorphs($name, $indexName = null)
{
$this->string("{$name}_type");

$this->string("{$name}_id");

$this->index(["{$name}_type", "{$name}_id"], $indexName);
}

/**
* Add nullable columns for a polymorphic table using string as IDs (mixed of UUID/ULID & incremental integer).
*
* @param string $name
* @param string|null $indexName
* @return void
*/
public function nullableStringableMorphs($name, $indexName = null)
{
$this->string("{$name}_type")->nullable();

$this->string("{$name}_id")->nullable();

$this->index(["{$name}_type", "{$name}_id"], $indexName);
}

/**
* Add the proper columns for a polymorphic table using numeric IDs (incremental).
*
Expand Down
18 changes: 14 additions & 4 deletions src/Illuminate/Database/Schema/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ class Builder
/**
* The default relationship morph key type.
*
* @var string
* @var string|null
*/
public static $defaultMorphKeyType = 'int';
public static $defaultMorphKeyType = null;

/**
* Create a new database Schema manager.
Expand Down Expand Up @@ -81,8 +81,8 @@ public static function defaultStringLength($length)
*/
public static function defaultMorphKeyType(string $type)
{
if (! in_array($type, ['int', 'uuid', 'ulid'])) {
throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', or 'ulid'.");
if (! in_array($type, ['int', 'uuid', 'ulid', 'string'])) {
throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', 'ulid', or 'string'.");
}

static::$defaultMorphKeyType = $type;
Expand All @@ -108,6 +108,16 @@ public static function morphUsingUlids()
static::defaultMorphKeyType('ulid');
}

/**
* Set the default morph key type for migrations to string as IDs (mixed of UUID/ULID & incremental integer).
*
* @return void
*/
public static function morphUsingString()
{
static::defaultMorphKeyType('string');
}

/**
* Create a database in the schema.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/Database/DatabaseSchemaBlueprintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class DatabaseSchemaBlueprintTest extends TestCase
protected function tearDown(): void
{
m::close();
Builder::$defaultMorphKeyType = 'int';
Builder::$defaultMorphKeyType = null;
}

public function testToSqlRunsCommandsFromBlueprint()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder as SchemaBuilder;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Testing\Assert;
use Orchestra\Testbench\Attributes\WithMigration;
use Orchestra\Testbench\Factories\UserFactory;

#[WithMigration]
class EloquentPolymorphicWithStringMorphTypeTest extends DatabaseTestCase
{
/** @inheritDoc */
protected function setUp(): void
{
SchemaBuilder::morphUsingString();

parent::setUp();
}

/** @inheritDoc */
protected function tearDown(): void
{
parent::tearDown();

SchemaBuilder::$defaultMorphKeyType = null;
}

/** @inheritDoc */
protected function afterRefreshingDatabase()
{
Schema::create('integrations', function (Blueprint $table) {
$table->id();
$table->nullableMorphs('owner');
$table->string('provider');
});

$user = UserFactory::new()->create([
'name' => 'Taylor Otwell',
'email' => '[email protected]',
]);

DB::table('integrations')->insert([
'owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class,
'owner_id' => $user->id,
'provider' => 'dummy_provider',
]);
}

public function test_it_can_query_from_polymorphic_model()
{
$user = EloquentPolymorphicWithStringMorphTypeTestUser::first();

$user->loadMissing('integrations');

Assert::assertArraySubset([
['owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'],
], EloquentPolymorphicWithStringMorphTypeTestIntegration::where('owner_id', $user->id)->where('owner_type', EloquentPolymorphicWithStringMorphTypeTestUser::class)->get()->toArray());
}

public function test_it_can_query_using_relationship()
{
$user = EloquentPolymorphicWithStringMorphTypeTestUser::first();

Assert::assertArraySubset([
['owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'],
], $user->integrations()->get()->toArray());
}

public function test_it_can_query_using_load_missing()
{
$user = EloquentPolymorphicWithStringMorphTypeTestUser::query()->where('email', '[email protected]')->first();

$user->loadMissing('integrations');

Assert::assertArraySubset([
'name' => 'Taylor Otwell',
'integrations' => [
['owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'],
],
], $user->toArray());
}
}

class EloquentPolymorphicWithStringMorphTypeTestUser extends Authenticatable
{
protected $fillable = ['*'];
protected $table = 'users';

public function integrations()
{
return $this->morphMany(EloquentPolymorphicWithStringMorphTypeTestIntegration::class, 'owner');
}
}

class EloquentPolymorphicWithStringMorphTypeTestIntegration extends Model
{
protected $fillable = ['*'];
protected $table = 'integrations';

public function owner()
{
return $this->morphTo('owner');
}
}