Representing versions of objects with CakePHP - php

Have just started to get into CakePHP since a couple of weeks back. I have some experience with MVC-frameworks, but this problem holds me back a bit.
I am currently working on a model foo, containing a primary id and some attributes. Since a complete history of the changes of foo is necessary, the content of foo is saved in the table foo_content. The two tables are connected through foo_content.foo_id = foo.id, in Cake with a foo hasMany foo_content-relationship.
To track the versions of foo, foo_content also contains the column version, and foo itself the field currentVersion. The version is an number incremented by one everytime the user updates foo. This is an older native PHP-app btw, to be rewritten on top of Cake. 9 times out of 10 in the app, the most recent version (foo.currentVersion) is the db-entry that need to be represented in the frontend.
My question is simply: is there someway of representing this directly in the model? Or does this kind of logic simply need to be defined in the controller?
Most grateful for your help!

For always getting the latest version automatically, you can easily make another association:
public $hasOne = array(
'CurrentContent' => array(
…
'condition' => array('FooContent.version' => 'Foo.currentVersion')
)
)
That, or you add a 'order' => array('FooContent.version' => 'desc') clause to your hasMany relationship and always work with $foo['FooContent'][0].
For automatically creating archived versions, you can work with beforeSave and/or afterSave callbacks.

Related

CakePHP Containable is generating far too many queries

I'm using ContainableBehavior to get deep associations on a find in my CakePHP 2.4 app. However, this is resulting in far too many SQL queries - into the thousands on one page.
To simplify my relations a little, it looks like this:
Post
hasMany
- Sighting
hasMany
- Field
belongsTo
- Fieldtype
Now, when I do a find() specifying these in my contain statement, my DebugKit MySQL log is filled with queries like this, repeated over and over:
SELECT `Fieldtype`.`id`, `Fieldtype`.`name` FROM `dev_db`.`fieldtypes`
AS `Fieldtype` WHERE `Fieldtype`.`id` = 48
Basically, it's doing a manual find for each and every Field's Fieldtype, even when it should already have the data.
Is there a solution for returning this kind of deep association that does not duplicate queries? I've tried using the Linkable behavior, however
That only seems to work with one-to-one relationships (you can't use
Link like Contain to get data from a hasMany relationship)
You have to start with your main model- you can't Contain all your
child records, and then use Linkable to hook up the models they belongTo.
Code:
$this->Paginator->settings['contain'] = array(
'User',
'Sighting' => array(
'Field' => array('Fieldtype')
),
);

Proper CakePHP model to natively use multiple select and "dynamic" multiple select (e.g. tags)

In my schema Test HABTM Variable (originally I used hasMany but I found more documentation using HABTM even though it seemed overkill to me).
I want to use a multiple select in a CakePHP form and I don't want to have trouble saving, retrieving and pre-filling the data (ie. ugly array manipulations in beforeSave and beforeFind, extra queries for retrieving the selected options and other things that may cause me to lose my hair even earlier than I probably will anyway).
One usecase is a multiple select where the options are known beforehand,
the other one needs to allow for creating new and deleting old options (I'm using select2).
With the help of cake bake and some model HABTM documentation that was I missing when I read the FormHelper documentation I found out that
I have to name my multiple select form field like the model that belongs to it i.e. Variable.
It's implicit in the FormHelper documentation, but definitely could be highlighted more.
Also implicit: Because the find() operation gets the possible values for a field, I had to call the fields in my Variable model id and name (how else would it know from the Model name in the input call what to display). I can change find's options but that broke the convention at some other step I think.
Inconsistently if I want to supply a list of possible values in the controller, I have to set a variable that is lowercase, camelized and pluralized (not uppercase and singular like my Model and like I have to name the form field, not lowercase, underscore-separated and singular like my name field and my table).
I thought I didn't need to set the possible options in the controller (because I either know them or they're added on-the-fly by the user, I don't really want to populate the DB with them beforehand), but I tried to wrap my head around the convention. I could not get it to work unless I populated the DB with them beforehand and set them in the controller.
That seems fragile or at least more narrow compared to the treatment of single selects.
My continuing problems
With HABTM I can't create new options on-the-fly (it's okay for one usecase, but not for another, which is more like tagging).
With HABTM I don't want to have to populate the DB with options. And I don't really want to set the options in the controller, but in the view (because I anticipate this will cause problems with multiple "Tests" on one page, if it doesn't then it's okay).
With HABTM it doesn't work when I change the Form->input to Form->select (doesn't really matter, but adds to that feeling of my solution being fragile).
With hasMany I got as far as automatically selecting values I filled in in the DB, but it does not destroy or create associations (i.e. delete no longer selected options in the child table, even though it is declared as dependent). I think here it's due to the fact, that I don't properly pass the IDs of the child table, but how would be the Cake way to do that?
Sample code
/* Model Variable.php */
class Variable extends AppModel {
public $belongsTo = 'Test';
}
/* Model Test.php */
class Test extends AppModel {
public $belongsTo = 'Study';
public $hasAndBelongsToMany = array(
'Variable' => array(
'className' => 'variable',
'joinTable' => 'tests_to_variables',
'foreignKey' => 'test_id',
'associationForeignKey' => 'variable_id',
'with' => 'TestsToVariables',
),
);
}
/* in baked TestsController.php */
$variables = $this->Test->Variable->find('list');
$this->set(compact('variables'));
/* in edit.ctp, the baked view */
echo $this->Form->input('Variable');
Cake Form::input() has options for multi select, it also auto fills the values and selects previously saved data.
How does this not fit your needs, Have you even tried what is explained in the book?
http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#creating-form-elements

