Skip to content

Commit

Permalink
Merge pull request #5640 from Laravel-Backpack/handle-dynamic-model-r…
Browse files Browse the repository at this point in the history
…elations

Handle dynamic model relations
  • Loading branch information
pxpm authored Aug 28, 2024
2 parents c7be222 + d12571b commit c6b033e
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ protected function makeSureColumnHasEntity($column)
}

// if there's a method on the model with this name
if (method_exists($this->model, $column['name'])) {
if (method_exists($this->model, $column['name']) || $this->model->isRelation($column['name'])) {
// check model method for possibility of being a relationship
$column['entity'] = $this->modelMethodIsRelationship($this->model, $column['name']);

Expand Down
4 changes: 2 additions & 2 deletions src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,15 @@ protected function makeSureFieldHasEntity($field)

//if the name is dot notation we are sure it's a relationship
if (strpos($field['name'], '.') !== false) {
$possibleMethodName = Str::of($field['name'])->before('.');
$possibleMethodName = Str::of($field['name'])->before('.')->value();
// check model method for possibility of being a relationship
$field['entity'] = $this->modelMethodIsRelationship($model, $possibleMethodName) ? $field['name'] : false;

return $field;
}

// if there's a method on the model with this name
if (method_exists($model, $field['name'])) {
if (method_exists($model, $field['name']) || $model->isRelation($field['name'])) {
// check model method for possibility of being a relationship
$field['entity'] = $this->modelMethodIsRelationship($model, $field['name']);

Expand Down
11 changes: 6 additions & 5 deletions src/app/Library/CrudPanel/Traits/Relationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function getRelationInstance($field)
$possible_method = Str::before($entity, '.');
$model = isset($field['baseModel']) ? app($field['baseModel']) : $this->model;

if (method_exists($model, $possible_method)) {
if (method_exists($model, $possible_method) || $model->isRelation($possible_method)) {
$parts = explode('.', $entity);
// here we are going to iterate through all relation parts to check
foreach ($parts as $i => $part) {
Expand Down Expand Up @@ -271,7 +271,7 @@ private function getOverwrittenNameForBelongsTo($field)
{
$relation = $this->getRelationInstance($field);

if (Str::afterLast($field['name'], '.') === $relation->getRelationName()) {
if (Str::afterLast($field['name'], '.') === $relation->getRelationName() || Str::endsWith($relation->getRelationName(), '{closure}')) {
return $relation->getForeignKeyName();
}

Expand Down Expand Up @@ -319,15 +319,16 @@ private static function getPivotFieldStructure($field)
* If the return type extends the Relation class is for sure a relation
* Otherwise we just assume it's a relation.
*
* DEV NOTE: In future versions we will return `false` when no return type is set and make the return type mandatory for relationships.
* This function should be refactored to only check if $returnType is a subclass of Illuminate\Database\Eloquent\Relations\Relation.
*
* @param $model
* @param $method
* @return bool|string
*/
private function modelMethodIsRelationship($model, $method)
{
if (! method_exists($model, $method) && $model->isRelation($method)) {
return $method;
}

$methodReflection = new \ReflectionMethod($model, $method);

// relationship methods function does not have parameters
Expand Down
4 changes: 2 additions & 2 deletions src/app/Library/CrudPanel/Traits/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private function getModelAttributeValueFromRelationship($model, $field)
{
[$relatedModel, $relationMethod] = $this->getModelAndMethodFromEntity($model, $field);

if (! method_exists($relatedModel, $relationMethod)) {
if (! method_exists($relatedModel, $relationMethod) && ! $relatedModel->isRelation($relationMethod)) {
return $relatedModel->{$relationMethod};
}

Expand Down Expand Up @@ -157,7 +157,7 @@ private function getModelAttributeValueFromRelationship($model, $field)
break;
case 'HasOne':
case 'MorphOne':
if (! method_exists($relatedModel, $relationMethod)) {
if (! method_exists($relatedModel, $relationMethod) && ! $relatedModel->isRelation($relationMethod)) {
return;
}

Expand Down
17 changes: 17 additions & 0 deletions tests/Unit/CrudPanel/CrudPanelColumnsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -928,4 +928,21 @@ public function testColumnArrayDefinitionLinkToRouteNameAndAdditionalParameters(
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show?test=testing&test2=Some%20Content', $url);
}

public function testItCanInferFieldAttributesFromADynamicRelation()
{
User::resolveRelationUsing('dynamicRelation', function ($user) {
return $user->belongsTo(\Backpack\CRUD\Tests\config\Models\Bang::class);
});

$this->crudPanel->setModel(User::class);
$this->crudPanel->addColumn('dynamicRelation');

$column = $this->crudPanel->columns()['dynamicRelation'];

$this->assertEquals('dynamicRelation', $column['name']);
$this->assertEquals('name', $column['attribute']);
$this->assertEquals('relationship', $column['type']);
$this->assertEquals('BelongsTo', $column['relation_type']);
}
}
142 changes: 142 additions & 0 deletions tests/Unit/CrudPanel/CrudPanelCreateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,38 @@ public function testCreateWithOneToOneRelationship()
$this->assertEquals($account_details->nickname, $account_details_nickname);
}

public function testCreateWithOneToOneDynamicRelationship()
{
User::resolveRelationUsing('dynamicRelation', function ($user) {
return $user->hasOne(\Backpack\CRUD\Tests\config\Models\AccountDetails::class);
});
$this->crudPanel->setModel(User::class);
$this->crudPanel->addFields($this->userInputFieldsNoRelationships);
$this->crudPanel->addFields([
[
'name' => 'dynamicRelation.nickname',
],
[
'name' => 'dynamicRelation.profile_picture',
],
]);
$faker = Factory::create();
$account_details_nickname = $faker->name;
$inputData = [
'name' => $faker->name,
'email' => $faker->safeEmail,
'password' => Hash::make($faker->password()),
'dynamicRelation' => [
'nickname' => $account_details_nickname,
'profile_picture' => 'test.jpg',
],
];
$entry = $this->crudPanel->create($inputData);
$account_details = $entry->accountDetails()->first();

$this->assertEquals($account_details->nickname, $account_details_nickname);
}

public function testCreateWithOneToOneRelationshipUsingRepeatableInterface()
{
$this->crudPanel->setModel(User::class);
Expand Down Expand Up @@ -246,6 +278,50 @@ public function testCreateWithOneToManyRelationship()
$this->assertEquals($article->id, $entry->id);
}

public function testCreateWithOneToManyDynamicRelationship()
{
Article::resolveRelationUsing('dynamicRelation', function ($article) {
return $article->belongsTo(\Backpack\CRUD\Tests\config\Models\User::class, 'user_id');
});

$this->crudPanel->setModel(Article::class);
$this->crudPanel->addFields([
[
'name' => 'id',
'type' => 'hidden',
], [
'name' => 'content',
], [
'name' => 'tags',
], [
'name' => 'dynamicRelation',
],
]);
$faker = Factory::create();
$inputData = [
'content' => $faker->text(),
'tags' => $faker->words(3, true),
'dynamicRelation' => 1,
'metas' => null,
'extras' => null,
'cast_metas' => null,
'cast_tags' => null,
'cast_extras' => null,
];

$entry = $this->crudPanel->create($inputData);

unset($inputData['dynamicRelation']);
$inputData['user_id'] = 1;

$userEntry = User::find(1);
$article = Article::where('user_id', 1)->with('dynamicRelation')->get()->last();
$this->assertEntryEquals($inputData, $entry);
$this->assertEquals($article->user_id, $entry->user_id);
$this->assertEquals($article->id, $entry->id);
$this->assertEquals($article->user_id, $entry->dynamicRelation->id);
}

public function testCreateWithManyToManyRelationship()
{
$this->crudPanel->setModel(User::class);
Expand All @@ -265,6 +341,45 @@ public function testCreateWithManyToManyRelationship()
$this->assertEntryEquals($inputData, $entry);
}

public function testCreateWithManyToManyDynamicRelationship()
{
User::resolveRelationUsing('dynamicRelation', function ($user) {
return $user->belongsToMany(\Backpack\CRUD\Tests\config\Models\Role::class, 'user_role');
});

$this->crudPanel->setModel(User::class);
$this->crudPanel->addFields([
[
'name' => 'id',
'type' => 'hidden',
], [
'name' => 'name',
], [
'name' => 'email',
'type' => 'email',
], [
'name' => 'password',
'type' => 'password',
], [
'name' => 'dynamicRelation',
],
]);
$faker = Factory::create();
$inputData = [
'name' => $faker->name,
'email' => $faker->safeEmail,
'password' => Hash::make($faker->password()),
'remember_token' => null,
'dynamicRelation' => [1, 2],
];

$entry = $this->crudPanel->create($inputData);

$this->assertInstanceOf(User::class, $entry);
$this->assertEntryEquals($inputData, $entry);
$this->assertCount(2, $entry->dynamicRelation);
}

public function testGetRelationFields()
{
$this->markTestIncomplete('Not correctly implemented');
Expand Down Expand Up @@ -1783,6 +1898,33 @@ public function testItCanRegisterModelEventsInTheFields()
$this->assertEquals('[email protected]', User::latest('id')->first()->email);
}

public function testItCanCreateDynamicRelationships()
{
User::resolveRelationUsing('dynamicRelation', function ($user) {
return $user->belongsTo(\Backpack\CRUD\Tests\config\Models\Bang::class, 'bang_relation_field');
});

$this->crudPanel->setModel(User::class);
$this->crudPanel->addFields($this->userInputFieldsNoRelationships);
$this->crudPanel->addField([
'name' => 'dynamicRelation',
]);

$faker = Factory::create();

$inputData = [
'name' => $faker->name,
'email' => $faker->safeEmail,
'password' => Hash::make($faker->password()),
'remember_token' => null,
'dynamicRelation' => 1,
];

$entry = $this->crudPanel->create($inputData);

$this->assertEquals($entry->dynamicRelation()->first()->name, Bang::find(1)->name);
}

private function getPivotInputData(array $pivotRelationData, bool $initCrud = true, bool $allowDuplicates = false)
{
$faker = Factory::create();
Expand Down
22 changes: 22 additions & 0 deletions tests/Unit/CrudPanel/CrudPanelFieldsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,28 @@ public function testItCanGetFieldNamesFromNamesWithCommas()
$this->crudPanel->addField('test1, test2');
$this->assertEquals(['test1', 'test2'], $this->crudPanel->getAllFieldNames());
}

public function testItCanInferFieldAttributesFromADynamicRelation()
{
User::resolveRelationUsing('dynamicRelation', function ($user) {
return $user->hasOne(\Backpack\CRUD\Tests\config\Models\AccountDetails::class);
});

$this->crudPanel->setModel(User::class);
$this->crudPanel->addField('dynamicRelation.nickname');

$this->assertEquals([
'name' => 'dynamicRelation[nickname]',
'type' => 'relationship',
'entity' => 'dynamicRelation.nickname',
'relation_type' => 'HasOne',
'attribute' => 'nickname',
'model' => 'Backpack\CRUD\Tests\Config\Models\AccountDetails',
'multiple' => false,
'pivot' => false,
'label' => 'DynamicRelation.nickname',
], $this->crudPanel->fields()['dynamicRelation.nickname']);
}
}

class Invokable
Expand Down
2 changes: 1 addition & 1 deletion tests/config/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class User extends Model

protected $table = 'users';

protected $fillable = ['name', 'email', 'password', 'extras'];
protected $fillable = ['name', 'email', 'password', 'extras', 'bang_relation_field'];

public function identifiableAttribute()
{
Expand Down

0 comments on commit c6b033e

Please sign in to comment.