Doctrine: How to traverse from an entity to another 'linked' entity? - doctrine

I'm loading 3 different tables using a cross-join in Doctrine_RawSql. This brings me back the following object:
User -> User class (doctrine base class)
Settings -> DoctrineCollection of Setting
User_Settings -> DoctrineCollection of User_Setting
The object above is the result of a many-to-many relationship between User and Setting where User_Setting acts as a reference table. User_Setting also contains another field named value. This obviously contains the value of the corresponding Setting.
All good so far, however the Settings and User_Settings properties of the returned User object are in no way linked to each other (apart from the setting_id field ofcourse).
Is there any direct way to traverse directly from the Settings property to the corresponding User_Settings property?
This is the corresponding query:
$sets = new Doctrine_RawSql();
$sets->select('{us.*}, {s.*}, {uset.*}')
->from('(User us CROSS JOIN Setting s) LEFT JOIN User_Setting uset ON us.user_id = uset.user_id AND s.setting_id = uset.setting_id')
->addComponent('us', 'User us')
->addComponent('uset', 'us.User_Setting uset')
->addComponent('s', 'us.Setting s')
->where('s.category_id = ? AND us.user_id = ?',array(1, 1));
$sets = $sets->execute();
Edit:
1: this is the related YAML markup
//User relations:
Setting:
class: Setting
foreignAlias: User
refClass: User_Setting
local: user_id
foreign: setting_id
//Setting relations:
User:
class: User
foreignAlias: Setting
refClass: User_Setting
local: setting_id
foreign: user_id
//User_Setting relations:
Setting:
foreignAlias: User_Setting
local: setting_id
foreign: setting_id
User:
foreignAlias: User_Setting
local: user_id
foreign: user_id
2. This is the object code (which is generated from YAML):
//BaseUser setup()
$this->hasMany('Setting', array(
'refClass' => 'User_Setting',
'local' => 'user_id',
'foreign' => 'setting_id'));
$this->hasMany('User_Setting', array(
'local' => 'user_id',
'foreign' => 'user_id'));
//BaseSetting setup()
$this->hasMany('User', array(
'refClass' => 'User_Setting',
'local' => 'setting_id',
'foreign' => 'user_id'));
$this->hasMany('User_Setting', array(
'local' => 'setting_id',
'foreign' => 'setting_id'));
//BaseUser_Setting setup()
$this->hasOne('Setting', array(
'local' => 'setting_id',
'foreign' => 'setting_id'));
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'user_id'));

Actually you should define one-to-many relationship in the ref table, but using hasOne() :
class User_Setting
{
....
public function setUp()
{
parent::setUp();
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'));
$this->hasOne('Setting', array(
'local' => 'setting_id',
'foreign' => 'id'));
}
}
Then you will have a Doctrine_Record instead of a Doctrine_Collection.

You can define missing relations in User_Setting class:
class User_Setting
{
// ...
public function setUp()
{
$this->hasMany('Users', array(
'class' => 'User',
'local' => 'user_id',
'foreign' => 'id',
));
$this->hasMany('Settings', array(
'class' => 'Setting',
'local' => 'setting_id',
'foreign' => 'id',
));
}
}

Related

Laravel 9 factory and creating related 1:1 models

