Skip to content

Commit

Permalink
Feature: Add Config Option to Enforce Nullable Relationships (#1580)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jeramyhing authored Oct 30, 2024
1 parent 90e7c5a commit 472c1ba
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- Drop support for Laravel versions earlier than 11.15.

### 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)

Expand Down
23 changes: 23 additions & 0 deletions config/ide-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,29 @@
*/
'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
Expand Down
4 changes: 3 additions & 1 deletion src/Console/ModelsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -836,13 +836,15 @@ protected function isRelationNullable(string $relation, Relation $relationObj):
$fkProp = $reflectionObj->getProperty('foreignKey');
$fkProp->setAccessible(true);

$enforceNullableRelation = $this->laravel['config']->get('ide-helper.enforce_nullable_relationships', true);

foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) {
if (isset($this->nullableColumns[$foreignKey])) {
return true;
}

if (!in_array($foreignKey, $this->foreignKeyConstraintsColumns, true)) {
return true;
return $enforceNullableRelation;
}
}

Expand Down
19 changes: 19 additions & 0 deletions tests/Console/ModelsCommand/Relations/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,23 @@ public function test(): void
$this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
$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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 472c1ba

Please sign in to comment.