CakePHP Displaying Field Names via Two Associations - mysql

I apologize for the confusing title, I was a little stumped as to how to word my question.
I am new to CakePHP, but am following along through the cookbook/tutorials nicely, however I have come up against something which I cannot find an answer to.
My structure is as follows:
'Invoices' hasMany 'InvoiceHistory'
'InvoiceHistory' belongsTo 'InvoiceHistoryDeliveryStatus'
Whereby, an invoice can have multiple invoice histories, and each history contains a delivery status id, which links to a name.
On the Invoice view (index.ctp) I am displaying a list of all invoices but wish to display the Most Recent Delivery Status Name (InvoiceHistory contains a date field so it can be sorted) - thereby displaying the 'current Delivery Status'.
When I do:
$this->set('invoices', $this->Invoice->find('all'));
It does not go deep enough in what it returns to provide me with Delivery Status Names, nor have I deduced a way of only returning the most recent Invoice History within my result. I know how to do this manually with a MYSQL query but I figured that is probably just plain wrong.
What is the correct way of going about this while following CakePHP conventions?

Use Containable
$this->Invoice->Behaviors->attach('Containable');
$this->set('invoices', $this->Invoice->find('all', array(
'contain' => array(
'InvoiceHistory' => array(
'InvoiceHistoryDeliveryStatus'
)
)
));

From what I can tell, I think you should check out the Containable behavior.

Related

Database - logging the changes of entity

The question is about how to do it properly.
Situation:
I have a table with products and I need to log changes to it so that it would be easy to see by who and when some property was changed.
Every product entity contains (just a few properties to name):
expire_date - DATETIME format
series - VARCHAR(255)
package - INT(32 ) (how many items are in package)
In the future I will have more properties to log. (about 10-15)
This is its basic structure for table which will store information about changes.
[
'id' => 'pk',
'type' => 'INT(32)',
'product_id' => 'INT(32)',
'user_id' => 'INT(32)',
'data_old' => 'VARCHAR(255)',
'data_new' => 'VARCHAR(255)',
'created' => 'DATETIME'
], 'ENGINE=InnoDB CHARSET=utf8');
This will work fine for series attribute.
But what's about expire_date and package?
Storing them into VARCHAR would be silly, I guess.
I face the following solutions:
Storing all attributes in VARCHAR
Creating field for each type of attribute, so I'll end up with fields as data_old_varchar, data_new_varchar, data_old_int, data_new_int, data_new_datetime, data_old_datetime
Creating a separate table for each type of attributes , e.g.I would have tables product_logging_varchar, product_logging_int, product_logging_datetime
Creating a separate table for logging of each type of attribute, e.g. product_logging_series, product_logging_expire_date, product_logging_.... ( and about 15 tables more ,poll)
I like none of options above.
Is there any better solution?
On my experience, I've always implemented tables audit (logging) by creating a copy of each table with a prefix (audit_TableName) and 3 more columns:
Username // Who do action
When // Date of action
What // Insert, Update or Delete
Usually (Oracle, MySql) trigger has been used to fill this table.
So for each table, 1 more table and 3 triggers (insert,update,delete).
Its a bit heavy and painful if database structure change often but this solution always worked for get any data historical/logging/audit, as :
Get all history of a specific data
Get update timeline / user activity
Reload a previous state of data
Activity happen in interval of time/date
...
I heard of a tool who do well the job : https://flywaydb.org/, but never used it yet.

yii pagination issue trying to use 2 criterias

