diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php index 9c65627..8e539d9 100644 --- a/src/Console/ModelsCommand.php +++ b/src/Console/ModelsCommand.php @@ -44,6 +44,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Database\Schema\Builder; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; @@ -943,13 +944,55 @@ class ModelsCommand extends Command } } - if ($this->relatedModelUsesSoftDeletes($relationObj)) { + if ( + $this->relatedModelUsesSoftDeletes($relationObj) + && !$this->relationIncludesNonTrashedParents($relationObj) + ) { return true; } return false; } + /** + * Check whether the relation explicitly opts into returning non-soft-deleted parents + * via ->withTrashed(), in which case the SoftDeletes-based nullability no longer applies. + * + * Returns false for ->onlyTrashed() and ->withoutTrashed(), which still leave the + * relation potentially empty depending on the parent's soft-delete state. + * + * @param Relation $relationObj + * + * @return bool + */ + protected function relationIncludesNonTrashedParents(Relation $relationObj): bool + { + $query = $relationObj->getQuery(); + + if (!in_array(SoftDeletingScope::class, $query->removedScopes(), true)) { + return false; + } + + $relatedModel = $relationObj->getRelated(); + + if (!method_exists($relatedModel, 'getQualifiedDeletedAtColumn')) { + return true; + } + + $deletedAtColumn = $relatedModel->getQualifiedDeletedAtColumn(); + + foreach ($query->getQuery()->wheres ?? [] as $where) { + if ( + ($where['column'] ?? null) === $deletedAtColumn + && in_array($where['type'] ?? null, ['Null', 'NotNull'], true) + ) { + return false; + } + } + + return true; + } + /** * Check if the related model uses the SoftDeletes trait * diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php index a368db8..687afa7 100644 --- a/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/Models/ModelWithRelations.php @@ -17,6 +17,21 @@ class ModelWithRelations extends Model return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id'); } + public function softDeletableWithTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withTrashed(); + } + + public function softDeletableOnlyTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->onlyTrashed(); + } + + public function softDeletableWithoutTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withoutTrashed(); + } + public function nonSoftDeletable(): BelongsTo { return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id'); diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php index 16f1cd0..6760e30 100644 --- a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__testSoftDeletesForceNullableDisabled__1.php @@ -16,6 +16,9 @@ use Illuminate\Database\Eloquent\Relations\HasOne; * @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 + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel $softDeletableOnlyTrashed + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel $softDeletableWithTrashed + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel $softDeletableWithoutTrashed * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newQuery() * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations query() @@ -33,6 +36,21 @@ class ModelWithRelations extends Model return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id'); } + public function softDeletableWithTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withTrashed(); + } + + public function softDeletableOnlyTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->onlyTrashed(); + } + + public function softDeletableWithoutTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withoutTrashed(); + } + public function nonSoftDeletable(): BelongsTo { return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id'); diff --git a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php index 77c51f0..14ea4c6 100644 --- a/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php +++ b/tests/Console/ModelsCommand/SoftDeletesRelations/__snapshots__/Test__test__1.php @@ -16,6 +16,9 @@ use Illuminate\Database\Eloquent\Relations\HasOne; * @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 + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel|null $softDeletableOnlyTrashed + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel $softDeletableWithTrashed + * @property-read \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SoftDeletesRelations\Models\SoftDeletableModel|null $softDeletableWithoutTrashed * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations newQuery() * @method static \Illuminate\Database\Eloquent\Builder|ModelWithRelations query() @@ -33,6 +36,21 @@ class ModelWithRelations extends Model return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id'); } + public function softDeletableWithTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withTrashed(); + } + + public function softDeletableOnlyTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->onlyTrashed(); + } + + public function softDeletableWithoutTrashed(): BelongsTo + { + return $this->belongsTo(SoftDeletableModel::class, 'soft_deletable_model_id')->withoutTrashed(); + } + public function nonSoftDeletable(): BelongsTo { return $this->belongsTo(NonSoftDeletableModel::class, 'non_soft_deletable_model_id');