I'd like to use Laravel factories to populate some dummy data in my test environment. I have a Locations model and a LocationDetails model, which have a 1:1 relationship and a record needs to be created in the LocationDetails table whenever a new Location is created.
How can I do this in a factory such that there's exactly 1 LocationDetails record for each Location record.
Here's what I have in my DatabaseSeeder class:
if(env('APP_ENV') === 'local') {
Client::factory()->count(25)->create();
Location::factory()->count(30)->create();
}
My location factory definition:
public function definition()
{
return [
'name' => $this->faker->company,
'client_id' => Client::all()->random()->id,
'location_status_id' => LocationStatus::all()->random()->id,
'address' => $this->faker->streetAddress,
'city' => $this->faker->city,
'state' => $this->faker->StateAbbr,
'zip_code' => $this->faker->numerify('#####'),
'phone' => $this->faker->numerify('###-###-####'),
'email' => $this->faker->safeEmail,
'is_onboarding_completed' => 1,
];
}
It looks like Laravel has an afterCreate() callback for a factory, but I'm just not clear on how to accomplish this.
Does this work
Location::factory()
->has(LocationDetails::factory())
->count(30)
->create();
Or you can have a state defined on the LocationFactory
//LocationFactory
//Assuming location_id is the foreign key on LocationDetails model
public function withLocationDetail()
{
return $this->afterCreating(function (Location $location) {
LocationDetails::factory()->create(['location_id' => $location->id]);
});
}
And then use it like
Location::factory()
->withLocationDetail()
->count(30)
->create();

ListEntries in table for relationship on show page - backpack for laravel

Just new with backpack. I search on official site and googled it, but dit not found an answer
In laravel 7, using Backpack 4.1
My data model is : Customer has many addresses
Relationship is configured in the Customer model :
public function addresses()
{
return $this->hasMany(\App\Models\Address::class, 'user_id');
}
Relationship is configured in the Address model :
public function customer()
{
return $this->belongsTo(\App\Models\Customer::class);
}
public function country()
{
return $this->belongsTo(\App\Models\Country::class);
}
public function address_type()
{
return $this->belongsTo(\App\Models\AddressType::class);
}
In my customer show page, I would like to show all customer addresses in a table, just under the customer details.
So in my CustomerCrudController, I have implemented this method :
protected function setupShowOperation()
{
$this->crud->set('show.setFromDb', false);
$this->crud->addColumn(['name' => 'name', 'type' => 'text', 'label' => __('models/customers.fields.name'), ]);
$this->crud->addColumn(['name' => 'email', 'type' => 'email', 'label' => __('models/customers.fields.email'), ]);
$this->crud->addColumn([
'name' => 'addresses',
'label' => __('models/addresses.plural'),
'type' => 'table',
'columns' => [
'address_type_id' => __('models/addresses.fields.address_type'),
'address_type.name' => __('models/addresses.fields.address_type'),
'address1' => __('models/addresses.fields.address1'),
'address2' => __('models/addresses.fields.address2'),
'city' => __('models/addresses.fields.address2'),
'postal_code' => __('models/addresses.fields.address2'),
'country.name' => __('models/countries.singular'),
],
]);
}
When I go on my page : /admin/customer/3/show,
In my debugbar, I saw the query how load addresses
select * from `addresses` where `addresses`.`user_id` = 3 and `addresses`.`user_id` is not null
I have the table rendered with the corresponding number of lines from data in DB, but rows are blank.
Is this the correct way to do that ? What are the correct parameters ?
Is there a way to show a table with action buttons (show entry, edit) - same as in List view ?
Should it be implemented in another way ?
Hope I'm clear.
Thanks
Don't know if it is a laravel bug, but my solution was to create my own table blade, base on the file :
\vendor\backpack\crud\src\resources\views\crud\columns\table.blade.php
and have created my own :
\resources\views\vendor\backpack\crud\columns\address_table.blade.php
I have juste changed the file:40
#elseif( is_object($tableRow) && property_exists($tableRow, $tableColumnKey) )
to
#elseif( is_object($tableRow) && isset($tableRow->{$tableColumnKey}) )
now, in my CustomerCrudController.php :
protected function setupShowOperation()
{
$this->crud->set('show.setFromDb', false);
$this->crud->addColumn(['name' => 'name', 'type' => 'text', 'label' => __('models/customers.fields.name'),]);
$this->crud->addColumn(['name' => 'email', 'type' => 'email', 'label' => __('models/customers.fields.email'),]);
$this->crud->addColumn([
'name' => 'addresses',
'label' => __('models/addresses.plural'),
'type' => 'address_table', // my custom type
'model' => \App\Models\Address::class,
'entity' => 'addresses',
'columns' => [
'address_type_name' => __('models/addresses.fields.address_type'),
'postal_code' => __('models/addresses.fields.postal_code'),
'city' => __('models/addresses.fields.city'),
'address1' => __('models/addresses.fields.address1'),
'address2' => __('models/addresses.fields.address1'),
],
]);
}
And I've added an accessor in my model (Address.php)
public function getAddressTypeNameAttribute()
{
return "{$this->address_type->name}";
}
Don't know if there is a better way ...
Hope this will help others.
I use Laravel 8,
In addition for the answer above, and based on this answer https://stackoverflow.com/a/65072393 and https://stackoverflow.com/a/43011286/1315632 regarding PHP function property_exists vs Laravel magic methods to create dynamic properties and methods
After creating the overwrite column php artisan backpack:publish crud/columns/table
I change line 40 in file:\resources\views\vendor\backpack\crud\columns\table.blade.php into
#elseif( is_object($tableRow) && ( property_exists($tableRow, $tableColumnKey) || property_exists((object)$tableRow->toArray(), $tableColumnKey) ) )
adding OR checking from answer https://stackoverflow.com/a/65072393

