CakePHP model design - better way? - model-view-controller

I have a model "Goal" that has four types of children (which all have different fields):
campaign_roi
call_success
phone_skill
call_qluality
The goal table contains a field "model" which specifies one of the four models above.
But one "Goal" only has one of the 4 children. They will never have two types of children. Is there a better way than creating a model for each child? And if not, how can I dynamically specify which child type any goal is?
For instance, I want to see a goal and it's child - in this case a campaign_roi. How can I set it up where I can do this
$this->Goal->find('first', array(
'conditions' => array(
'Goal.id' => $id
)
)
And it returns information from the Goal model and it's child campaign_roi?

Let's assume that the 4 types of models hold information that can't be merged in to a single type. That is, they have no commonality, except that they all have a goal_id and are mutually exclusive. Let's assume that an instance of any one of these models can only belong to one goal.
You can set up your Goal model, using the belongsTo relationship, and the conditions clause, so that it will attempt to load a child of each type.
class Goal extends AppModel {
/* ... */
var $belongsTo = array(
'CampaignRoi' => array(
'className' => 'CampaignRoi',
'conditions' => array('Goal.model' => 'CampaignRoi'),
'foreignKey' => 'model_id'
),
'CallSuccess' => array(
'className' => 'CallSuccess',
'conditions' => array('Goal.model' => 'CallSuccess'),
'foreignKey' => 'model_id'
),
'PhoneSkill' => array(
'className' => 'PhoneSkill',
'conditions' => array('Goal.model' => 'PhoneSkill'),
'foreignKey' => 'model_id'
),
'CallQuality' => array(
'className' => 'CallQuality',
'conditions' => array('Goal.model' => 'CallQuality'),
'foreignKey' => 'model_id'
)
);
/* ... */
}
In your goal table, you'll have the model field and a model_id field. The model field will be populated with CampaignRoi, CallSuccess, PhoneSkill or CallQuality. After you've done your find on Goal, you'll still need to inspect goal.model to determine what type of child you need to work with.
This is a method I've used several times, though I can't quite remember what happens with the other models. At worst, it's an empty array or field (i.e. empty($goal['CampaignRoi']) == true, if $goal['Goal']['model'] == 'CallSuccess').
Sub-typing in relational databases is never fun.

For model inheritance, we can create sub_table with main_table_id to point back to the main_table. But it's quite convoluted when you need to get a full record.
If that 4 fields are the only difference between the children, you can put them all in the goals table. And if they are the same data type, you can use just one field. You can write custom find methods in the model (findModel1(),...) or define an array('model1name'=>'campaign_roi',...) for easy reference later when you do find().
How you design it is really a trade-off between robustness/extensibility and convenience.

Can't you work it the other way around? Not from the goal point of view, but from the others?
Don't specify no relation in the Goal model, specify hasOne relation in CampaignRoi, CallSuccess, PhoneSkill and CallQuality.
Do your queries thru CampaignRoi, CallSuccess, PhoneSkill and CallQuality.

Related

Two fields overwriting each others values on saving in Backpack for Laravel CRUD (spatie/laravel-tags implementation issues)?

I am trying to use spatie/laravel-tags together with Backpack for Laravel. I have 2 types of tags defined. Currently I have extended the Tag model from spatie/laravel-tags as MyCategory and MyTag and added global scopes to separate the two tag types. This works to the extent that it will display the current categories and tags correctly in Backpack, but when I try to save any changes it will only save the entries in the last field, and delete everything in the first field.
Here is my current field configuration for my CRUD:
$this->crud->addField([
'name' => 'categories',
'label' => 'Categories',
'type' => 'select2_multiple',
'tab' => 'Overview',
'attribute' => 'name',
'model' => 'App\MyCategory',
'pivot' => true,
]);
$this->crud->addField([
'name' => 'tags',
'label' => 'Tags',
'type' => 'select2_multiple',
'tab' => 'Overview',
'attribute' => 'name',
'model' => 'App\MyTag',
'pivot' => true,
]);
When I check Laravel Telescope I see that the same thing happens for both fields. First all current tags (regardless of type) for the item I am saving are deleted, and the new tags from the field are added. This is then repeated for the second field, which of course deletes the tags from the first field that should also be kept.
It seems that GlobalScope on my extended Tag models does not stick around for this part. Is there any way to reintroduce the scopes into the queries run by backpack to get these tags to save correctly?
In my CrudController I created custom update and store functions. See the update example below. This seems to work fine. There are still 2 queries being run, with the second one undoing the first one, but for my purposes this is a good enough workaround to be able to have 2 fields with different tag types in the same form in Backpack, using spatie/laravel-tags.
public function update(UpdateRequest $request)
{
$request = request();
// Merge the values from the two tag fields together into the second field
$request->merge(['tags' => array_merge((array)$request->input('categories'), (array)$request->input('tags'))]);
$redirect_location = $this->traitUpdate($request);
return $redirect_location;
}

After an ajax need to load a field value and create a select_from_array with the range value

Iam trying and not finding a way to load the value from a field and generate a select_from_array based on its range.
For example:
I have 2 select box
Brand -> loads -> Model (using backpack field types, and its working good)
`'type' => 'select2_from_ajax',
'name' => 'camera_model_id',
'entity' => 'camera_model',
'attribute' => 'name',
'data_source' => url('camera-brands'),
'placeholder' => 'Selecione o Modelo',
'minimum_input_length' => 0,
'dependencies' => ['camera_brand_id'],`
But, after the user selects this last selectBox, I need that another field was modified
`'name' => 'channel',
'label' => "Canal da Câmera",
'type' => 'select2_from_array',
'options' => ['' => '',
'01' => '01',
'02' => '02', ...`
So, the options could be filled with the maximum of the field I registered in the Model field database.
Is it possible? or maybe another approach to achieve the solution?
Thanks in advance!
To have an input that depends on the value of another input, you can make both your fields select2_from_ajax.
That way:
you will have the value of all inputs in the controller (the controller that returns the ajax results; then you can return a filtered set of results depending on how the form is filled so far - CategoryController::index() in the documentation example);
you can use the "dependencies" attribute on the selec2_from_ajax fields, so that when one field is reset, both are;
I hope the answer helps someone. Cheers!

cakephp 2.1 contain second level different field name

I can't get the values from a second level contain using different field name as link.
Asistencia model:
var $belongsTo = array('Employee');
Horario model:
var $belongsTo = array('Employee');
Employee model:
var $hasMany = array(
'Horario' => array(
'foreignKey' => 'employee_id',
'dependent' => true
),
'Asistencia' => array(
'foreignKey' => 'employee_id',
'dependent' => true
)
);
I'll explain using these values on my example record:
Asistencia: employee_id = 3701
Employee : id = 3701
In my find() from Asistencia, I get to contain Employee by switching Employee primaryKey just fine:
$this->Asistencia->Employee->primaryKey = 'id';
$this->paginate = array(
'contain' => array(
'Employee' => array(
'foreignKey' => 'employee_id',
//'fields' => array('id', h('emp_ape_pat'), h('emp_ape_mat'), h('name')),
'Horario' => array(
'foreignKey' => 'employee_id',
//'fields' => array('id' )
))
),
'conditions' => $conditions,
'order' => 'Asistencia.employee_id');
However, my Horario record is linked to Employees via another field: emp_appserial
Employee : emp_appserial = 373
Horario : employee_id = 373
How can my Employee array contain Horario array? (they do share the value just mentioned).
Currently, the Horario contain is using the value on Asistencia.employee_id and Employee.id (3701). (checked the sql_dump and is trying to get the Horario via
"WHERE `Horario`.`employee_id` = (3701)"
but for the Employee to contain Horario, it should use the value on Employee.emp_appserial and Horario.employee_id (373).
This is what i get (empty array at bottom)
array(
(int) 0 => array(
'Asistencia' => array(
'id' => '5',
'name' => null,
'employee_id' => '3701',
'as_date' => '2012-11-19',
),
'Employee' => array(
'id' => '3701',
'emp_appserial' => '373',
'emp_appstatus' => '8',
'AgentFullName' => '3701 PEREZ LOMELI JORGE LORENZO',
'FullNameNoId' => 'PEREZ LOMELI JORGE LORENZO',
'Horario' => array()
)))
Please notice:
'employee_id' => '3701', (Asistencia)
and
'emp_appserial' => '373', (Employee)
my Horario has 'employee_id' = 373.
How could I make the switch so the relation Employee<->Horario is based on emp_appserial?
Thank you in advance for any help.
Firstly you may be getting problems because you're using Spanish words for your Model names and I suspect you're also using them for Controllers.
This is a very bad practice since CakePHP's idea is:
Convention over configuration.
This is achieved through the Inflector class which "takes a string and can manipulate it to handle word variations such as pluralizations or camelizing and is normally accessed statically". But this works ONLY WITH English words. What this means for you is that you may be missing the data because Cake is not able to build the DB queries right, since you're using Spanish words. By doing so you're making the use of CakePHP's flexible persistence layer obsolete - you will have to make all the configurations by hand. Also most likely pagination will NOT WORK. Other parts of the framework may also not work properly:
HtmlHelper, FormHelper, Components, etc. ...
These problems will expand if you try to use more complex Model associations such as HABTM or "hasMany through the Join Model".
So I do not know why exactly you aren't seeing the Horario record, but what I just explained most likely is your problem.
What you're trying to do is against the core principles of CakePHP and you'd save yourself a lot of problems if you refactor a bit and use English words. Your other option is NOT TO use Cake.

Yii CGridview - search/sort works, but values aren't being displayed on respective cells

I am semi-frustrated with this Yii CGridView problem and any help or guidance would be highly appreciated.
I have two related tables shops (shop_id primary) and contacts (shop_id foreign) such that a single shop may have multiple contacts. I'm using CGridview for pulling records and sorting and my relation function in shops model is something like:
'shopscontact' => array(self::HAS_MANY, 'Shopsmodel', 'shop_id');
On the shop grid, I need to display the shop row with any one of the available contacts. My attempt to filter, search the Grid has worked pretty fine, but I'm stuck in one very strange problem. The respective grid column does not display the value that is intended.
On CGridview file, I'm doing something like
array(
'name' => 'shopscontact.contact_firstname',
'header' => 'First Name',
'value' => '$data->shopscontact->contact_firstname'
),
to display the contact's first name. However, even under circumstances that searching/sorting are both working (I found out by checking the db associations), the grid column comes out empty! :( And when I do a var_dump
array(
'name' => 'shopscontact.contact_firstname',
'header' => 'First Name',
'value' => 'var_dump($data->shopscontact)'
),
The dump shows record values in _private attributes as follows:
private '_attributes' (CActiveRecord) =>
array
'contact_firstname' => string 'rec1' (length=4)
'contact_lastname' => string 'rec1 lsname' (length=11)
'contact_id' => string '1' (length=1)
< Edit: >
My criteria code in the model is as follows:
$criteria->with = array(
'owner',
'states',
'shopscontacts' => array(
'alias' => 'shopscontacts',
'select' => 'shopscontacts.contact_firstname,shopscontacts.contact_lastname',
'together' => true
)
);
< / Edit >
How do I access the values in their respective columns? Please help! :(
Hmm, I have not used the with() and together() methods much. What's interesting is how in the 'value' part of the column, $data->shopscontacts loads up the relation fresh, based on the relations() definition (and is not based on the criteria you declared).
A cleaner way to handle the array output might be like this:
'value' => 'array_shift($data->shopscontacts)->contact_lastname'
Perhaps a better way to do this, though, would be to set up a new (additional) relation, like this in your shops model:
public function relations()
{
return array(
'shopscontacts' => array(self::HAS_MANY, 'Shopsmodel', 'shop_id'), // original
'firstShopscontact' => array(self::HAS_ONE, 'Shopsmodel', 'shop_id'), // the new relation
);
}
Then, in your CGridView you can just set up a column like so:
'columns'=>array(
'firstShopscontact.contact_lastname',
),
Cheers
Since 'shopscontact' is the name of the has-many relation, $data->shopscontact should be returning an array with all the shops related... did you modify the relation in order to return only one record (if I didn't get you wrong, you only need to display one, right?)? If you did it, may I see your filtering code?
P.S. A hunch to get a fast but temporal solution: have you tried 'value' => '$data->shopscontact['contact_firstname']'?

Cakephp 2.0: How to provide ORDER BY instructions for data retrieved recursively through find()?

I am trying to set up a model with a lot of hasMany/hasOne relationships, and I'd like for the owned models to be presented, alphabetically, in a drop-down menu within the "Add" view. I am not sure how to provide ORER BY instructions to models that are retrieved recursively. This is what I've tried:
$services = $this->service->find('all', array(
'order'=>array(
'Service.start_year DESC',
'Servicecat.title DESC'
),
'recursive'=>0
) );
But of course this yields no change. I have a suspicion it's not very complicated, I just can't find a solution in the cookbook/api. Ideas?
You could do this a few ways. I prefer to use the containable behavior for models and then it allows you to finely control how the models are retrieved. You could also setup order in your model's relationship definition.
Model Way
$hasMany = array(
'Servicecat' => array(
'order' => array(
'Servicecat.title DESC'
)
)
);
Containable Way
In your models, set them to use the containable behavior:
public $actsAs = array('Containable');
Then, when you find from your controller, you can explicitly state which models are linked. You can use multidimensional arrays to determine the depth of recursion.
$services = $this->Service->find(
'all',
array(
'contains' => array(
'Servicecat' => array(
'order' => array('Servicecat.title' => 'DESC')
)
),
'order' => array('Services.start_year' => 'DESC')
)
);

Resources