Magento JoinLeft() in custom orders grid causing SQL integrity constrain violation for non-admin user in multi-website setup - mysql

I have extended the Mage_Adminhtml_Block_Sales_Order_Grid class with a custom module to add several customer attributes (Magento EE 1.10) to the grid.
I added the custom attributes to the collection in my MyCompany_MyModule_Block_Adminhtml_Order_Grid class in the _prepareCollection() method using three joins like this:
protected function _prepareCollection()
{
$collection = Mage::getResourceModel($this->_getCollectionClass());
//get the table names for the customer attributes we'll need
$customerEntityVarchar = Mage::getSingleton('core/resource')
->getTableName('customer_entity_varchar');
$customerEntityInt = Mage::getSingleton('core/resource')
->getTableName('customer_entity_int');
// add left joins to display the necessary customer attribute values
$collection->getSelect()->joinLeft(array(
'customer_entity_int_table'=>$customerEntityInt),
'`main_table`.`customer_id`=`customer_entity_int_table`.`entity_id`
AND `customer_entity_int_table`.`attribute_id`=148',
array('bureau'=>'value'));
$collection->getSelect()->joinLeft(array(
'customer_entity_varchar_table'=>$customerEntityVarchar),
'`main_table`.`customer_id`=`customer_entity_varchar_table`.`entity_id`
AND `customer_entity_varchar_table`.`attribute_id`=149',
array('index_code'=>'value'));
$collection->getSelect()->joinLeft(array(
'customer_entity_varchar_2_table'=>$customerEntityVarchar),
'`main_table`.`customer_id`=`customer_entity_varchar_2_table`.`entity_id`
AND `customer_entity_varchar_2_table`.`attribute_id`=150',
array('did_number'=>'value'));
$this->setCollection($collection);
return parent::_prepareCollection();
}
UPDATE: While everything displays fine when viewing orders, things are not fine when I try to search / filter orders by any of the text join fields (index_code or did_number). The result is a SQL error: "SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'store_id' in where clause is ambiguous."
This problem also exists if I remove all but one of the leftJoin() statements, so something is going wrong with both (either) of the joins with the customer_entity_varchar table.

As now there are two columns with the name store_id, you have to specify filter_index when you add the column to the grid:
$this->addColumn('store_id', array(
...
'filter_index'=>'main_table.store_id',
));
So that it knows which one you are referring while filtering.
I hope it helps!

More than likely it is because you are joining customer_entity_varchar_table twice.
$collection->getSelect()->joinLeft(array(
'customer_entity_varchar_table'=>$customerEntityVarchar),
'`main_table`.`customer_id`=`customer_entity_varchar_table`.`entity_id`
AND `customer_entity_varchar_table`.`attribute_id`=149',
array('index_code'=>'value'));
$collection->getSelect()->joinLeft(array(
'customer_entity_varchar_2_table'=>$customerEntityVarchar),
'`main_table`.`customer_id`=`customer_entity_varchar_2_table`.`entity_id`
AND `customer_entity_varchar_2_table`.`attribute_id`=150',
array('did_number'=>'value'));
You may want to combine those, you can also try and print the SQL to see what the Query looks like:
$collection->getSelect()->getSelectSql();
More info on collections: http://blog.chapagain.com.np/magento-collection-functions/