Find query with condition depending on parent model

For the purpose of this question, this is the association tree (all -> means hasMany), all database structure code adheres to CakePHP conventions.
Forum -> Section (forum_id) -> Topic (section_id) -> Reply (topic_id)
I'd like to run a $this->Reply->find query with certain conditions, and I would like the returned $data["Reply"] array to only return replies where they belong to forum_id=X.
For example, I run a $this->Reply->find with certain conditions (these don't matter), and it returns two results with different parents, and when you go up and up until you reach Forum.id (or Section.forum_id), they differ in forum_id.
What I want is to filter the results so they belong to a certain forum_id. Since forum_id is not a field in the Reply model itself, but instead in Section (which is two "layers" up), I can't use a conditions entry to filter the result.
What should I do?
Simple as this:
<?php
$this->Reply->find('all', array(
'joins' => array(
Reply::joinLeft('Topic'),
Topic::joinLeft('Section'),
Section::joinLeft('Forum'),
),
'conditions' => array('Forum.id' => $forumId),
));
https://github.com/tigrang/EasyJoin - This will determine the relationship between the models and create the join arrays for you.
If you don't want to use the plugin, you'll have to specify the joins array manually or rebind the models to be able to use Containable as it would currently create multiple queries rather than joins.

Taking the data mapper approach in Zend Framework

Let's assume the following tables setup for a Zend Framework app.
user (id)
groups (id)
groups_users (id, user_id, group_id, join_date)
I took the Data Mapper approach to models which basically gives me:
Model_User, Model_UsersMapper, Model_DbTable_Users
Model_Group, Model_GroupsMapper, Model_DbTable_Groups
Model_GroupUser, Model_GroupsUsersMapper, Model_DbTable_GroupsUsers (for holding the relationships which can be seen as aentities; notice the "join_date" property)
I'm defining the _referenceMap in Model_DbTable_GroupsUsers:
protected $_referenceMap = array (
'User' => array (
'columns' => array('user_id'),
'refTableClass' => 'Model_DbTable_Users',
'refColumns' => array('id')
),
'App' => array (
'columns' => array('group_id'),
'refTableClass' => 'Model_DbTable_Groups',
'refColumns' => array('id')
)
);
I'm having these design problems in mind:
1) The Model_Group only mirrors the fields in the groups table. How can I return a collection of groups a user is a member of and also the date the user joined that group for every group? If I just added the property to the domain object, then I'd have to let the group mapper know about it, wouldn't I?
2) Let's say I need to fetch the groups a user belongs to. Where should I put this logic? Model_UsersMapper or Model_GroupsUsersMapper?
I also want to make use of the referencing map (dependent tables) mechanism and probably use findManyToManyRowset or findDependentRowset, something like:
$result = $this->getDbTable()->find($userId);
$row = $result->current();
$groups = $row->findManyToManyRowset(
'Model_DbTable_Groups',
'Model_DbTable_GroupsUsers'
);
This would produce two queries when I could have just written it in a single query. I will place this in the Model_GroupsUsersMapper class.
An enhancement would be to add a getGroups method to the Model_User domain object which lazily loads the groups when needed by calling the appropriate method in the data mapper, which begs for the second question. Should I allow the domain object know about the data mapper?
This can be pretty confusing issue because a relational database can be hard to map to an object model.
I'd be inclined to focus on your object model requirements first. For example if in your object model it makes sense for your user object to (almost) always be aware of the groups then this relationship should be incorporated into the user class. If on the other hand you often need to use users without needing to know what groups they are a part of perhaps you can have two user classes, a base class and a group aware extended version (base_user and group_user).
I would try to avoid letting the domain object know about the data layer as that's kind of the whole point to using this pattern. The data layer should pretty much just be a dumb factory that instantiates your domain objects.
Just my take on it :)

