diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index 44531957d5b7..36436037e75f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -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; /** @@ -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); @@ -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))) { @@ -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( @@ -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'; + } } 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() diff --git a/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php new file mode 100644 index 000000000000..83bd71c41382 --- /dev/null +++ b/tests/Integration/Database/EloquentPolymorphicWithStringMorphTypeTest.php @@ -0,0 +1,110 @@ +id(); + $table->nullableMorphs('owner'); + $table->string('provider'); + }); + + $user = UserFactory::new()->create([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + ]); + + 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', 'taylor@laravel.com')->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'); + } +}