The problem appears to exist in two different places. One case is if logged in as a user with a single store, the other as a user who can filter various stores.
Single store user
The solution I went with was to override the addAttributeToFilter method on the collection class. Not knowing exactly what changing the Enterprise_AdminGws_Model_Collections::addStoreAttributeToFilter method would affect other behavior I wanted to avoid that, and I found adding a filter index in Mage_Adminhtml_Block_Sales_Order_Grid as Javier suggested did not work.
Instead I added the following method to Mage_Sales_Model_Resource_Order_Grid_Collection:
/**
* {#inheritdoc}
*/
public function addAttributeToFilter($attribute, $condition = null)
{
if (is_string($attribute) && 'store_id' == $attribute) {
$attribute = 'main_table.' . $attribute;
}
return parent::addFieldToFilter($attribute, $condition);
}
A patch can be found here: https://gist.github.com/josephdpurcell/baf93992ff2d941d02c946aeccd48853
Multi-store user
If a user can filter orders by store at admin/sales_order, the following change is also needed to Mage_Adminhtml_Block_Sales_Order_Grid around line 75:
if (!Mage::app()->isSingleStoreMode()) {
$this->addColumn('store_id', array(
'header' => Mage::helper('sales')->__('Purchased From (Store)'),
'index' => 'store_id',
'type' => 'store',
'store_view'=> true,
'display_deleted' => true,
'filter_index' => 'main_table.store_id',
));
}
A patch can be found here: https://gist.github.com/josephdpurcell/c96286a7c4d2f5d1fe92fb36ee5d0d5a

I had the same bug, after grepping the code, I finally found the troublemaker which is in the Enterprise_AdminGws_Model_Collections class at line ~235:
/**
* Add store_id attribute to filter of EAV-collection
*
* #param Mage_Eav_Model_Entity_Collection_Abstract $collection
*/
public function addStoreAttributeToFilter($collection)
{
$collection->addAttributeToFilter('store_id', array('in' => $this->_role->getStoreIds()));
}
You have to replace 'store_id' by 'main_table.store_id', of course you'll have to extend that particular method in your own rewrite to stick into Magento guidelines :p
Hope it helps!

Related

Laravel Many To Many Merges Pivot?

I must be going insane or be really tired. So I have this situation where I get a collection of all the Roles assigned to the User. That part goes ok.... however I noticed something super strange.
I am using Laravel 8 and PHP8 (not the strange part).
For some reason, I do not get only the result from the other table but also pivot data is merged in. I can't tell why this is happening. Here is the example:
Relationship on user model:
/**
* Relationship with roles model.
*
* #return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(
Role::class,
'role_user',
'user_id',
'role_id'
)->withTimestamps();
}
Relationship on the Role model:
/**
* Relationship with users table.
*
* #return BelongsToMany
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(
User::class,
'role_user',
'role_id',
'user_id'
)->withTimestamps();
}
In the user model, I have this.
$this->roles->each(function($role) {
dd($role);
});
I was expecting to get a dump of related model however for some weird reason what I get is pivot table merged with the model:
"id" => 7 // this is the relation ID from the pivot table
"display_name" => "Administrator" // this is from Role model
"code" => "admin" // role model
"description" => "Super User - can do everything in the system. This role should only be assigned to IT staff member." // role model
"created_at" => "2021-10-01 11:00:00" // pivot table
"updated_at" => null // pivot table
"deleted_at" => null // pivot table
"role_id" => 1 // pivot table
"user_id" => 2 // pivot table
Either I am doing something very wrong or I am missing something very obvious. Does anyone know what in the world is happening here?
Just to add: the data is from both places but the result is just a Role model as expected.
Should I not just get the role model without the pivot stuff in it? It is overriding my role model fields.
EDIT:
Parenthesis seems to make a difference. The data is still merged. However, when I do it like this looks like data from end model is merged (so it overrides) to data from the pivot. So I get correct ID.
$this->roles()->each(function($role) {
echo $role;
});
But this gives me this weird pivot merged version with wrong ID.
$this->roles->each(function($role) {
echo $role;
});
I know what that was exactly. Without thinking I've added the ID column into the pivot table.
This ID from pivot was overriding my ID from my end model. After I've removed it the problem is gone.
I don't know why Laravel would by default add these fields and merge with pivot columns... I guess it just does that for no reason. Although I don't understand what's the point if there is a separate mechanism to access the pivot table (pivot relationship on the model).
This makes me think I did something wrong. But yeah, hope it helps. If anyone knows why Laravel automatically adds pivot stuff, let me know.

Yii2 relation based on attribute values instead of keys

I have 2 tables in the db (mysql), and between the 2 there is no classic relationship through keys or ids. The only way I could define relationship would be through attribute values. E.g. table wheel and car and certain wheels would match certain cars because of the size only. Can it be defined on DB level, and/or in yii2, and if yes, how?
In the relations I can add an onCondition(), but you have to define an attribute (???), too:
public function getWheels() {
return $this->hasMany(\app\models\Wheel::className(), ['???' => '???'])->onCondition(['<', 'wheelsize', $this->wheelsize]);
}
I could use a fake attribute and set it in all records like to 1, but it seems a little bit odd for me.
I find nothing on the web regarding this or maybe I'm just searching the wrong way, or maybe I'm trying something that's totally bad practice. Can you please point me to the right direction?
Hypothetically you can set an empty array as a link, but for security reasons (I think) the condition "0 = 1" is automatically added in the select.
I faced your own problem several times and the best solution I could find was to use ActiveQuery explicitly (similar to what happens for hasOne and hasMany):
public function getWheels() {
return new ActiveQuery(\app\models\Wheel::className(), [
'where' => 'my condition' // <--- inserte here your condition as string or array
'multiple' => true // true=hasMany, false=hasOne
// you can also add other configuration params (select, on condition, order by, ...
]);
}
This way you can get both the array and the ActiveQuery to add other conditions:
var_dump($model->wheels); // array of wheels objects
var_dump($model->getWheels()); // yii\db\ActiveQuery object
$model->getWheels()->andWhere(...); // customize active query
I don't think that you could achieve this through relation.
But there is a way to work around the limitation.
<?php
namespace app\models;
class Car extend \yii\db\ActiveRecord
{
/**
* #var \app\models\Wheel
*/
private $_wheels;
/**
* #return \app\models\Wheel[]
*/
public function getWheels()
{
if (!$this->_wheels) {
$this->_wheels = Wheel::find()
->where(['<', 'wheelsize', $this->wheelsize])
//->andWhere() customize your where here
->all();
}
return $this->_wheels;
}
}
Then you could access the wheels attribute just as relation does.
<?php
$car = Car::find(1);
$car->wheels;
Beware that this way does not support Eager Loading

