Skip to content

[8.x] Add support for disallowing class morphs #38656

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

Merged
merged 3 commits into from
Sep 3, 2021
Merged
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
29 changes: 29 additions & 0 deletions src/Illuminate/Database/ClassMorphViolationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Illuminate\Database;

use RuntimeException;

class ClassMorphViolationException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;

/**
* Create a new exception instance.
*
* @param object $model
*/
public function __construct($model)
{
$class = get_class($model);

parent::__construct("No morph map defined for model [{$class}].");

$this->model = $class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Database\Eloquent\Concerns;

use Closure;
use Illuminate\Database\ClassMorphViolationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -731,6 +732,10 @@ public function getMorphClass()
return array_search(static::class, $morphMap, true);
}

if (Relation::requiresMorphMap()) {
throw new ClassMorphViolationException($this);
}

return static::class;
}

Expand Down
42 changes: 42 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ abstract class Relation
*/
public static $morphMap = [];

/**
* Prevents morph relationships without a morph map.
*
* @var bool
*/
protected static $requireMorphMap = false;

/**
* The count of self joins.
*
Expand Down Expand Up @@ -376,6 +383,41 @@ protected function whereInMethod(Model $model, $key)
: 'whereIn';
}

/**
* Prevent polymorphic relationships from being used without model mappings.
*
* @param bool $requireMorphMap
* @return void
*/
public static function requireMorphMap($requireMorphMap = true)
{
static::$requireMorphMap = $requireMorphMap;
}

/**
* Determine if polymorphic relationships require explicit model mapping.
*
* @return bool
*/
public static function requiresMorphMap()
{
return static::$requireMorphMap;
}

/**
* Define the morph map for polymorphic relations and require all morphed models to be explicitly mapped.
*
* @param array|null $map
* @param bool $merge
* @return array
*/
public static function enforceMorphMap(array $map, $merge = true)
{
static::requireMorphMap();

return static::morphMap($map, $merge);
}

/**
* Set or get the morph map for polymorphic relations.
*
Expand Down
65 changes: 65 additions & 0 deletions tests/Integration/Database/EloquentStrictMorphsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\ClassMorphViolationException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Orchestra\Testbench\TestCase;

class EloquentStrictMorphsTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

Relation::requireMorphMap();
}

public function testStrictModeThrowsAnExceptionOnClassMap()
{
$this->expectException(ClassMorphViolationException::class);

$model = TestModel::make();

$model->getMorphClass();
}

public function testStrictModeDoesNotThrowExceptionWhenMorphMap()
{
$model = TestModel::make();

Relation::morphMap([
'test' => TestModel::class,
]);

$morphName = $model->getMorphClass();
$this->assertEquals('test', $morphName);
}

public function testMapsCanBeEnforcedInOneMethod()
{
$model = TestModel::make();

Relation::requireMorphMap(false);

Relation::enforceMorphMap([
'test' => TestModel::class,
]);

$morphName = $model->getMorphClass();
$this->assertEquals('test', $morphName);
}

protected function tearDown(): void
{
parent::tearDown();

Relation::morphMap([], false);
Relation::requireMorphMap(false);
}
}

class TestModel extends Model
{
}