Feature: Add Config Option to Enforce Nullable Relationships (#1580)

* feat: add config option for nullable relationships

Introduce enforce_nullable_relationships configuration to control nullable Eloquent relationships.

* refactor: update logic for nullable relationships

Update isRelationNullable method to respect new config option.

* test: add tests for nullable relationship config

- Verify behavior of enforce_nullable_relationships configuration option.
- Create snapshot to reflect enforce_nullable_relationships set to false.

* docs: improve documented context and usage

* docs: update CHANGELOG with enforce_nullable_relationships option
This commit is contained in:
Jeramy Hing
2024-10-30 22:35:58 +01:00
committed by GitHub
parent 90e7c5ac47
commit 472c1bad5d
5 changed files with 312 additions and 1 deletions
+1
View File
@@ -39,6 +39,7 @@
- Drop support for Laravel versions earlier than 11.15. - Drop support for Laravel versions earlier than 11.15.
### Added ### Added
- Introduce `enforce_nullable_relationships` configuration option to control how nullable Eloquent relationships are enforced during static analysis. This provides flexibility for scenarios where application logic ensures data integrity without relying on database constraints. [#1580 / jeramyhing](https://github.com/barryvdh/laravel-ide-helper/pull/1580)
- Add support for AsCollection::using and AsEnumCollection::of casts [#1577 / uno-sw](https://github.com/barryvdh/laravel-ide-helper/pull/1577) - Add support for AsCollection::using and AsEnumCollection::of casts [#1577 / uno-sw](https://github.com/barryvdh/laravel-ide-helper/pull/1577)
+23
View File
@@ -299,6 +299,29 @@ return [
*/ */
'additional_relation_return_types' => [], 'additional_relation_return_types' => [],
/*
|--------------------------------------------------------------------------
| Enforce nullable Eloquent relationships on not null columns
|--------------------------------------------------------------------------
|
| When set to true (default), this option enforces nullable Eloquent relationships.
| However, in cases where the application logic ensures the presence of related
| records it may be desirable to set this option to false to avoid unwanted null warnings.
|
| Default: true
| A not null column with no foreign key constraint will have a "nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation|null $notNullColumnWithNoForeignKeyConstraint
|
| Option: false
| A not null column with no foreign key constraint will have a "not nullable" relationship.
| * @property int $not_null_column_with_no_foreign_key_constraint
| * @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
|
*/
'enforce_nullable_relationships' => true,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Run artisan commands after migrations to generate model helpers | Run artisan commands after migrations to generate model helpers
+3 -1
View File
@@ -836,13 +836,15 @@ class ModelsCommand extends Command
$fkProp = $reflectionObj->getProperty('foreignKey'); $fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(true); $fkProp->setAccessible(true);
$enforceNullableRelation = $this->laravel['config']->get('ide-helper.enforce_nullable_relationships', true);
foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) { foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) {
if (isset($this->nullableColumns[$foreignKey])) { if (isset($this->nullableColumns[$foreignKey])) {
return true; return true;
} }
if (!in_array($foreignKey, $this->foreignKeyConstraintsColumns, true)) { if (!in_array($foreignKey, $this->foreignKeyConstraintsColumns, true)) {
return true; return $enforceNullableRelation;
} }
} }
@@ -46,4 +46,23 @@ class Test extends AbstractModelsCommand
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay()); $this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
$this->assertMatchesMockedSnapshot(); $this->assertMatchesMockedSnapshot();
} }
public function testRelationNotNullable(): void
{
// Disable enforcing nullable relationships
Config::set('ide-helper.enforce_nullable_relationships', 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();
// Re-enable default enforcing nullable relationships
Config::set('ide-helper.enforce_nullable_relationships', true);
}
} }
@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Relations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
*
*
* @property int $id
* @property int $not_null_column_with_foreign_key_constraint
* @property int $not_null_column_with_no_foreign_key_constraint
* @property int|null $nullable_column_with_foreign_key_constraint
* @property int|null $nullable_column_with_no_foreign_key_constraint
* @property-read BelongsToVariation $notNullColumnWithForeignKeyConstraint
* @property-read BelongsToVariation $notNullColumnWithNoForeignKeyConstraint
* @property-read BelongsToVariation|null $nullableColumnWithForeignKeyConstraint
* @property-read BelongsToVariation|null $nullableColumnWithNoForeignKeyConstraint
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation query()
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation whereNotNullColumnWithForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation whereNotNullColumnWithNoForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation whereNullableColumnWithForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|BelongsToVariation whereNullableColumnWithNoForeignKeyConstraint($value)
* @mixin \Eloquent
*/
class BelongsToVariation extends Model
{
public function notNullColumnWithForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(self::class, 'not_null_column_with_foreign_key_constraint');
}
public function notNullColumnWithNoForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(self::class, 'not_null_column_with_no_foreign_key_constraint');
}
public function nullableColumnWithForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(self::class, 'nullable_column_with_foreign_key_constraint');
}
public function nullableColumnWithNoForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(self::class, 'nullable_column_with_no_foreign_key_constraint');
}
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Relations\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
*
*
* @property int $id
* @property int $not_null_column_with_foreign_key_constraint
* @property int $not_null_column_with_no_foreign_key_constraint
* @property int|null $nullable_column_with_foreign_key_constraint
* @property int|null $nullable_column_with_no_foreign_key_constraint
* @property-read CompositeBelongsToVariation $bothNonNullableWithForeignKeyConstraint
* @property-read CompositeBelongsToVariation $nonNullableMixedWithoutForeignKeyConstraint
* @property-read CompositeBelongsToVariation|null $nullableMixedWithForeignKeyConstraint
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation query()
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation whereNotNullColumnWithForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation whereNotNullColumnWithNoForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation whereNullableColumnWithForeignKeyConstraint($value)
* @method static \Illuminate\Database\Eloquent\Builder|CompositeBelongsToVariation whereNullableColumnWithNoForeignKeyConstraint($value)
* @mixin \Eloquent
*/
class CompositeBelongsToVariation extends Model
{
public $table = 'belongs_to_variations';
public function bothNonNullableWithForeignKeyConstraint(): BelongsTo
{
// Note, duplicating the keys here for simplicity.
return $this->belongsTo(
self::class,
['not_null_column_with_foreign_key_constraint', 'not_null_column_with_foreign_key_constraint'],
['not_null_column_with_foreign_key_constraint', 'not_null_column_with_foreign_key_constraint'],
);
}
public function nonNullableMixedWithoutForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(
self::class,
['not_null_column_with_foreign_key_constraint', 'not_null_column_with_no_foreign_key_constraint'],
['not_null_column_with_foreign_key_constraint', 'not_null_column_with_no_foreign_key_constraint'],
);
}
public function nullableMixedWithForeignKeyConstraint(): BelongsTo
{
return $this->belongsTo(
self::class,
['nullable_column_with_no_foreign_key_constraint', 'not_null_column_with_foreign_key_constraint'],
['nullable_column_with_no_foreign_key_constraint', 'not_null_column_with_foreign_key_constraint'],
);
}
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Relations\Models;
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Relations\ModelsOtherNamespace\AnotherModel;
use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\Relations\Traits\HasTestRelations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
/**
*
*
* @property int $id
* @property-read Simple $relationBelongsTo
* @property-read AnotherModel $relationBelongsToInAnotherNamespace
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationBelongsToMany
* @property-read int|null $relation_belongs_to_many_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationBelongsToManyWithSub
* @property-read int|null $relation_belongs_to_many_with_sub_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationBelongsToManyWithSubAnother
* @property-read int|null $relation_belongs_to_many_with_sub_another_count
* @property-read AnotherModel $relationBelongsToSameNameAsColumn
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationHasMany
* @property-read int|null $relation_has_many_count
* @property-read Simple|null $relationHasOne
* @property-read Simple $relationHasOneWithDefault
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationMorphMany
* @property-read int|null $relation_morph_many_count
* @property-read Simple|null $relationMorphOne
* @property-read Model|\Eloquent $relationMorphTo
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationMorphedByMany
* @property-read int|null $relation_morphed_by_many_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationSampleRelationType
* @property-read int|null $relation_sample_relation_type_count
* @property-read Model|\Eloquent $relationSampleToAnyMorphedRelationType
* @property-read \Illuminate\Database\Eloquent\Collection<int, Simple> $relationSampleToAnyRelationType
* @property-read int|null $relation_sample_to_any_relation_type_count
* @property-read Simple $relationSampleToBadlyNamedNotManyRelation
* @property-read Simple $relationSampleToManyRelationType
* @method static \Illuminate\Database\Eloquent\Builder|Simple newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Simple query()
* @method static \Illuminate\Database\Eloquent\Builder|Simple whereId($value)
* @mixin \Eloquent
*/
class Simple extends Model
{
use HasTestRelations;
// Regular relations
public function relationHasMany(): HasMany
{
return $this->hasMany(Simple::class);
}
public function relationHasOne(): HasOne
{
return $this->hasOne(Simple::class);
}
public function relationHasOneWithDefault(): HasOne
{
return $this->hasOne(Simple::class)->withDefault();
}
public function relationBelongsTo(): BelongsTo
{
return $this->belongsTo(Simple::class);
}
public function relationBelongsToMany(): BelongsToMany
{
return $this->belongsToMany(Simple::class);
}
public function relationBelongsToManyWithSub(): BelongsToMany
{
return $this->belongsToMany(Simple::class)->where('foo', 'bar');
}
public function relationBelongsToManyWithSubAnother(): BelongsToMany
{
return $this->relationBelongsToManyWithSub()->where('foo', 'bar');
}
public function relationMorphTo(): MorphTo
{
return $this->morphTo();
}
public function relationMorphOne(): MorphOne
{
return $this->morphOne(Simple::class, 'relationMorphTo');
}
public function relationMorphMany(): MorphMany
{
return $this->morphMany(Simple::class, 'relationMorphTo');
}
public function relationMorphedByMany(): MorphToMany
{
return $this->morphedByMany(Simple::class, 'foo');
}
// Custom relations
public function relationBelongsToInAnotherNamespace(): BelongsTo
{
return $this->belongsTo(AnotherModel::class);
}
public function relationBelongsToSameNameAsColumn(): BelongsTo
{
return $this->belongsTo(AnotherModel::class, __FUNCTION__);
}
public function relationSampleToManyRelationType()
{
return $this->testToOneRelation(Simple::class);
}
public function relationSampleRelationType()
{
return $this->testToManyRelation(Simple::class);
}
public function relationSampleToAnyRelationType()
{
return $this->testToAnyRelation(Simple::class);
}
public function relationSampleToAnyMorphedRelationType()
{
return $this->testToAnyMorphedRelation(Simple::class);
}
public function relationSampleToBadlyNamedNotManyRelation()
{
return $this->testToBadlyNamedNotManyRelation(Simple::class);
}
}