From 55e2c1ff302f56b7eadf76f73ea074f53edfc406 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Thu, 30 Jan 2025 17:31:08 +0800 Subject: [PATCH 1/8] Fix Type Mismatch in Polymorphic Relationships When Using PostgreSQL fixes #54401 Signed-off-by: Mior Muhammad Zaki --- .../Eloquent/Relations/MorphOneOrMany.php | 14 +++++++- src/Illuminate/Database/Schema/Blueprint.php | 36 +++++++++++++++++++ src/Illuminate/Database/Schema/Builder.php | 18 +++++++--- .../Database/DatabaseSchemaBlueprintTest.php | 2 +- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index 44531957d5b7..ea8fdf614044 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Schema\Builder as SchemaBuilder; use Illuminate\Support\Str; /** @@ -58,7 +59,18 @@ 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); + } } } diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index ca2eed4eb55b..603b2dedd657 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -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); } @@ -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). * diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 9af11e2e0836..bd1f2dcaab74 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -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. @@ -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; @@ -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. * diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index f92672b2f63e..b5a0facade8f 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -19,7 +19,7 @@ class DatabaseSchemaBlueprintTest extends TestCase protected function tearDown(): void { m::close(); - Builder::$defaultMorphKeyType = 'int'; + Builder::$defaultMorphKeyType = null; } public function testToSqlRunsCommandsFromBlueprint() From edbc095bc6678709d5fbe385763b987935a08f80 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 31 Jan 2025 21:06:08 +0800 Subject: [PATCH 2/8] wip Signed-off-by: Mior Muhammad Zaki --- .../Eloquent/Relations/MorphOneOrMany.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index ea8fdf614044..f964ff27c615 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -5,6 +5,7 @@ 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; /** @@ -54,6 +55,7 @@ public function __construct(Builder $query, Model $parent, $type, $id, $localKey * * @return void */ + #[\Override] public function addConstraints() { if (static::$constraints) { @@ -75,6 +77,7 @@ public function addConstraints() } /** @inheritDoc */ + #[\Override] public function addEagerConstraints(array $models) { parent::addEagerConstraints($models); @@ -125,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))) { @@ -139,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( @@ -188,4 +193,27 @@ 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(function ($key) use ($castKeyToString) { + return $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'; + } } From 4a6cf30faa25646ea3dc2c9a7a5f860e9c573434 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 31 Jan 2025 21:44:56 +0800 Subject: [PATCH 3/8] wip Signed-off-by: Mior Muhammad Zaki --- ...uentPolymorphicWithStringMorphTypeTest.php | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php new file mode 100644 index 000000000000..94235a7c4ce1 --- /dev/null +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -0,0 +1,90 @@ +id(); + $table->nullableMorphs('owner'); + $table->string('provider'); + }); + + $user = UserFactory::new()->create([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => bcrypt('password'), + ]); + + DB::table('integrations')->insert([ + 'owner_type' => User::class, + 'owner_id' => $user->id, + 'provider' => 'dummy_provider' + ]); + } + + public function test_it_can_query_using_load_missing() + { + $user = User::query()->where('email', 'taylor@laravel.com')->first(); + + $user->loadMissing('integrations'); + + Assert::assertArraySubset([ + 'name' => 'Taylor Otwell', + 'integrations' => [ + ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], + ], + ], $user->toArray()); + } +} + +class User extends Authenticatable +{ + protected $fillable = ['*']; + + public function integrations() + { + return $this->morphMany(Integration::class, 'owner'); + } +} + +class Integration extends Model +{ + protected $fillable = ['*']; + + public function owner() + { + return $this->morphTo('owner'); + } +} + From 623d9056d84af32c0b4d9da0467a0cf198ce178a Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 31 Jan 2025 13:45:56 +0000 Subject: [PATCH 4/8] Apply fixes from StyleCI --- .../Database/EloquentPolymorphicWithStringMorphTypeTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php index 94235a7c4ce1..fce6cadf3b2a 100644 --- a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -49,7 +49,7 @@ protected function afterRefreshingDatabase() DB::table('integrations')->insert([ 'owner_type' => User::class, 'owner_id' => $user->id, - 'provider' => 'dummy_provider' + 'provider' => 'dummy_provider', ]); } @@ -87,4 +87,3 @@ public function owner() return $this->morphTo('owner'); } } - From 3b03f9429df50f98629c3d2af7997c20e14ca839 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 31 Jan 2025 21:48:55 +0800 Subject: [PATCH 5/8] wip Signed-off-by: Mior Muhammad Zaki --- ...uentPolymorphicWithStringMorphTypeTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php index 94235a7c4ce1..58aa2f7cdaf8 100644 --- a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -53,6 +53,26 @@ protected function afterRefreshingDatabase() ]); } + public function test_it_can_query_from_polymorphic_model() + { + $user = User::first(); + + $user->loadMissing('integrations'); + + Assert::assertArraySubset([ + ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], + ], Integration::where('owner_id', $user->id)->where('owner_type', User::class)->get()->toArray()); + } + + public function test_it_can_query_using_relationship() + { + $user = User::first(); + + Assert::assertArraySubset([ + ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], + ], $user->integrations()->get()->toArray()); + } + public function test_it_can_query_using_load_missing() { $user = User::query()->where('email', 'taylor@laravel.com')->first(); From a2504e0d44b12847cf0b958e1bc836cc0fc04fb3 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 31 Jan 2025 21:52:27 +0800 Subject: [PATCH 6/8] wip Signed-off-by: Mior Muhammad Zaki --- ...uentPolymorphicWithStringMorphTypeTest.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php index 58aa2f7cdaf8..5d2b0546183d 100644 --- a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -47,7 +47,7 @@ protected function afterRefreshingDatabase() ]); DB::table('integrations')->insert([ - 'owner_type' => User::class, + 'owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class, 'owner_id' => $user->id, 'provider' => 'dummy_provider' ]); @@ -55,52 +55,54 @@ protected function afterRefreshingDatabase() public function test_it_can_query_from_polymorphic_model() { - $user = User::first(); + $user = EloquentPolymorphicWithStringMorphTypeTestUser::first(); $user->loadMissing('integrations'); Assert::assertArraySubset([ - ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], - ], Integration::where('owner_id', $user->id)->where('owner_type', User::class)->get()->toArray()); + ['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 = User::first(); + $user = EloquentPolymorphicWithStringMorphTypeTestUser::first(); Assert::assertArraySubset([ - ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], + ['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 = User::query()->where('email', 'taylor@laravel.com')->first(); + $user = EloquentPolymorphicWithStringMorphTypeTestUser::query()->where('email', 'taylor@laravel.com')->first(); $user->loadMissing('integrations'); Assert::assertArraySubset([ 'name' => 'Taylor Otwell', 'integrations' => [ - ['owner_type' => User::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], + ['owner_type' => EloquentPolymorphicWithStringMorphTypeTestUser::class, 'owner_id' => $user->getKey(), 'provider' => 'dummy_provider'], ], ], $user->toArray()); } } -class User extends Authenticatable +class EloquentPolymorphicWithStringMorphTypeTestUser extends Authenticatable { protected $fillable = ['*']; + protected $table = 'users'; public function integrations() { - return $this->morphMany(Integration::class, 'owner'); + return $this->morphMany(EloquentPolymorphicWithStringMorphTypeTestIntegration::class, 'owner'); } } -class Integration extends Model +class EloquentPolymorphicWithStringMorphTypeTestIntegration extends Model { protected $fillable = ['*']; + protected $table = 'integrations'; public function owner() { From e744b1165f4b516b7b71653de7109bbcb0009d85 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Fri, 31 Jan 2025 21:55:10 +0800 Subject: [PATCH 7/8] wip Signed-off-by: Mior Muhammad Zaki --- .../Database/EloquentPolymorphicWithStringMorphTypeTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php index 118fd306d22a..83bd71c41382 100644 --- a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -43,7 +43,6 @@ protected function afterRefreshingDatabase() $user = UserFactory::new()->create([ 'name' => 'Taylor Otwell', 'email' => 'taylor@laravel.com', - 'password' => bcrypt('password'), ]); DB::table('integrations')->insert([ From 210096b62b7d69f1f7b55557ab9fc43fb9345f66 Mon Sep 17 00:00:00 2001 From: Mior Muhammad Zaki Date: Sat, 1 Feb 2025 14:08:05 +0800 Subject: [PATCH 8/8] Update MorphOneOrMany.php --- .../Database/Eloquent/Relations/MorphOneOrMany.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index f964ff27c615..36436037e75f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -201,9 +201,8 @@ protected function getKeys(array $models, $key = null) $castKeyToString = in_array(SchemaBuilder::$defaultMorphKeyType, ['uuid', 'ulid', 'string']); return (new Collection(parent::getKeys($models, $key))) - ->transform(function ($key) use ($castKeyToString) { - return $castKeyToString === true ? (string) $key : $key; - })->all(); + ->transform(fn ($key) => $castKeyToString === true ? (string) $key : $key) + ->all(); } /** @inheritDoc */