Make Soft Deleted Relationships Nullable (#1749)

This commit is contained in:
Evan Burrell
2026-01-11 14:34:18 +01:00
committed by GitHub
parent 2ac73f2953
commit b0dcd7c62f
9 changed files with 381 additions and 0 deletions
+23
View File
@@ -339,6 +339,29 @@ return [
'enforce_nullable_relationships' => true,
/*
|--------------------------------------------------------------------------
| Make soft deletable relations nullable
|--------------------------------------------------------------------------
|
| When set to true (default), relationships to models using SoftDeletes trait
| will be marked as nullable. This is because soft-deleted records are excluded
| from queries by default, meaning even non-nullable foreign keys can return
| null when the related model is soft-deleted.
|
| Default: true
| A relationship to a soft-deletable model will include |null in the type:
| * @property-read Team|null $team
|
| Option: false
| A relationship to a soft-deletable model will NOT include |null (unless
| nullable for other reasons such as nullable foreign key column):
| * @property-read Team $team
|
*/
'soft_deletes_force_nullable' => true,
/*
|--------------------------------------------------------------------------
| Run artisan commands after migrations to generate model helpers
+22
View File
@@ -911,9 +911,31 @@ class ModelsCommand extends Command
}
}
if ($this->relatedModelUsesSoftDeletes($relationObj)) {
return true;
}
return false;
}
/**
* Check if the related model uses the SoftDeletes trait
*
* @param Relation $relationObj
*
* @return bool
*/
protected function relatedModelUsesSoftDeletes(Relation $relationObj): bool
{
if (!$this->laravel['config']->get('ide-helper.soft_deletes_force_nullable', true)) {
return false;
}
$relatedModel = $relationObj->getRelated();
return in_array('Illuminate\\Database\\Eloquent\\SoftDeletes', class_uses_recursive($relatedModel));
}
/**
* Check if the morphTo relation is nullable
*
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
class ModelWithRelations extends Model
{
protected $table = 'models_with_relations';
public function softDeletable(): BelongsTo
{
return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id');
}
public function nonSoftDeletable(): BelongsTo
{
return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id');
}
public function softDeletableHasOne(): HasOne
{
return $this->hasOne(SoftDeletableModel::class, 'model_with_relations_id');
}
public function nonSoftDeletableHasOne(): HasOne
{
return $this->hasOne(NonSoftDeletableModel::class, 'model_with_relations_id');
}
}
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
class NonSoftDeletableModel extends Model
{
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SoftDeletableModel extends Model
{
use SoftDeletes;
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations;
use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\AbstractModelsCommand;
use Illuminate\Support\Facades\Config;
class Test extends AbstractModelsCommand
{
public function test(): void
{
$command = $this->app->make(ModelsCommand::class);
$tester = $this->runCommand($command, [
'--write' => true,
]);
$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
$this->assertMatchesMockedSnapshot();
}
public function testSoftDeletesForceNullableDisabled(): void
{
Config::set('ide-helper.soft_deletes_force_nullable', false);
$command = $this->app->make(ModelsCommand::class);
$tester = $this->runCommand($command, [
'--write' => true,
]);
$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
$this->assertMatchesMockedSnapshot();
Config::set('ide-helper.soft_deletes_force_nullable', true);
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @property int $id
* @property int $soft_deletable_model_id
* @property int $non_soft_deletable_model_id
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\NonSoftDeletableModel $nonSoftDeletable
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\NonSoftDeletableModel|null $nonSoftDeletableHasOne
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel $softDeletable
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel|null $softDeletableHasOne
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereNonSoftDeletableModelId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereSoftDeletableModelId($value)
* @mixin \Eloquent
*/
class ModelWithRelations extends Model
{
protected $table = 'models_with_relations';
public function softDeletable(): BelongsTo
{
return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id');
}
public function nonSoftDeletable(): BelongsTo
{
return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id');
}
public function softDeletableHasOne(): HasOne
{
return $this->hasOne(SoftDeletableModel::class, 'model_with_relations_id');
}
public function nonSoftDeletableHasOne(): HasOne
{
return $this->hasOne(NonSoftDeletableModel::class, 'model_with_relations_id');
}
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int|null $model_with_relations_id
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel whereModelWithRelationsId($value)
* @mixin \Eloquent
*/
class NonSoftDeletableModel extends Model
{
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property int|null $model_with_relations_id
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereModelWithRelationsId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel withoutTrashed()
* @mixin \Eloquent
*/
class SoftDeletableModel extends Model
{
use SoftDeletes;
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @property int $id
* @property int $soft_deletable_model_id
* @property int $non_soft_deletable_model_id
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\NonSoftDeletableModel $nonSoftDeletable
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\NonSoftDeletableModel|null $nonSoftDeletableHasOne
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel|null $softDeletable
* @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel|null $softDeletableHasOne
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereNonSoftDeletableModelId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ModelWithRelations whereSoftDeletableModelId($value)
* @mixin \Eloquent
*/
class ModelWithRelations extends Model
{
protected $table = 'models_with_relations';
public function softDeletable(): BelongsTo
{
return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id');
}
public function nonSoftDeletable(): BelongsTo
{
return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id');
}
public function softDeletableHasOne(): HasOne
{
return $this->hasOne(SoftDeletableModel::class, 'model_with_relations_id');
}
public function nonSoftDeletableHasOne(): HasOne
{
return $this->hasOne(NonSoftDeletableModel::class, 'model_with_relations_id');
}
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int|null $model_with_relations_id
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NonSoftDeletableModel whereModelWithRelationsId($value)
* @mixin \Eloquent
*/
class NonSoftDeletableModel extends Model
{
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property int|null $model_with_relations_id
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel whereModelWithRelationsId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|SoftDeletableModel withoutTrashed()
* @mixin \Eloquent
*/
class SoftDeletableModel extends Model
{
use SoftDeletes;
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SoftDeletesRelationsTable extends Migration
{
public function up(): void
{
Schema::create('soft_deletable_models', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('model_with_relations_id')->nullable();
$table->softDeletes();
});
Schema::create('non_soft_deletable_models', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('model_with_relations_id')->nullable();
});
Schema::create('models_with_relations', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('soft_deletable_model_id');
$table->unsignedBigInteger('non_soft_deletable_model_id');
$table->foreign('soft_deletable_model_id')
->references('id')
->on('soft_deletable_models');
$table->foreign('non_soft_deletable_model_id')
->references('id')
->on('non_soft_deletable_models');
});
}
}