diff --git a/config/ide-helper.php b/config/ide-helper.php index 8ca0e08..4b9faea 100644 --- a/config/ide-helper.php +++ b/config/ide-helper.php @@ -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 diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index 47257d5..9c85c09 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -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 * diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php new file mode 100644 index 0000000..a368db8 --- /dev/null +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php @@ -0,0 +1,34 @@ +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'); + } +} diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/Models/NonSoftDeletableModel.php b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/NonSoftDeletableModel.php new file mode 100644 index 0000000..3a8dafb --- /dev/null +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/NonSoftDeletableModel.php @@ -0,0 +1,11 @@ +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); + } +} diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php new file mode 100644 index 0000000..16f1cd0 --- /dev/null +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php @@ -0,0 +1,99 @@ +|ModelWithRelations newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations query() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations whereNonSoftDeletableModelId($value) + * @method static \Illuminate\Database\Eloquent\Builder|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'); + } +} +|NonSoftDeletableModel newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel query() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel whereModelWithRelationsId($value) + * @mixin \Eloquent + */ +class NonSoftDeletableModel extends Model +{ +} +|SoftDeletableModel newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel query() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereModelWithRelationsId($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel withTrashed(bool $withTrashed = true) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel withoutTrashed() + * @mixin \Eloquent + */ +class SoftDeletableModel extends Model +{ + use SoftDeletes; +} diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php new file mode 100644 index 0000000..77c51f0 --- /dev/null +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php @@ -0,0 +1,99 @@ +|ModelWithRelations newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations query() + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations whereNonSoftDeletableModelId($value) + * @method static \Illuminate\Database\Eloquent\Builder|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'); + } +} +|NonSoftDeletableModel newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel query() + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|NonSoftDeletableModel whereModelWithRelationsId($value) + * @mixin \Eloquent + */ +class NonSoftDeletableModel extends Model +{ +} +|SoftDeletableModel newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel query() + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel whereModelWithRelationsId($value) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel withTrashed(bool $withTrashed = true) + * @method static \Illuminate\Database\Eloquent\Builder|SoftDeletableModel withoutTrashed() + * @mixin \Eloquent + */ +class SoftDeletableModel extends Model +{ + use SoftDeletes; +} diff --git a/tests/Console/ModelsCommand/migrations/____soft_deletes_relations_table.php b/tests/Console/ModelsCommand/migrations/____soft_deletes_relations_table.php new file mode 100644 index 0000000..2dde7b4 --- /dev/null +++ b/tests/Console/ModelsCommand/migrations/____soft_deletes_relations_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}