correct value instead of array

scenario is crm with tables account, account_contact, contact and account_contact_role. The latter contains roles like 'project lead' or 'account manager' for the combos defined in the junction table.
My challenge is the account view, that is listing also the connected persons with their roles. I want my grid to show: Doe | John | employee.
The problem is now when the contact has 2+ entries in the junction table. How can I print the correct role for the row? As you can see in the code I solved it the static way which shows only 1 out of n times the correct value. Tried it with inner join without success. Is it a problem of the search in the model or with the access in the view?
the relation from the account model to the contacts:
public function getContacts($role = null)
{
// many-to-many
return $this->hasMany(ContactRecord::className(), ['id' => 'contact_id'])
->via('accountContacts')
->innerJoinWith(['accountContacts.role'])
->andWhere(['account_contact.account_id' => $this->id])
->andWhere(['account_contact_role.type' => $role])
;
}
the view
<?= \yii\grid\GridView::widget([
'dataProvider' => new \yii\data\ActiveDataProvider([
'query' => $model->getContacts('internal'),
'pagination' => false
]),
'columns' => [
'lastname',
'firstname',
[
'label' => 'Role',
'attribute' => 'accountContacts.0.role.name',
],
[
'class' => \yii\grid\ActionColumn::className(),
'controller' => 'contacts',
'header' => Html::a('<i class="glyphicon glyphicon-plus"></i> Add New', ['contact-records/create', 'account_id' => $model->id]),
'template' => '{update}{delete}',
]
]
]); ?>
defined relations are:
account has many accountContacts has one contact
accountContacts has one accountContactRole
Many thanks in advance!
You are showing account's contacts, so you have to list from Contact model.
Inside Contact model (or Contact ActiveQuery file):
public static function queryContactsFromAccountAndRole($account, $role = null)
{
// many-to-many
return ContactRecord::find()->innerJoinWith(['accountContacts' => function($q) use($account, $role) {
$q->innerJoinWith(['accountContactsRole'])
->andWhere(['account_contact.account_id' => $account->id])
->andWhere(['account_contact_role.type' => $role]);
}])
->andWhere(['account_contact.account_id' => $account->id]);
}
Now you have one record for each contact and the gridview will show all contacts.

Add drop down list attribute to category - set source