Yii2: How to access a table column via a relation

I have two tables, dpd and cabang. One cabang can only have one dpd and each cabang should have a dpd.
This is the relation in my "Cabang" model :
public function getIdDpd()
{
return $this->hasOne(Dpd::className(), ['id_dpd' => 'id_dpd']);
}
I try to access "dpd" attribute in "dpd" table in my view, I tried to var_dump it but still got the same error :
$model = Cabang::find()
->joinWith('idDpd')
->all();
var_dump($model->dpd);
Note : 'dpd' in var_dump($model->dpd) refered to the column name in 'Dpd' table. I have a column name 'dpd' in my 'dpd' table. I also tried $model->idDpd->dpd but it return the same error.
What am I doing wrong? Thanks
Your mistake is
$model->dpd
where you are trying to access it assuming that $model has the Dpd object, but it is holding the Cabang object and you should use the relation name to get the Dpd object and then call the field name like below.
$model->idDpd->dpd
EDIT: You are using ->all() in your query not ->one() and hence you cannot simply do $model->idDpd->dpd right after the query, you need to loop through the records and then call the relation, i thought you might be doing the same way but anyway see below how to use it.
$cabangs = Cabang::find()
->joinWith('idDpd')
->all();
foreach($cabangs as $index=>$model){
$model->idDpd->dpd;
}

How can I have the name of my entity instead of the id in the related tables

