Yii model: Dynamic table relations - relational-database

Table.linkedIndex is related to LinkedIndex.ID. The value of the field LinkedIndex.TableName is either Linked1 or Linked2 and defines which of these tables is related to a row in Table.
Now i want to make a dynamical link with Yii models so that i can easily get from a Table row to the corresponding Linked1 or Linked2 row:
Table.linkedID = [LinkedIndex.TableName].ID
Example
Table values:
LinkedIndex values:
Now I should get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;
Model
In the model Table, I tried to make the relation to the table with the name of the value of linkedIndex.TableName:
public function relations()
{
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
'linked' => array(
self::HAS_ONE,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
)
}
But then I get the error:
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
Is there any way to make a dynamic relation Table.linkedID -> [LinkedIndex.TableName].ID with Yii Models?

Per the Yii docs here:
http://www.yiiframework.com/doc/api/1.1/CActiveRecord#relations-detail
I'd suggest using self::HAS_ONE instead (unless there can be multiple rows in LinkedIndex with the same ID - although from the looks of above, I doubt that's the case).
You can link tables together that have different keys by following the schema:
foreign_key => primary_key
In case you need to specify custom PK->FK association you can define it as array('fk'=>'pk'). For composite keys it will be array('fk_c1'=>'pk_с1','fk_c2'=>'pk_c2').
so in your case:
public function relations(){
return array(
'linkedIndex' => array(self::HAS_ONE, 'LinkedIndex', array('ID' => 'linkedIndex')),
);
}
where LinkedIndex is the class name for the LinkedIndex model (relative to your Table model - i.e. same folder. You could change that, of course) and array('ID' => 'linkedIndex') specifies the relationship as LinkedIndex.ID = Table.linkedIndex.
Edit
Looking at your updated example, I think you're misunderstanding how the relations function works. You're getting the error
include(linkedIndex.TableName.php) [function.include]: failed to open stream: No such file or directory
because you're trying to create another relation here:
'linked' => array(
self::BELONGS_TO,
'linkedIndex.TableName',
array('ID' => 'linkedID'),
)
This part: linkedIndex.TableName refers to a new model class linkedIndex.TableName, so Yii attempts to load that class' file linkedIndex.TableName.php and throws an error since it doesn't exist.
I think what you're looking for is to be able to access the value TableName within the table LinkedIndex, correct? If so, that's accessible from within the Table model via:
$this->linkedIndex->TableName
This is made possible by the relation we set up above. $this refers to the Table model, linkedIndex refers to the LinkedIndex relation we made above, and TableName is an attribute of that LinkedIndex model.
Edit 2
Per your comments, it looks like you're trying to make a more complex relationship. I'll be honest that this isn't really the way you should be using linking tables (ideally you should have a linking table between two tables, not a linking table that says which 3rd table to link to) but I'll try and answer your question as best as possible within Yii.
Ideally, this relationship should be made from within the LinkedIndex model, since that's where the relationship lies.
Since you're using the table name as the linking factor, you'll need to create a way to dynamically pass in the table you want to use after the record is found.
You can use the LinkedIndex model's afterFind function to create the secondary link after the model is created within Yii, and instantiate the new linked model there.
Something like this for your LinkedIndex model:
class LinkedIndex extends CActiveRecord{
public $linked;
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return 'LinkedIndex';
}
public function afterFind(){
$this->linked = new Linked($this->TableName);
parent::afterFind();
}
//...etc.
}
The afterFind instantiates a new Linked model, and passes in the table name to use. That allows us to do something like this from within the Linked model:
class Linked extends CActiveRecord{
private $table_name;
public function __construct($table_name){
$this->table_name = $table_name;
}
public static function model($className = __CLASS__){
return parent::model($className);
}
public function tableName(){
return $this->table_name;
}
//...etc.
}
which is how we dynamically create a class with interchangeable table names. Of course, this fails of the classes need to have separate operations done per-method, but you could check what the table_name is and act accordingly (that's pretty janky, but would work).
All of this would result in being to access a property of the linked table via (from within the Table model):
$this->linkedIndex->linked->foo;

Because the value of LinkedIndex.TableName and Table.linkedID is needed to get the values, I moved the afterFind, suggested by M Sost, directly into the Table-Class and changed its content accordingly. No more need for a virtual model.
class Table extends CActiveRecord {
public $linked; // Needs to be public, to be accessible
// ...etc.
public function afterFind() {
$model = new $this->linkedIndex->TableName;
$this->linked = $model::model()->findByPk( $this->linkedID );
parent::afterFind();
}
// ...
}
Now I get the row from Linked2 where ID=2:
$model = Table::model()->findByPk(0);
$row = $model->linked;

Related

Optimization of Laravel pivot table relationship

I have a pivot table called invite_riskarea which is designed as follows:
This table handles the permissions that have a specific user (through an invite id) to access to specific riskfields. Each riskfield is associated with a riskarea which acts as the main container of specific riskfields.
Within the model Invite I have this relationship:
public function riskareas()
{
return $this->belongsToMany(Riskarea::class)->withPivot('riskfield_id', 'insert', 'edit', 'view');
}
In this way I can return all the riskareas associated with a specific invite, and I should be able to return all the riskfields associated with a specific riskarea in the same invite model.
As you can see from the table invite_riskarea, I have three columns called insert, edit, and delete. These columns manage the types of permissions assigned to a specific user (via an invite id) for a specific riskfield belonging to a riskarea.
I'm trying to retrieve the riskarea permission in the following way:
$invite = Invite::where('id', 58)->first();
$riskarea = $invite->riskareas[0];
$riskfield = $riskareas->riskfields[0];
echo 'view permission => ' . $riskfield->insert;
The problem's that I'm not able to setup a correct relationship in the Invite model that returns me the pivot data of the permissions columns only for the riskfield associated with the riskarea.
So I have manage to handle this situation in this way:
$riskareas = Riskarea::all();
foreach ($riskareas as &$riskarea) {
foreach ($riskarea->riskfields as &$riskfield) {
$result = DB::table('invite_riskarea')
->select('insert', 'edit', 'view')
->where([
'riskarea_id' => $riskarea->id,
'riskfield_id' => $riskfield->id
])
->first();
if ($result) {
$riskfield->insert = $result->insert;
$riskfield->edit = $result->edit;
$riskfield->view = $result->view;
}
}
}
Essentially, I get all the riskareas, and then I iterate over the riskfields associated. For each riskfield, I get the permissions in the invite_riskarea table and then I have the correct structure that I want.
So to summarize:
Is it actually possible create a model relationship that returns the permissions for riskfield and not for riskarea?
Is my table implementation good enough to handle that situation?
I suggest you define back the many-to-many relation for the Riskfield model with the Invite model.
You can also define a direct many-to-many relationship with riskfield in the Invite model. This is how convenient it is for you personally.
And so the inverse many-to-many relationship
public function invites()
{
return $this->belongsToMany(Invite::class)->withPivot('insert', 'edit', 'view');
}
Then get all objects' Riskfields that are associated with the specified invite:
$riskfields = Riskfields::wherehas('invites' . function (Builder $query) use ($invite_id) {
$query->where('invites.id', $invite_id);
})->with('invites')->get();
Then you can access the desired fields of the pivot table in the specified way:
foreach ($riskfields as $riskfield) {
foreach ($riskfield->invites as $invite) {
$insertRiskField = $invite->pivot->insert;
$editRiskField = $invite->pivot->edit;
$viewRiskField = $invite->pivot->view;
}
}
Eager loading executes one query to the database
Yes
Documentation Laravel

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

Insert a field with value whenever new instance of modal is created

I want to insert a field with value whenever new row is created for a modal.
Ex: Suppose this is my user.php modal
class User extends Authenticatable
{
protected $guarded = ['id'];
}
What i want is in my application anywhere when i insert a row in user table, then i want to insert an extra column code with its value in user table.
Ex: If i do below in my application
User::create(['name'=>'xyz', 'password' => 'kajsndjk']);
then it should insert an extra column code =>'Qwedf' also in my table.
In my application there are many places where i am creating the users, so i don't want to remember every time to insert code column.
Please suggest how can i achieve it.
Overriding the static create function on the User class is the only thing that will work in my opinion.
public static function create(array $attributes = [])
{
$object = parent::create($attributes);
$object->code = 'some text';
$object->save();
return $object;
}
I've tested and like I expected, oseintow's answer will not work, because it would work only if you directly modified code variable, which you obviously are not doing.
Add this mutator to your User model
public function setCodeAttribute($value)
{
$this->attributes['code'] = "Qwedf";
}
Anytime you are saving a record code will be assigned the Qwedf value

Relationships between tables in laravel using backpack package

I am using backpack CRUD package to create my website project in laravel 5.2
I want to establish a relationship between two tables. First table is called customer and second table is called transaction. Each customer has many transaction(1:N relationship).
Customer table record:
ID Name
123456 xyz
Transaction table record:
ID CustomerID
101010 123456
I know that I have to specify the relation in the customer model. But, how can I display the result of the relationship in CRUD ?
You should have relationships on both the Transaction and the Customer models, so you can do $customer->transactions and $transaction->customer:
class Customer extends Model
{
/**
* Get the comments for the blog post.
*/
public function transactions()
{
return $this->hasMany('App\Transactions', 'CustomerID', 'ID');
}
}
and
class Transaction extends Model
{
/**
* Get the comments for the blog post.
*/
public function customer()
{
return $this->belongsTo('App\Customer', 'CustomerID', 'ID');
}
}
Spend some time in the Eloquent Relationships Documentation. It's really important to understand them if you want to be a Laravel developer.
In order to display the relationship in the CRUD, you can then use Backpack's select column type to display it in the table view and select or select2 field types to display it in the add/edit views. Read the CRUD Example Entity to better understand how that works.
First of all when you are creating migrations for both tables, table which contain Foreign Key (FK) must have field like this:
public function up(){
$table->increments('id');
$table->integer('customerID')->unsigned();
}
After that you are need to call next command into console
php artisan migrate
Next is going next commands:
php arisan backpack:crud customers
php arisan backpack:crud transactions
After that you need to define functions in models which returns values from other tables. Customer models need to have next function
public function transactions(){
return $this->hasMany('Transaction');
}
Transaction model must have next function
public function customer() {
return $this->belongsTo('Customer');
}
Next you must add CRUD field in Customer controller to display
transactions in select box.
$this->crud->addField([
'label' => 'Transactions', // Label for HTML form field
'type' => 'select2', // HTML element which displaying transactions
'name' => 'customerID', // Table column which is FK for Customer table
'entity'=> 'customer', // Function (method) in Customer model which return transactions
'attribute' => 'ID', // Column which user see in select box
'model' => 'Transaction' // Model which contain FK
]);
Hope this helps :)
After you built onetomany relationship with transaction, you can get the results.
$customer=Customer::where(['id'=>'123456'])->with('transaction')
->first();
print_r($customer->Name); // gives the customer name
foreach($customer->transaction as $cid)
{
print_r($cid->CustomerID); // gives the customer id
}
Laravel Relationships Documentation is always helpful. Go through it.

