Skip to content

[11.x] Fix type mismatch in Polymorphic Relationships When Using PostgreSQL #54414

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

Draft
wants to merge 27 commits into
base: 11.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
24 changes: 16 additions & 8 deletions src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,20 @@ protected function newHasOneThrough(Builder $query, Model $farParent, Model $thr
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @param string|null $morphKeyType
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, $this>
*/
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)
public function morphOne($related, $name, $type = null, $id = null, $localKey = null, $morphKeyType = null)
{
$instance = $this->newRelatedInstance($related);

[$type, $id] = $this->getMorphs($name, $type, $id);

$localKey = $localKey ?: $this->getKeyName();

return $this->newMorphOne($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey);
$morphKeyType ??= $this->getKeySchemaType();

return $this->newMorphOne($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey, $morphKeyType);
}

/**
Expand All @@ -214,11 +217,12 @@ public function morphOne($related, $name, $type = null, $id = null, $localKey =
* @param string $type
* @param string $id
* @param string $localKey
* @param string $morphKeyType
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<TRelatedModel, TDeclaringModel>
*/
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey)
protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey, $morphKeyType)
{
return new MorphOne($query, $parent, $type, $id, $localKey);
return new MorphOne($query, $parent, $type, $id, $localKey, $morphKeyType);
}

/**
Expand Down Expand Up @@ -514,9 +518,10 @@ protected function newHasManyThrough(Builder $query, Model $farParent, Model $th
* @param string|null $type
* @param string|null $id
* @param string|null $localKey
* @param string|null $morphKeyType
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, $this>
*/
public function morphMany($related, $name, $type = null, $id = null, $localKey = null)
public function morphMany($related, $name, $type = null, $id = null, $localKey = null, $morphKeyType = null)
{
$instance = $this->newRelatedInstance($related);

Expand All @@ -527,7 +532,9 @@ public function morphMany($related, $name, $type = null, $id = null, $localKey =

$localKey = $localKey ?: $this->getKeyName();

return $this->newMorphMany($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey);
$morphKeyType ??= $this->getKeySchemaType();

return $this->newMorphMany($instance->newQuery(), $this, $instance->qualifyColumn($type), $instance->qualifyColumn($id), $localKey, $morphKeyType);
}

/**
Expand All @@ -541,11 +548,12 @@ public function morphMany($related, $name, $type = null, $id = null, $localKey =
* @param string $type
* @param string $id
* @param string $localKey
* @param string $morphKeyType
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<TRelatedModel, TDeclaringModel>
*/
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey)
protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey, $morphKeyType)
{
return new MorphMany($query, $parent, $type, $id, $localKey);
return new MorphMany($query, $parent, $type, $id, $localKey, $morphKeyType);
}

/**
Expand Down
22 changes: 22 additions & 0 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,28 @@ public function getKeyType()
return $this->keyType;
}

/**
* Get the schema key type.
*
* @return string
*/
public function getKeySchemaType()
{
if ($this->getKeyType() === 'int') {
return 'int';
}

$uses = class_uses_recursive($this);

if (in_array(Concerns\HasUlids::class, $uses, true)) {
return 'ulid';
} elseif (in_array(Concerns\HasUuids::class, $uses, true)) {
return 'uuid';
}

return $this->getKeyType();
}

/**
* Set the data type for the primary key.
*
Expand Down
81 changes: 79 additions & 2 deletions src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

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;
use InvalidArgumentException;

/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
Expand All @@ -29,6 +32,13 @@ abstract class MorphOneOrMany extends HasOneOrMany
*/
protected $morphClass;

/**
* The morph key type.
*
* @var string|null
*/
protected $morphKeyType = null;

/**
* Create a new morph one or many relationship instance.
*
Expand All @@ -37,32 +47,71 @@ abstract class MorphOneOrMany extends HasOneOrMany
* @param string $type
* @param string $id
* @param string $localKey
* @param string|null $morphKeyType
* @return void
*/
public function __construct(Builder $query, Model $parent, $type, $id, $localKey)
public function __construct(Builder $query, Model $parent, $type, $id, $localKey, $morphKeyType = null)
{
$this->morphType = $type;

$this->morphClass = $parent->getMorphClass();

if (! is_null($morphKeyType)) {
$this->morphKeyType($morphKeyType);
}

parent::__construct($query, $parent, $id, $localKey);
}

/**
* Define the morph key type.
*
* @param string $type
* @return $this
*/
public function morphKeyType(string $type)
{
if (! in_array($type, ['int', 'uuid', 'ulid', 'string'])) {
throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', 'ulid', or 'string'.");
}

$this->morphKeyType = $type;

return $this;
}

/**
* Set the base constraints on the relation query.
*
* @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();

return;
}

$query = $this->getRelationQuery();

$morphKeyType = $this->morphKeyType ?? SchemaBuilder::$defaultMorphKeyType;

$query->where($this->foreignKey, '=', transform($this->getParentKey(), fn ($key) => match ($morphKeyType) {
'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 @@ -115,6 +164,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 @@ -129,6 +179,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 @@ -178,4 +229,30 @@ protected function getPossibleInverseRelations(): array
...parent::getPossibleInverseRelations(),
]);
}

/** @inheritDoc */
#[\Override]
protected function getKeys(array $models, $key = null)
{
$morphKeyType = $this->morphKeyType ?? SchemaBuilder::$defaultMorphKeyType;

$castKeyToString = in_array($morphKeyType, ['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)
{
$morphKeyType = $this->morphKeyType ?? SchemaBuilder::$defaultMorphKeyType;

if (! in_array($morphKeyType, ['uuid', 'ulid', 'string'])) {
return parent::whereInMethod($model, $key);
}

return 'whereIn';
}
}
59 changes: 42 additions & 17 deletions src/Illuminate/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Closure;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Grammars\Grammar;
use Illuminate\Database\Schema\Grammars\MySqlGrammar;
Expand Down Expand Up @@ -1039,23 +1038,13 @@ public function foreignIdFor($model, $column = null)

$column = $column ?: $model->getForeignKey();

if ($model->getKeyType() === 'int') {
return $this->foreignId($column)
->table($model->getTable())
->referencesModelColumn($model->getKeyName());
}

$modelTraits = class_uses_recursive($model);

if (in_array(HasUlids::class, $modelTraits, true)) {
return $this->foreignUlid($column, 26)
->table($model->getTable())
->referencesModelColumn($model->getKeyName());
}
$definition = match ($model->getKeySchemaType()) {
'int' => $this->foreignId($column),
'ulid' => $this->foreignUlid($column, 26),
default => $this->foreignUuid($column),
};

return $this->foreignUuid($column)
->table($model->getTable())
->referencesModelColumn($model->getKeyName());
return $definition->table($model->getTable())->referencesModelColumn($model->getKeyName());
}

/**
Expand Down Expand Up @@ -1486,6 +1475,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 +1495,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
Loading
Loading