From bef505b71ad066825cfdfeef97c542a8ca30bd9d Mon Sep 17 00:00:00 2001 From: pxpm Date: Wed, 28 Aug 2024 15:15:02 +0100 Subject: [PATCH 1/5] handle dynamic relations --- .../Traits/ColumnsProtectedMethods.php | 2 +- .../Traits/FieldsProtectedMethods.php | 4 ++-- .../CrudPanel/Traits/Relationships.php | 6 ++++- tests/Unit/CrudPanel/CrudPanelFieldsTest.php | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/app/Library/CrudPanel/Traits/ColumnsProtectedMethods.php b/src/app/Library/CrudPanel/Traits/ColumnsProtectedMethods.php index aaa8f9c3e6..a61f0c69af 100644 --- a/src/app/Library/CrudPanel/Traits/ColumnsProtectedMethods.php +++ b/src/app/Library/CrudPanel/Traits/ColumnsProtectedMethods.php @@ -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']); diff --git a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php index 7cabb22752..fd1e17c9b9 100644 --- a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php +++ b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php @@ -149,7 +149,7 @@ 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; @@ -157,7 +157,7 @@ protected function makeSureFieldHasEntity($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']); diff --git a/src/app/Library/CrudPanel/Traits/Relationships.php b/src/app/Library/CrudPanel/Traits/Relationships.php index 140f6fcd92..0a54281c55 100644 --- a/src/app/Library/CrudPanel/Traits/Relationships.php +++ b/src/app/Library/CrudPanel/Traits/Relationships.php @@ -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) { @@ -328,6 +328,10 @@ private static function getPivotFieldStructure($field) */ private function modelMethodIsRelationship($model, $method) { + if($model->isRelation($method)) { + return $method; + } + $methodReflection = new \ReflectionMethod($model, $method); // relationship methods function does not have parameters diff --git a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php index a8d2835ed9..37ca4fd689 100644 --- a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php +++ b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php @@ -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 From cad9a440e95b5cddfe47cd26ef7d54da544e4940 Mon Sep 17 00:00:00 2001 From: pxpm Date: Wed, 28 Aug 2024 15:30:32 +0100 Subject: [PATCH 2/5] fix condition --- src/app/Library/CrudPanel/Traits/Relationships.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/Library/CrudPanel/Traits/Relationships.php b/src/app/Library/CrudPanel/Traits/Relationships.php index 0a54281c55..38d0ad0a09 100644 --- a/src/app/Library/CrudPanel/Traits/Relationships.php +++ b/src/app/Library/CrudPanel/Traits/Relationships.php @@ -319,19 +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($model->isRelation($method)) { + if(! method_exists($model, $method) && $model->isRelation($method)) { return $method; } - + $methodReflection = new \ReflectionMethod($model, $method); // relationship methods function does not have parameters From c76487b6b0348df514dbbb7a747db71a5e617b26 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 28 Aug 2024 14:30:48 +0000 Subject: [PATCH 3/5] Apply fixes from StyleCI [ci skip] [skip ci] --- src/app/Library/CrudPanel/Traits/Relationships.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/Library/CrudPanel/Traits/Relationships.php b/src/app/Library/CrudPanel/Traits/Relationships.php index 38d0ad0a09..f3524119bc 100644 --- a/src/app/Library/CrudPanel/Traits/Relationships.php +++ b/src/app/Library/CrudPanel/Traits/Relationships.php @@ -325,7 +325,7 @@ private static function getPivotFieldStructure($field) */ private function modelMethodIsRelationship($model, $method) { - if(! method_exists($model, $method) && $model->isRelation($method)) { + if (! method_exists($model, $method) && $model->isRelation($method)) { return $method; } From 96b6254ee62514c2e5432dba4984e6af5400ddee Mon Sep 17 00:00:00 2001 From: pxpm Date: Wed, 28 Aug 2024 16:39:06 +0100 Subject: [PATCH 4/5] add more tests --- .../CrudPanel/Traits/Relationships.php | 2 +- src/app/Library/CrudPanel/Traits/Update.php | 4 +- tests/Unit/CrudPanel/CrudPanelColumnsTest.php | 17 +++ tests/Unit/CrudPanel/CrudPanelCreateTest.php | 144 ++++++++++++++++++ tests/Unit/CrudPanel/CrudPanelFieldsTest.php | 2 +- tests/config/Models/User.php | 2 +- 6 files changed, 166 insertions(+), 5 deletions(-) diff --git a/src/app/Library/CrudPanel/Traits/Relationships.php b/src/app/Library/CrudPanel/Traits/Relationships.php index f3524119bc..04788bf668 100644 --- a/src/app/Library/CrudPanel/Traits/Relationships.php +++ b/src/app/Library/CrudPanel/Traits/Relationships.php @@ -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(); } diff --git a/src/app/Library/CrudPanel/Traits/Update.php b/src/app/Library/CrudPanel/Traits/Update.php index bdd3dbbd6d..cf894c236d 100644 --- a/src/app/Library/CrudPanel/Traits/Update.php +++ b/src/app/Library/CrudPanel/Traits/Update.php @@ -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}; } @@ -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; } diff --git a/tests/Unit/CrudPanel/CrudPanelColumnsTest.php b/tests/Unit/CrudPanel/CrudPanelColumnsTest.php index b189a5c455..7baf104e77 100644 --- a/tests/Unit/CrudPanel/CrudPanelColumnsTest.php +++ b/tests/Unit/CrudPanel/CrudPanelColumnsTest.php @@ -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']); + } } diff --git a/tests/Unit/CrudPanel/CrudPanelCreateTest.php b/tests/Unit/CrudPanel/CrudPanelCreateTest.php index b0d8b2edcb..7d42143d55 100644 --- a/tests/Unit/CrudPanel/CrudPanelCreateTest.php +++ b/tests/Unit/CrudPanel/CrudPanelCreateTest.php @@ -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); @@ -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); @@ -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'); @@ -1783,6 +1898,35 @@ public function testItCanRegisterModelEventsInTheFields() $this->assertEquals('backpack@laravel.com', 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(); diff --git a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php index 37ca4fd689..bcb429260a 100644 --- a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php +++ b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php @@ -640,7 +640,7 @@ public function testGetStrippedSaveRequestWithClass() $this->assertIsArray($result); $this->assertSame(['invokable' => 'invokable'], $result); } - + public function testItDoesNotUseProtectedMethodsAsRelationshipMethods() { $this->crudPanel->setModel(User::class); diff --git a/tests/config/Models/User.php b/tests/config/Models/User.php index 9cf264c9a9..bb3ccbc3b9 100644 --- a/tests/config/Models/User.php +++ b/tests/config/Models/User.php @@ -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() { From d12571b170b207f734c9e1f6c576de4a6b8dc0b7 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 28 Aug 2024 15:39:24 +0000 Subject: [PATCH 5/5] Apply fixes from StyleCI [ci skip] [skip ci] --- src/app/Library/CrudPanel/Traits/Relationships.php | 2 +- tests/Unit/CrudPanel/CrudPanelCreateTest.php | 4 +--- tests/Unit/CrudPanel/CrudPanelFieldsTest.php | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/Library/CrudPanel/Traits/Relationships.php b/src/app/Library/CrudPanel/Traits/Relationships.php index 04788bf668..af108929f2 100644 --- a/src/app/Library/CrudPanel/Traits/Relationships.php +++ b/src/app/Library/CrudPanel/Traits/Relationships.php @@ -271,7 +271,7 @@ private function getOverwrittenNameForBelongsTo($field) { $relation = $this->getRelationInstance($field); - if (Str::afterLast($field['name'], '.') === $relation->getRelationName() || Str::endsWith($relation->getRelationName(), '{closure}') ) { + if (Str::afterLast($field['name'], '.') === $relation->getRelationName() || Str::endsWith($relation->getRelationName(), '{closure}')) { return $relation->getForeignKeyName(); } diff --git a/tests/Unit/CrudPanel/CrudPanelCreateTest.php b/tests/Unit/CrudPanel/CrudPanelCreateTest.php index 7d42143d55..813f7fbde8 100644 --- a/tests/Unit/CrudPanel/CrudPanelCreateTest.php +++ b/tests/Unit/CrudPanel/CrudPanelCreateTest.php @@ -279,7 +279,7 @@ public function testCreateWithOneToManyRelationship() } public function testCreateWithOneToManyDynamicRelationship() - { + { Article::resolveRelationUsing('dynamicRelation', function ($article) { return $article->belongsTo(\Backpack\CRUD\Tests\config\Models\User::class, 'user_id'); }); @@ -1925,8 +1925,6 @@ public function testItCanCreateDynamicRelationships() $this->assertEquals($entry->dynamicRelation()->first()->name, Bang::find(1)->name); } - - private function getPivotInputData(array $pivotRelationData, bool $initCrud = true, bool $allowDuplicates = false) { $faker = Factory::create(); diff --git a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php index bcb429260a..37ca4fd689 100644 --- a/tests/Unit/CrudPanel/CrudPanelFieldsTest.php +++ b/tests/Unit/CrudPanel/CrudPanelFieldsTest.php @@ -640,7 +640,7 @@ public function testGetStrippedSaveRequestWithClass() $this->assertIsArray($result); $this->assertSame(['invokable' => 'invokable'], $result); } - + public function testItDoesNotUseProtectedMethodsAsRelationshipMethods() { $this->crudPanel->setModel(User::class);