I added "brand" attribute (drop down list) for products.
I'm trying add new attribute (drop down list) to category with values from "brand" attribute.
What should I do to set correct source for this category attribute.
Please see my piece of code in mysql setup file:
$this->startSetup();
$this->addAttribute('catalog_category', 'brand', array(
'group' => 'General',
'type' => 'int'
'backend' => '',
'frontend_input' => '',
'frontend' => '',
'label' => 'brand',
'input' => 'select'
'class' => '',
'source' => 'mymodule/selecattributes',
'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL,
'visible' => true,
'frontend_class' => '',
'required' => false,
'user_defined' => true,
'default' => '',
'position' => 100,
));
$this->endSetup();
Thanks in advance.
EDIT 1
I added class MyPackage_MyModule_Model_SelectAttributes extends extends Mage_Eav_Model_Entity_Attribute_Source_Abstract :
class MyPackage_MyModule_Model_SelectAttributes extends Mage_Eav_Model_Entity_Attribute_Source_Abstract{
public function getAllOptions()
{
$attributeCode = 'brand';
$attribute = Mage::getModel('eav/config')->getAttribute('catalog_product', $attributeCode); //here, "color" is the attribute_code
$allOptions = $attribute->getSource()->getAllOptions(true, true);
foreach ($allOptions as $instance) {
$myArray[$instance['value']] = $instance['label'];
}
return $myArray;
}
}
EDIT 2
When I open category admin page I get this error:
"Source model "mymodule/selectattributes" not found for attribute "brands""
I'm guessing that your source model is in a file named Selectattributes.php. If you are using a case-sensitive filesystem you will need to do one of two things:
Rename your class definition file to Selectattributes.php
or
Change the source_model value for your brands attribute to mymodule/selectAttributes
When Magento is attempting to instantiate the source model for your attribute, the classname (and hence the autoload include path) being calculated work as follows:
MyPackage_MyModule_Model_Selectattributes
MyPackage/MyModule/Model/Selectattributes.php
Note the issue with the filename. Note that this should be working in a non-case-sensitive filesystem.

How can I make the relation name independent of the related model in CI Datamapper?

When designing a relationship in Datamapper one is bound to call the relationship the same name as the related object, which is not too handy when you have something like Application_Model_User as a class name. For those of you who will rush to say that there is a configuration option with "class" key, I know. Been there tried that. It only works for getting a related object, not for updating them.
Here is a code snippet to reproduce the problem:
// User Model
class UserModel extends Datamapper
{
public $table = 'users';
public $has_many = array(
'roles' => array(
'class' => 'RoleModel',
'other_field' => 'usermodel',
'join_other_as' => 'role',
'join_self_as' => 'user',
'join_table' => 'users_roles'
),
);
}
class RoleModel extends DataMapper
{
public $table = 'roles';
public $has_many = array(
'usermodel' => array('class' => 'UserModel',
'other_field' => 'roles',
'join_other_as'=> 'user',
'join_self_as' => 'role',
'join_table' => 'users_roles' )
);
}
// controller code. Make sure you have a role with INT id = 2, and a user with INT id = 5 in your db
$user = new UserModel(2);
$role = new RoleModel(5);
$user->save($role);
This code gives an "Unable to relate usermodel with rolemodel." error, however it does work properly (meaning a new record is inserted in the join table user_roles) if the relation is renamed from "roles" to "rolemodel".
So, if there are any avid users of CI's Datamapper that could help, please let me know how to properly define relationships.
UPDATE
You can save an object as a relation using the relationship key:
$object->save( $related, $relationship_key ).
So you would need to use
$user->save($role, "roles");
See the bottom of this web page:
http://datamapper.wanwizard.eu/pages/save.html
Leaving this bit in case it helps someone else out.
It looks like you want to have a custom name on a relationship. (That's what I get after wading through all of the cynicism) -
You get to name the relationship anything that you want with the key in the relationship array. So, in the following snippet, you use book <-- this does or does not have to be the same name as the class - that's what the class key is for.
class Author extends DataMapper {
$has_many = array(
'book' => array( // YOU USE THIS KEY TO NAME THE RELATIONSHIP
'class' => 'book',
'other_field' => 'author',
'join_self_as' => 'author',
'join_other_as' => 'book',
'join_table' => 'authors_books'
)
);
}
If this is not working for you, my guess is you have something else wrong in the set up of your relationships.
http://datamapper.wanwizard.eu/pages/advancedrelations.html

Resources