Storing sort order for items held in an HABTM association - CakePHP

In an ActiveRecord (CakePHP flavored) setup I have a HasAndBelongsToMany association setup on Videos and Bins: A Bin can store n references to Videos, and Videos can belong to n Bins.
I need to be able to manually set and store the display order of the Videos within a particular Bin (so the client can have his Videos in a particular order within the Bin.) My initial thought is to create a field in Bin that stores an array of Video IDs in the order they need to appear. Is this the most efficient way to handle this?
If so, when I then get the HABTM query result, what is the cleanest/most efficient way to re-order the returned query to match the sorted array of ID's?
The Videos associated with a Bin are returned as an array:
[Video] => Array
(
[0] => Array
(
[id] => 49b2de95-0338-48b9-bc88-01ba35664358
...
)
[1] => Array
(
[id] => 49b1ccad-b770-410e-be46-03a035664358
...
)
Or is there a better way to achieve what I'm trying to do without using HABTM associations?
Thanks in advance -
What to do when HABTM becomes complicated?
By default when saving a HasAndBelongsToMany relationship, Cake will delete all rows on the join table before saving new ones. For example if you have a Club that has 10 Children associated. You then update the Club with 2 children. The Club will only have 2 Children, not 12.
Also note that if you want to add more fields to the join (when it was created or meta information) this is possible with HABTM join tables, but it is important to understand that you have an easy option.
HasAndBelongsToMany between two models is in reality shorthand for three models associated through both a hasMany and a belongsTo association.
Consider this example:
Child hasAndBelongsToMany Club
Another way to look at this is adding a Membership model
Child hasMany Membership
Membership belongsTo Child,
ClubClub hasMany Membership.
These two examples are almost the exact same. They use the same amount and named fields in the database and the same amount of models. The important differences are that the "join" model is named differently and it's behavior is more predictable.
In your example, you need a way to add and remove without editing other users Video links, therefore standard habtm will not suit you very well. Create a model for this "join" similar to the Membership model described above. Further, if you added a weight field, you could use the ordered behavior (that I wrote) to order each set of videos per bin. Then you would use the following fields
id, bin_id, video_id, weight
And set up bin_id as the 'foreign_key' in the behavior configuartion. Good luck
Well I tried to solve just this problem and think I found the simplest solution possible:
When saving a new order of the related models you delete the existing relations and add the new ones in the desired order. This means the ids created run in your sort order and you can simply sort by id to display the order.
public $hasAndBelongsToMany = array(
'Item' => array(
'order' => 'ItemFoldersItem.id ASC, Item.name DESC',
)
);
Pretty simple isn't it?
Don't know about "most efficient" way to implement what you want, that depends on your whole application and what you want to accomplish. I'd just keep in mind that the most simple solution is often the best.
However, I can give you a tip for sorting your Video array! It is fairly easy:
$yourData = Set::sort($yourData, '/Video/id', 'asc');
Take a look at the Set class for more candy. :)

Categories