Many-To-Many Column not found Issue in Subfield relationships - php

I am using Laravel BackPack Pro and I am receiving an error when trying to save a relationship in a subfield.
My Model structure:
Event Model 1-n Badge n-m Packages
The error I receive when trying to save.
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'packages' in 'field list'
update `badges` set `packages` = ["1"], `badges`.`updated_at` = 2022-06-28 22:40:36 where `id
The following is my configuration in my EventCrudController. The Idea is to attach different Badges to an event and only make it available to certain badges.
CRUD::field('badges')
->type('relationship')
->subfields([
[ // relationship
'name' => 'packages', // the method on your model that defines the relationship
'type' => "relationship",
// OPTIONALS:
// 'label' => "Category",
// 'attribute' => "title", // attribute on model that is shown to user
// 'placeholder' => "Select a category", // placeholder for the select2 input
],
]);
The weird thing is, that using laravel tinker I can successfully attach any packages. And it displays correctly in the admin but it does not let me save anything. I am unsure if this is a bug or just me configuring it wrongly.
class Badge extends Model
{
use \Backpack\CRUD\app\Models\Traits\CrudTrait;
public $guarded = [];
public function packages()
{
return $this->belongsToMany(Package::class,'badge_package');
}
enter image description here

According to https://github.com/Laravel-Backpack/CRUD/issues/4142 it is currently not supported.

Related

Can't get backpack to pass correct value when inserting/updating a many to many relationship

I have created a many to many relationship between two tables with a third pivot table.
The thing that makes the situation a little difficult is I am linking the Apps table based on name and not ID. It is because I update the App list from a third party and app name will always be consistent, where ID can possibly change if App is removed at some point, and then re-added, etc.
Apps
id
name // This is the name of the app, it will never change for a particular app and is short, all lowercase, no spaces, and unique
label // This is the user friendly name
Plans
id
name
etc
apps_plans pivot table
id
apps_name
plans_id
I've finally got everything working perfectly in Laravel itself, but I cannot figure out at all how to get this to work correctly in Backpack for my Admin portal. I've gotten it to the point where everything works perfect until I try to update or create a new plan. The Apps I select using the select2 type, it tries to insert them into the pivot table with an ID number and not with the name.
Randomizing some names, my mistake if things don't match perfectly. This aspect works fine from all tests I've done:
Plans Model:
{
use CrudTrait;
protected $table = 'plans';
protected $guarded = ['id'];
public function apps()
{
return $this->belongsToMany('App\Apps', 'apps_plans', 'plans_id', 'apps_name', 'id', 'name');
}
}
Apps Model:
class Apps extends Model
{
use CrudTrait;
protected $table = 'apps';
protected $guarded = ['id'];
protected $casts = [
'json' => 'array',
];
public function plans()
{
return $this->belongsToMany('App\Plan', 'apps_plans', 'apps_name', 'plans_id', 'name', 'id');
}
}
**Note I removed the fillable variable , I didn't want to expose all variables in my columns.
Backpack Plans CrudController:
public function setup()
{
CRUD::setModel(\App\Plan::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/plan');
CRUD::setEntityNameStrings('plan', 'plans');
$this->crud->addColumn([
'name' => 'apps',
'type' => 'relationship',
'label' => 'Apps',
'entity' => 'apps',
'attribute' => 'label',
'model' => \App\Apps::class,
]);
}
protected function setupCreateOperation()
{
CRUD::setValidation(PlanRequest::class);
CRUD::setFromDb(); // fields
$this->crud->addField('apps', [
'name' => 'apps',
'type' => 'select2_multiple',
'entity' => 'apps',
'attribute' => 'label',
'label' => 'Apps',
'pivot' => true,
]);
I removed quite a bit to keep my project details private, I hope it makes sense. I think all important details are still in. Anyone know if this is an issue with Backpack? Or did I miss an option somewhere, where you can set which column it uses for the relationship. It is clearly not taking it from the model because the models work just as intended on their own...
Thanks!
Edit: here is my migration I am using, it works flawlessly--even in phpmyadmin it gives me a drop down of items to select from
{
Schema::create('apps_plans', function (Blueprint $table) {
$table->id();
$table->string('apps_name');
$table->foreign('apps_name')->references('name')->on('apps');
$table->unsignedBigInteger('plans_id');
$table->foreign('plans_id')->references('id')->on('plans');
});
}
EDIT 2:
This is the error I am getting when trying to do a Create or Update:
{
"error": "There is a problem with your request",
"message": "SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`api`.`apps_plans`, CONSTRAINT `apps_plans_apps_name_foreign` FOREIGN KEY (`apps_name`) REFERENCES `apps` (`name`)) (SQL: insert into `apps_plans` (`apps_name`, `plans_id`) values (2, 4))"
}
Again I removed some details that were very specific to my project but I don't think I changed any of the logic in the error message. You can see everything looks great about the query except that at the very end, it is inserting the App ID instead of the App name as it should be.
I suspect that the current configuration will have the same result in Laravel directly. ie running something like $plan = Plan::find($somePlanId); $app = App::find($someAppId); $plan->apps()->attach($app); would result in the same error.
Since name is the key that matters for the apps table, consider dropping the autoincrementing id for that table and instead setting
In the migration for the apps table, do:
$table->string('name')->primary();
Then in your apps model, do:
protected $primaryKey = 'name';
public $incrementing = false;
protected $keyType = 'string';
Now, Laravel (and by proxy Backpack) should treat the relationship the way you expect.

Laravel Backpack - Show specific attribute from relationship function

I have the registered Comment model which has a User reference, like this:
public function user() {
return $this->belongsTo('App\User');
}
This function returns an instance of a User, and that's correct, but I don't know how to register the User column the get que user property using Backpack. Instead, I'm get a JSON representation of my model:
So, how do I get a specific field from my relationship function?
Sounds like the select column is a perfect match for you. Just use it in your EntityCrudController's setup() method, like so:
$this->crud->addColumn([
// 1-n relationship
'label' => "User", // Table column heading
'type' => "select",
'name' => 'user_id', // the column that contains the ID of that connected entity;
'entity' => 'user', // the method that defines the relationship in your Model
'attribute' => "user", // foreign key attribute that is shown to user
'model' => "App\Models\User", // foreign key model
]);
The "attribute" tells CRUD what to show in the table cell (the name, the id, etc).
If you do:
$user = $comment->user->user;
You'll get 'test'; (from your example)
It may seem confusing because your User model has a user attribute. Maybe you can call it 'name' instead of 'user'. That way you'll call it:
$username = $comment->user->name;
Remember to check if the relationship exists before calling a property on the related model.
if(!is_null($comment->user)) {
$username = $comment->user->user;
}
Or:
$username = !is_null($comment->user) ? $comment->user->user : 'No user';
If you need to get some field from deeper relation, you can use closure column type:
$this->crud->addColumn([
'label' => trans('backend.column_names.agency_linked_service'),
'name' => 'agency_service_id',
'type' => 'closure',
'function' => function (AgencyLinkedServices $entry) {
return $entry->agencyService->agencyCategory->title;
}
]);

Yii2 filter on related model where relation name is lower camelCase

I've successfully been working through implementing http://www.yiiframework.com/wiki/653/displaying-sorting-and-filtering-model-relations-on-a-gridview/. Which has been working fine where my relation name is just a single word. But where my relation name is something like subSector I'm getting: Column not found: 1054 Unknown column 'subSector.sub_sector' in 'where clause'.
public function search($params)
{
$query = Product::find();
// add in relation to be able to search with
$query->joinWith(['sector', 'subSector'];
...
$dataProvider->sort->attributes['sub_sector_search'] = [
// The tables are the ones our relation are configured to
'asc' => ['subSector.sub_sector' => SORT_ASC],
'desc' => ['subSector.sub_sector' => SORT_DESC],
];
...
$query->andFilterWhere([
'product_id' => $this->product_id,
...
])
->andFilterWhere(['like', 'subSector.sub_sector', $this->sub_sector_search])
I've also added the parameter below the class initialization and added the safe term in the rules.
So far all 3 single word relations work for filtering and both model relations that are camelCase return unknown column.
Use this instead:
->andFilterWhere(['like', Subsector::tableName() . '.sub_sector', $this->sub_sector_search])
and so on.
This will resolve duplicate columns problem and if the table name will change in the future, you just only need to change tableName() method in your model without replacing it in all filters, etc.
Framework interprets this: 'subSector.sub_sector' as just column name prefixed with table name, so if your table named differently than subSector, for example subsectors, it won't work.

How to filter associated HABTM data with FriendsOfCake Search for CakePHP 3

Plugin: FriendsOfCake/Search
CakePHP: 3.1.4
I'm using the plugin to filter my index.ctp view data with a form.
This similar question:
How to Filter on Associated Data
is about a belongsTo association. My question is specifically about associated HABTM data where my associated table is linked through a joinTable and not directly. The normal setup in the Model like the following is not working in this case:
->value('painting', [
field' => $this->Paintings->target()->aliasField('id')
)]
My tables are set up like:
Tickets belongsToMany Paintings
Paintings belongsToMany Tickets
with joinTable tickets_paintings
Here is the main setup:
class TicketsTable extends Table
{
public function initialize(array $config)
{
...
$this->belongsToMany('Paintings', [
'foreignKey' => 'ticket_id',
'targetForeignKey' => 'painting_id',
'joinTable' => 'tickets_paintings'
]);
}
public function searchConfiguration()
{
$search = new Manager($this);
$search->value('status', [
'field' => $this->aliasField('active'),
])->like('member_name', [
'field' => $this->Members->target()->aliasField('surname'),
'filterEmpty' => true
])->value('painting', [
'field' => $this->Paintings->target()->aliasField('id'), // not working
]);
return $search;
}
class TicketsController extends AppController
{
public function index()
{
$query = $this->Tickets
->find('search',
$this->Tickets->filterParams($this->request->query))
->contain(['Members', 'Paintings', 'Appointments']);
...
}
Everything else is working and the parameters are added to the URL when I filter etc., so I only put in the parts where sth has to be wrong.
After filtering I get an error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Paintings.id' in 'where clause'
The contain works properly when just displaying data from the Paintings table in the Tickets view.
But in the code from the SQL query I can see, that all contained tables (Members, Appoinments) are joined for the query except the Paintings table, so obviously it can not find the column...And I guess it can't really join it directly anyway since they are only connected through the joinTable.
I'm new to CakePHP and I can't really figure out what I'm doing wrong here, so hopefully someone can help me out a bit.
Do I have to use a different syntax in the plugin settings? Do I have to set up my Tables differently? Or how exactly can I tell the query to incorporate the habtm related table in the search?
Thanks!
The available search methods rely on the field being available in the main query (hasMany and belongsToMany associations are being being retrieved in separate queries).
While you could join it in manually in the controller, using a callback- or a finder-filter is probably the better approach, that way you can modify the query in the model layer, and you could easily utilize Query::matching() to filter by associated data.
Here's an (untested) example that should give you a hint:
use Cake\ORM\Query;
use Search\Type\Callback; // This changed in master recently
// now it's Search\Model\Filter\Callback
// ...
public function searchConfiguration()
{
$search = new Manager($this);
$search
// ...
->callback('painting', [
'callback' => function (Query $query, array $args, Callback $type) {
return $query
->distinct($this->aliasField('id'))
->matching('Paintings', function (Query $query) use ($args) {
return $query
->where([
$this->Paintings->target()->aliasField('id') => $args['painting']
]);
});
}
]);
return $search;
}
See also
https://github.com/FriendsOfCake/search/blob/9e12117404f824847b2d1aa093f3d52736b658b4/README.md#types
https://github.com/FriendsOfCake/search/blob/master/README.md#filters
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data

Cakephp 3: Set virtual property with condition on belongsToMany association

I have two tables, Tickets and Paintings. A ticket can have many paintings and a painting can be used on many tickets. They have a join table called tickets_paintings with ticket_id and painting_id. Here's how the tables are set:
class TicketsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Paintings', [
'foreignKey' => 'ticket_id',
'targetForeignKey' => 'painting_id',
'joinTable' => 'tickets_paintings'
]);
}
class PaintingsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Tickets', [
'foreignKey' => 'painting_id',
'targetForeignKey' => 'ticket_id',
'joinTable' => 'tickets_paintings'
]);
}
Every Ticket has a field "active" that is boolean and tells me if the ticket is currently in use or not. In the edit function the field can be changed to true or false.
But now I also need a property for every Painting, that tells me if this painting is currently used on an active ticket. In this case it would be unavailable to be used on another ticket.
I thought I could add a boolean virtual property for a painting, that checks if it is (currently) associated with any ticket that has active => true and is therefore also set true or false.
Like: "Does this painting belong to a ticket with state active => 0".
And then set the virtual property accordingly, so I can display it in a view.
I managed to create a virtual property for a painting that checks if there is an entry for its ID inside the join table so far:
class Painting extends Entity
{
protected function _getIsAvailable(){
$TicketsTable = TableRegistry::get('TicketsPaintings');
$exists = $TicketsTable->exists(['painting_id' => $this->id]);
if($exists == true){
return 1;
} else {
return 0;
}
}
}
A) How could I add a condition that checks if any matching ticket_id in the join table has an active=> 1 in the original Tickets table?
The query would have to check the ticket_id in the join table in an DESC order and return true as soon as the first active ticket is found (so it does not check the whole thing every time)
I don't even understand how to access that "active" property on the tickets table to check it. I assume I would have to use the " has Many through" option?! But even trying to following the explanation in the book and this question exactly (for hours), I am not able to get this to work for my example because I don't understand the correct syntax I had to use for my tables.
B) Is it even possible or "advisable" to check sth like this inside the Entity?
I tried to use an extra db field in the paintings table first but it seemed much more complicated to check and set another value every time a ticket is edited (not that I managed to make that work either)...and the active value is pretty much doubled then for ticket and painting. So I thought the virtual property would be easier to handle. Or am I on the completely wrong train?
Thanks for any tips or advice on this nightmare =)!
Since nobody came to my rescue I managed to put something together and actually create the virtual property I wanted. I don't know if this is the best way to go but it works for me and I will put it here in case anyone is searching for similar case.
I created a finder method inside the Paintings Table that joins the 3 tables and filters out all paintings that are connected to an active ticket.
class PaintingsTable extends Table
{
public function findNotavailable(Query $query, array $options){
$query
->join([
'tickets_paintings' =>[
'table' => 'tickets_paintings',
'type' => 'LEFT',
'conditions' => 'tickets_paintings.painting_id = Paintings.id'
],
'tickets' => [
'table' => 'tickets',
'type' => 'LEFT',
'conditions' => 'tickets_paintings.ticket_id = Tickets.id'
],
])
->select(['Paintings.id'])
->order(['tickets_paintings.ticket_id' => 'DESC'])
->where([
'Tickets.active' => false
]);
return $query;
}
}
Then in the Entity of the Painting I check if its ID is inside this selection and set a boolean virtual property accordingly:
use Cake\ORM\TableRegistry;
class Painting extends Entity
{
protected function _getNotAvailable(){
$Paintings = TableRegistry::get('Paintings');
$notavailable = $Paintings->find('notavailable')
->where(['Paintings.id'=>$this->id]);
if($notavailable->isEmpty()){
// if no record --> painting is available
return 0;
} else {
// if record found --> painting is not available
return 1;
}
}
}
And in the view I display it like this:
In Paintings - view.ctp
<?= $painting->not_available ? __('not available') : __('available'); ?>
The only downside to it is that virtual properties can't be used for pagination as far as I understand the book. But I'm sure there's a workaround for that too.

Categories