Disclaimer I'm self taught. Got my rudimentary knowledge of php reading forums. I'm an sql newb, and know next to nothing about yii.
I've got a controller that shows the products on our webstore. I would like the out of stock products to show up on the last pages.
I know I could sort by stock quantity but would like the in stock products to change order every time the page is reloaded.
My solution (probably wrong but kinda works) is to run two queries. One for the product that has stock, sorted randomly. One for the out of stock product also ordered randomly. I then merge the two resulting arrays. This much has worked using the code below (although I feel like there must be a more efficient way than running two queries).
The problem is that this messes up the pagination. Every product returned is listed on the same page and changing pages shows the same results. As far as I can tell the pagination only works for 1 CDbCriteria at a time. I've looked at the yii docs for CPagination for a way around this but am not getting anywhere.
$criteria=new CDbCriteria;
$criteria->alias = 'Product';
$criteria->addCondition('(inventory_avail>0 OR inventoried=0)');
$criteria->addCondition('Product.parent IS NULL');
$criteria->addCondition('web=1');
$criteria->addCondition('current=1');
$criteria->addCondition('sell>sell_web');
$criteria->order = 'RAND()';
$criteria2=new CDbCriteria;
$criteria2->alias = 'Product';
$criteria2->addCondition('(inventory_avail<1 AND inventoried=1)');
$criteria2->addCondition('Product.parent IS NULL');
$criteria2->addCondition('web=1');
$criteria2->addCondition('current=1');
$criteria2->addCondition('sell>sell_web');
$criteria2->order = 'RAND()';
$crit1=Product::model()->findAll($criteria);
$crit2=Product::model()->findAll($criteria2);
$models=array_merge($crit1,$crit2);
//I know there is something wrong here, no idea how to fix it..
$count=Product::model()->count($criteria);
$pages=new CPagination($count);
//results per page
$pages->pageSize=30;
$pages->applyLimit($criteria);
$this->render('index', array(
'models' => $models,
'pages' => $pages
));
Clearly I am in over my head. Any help would be much appreciated.
Edit:
I figured that a third CDbCriteria that includes both the in stock and out of stock items could be used for the pagination (as it would include the same number of products as the combined results of the first 2). So I tried adding this (criteria1 and criteria2 remain the same):
$criteria3=new CDbCriteria;
$criteria3->alias = 'Product';
//$criteria3->addCondition('(inventory_avail>0 OR inventoried=0)');
$criteria3->addCondition('Product.parent IS NULL');
$criteria3->addCondition('web=1');
$criteria3->addCondition('current=1');
$criteria3->addCondition('sell>sell_web');
//$criteria3->order = 'RAND()';
$crit1=Product::model()->findAll($criteria);
$crit2=Product::model()->findAll($criteria2);
$models=array_merge($crit1,$crit2);
$count=Product::model()->count($criteria3);
$pages=new CPagination($count);
//results per page
$pages->pageSize=30;
$pages->applyLimit($criteria3);
$crit1=Product::model()->findAll($criteria);
$crit2=Product::model()->findAll($criteria2);
$models=array_merge($crit1,$crit2);
$this->render('index', array(
'models' => $models,
'pages' => $pages
));
I'm sure I'm missing something super obvious here... Been searching all day getting nowhere.
So you are running into what is IMO one of the potential drawbacks of natural language query builder frameworks. They can get your thinking on how you might approach a SQL problem going down a bad path when trying to work with the "out of the box" methods for building queries. Sometimes you might need to think about using raw SQL query capabilities that most every framework to provide in order to best address your problem.
So let's start with the basic SQL for how I would suggest you approach your problem. You can either work this into your query builder style (if possible) or make a raw query.
You could easily form a calculated field representing binary inventory status for sorting. Then also sort by another criteria secondarily.
SELECT
field1,
field2,
/* other fields */
IF(inventory_avail > 0, 1, 0) AS in_inventory
FROM product
WHERE /* where conditions */
ORDER BY
in_inventory DESC, /* sort items in inventory first */
other_field_to_sort ASC /* other sort criteria */
LIMIT ?, ? /* pagination row limit and offset */
Note that this approach only returns the rows of data you need to display. You move away from your current approach of doing a lot of work in the application to merge record sets and such.
I do question use of RAND() for pagination purposes as doing so will yield products potentially appearing on one page after another as the user paginates through the pages, with other products perhaps not showing up at all. Either that or you need to have some additional complexity added to your applicatoin to somehow track the "randomized" version of the entire result set for each specific user. For this reason, it is really unusual to see order randomization for paginated results display.
I know you mentioned you might like to spike out a randomized view to the user on a "first page". If this is a desire that is OK, but perhaps you decouple or differentiate that specific view from a wider paginated view of the product listing so as to not confuse the end user with a seemingly unpredictable pagination interface.
In your ORDER BY clause, you should always have enough sorting conditions to where the final (most specific) condition will guarantee you a predictable order result. Oftentimes this means you have to include an autoincrementing primary key field, or similar field that provides uniqueness for the row.
So let's say for example I had the ability for user to sort items by price, but you still obviously wanted to show all inventoried items first. Now let's say you have 100K products such that you will have many "pages" of products with a common price when ordered by price
If you used this for ordering:
ORDER BY in_inventory DESC, price ASC
You could still have the problem of a user seeing the same product repeated when navigating between pages, because a more specific criteria than price was not given and ordering beyond that criteria is not guaranteed.
You would probably want to do something like:
ORDER BY in_inventory DESC, price ASC, unique_id ASC
Such that the order is totally predictable (even though the user may not even know there is sorting being applied by unique id).

Design best practice - model vs controller vs UI - CakePHP, MySQL