I'm creating a project on CakePHP 3.x where I'm quite new. I'm having trouble with the hasMany related tables to get the name of my entities instead of their ids.
I'm coming from CakePHP 2.x where I used an App::import('controller', array('Users') but in the view to retrieve all data to display instead of the ids, which is said to be a bad practice. And I wouldn't like to have any code violation in my new code. Can anybody help me? here is the code :
public function view($id = null)
{
$this->loadModel('Users');
$relatedUser = $this->Users->find()
->select(['Users.id', 'Users.email'])
->where(['Users.id'=>$id]);
$program = $this->Programs->get($id, [
'contain' => ['Users', 'ProgramSteps', 'Workshops']
]);
$this->set(compact('program', 'users'));
$this->set('_serialize', ['ast', 'relatedUser']);
}
I expect to get the user's email in the relatedUsers of the program table but the actual output is:
Notice (8): Trying to get property 'user_email' of non-object [APP/Template\Asts\view.ctp, line 601].
Really need help
Thank you in advance.
You've asked it to serialize the relatedUser variable, but that's for JSON and XML views. You haven't actually set the relatedUser variable for the view:
$this->set(compact('program', 'users', 'relatedUser'));
Also, you're setting the $users variable here, but it's never been initialized.
In addition to #Greg's answers, the variable $relateduser is still a query object, meaning that trying to access the email property will fail. The query still needs to be executed first.
You can change the query to:
$relatedUser = $this->Users->find()
->select(['Users.id', 'Users.email'])
->where(['Users.id' => $id])
->first();
Now the query is executed and the only the first entry is returned.
There is are a number of ways to get a query to execute, a lot of them are implicit is use. See:
Cookbook > Retrieving Data & Results Sets

UPDATED: Magento add customer attribute filter to order grid

I have extended the Mage_Adminhtml_Block_Sales_Order_Grid class with a custom module to add several customer attributes (Magento EE 1.10) to the grid.
Two of the attributes I added are text fields (i.e. they live in the customer_entity_varchar table, and I was able to add them to the collection and display them in the grid. So far so good.
A third attribute is a select, so the values live in the customer_entity_int, the eav_attribute_option and the eav_attribute_option_value tables. I added the necessary values to the collection (using $collection->getSelect()->joinLeft(.....). Again, so far so good.
My problem is being able to display and filter the attribute at the same time.
Inside the _prepareColumns() function in my MyCompany_MyModule_Block_Adminhtml_Order_Grid class, if I add a column like this, - as expected - I can display the values of the attribute on each row, but I don't get a drop down filter in the header:
protected function _prepareColumns()
{
...
$this->addColumn('bureau', array(
'header' => Mage::helper('sales')->__('Bureau'),
'index' => 'bureau',
'type' => 'text'
));
...
}
Following the example of status, and adding the column like this, gives me the drop down filter in the header, but it no longer displays the values for the attribute in each row:
protected function _prepareColumns()
{
...
$this->addColumn('bureau', array(
'header' => Mage::helper('sales')->__('Bureau'),
'index' => 'bureau',
'type' => 'options',
'options' => $this->_getBureauOptions(),
'filter_index' => 'value_option_table.option_id'
));
...
}
protected function _getBureauOptions()
{
$bureau = Mage::getResourceModel('eav/entity_attribute_collection')
->setCodeFilter('bureau')
->getFirstItem();
$bureauOptions = $bureau->getSource()->getAllOptions(false);
$optionsArr = array();
foreach ($bureauOptions as $option) {
$optionsArr[$option['value']] = $option['label'];
}
return $optionsArr;
}
Any advice / explanation would be much appreciated.
UPDATE:
It turns out that my code also causes a SQL error in a multi-website environment when an admin user only has permissions for some websites:
"SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'store_id' in where clause is ambiguous"
#clockworkgeek had the answer to the first part of my question.
The problem was that my joinLeft() was retrieving text values from the attribute options, while I should have been retrieving integer values when using 'type => 'options'.
Once I changed my joinLeft() to only retrieve integer values from customer_entity_int (actually a simpler join), the filtering and display worked flawlessly - thank you sir.
I will re-post my second issue (about SQL errors caused by permissions) as a separate question.