laravel5-How can i hide the specific column of withPivot?

I use belongstomany function to get the models and used withPivot() to get an extra column data in the pivot table. However, the withPivot() function will add the 2 foreign keys in the output json. I want to hide thses keys and only show the column I want to display.
The follwing is my response data
{"status_code":"1","next_page":"2","dataset":[{"id":"829","req_start_time":"2016-03-13 14:55:00","req_end_time":"2016-03-13 09:55:00","content":[{"name":"\u9752\u6912\u7092\u725b\u8089","price":"120","pic_url":"","detail":"\u8d85\u7ea7\u597d\u5403\u7684\u54df","pivot":{"waiter_request_id":"829","goods_id":"2","count":"2"}}
as you see the pivot json object contains two id keys.
how could i remove them from the object? or some other ways to solve this get the right way of this need?
thx in advance!
Now the question already upgraded!
How can I add this 'count' parameter of pivot in the object which belongs to 'content'
Finally,I figured out the right way to solve this problem. Follwing the code:
class Goods extends Model{
......
......
public function newPivot(Model $parent, array $attributes, $table, $exists)
{
$pivot = new Pivot($parent, $attributes, $table, $exists);
$pivot->addHidden(
array(
'waiter_request_id',
'goods_id'
)
);
return $pivot;
}
}
you can rewrite this method to provide a new pivot object to the Model in your 'belongsToMany'