I have been struggling for a few days with this problem and finally seek the opinion of the experts and crowd at this website.
I have two tables - one is a template of workflow steps and the other is an instance of these workflow steps called events. The templates table contains information like step name, step type etc - very generic information. The event table contains a reference link back to the workflow step table and an additional column called notes - which stores data that the user logged as they logged a particular workflow step. Both Workflow Steps and Events are linked to a POST on the website
Workflow step templates can exist without events having yet occurred - that is the user may be still on Step 3 or Step 5 and not logged an event for Step 1, 2 , 4 - basically the order of steps is only suggestive but not binding. Workflow Steps have a sequence field that dictate the order in which they should appear on screen.
Events can also occur without a workflow step - in other words, a user can log a note outside the context of workflow steps. These are generic events and directly associated with the POST
I am able to successfully retrieve both of these values for a given POST - they are retrieved as two separate arrays. I am using CakePHP and MySQL
The UI needs to render a screen that shows all the workflow steps in order and corresponding events that have occured in correlation to these steps or outside of these steps. The ordering of the screen will be driven primarily by the sequence of workflow steps and secondarily by created_date for those events that are not associated with a particular workflow step
Problem statement -
1. Do I send two separate arrays (as noted in #4) to the UI and let the UI determine the complex logic of how to interweave the steps and events for display?
2. Do I process the interweaving of steps and events in the controller and then send to the UI a simple array that it can loop through and display?
3. I have tried moving this logic to the database but because of variations explained in #2 and #3 it becomes quite complicated
I am seeking advise on which would be a better option from a design practice as well as from a simplification point of view. I understand that I have given a limited picture here but am hoping that someone on this website may have run into a similar issue elsewhere.
Depending on how you are assigning events to users, I would make a hasOne relation in Event to Workflow. You would need another relationship for you users, hasOne or hasMany.
$hasOne = 'Workflow';
Obviously this would mean that your Event table would have a column called wordflow_id and would be associated with a single row in your workflow. In the controller I would call the Event with by the user.
$this->Event->findAllByUserId($user_id);
This should provide you with an array that might look something like this.
array(
[0] => array(
[Event] => array(
'id' => 1,
'name' => 'blah',
...
),
[User] => array(
'id' => 1,
'name' => 'Charles',
...
),
[Workflow] => array(
[0] => array(
'id' => 1,
'name' => 'more blagblagblag',
...
),
[1] => array(
'id' => 9,
'name' => 'sblagblsagblag',
...
),
[2] => array(
'id' => 42,
'name' => 'mordse d',
...
)
)
)
)
Call all your workflow templates
$this->Workflow->find('all');
Then I would user cake's built in SET:: functionality to print the workflow template in your view and use your Event call to fill in the data.
Please post more detail and your code, models, ect and I'm sure we can get you the exact query/logic you'll need to achieve this.
http://book.cakephp.org/2.0/en/core-utility-libraries/set.html
OK - I have solved this. I ended up moving the functionality to the Model.
I created two SQL queries - one that retrieves all the workflow steps along with any event information that maybe associated with each of them.
Then I created a second SQL that retrieves all those events that are stand-alone and not associated with any particular workflow step
I used UNION ALL to stack them on top of each other
I used a SORT on modified date and squence number so that all the steps and events appear in chronological order and sequence
I then passed this from the Model to the View (via the controller) and let the View iterate and display the elements. This approach actually simplified my View and Controller code immensely and even the Model code is quite simple since all it is a query statement with parameters.

Exposing table name and field names in request URL

I was tasked to create this Joomla component (yep, joomla; but its unrelated) and a professor told me that I should make my code as dynamic as possible (a code that needs less maintenance) and avoid hard coding. The approach we thought initially is take url parameters, turn them into objects, and pass them to query.
Let's say we want to read hotel with id # 1 in the table "hotels". lets say the table has the fields "hotel_id", "hotel_name" and some other fields.
Now, the approach we took in making the sql query string is to parse the url request that looked like this:
index.php?task=view&table=hotels&hotel_id=1&param1=something&param2=somethingelse
and turned it into a PHP object like this (shown in JSON equivalent, easier to understand):
obj = {
'table':'hotel',
'conditions':{
'hotel_id':'1',
'param1':'something',
'param2':'somethingelse'
}
and the SQL query will be something like this where conditions are looped and appended into the string where field and value of the WHERE clause are the key and value of the object (still in JSON form for ease):
SELECT * FROM obj.table WHERE hotel_id=1 AND param1=something and so on...
The problem that bugged me was the exposing of the table name and field names in the request url. I know it poses a security risk exposing items that should only be seen to the server side. The current solution I'm thinking is giving aliases to each and every table and field for the client side - but that would be hard coding, which is against his policy. and besides, if I did that, and had a thousand tables to alias, it would not be practical.
What is the proper method to do this without:
hard coding stuff
keep the code as dynamic and adaptable
EDIT:
Regarding the arbitrary queries (I forgot to include this), what currently stops them in the back end is a function, that takes a reference from a hard-coded object (more like a config file shown here), and parses the url by picking out parameters or matching them.
The config looks like:
// 'hotels' here is the table name. instead of parsing the url for a table name
// php will just find the table from this config. if no match, return error.
// reduces risk of arbitrary tables.
'hotels' => array(
// fields and their types, used to identify what filter to use
'structure' => array(
'hotel_id'=>'int',
'name'=>'string',
'description'=>'string',
'featured'=>'boolean',
'published'=>'boolean'
),
//these are the list of 'tasks' and accepted parameters, based on the ones above
//these are the actual parameter names which i said were the same as field names
//the ones in 'values' are usually values for inserting and updating
//the ones in 'conditions' are the ones used in the WHERE part of the query
'operations' =>array(
'add' => array(
'values' => array('name','description','featured'),
'conditions' => array()
),
'view' => array(
'values' => array(),
'conditions' => array('hotel_id')
),
'edit' => array(
'values' => array('name','description','featured'),
'conditions' => array('hotel_id')
),
'remove' => array(
'values' => array(),
'conditions' => array('hotel_id')
)
)
)
and so, from that config list:
if a parameters sent for a task is not complete, server returns an error.
if a parameter from the url is doubled, only the first parameter read is taken.
any other parameters not in the config are discarded
if that task is not allowed, it wont be listed for that table
if a task is not there, server returns an error
if a table is not there, server returns an error
I actually patterned this after seeing a component in joomla that uses this strategy. It reduces the model and controller to 4 dynamic functions which would be CRUD, leaving only the config file to be the only file editable later on (this was what I meant about dynamic code, I only add tables and tasks if further tables are needed) but I fear it may impose a security risk which I may have not known yet.
Any ideas for an alternative?
I have no problem with using the same (or very similar) names in the URL and the database — sure, you might be "exposing" implementation details, but if you're choosing radically different names in the URL and the DB, you're probably choosing bad names. I'm also a fan of consistent naming — communication with coders/testers/customers becomes much more difficult if everyone calls everything something slightly different.
What bugs me is that you're letting the user run arbitrary queries on your database. http://.../index.php?table=users&user_id=1, say? Or http://.../index.php?table=users&password=password (not that you should be storing passwords in plaintext)? Or http://.../index.php?table=users&age=11?
If the user connected to the DB has the same permissions as the user sitting in front of the web browser, it might make sense. Generally, that's not going to be the case, so you'll need some layer that knows what the user is and isn't allowed to see, and that layer is a lot easier to write correctly by whitelisting.
(If you've stuck enough logic into stored procedures, then it might work, but then your stored procedures will hard-code column names...)
When composing a SQL query with data from the input, it presents a security risk. But keep in mind that columns values are inserted to the fields by taking input from the user, analyzing it and composing a SQL query with it (except for prepared statements). So when done properly, you have nothing to worry about - simply restrict the user to those column & tables. Open source software's code/database is visible to all - and it doesn't harm the system so much as one would think.
Your aliasses could be a rot13() on the meta/name of your objects.
Although, if you escape the input accordingly when working with those names, I don't see any problem in exposing their names.

Calculate new average afterSave (or something better?) in CakePHP

I'm building a rating system where a user can rate something from 1-5 stars.
I was wondering if there's a way to automatically calculate all of a specific item's ratings (from the ratings table where model='x' and foreign_key='y') on afterSave or something similar.
I can do it in the ratings_controller just fine... just thought it might be more ideal to be done automatically in the model. Can anyone point me in the right direction for this?
I would LOVE to hear that there's some kind of association setting in CakePHP that allows it to do this for you - something like:
//Rating model
var $belongsTo = array(
'Restaurant' => array(
'averageValue' => 'rating
)
);
But - I'm sure that's asking to much :)
if you want to save the average into a field in items table then afterSave would probably be the best solution right now.
The only thing cake can automatically do for you is keeping track of how many ratings an item has (counterCache), but not other aggregate functions.
virtualField may be good, but I have never used that for aggregate functions, so I'm not sure. Besides, if your ratings don't change often, it would put unnecessary work on the system.
In Rating model:
function afterSave($created){
$avgValue = $this->Query('SELECT AVG(rating) as rating FROM ratings WHERE ratings.restaurant_id = '.$this->restaurant_id);
$this->Restaurant->updateRatingAverage($this->restaurant_id,$avgValue[0][0]['rating']);
}
In Restaurant model
function updateRatingAverage($id,$avg){
$this->id = $id;
$this->field('your_average_field_here',$avg);
}
you might want to log the $avgValue to see how it's structured, but I think I got that right.
I need to post and answer because I can't yet comment.
So, your (for example) restaurants hasMany ratings? Try virtualField in restaurants model where you calculate the rating every time restaurants are fetched from database. There might be need for GROUP BY like ypercube mentioned if you need to use AVG().