Yii composite primary keys with isNewRecord - php

I have a mysql table with composite keys ( user_id , category_id );
I am trying to update the last access for these records as following
$userCategory = new UserCategory;
$userCategory->user_id = 1;
$userCategory->category_id = 15;
echo $userCategory->isNewRecord; //always true
$userCategory->last_access = Now();
$userCategory->save();
The {$userCategory->isNewRecord} and when I try to save() the MySQL generates a duplicate error for the composite Primary keys.
I also added this to UserCategory model but didn't help
public function primaryKey() {
return array('user_id', 'category_id');
}
****Update:
Sorry for the confusion. My question is how to achieve the same result as "ON DUPLICATE KEY UPDATE" in the Yii framework. In other words, how to do the insert or update in one SQL query. if you look at the source code for save()
public function save($runValidation=true,$attributes=null)
{
if(!$runValidation || $this->validate($attributes))
//checking if new record
return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes);**
else
return false;
}

Actually, the problem is that if isNewRecord is always true, it means that Yii is going to use an INSERT statement instead of an UPDATE statement when saving the model to the database.. that is why you always get the duplicate pk error, even if it's composite.
Here is the official documentation about IsNewRecord . So, the problem is that you're using
$userCategory = new UserCategory; //Always a new record, tries to INSERT
So to resolve this you have to find the record and evaluate if it is found before saving it, instead. Documentation can also be read Here about the find() family of methods and their return value, the return values of the find() methods vary slightly on their nature:
find..() returns the record found or NULL if no record is found.
findAll..() returns an array containing all the records found or an empty array if no records are found.
You can use this return value to differentiate wether a primary key exists or not:
$userCategory = UserCategory::model()->findByAttributes(array('user_id '=>1,'category_id '=>15));
// if user does not exist, you need to create it
if ($userCategory == NULL) {
$userCategory = new UserCategory;
$userCategory->user_id = 1;
$userCategory->category_id = 15;
}
echo $userCategory->isNewRecord; //you will see the difference if it does exist or not exist
$userCategory->last_access = Now();
$userCategory->save();
This will ensure that the framework uses the INSERT or UPDATE statement correctly, avoiding the duplicate PK error you're getting.
Edit: Enhanced the example code to properly populate the record when it's new.

In your model, add the following method:
/**
* Uses the primary keys set on a new record to either create or update
* a record with those keys to have the last_access value set to the same value
* as the current unsaved model.
*
* Returns the model with the updated last_access. Success can be checked by
* examining the isNewRecord property.
*
* IMPORTANT: This method does not modify the existing model.
**/
public function updateRecord(){
$model = self::model()->findByPk(array('user_id'=>$this->user_id,'category_id'=>$this->category_id));
//model is new, so create a copy with the keys set
if(null === $model){
//we don't use clone $this as it can leave off behaviors and events
$model = new self;
$model->user_id = $this->user_id;
$model->category_id = $this->category_id;
}
//At this point we have a model ready for saving,
//and don't care if it is new or not
$model->last_access = $this->last_access;
$model->save(false);
return $model;
}
The above is inspired by a more general method that I use a lot to do a create-or-find-if-already-exists process.
Use the following code to execute this.
$userCategory = new UserCategory;
$userCategory->user_id = 1;
$userCategory->category_id = 15;
echo $userCategory->isNewRecord; //always true
$userCategory->last_access = Now();
$userCategory = $userCategory->updateRecord();
Note that only the last line is different from your code. The fact that the instance of the model declared with new UserCategory is not altered is intended behavior.
You can then verify in your code whether or not the model saved with the following:
if(!$userCategory->isNewRecord){
echo 'save succeeded';
}
else{
echo 'save failed';
}

If you're trying to update, you should load record, instead of creating a new one.
UserCategory::model()->findByPk(array('user_id'=> 1,'category_id '=> 15));
$userCategory->last_access = Now();
$userCategory->save();

in UserCategory.php
public function isNewRecord()
{
$result = $this->findByAttributes(array('user_id'=>$this->user_id,'category_id'=>$this->category_id));
if($result === NULL)
{
return true;
}
return false;
}
then in the controller
$userCategory = new UserCategory;
$userCategory->user_id = 1;
$userCategory->category_id = 15;
echo $userCategory->isNewRecord();
----

Another option is to modify the model to change the condition on the save function then call the parent save function: (this code goes in the UserCategory model)
public function save($runValidation=true,$attributes=null) {
$exists = UserCategory::model()->findByAttributes(array('category_id'=>$this->category_id,'user_id'=>$this->user_id));
if($exists) {
$this->isNewRecord = false;
}
return parent::save($runValidation,$attributes);
}
I just did a test and it seems to work correctly. You should just be able to do this:
$userCategory = new UserCategory;
$userCategory->user_id = 1;
$userCategory->category_id = 15;
$userCategory->last_access = Now();
$userCategory->save();
Should insert or update based off of whether it finds the record, so you don't have to change any of your other code.

Related

How to fetch a record having max column value in phalcon?

I am trying to fetch a row/record having max faq_order column.
Scenario: I have a table faq_category and it contains a field faq_order. FAQ_ORDER column is responsible for storing the order number.
While creating new record in faq_category I want to set faq_order but it should be having latest value. i.e let say if there is 2 previous records so that records will be having faq_order values 1, 2 respectively! Now on third new record it should set the faq_order to 3 but I tried the below code but didn't found a proper way.
Save function:
public function saveGeneralFaqCategoryAction(){
// Instantiate new form for EbFaqCategoryModel
$form = new EbFaqCategoryForm();
if( $form ->isValid($this->request->getPost())){
// Get the FAQ Category id (if any)
$id = $this->request->get( 'id', null );
// Get existing FAQ Category (if any) or create a new one
if( null !== $id && $id !== '' ) {
$faqCategory = EbFaqCategoryModel::findFirst( $id );
} else {
// Here we create new instance and I'm stuck here!
// Logic in my mind is get max order and +1 it and then save it
// in new instance
$faqCategory = new EbFaqCategoryModel();
//$maxOrder = EbFaqCategoryModel::get();
$faqCategory->setFaqOrder(); // On new I want to set max value
}
// Bind form with post data
$form->bind( $this->request->getPost(), $faqCategory );
$faqCategory->save();
} else {
// Send error Json response
return CxHelper::SendJsonError($form->getHtmlFormattedErrors());
}
// Return success
return array( 'data' => 'Success' );
}
Model:
/**
* Get Current Max Order
*
* #return array
*/
public static function getCurrentMaxOrder(){
$queryBuilder = new Builder();
return $queryBuilder
->from(array('c' => static::class))
->columns('c.*')
->where('c.faq_order', MAX) // HERE I want to get a Record having faq_order max
->orderBy("c.date_created desc")
->getQuery()
->execute()->setHydrateMode(Resultset::HYDRATE_ARRAYS)
->toArray();
}
You should be using ORM aggregation functions: https://docs.phalconphp.com/3.2/en/db-models#generating-calculations
Here is one way of doing it:
function beforeValidationOnCreate()
{
$this->faq_order = \YourModelClassame::maximum([
'column' => 'faq_order'
]) + 1;
}
This way when you are creating record from this table it will always have the highest faq_order value :)

Symfony - clone all values from one object onto another by ID?

I am stuck in a huge mess.
I am trying to write a method that should receive user_id and check if there is a profile change request attached to that user.
If there is (has field change_request set to true in users table), the change request should be applied -> all user data fields from change request table by that id should be moved to that user table.
My service
public function getUserApplyChangeRequest($id)
{
$a =$this->getUserRepository()->find($id);
$b =$this->getChangeProfileRequestRepository()->find($id);
$b = clone $a;
$this->em->persist($b);
$this->em->flush();
}
My controller..
public function userApplyChangeRequestAction($changeRequest)
{
$this->requirePostParams(['user_id']);
if ($changeRequest === 1){
$applyChange = $this->get('user')->getUserApplyChangeRequest($this->getUser());
}
return $this->success();
}
I need help because I am stuck and don't really know what to do wtih this lines of code but I putted an example of what I want to happen.
Easiest would be to set the properties yourself if there are only five:
public function getUserApplyChangeRequest($id)
{
$a =$this->getUserRepository()->find($id);
$b =$this->getChangeProfileRequestRepository()->find($id);
$a->setPropertyOne($b->getPropertyOne());
$a->setPropertyTwp($b->getPropertyTwo());
$this->em->persist($a);
$this->em->flush();
}
Other option is to get all the properties of your Change Object with doctrine and call the getters/setters that way (untested, make sure to add NULL checks and skip the ID field):
$props = $em->getClassMetadata(get_class($b))->getColumnNames();
foreach($props as $prop){
//get value from B
$reflectionMethod = new ReflectionMethod(get_class($b),'get'.ucfirst($prop));
$value = $reflectionMethod->invoke($b);
//set value in A
$reflectionMethod = new ReflectionMethod(get_class($a),'set'.ucfirst($prop));
$reflectionMethod->invoke($a, $value);
}

Representing a status array within a PHP class

I'm not entirely sure the best way to handle a status field that relates to integers in a database table within a class.
Say we have the following:
$status = array(1 => 'Active', 2 => 'Inactive', 3 => 'Cancelled');
I'm thinking for clarity in the system, I want class constants and a getter for retrieving the status in an associative array, and so the class uses something better defined than "1" or "2".
class User {
const USER_STATUS_ACTIVE = 1;
const USER_STATUS_INACTIVE = 2;
const USER_STATUS_CANCELLED = 3;
public function getStatusList() {
return array(User::USER_STATUS_ACTIVE => 'Active',User::USER_STATUS_INACTIVE => 'Inactive',User::USER_STATUS_ACTIVE => 'USER_STATUS_CANCELLED');
}
}
This then allows setting the status using the constants with:
class User {
private $status_id;
const USER_STATUS_ACTIVE = 1;
const USER_STATUS_INACTIVE = 2;
const USER_STATUS_CANCELLED = 3;
static public function statusList() {
return array(User::USER_STATUS_ACTIVE => 'Active',User::USER_STATUS_INACTIVE => 'Inactive',User::USER_STATUS_CANCELLED => 'Cancelled');
}
public function setStatus($status_id) {
try {
if (!key_exists($status_id, User::statusList())) throw new Exception('Invalid status ID');
$this->status_id = $status_id;
}
catch (Exception $e) {
die($e);
}
}
}
$a = new User();
$a->setStatus(User::USER_STATUS_ACTIVE);
Alternately methods can be created for setActive(), setInactive(), setCancelled().
But what I'm trying to figure out is how to best handle the actual status values.
Would it be better to break the statuses out to a static class?
class User {
private $status_id;
public function setStatus($status_id) {
try {
if (!key_exists($status_id, UserStatuses::statusList())) throw new Exception('Invalid status ID');
$this->status_id = $status_id;
}
catch (Exception $e) {
die($e);
}
}
}
class UserStatuses {
const USER_STATUS_ACTIVE = 1;
const USER_STATUS_INACTIVE = 2;
const USER_STATUS_CANCELLED = 3;
static public function statusList() {
return array(UserStatuses::USER_STATUS_ACTIVE => 'Active',UserStatuses::USER_STATUS_INACTIVE => 'Inactive',UserStatuses::USER_STATUS_CANCELLED => 'Cancelled');
}
}
Or is there something entirely different that would be better?
Using the ReflectionClass
I like you second example; it's a very simple and effective way to make an enum type (sort of). Actually, some of the pretty big php ORMs out there, like Doctrine operate using a similar pattern. The one thing I like to do to improve scalability is to use the ReflectionClass. This will allow you to add new values in the form of constants without having to change the function that returns the list of values. I would also refactor the code to check if the value is valid in the enum class as well to keep a better separation of concerns between the two classes
class User {
private $status_id;
public function setStatus($status_id) {
if (UserStatuses::isStatus($status_id)) {
$this->status_id = $status_id;
}
else {
throw new Exception('invalid status');
}
}
}
class UserStatuses {
const USER_STATUS_ACTIVE = 1;
const USER_STATUS_INACTIVE = 2;
const USER_STATUS_CANCELLED = 3;
public static function statusList() {
$oClass = new ReflectionClass(__CLASS__);
return $oClass->getConstants();
}
public static function isUserStatus($int) {
return in_array($int, self::statusList());
}
}
Referencing database IDs is not a good practice, in that the values of IDs are generally controlled by the database itself. It's true to say that no database column value is static, but the ID column on a database table is generally an internal ID that is auto-incremented and whose FK references are maintained via cascades in UPDATE and DELETE operations. In other words, the ID is the domain of the database, not your code.
A better practice is to include a custom unique field for your code, like this (I'm assuming MySQL here):
ALTER TABLE `my_reference_table` ADD COLUMN `internalCode` VARCHAR(256);
UPDATE `my_reference_table` SET `internalCode` = 'Active' WHERE `id` = 1;
UPDATE `my_reference_table` SET `internalCode` = 'Inactive' WHERE `id` = 2;
UPDATE `my_reference_table` SET `internalCode` = 'Cancelled' WHERE `id` = 3;
ALTER TABLE `my_reference_table` ALTER COLUMN `internalCode` VARCHAR(256) NOT NULL UNIQUE;
Once you've got a database data setup like this, then you can treat the column internalCode as a static element, have PHP constants that match those internal codes, and be sure that you're referring to the same row no matter if the ID changes.
In terms of storing these internal codes in PHP, I generally use an abstract class with a final private constructor so that it's very very clear that the class is not to be extended, and to only be referenced only in the static context, like so:
class UserStatusConstants {
const _ACTIVE = 'Active';
const _CANCELLED = 'Cancelled';
const _INACTIVE = 'Inactive';
final private function __construct() {}
}
You might be asking at this point why the constant names are prefixed with an underscore - that's to avoid the issue of having constant names that clash with reserved words in PHP.
Anyhow, once you've got this setup, there's various techniques you can use to set user_status_id values in your user table. Here's three I can think of:
UPDATE with JOIN queries
Doing an UPDATE with a JOIN against the user status table, and filtering on the internal code (see How to do 3 table JOIN in UPDATE query?)
SELECT then UPDATE
Start with a SELECT query against the user status table, filtering on internal code, then using that result to feed the user status id to the UPDATE query against the user table. This technique incurs an extra query, but if you store the results of the first query in a cache (e.g. Memcached, or a third party library) then this can speed up all queries using that data in the long run.
Stored procedure
You could create a stored procedure that takes the internal code as a parameter, as well as any other parameters you need to pass to update other fields in the user table

Update data into table from dynamically created input field

I have 2 models Tour.php
public function Itinerary()
{
return $this->hasMany('App\Itinerary', 'tour_id');
}
and
Itinerary.php
public function tour()
{
return $this->belongsTo('App\Tour', 'tour_id');
}
tours table:
id|title|content
itineraries table:
id|tour_id|day|itinerary
I have used vue js to create or add and remove input field for day and plan dynamically. And used the following code in tour.store method to insert into itineraries table:
$count = count($request->input('day'));
$temp_day = $request->input('day');
$temp_itinerary = $request->input('itinerary');
for($i = 0; $i < $count; ++$i)
{
$itinerary = new Itinerary;
$itinerary->tour_id = $tour->id;
$itinerary->plan = $temp_itinerary[$i];
$itinerary->day = $temp_day[$i];
$itinerary->save();
}
And was successful in inserting the records.And applied same code in tour.store method. Instead of updating the rows, it inserted new rows to the table. What would be the best solution for this ?
For updation try this code
$itinerary = Itinerary::find($tour_id);
$itinerary->plan = $temp_itinerary[$i];
$itinerary->day = $temp_day[$i];
$itinerary->save();
The way you are using is to insert/create new records. To update you can use.
Itinerary::find($tour_id)->update(
['column_name'=> value]
);
Where find method takes a primary key of the table.
This will update your existing record. You can update as many columns as you want just pass in array update takes. You can also use save method as mentioned in other answer.
Check Laravel Update Eloquent
EDIT
$iterneary = Itenerary::where('tour_id', $tour_id)->first();
Now you can update this iterneary object to whatever you want.
this is how i did it. First saved all the tours in $tours[] array.
foreach($tours as $tour) {
$itinerary->tour()->updateOrCreate(['id'=> $tour['id']],$tour);
}
updateOrCreate because you may need to add new tours while updating. I know this doesnt answer your issue exactly but this could atleast give you an idea.

Custom is_unique_logical_key - validation or callback?

Can I please have a design suggestion for the following problem:
I am using Codeigniter/Grocery_CRUD.
My system is multi tenanted - different autonomous sites - within the same client. I have quite a few instances of tables that have unique logical keys. One such table structure is:
equip_items
id (pk)
equip_type_id (fk to equip_types)
site_id (fk to sites)
name
Where (equip_type_id, site_id, name) together are a unique key in my db.
The issues is that when using a grocery_CRUD form to add or edit a record that breaks this database rule - the add or edit fails (due to the constraints in the db) but I get no feedback.
I need a variation on the is_unique form_validation rule by which I can specify the field*s* that must be unique.
The issues:
How to specify the rule? set_rules() is for a given field and I have multiple fields that the rule will apply to. Does that mean I should abandon the Form_validation pattern? Or do I follow the 'matches' rule pattern and somehow point to the other fields?
Perhaps a callback function would be better but this would mean writing a custom function in each model where I have this problem at last count this is 9 tables. It seems far better to do this in one place (extending form_validation).
Am I missing something already in codeigniter or grocery_CRUD that has already solved this problem?
Any suggestion/advice you might have would be appreciated.
EDIT:
Actually it appears the solution Johnny provided does not quite hit the mark - it enforces each field in unique_fields() being independently unique - the same as setting is_unique() on each one. My problem is that in my scenario those fields are a composite unique key (but not the primary key). I don't know if it is significant but further to the original problem statement: 1) site_id is a 'hidden' field_type - I don't want my users concerned they are on a different site so I'm dealing with site_id behind the scenes. 2) Same deal with an equip_status_id attribute (not part of the unique key). And 3) I have set_relations() on all these foreign key attributes and grocery_CRUD kindly deals with nice drop downs for me.
EDIT 2
I have solved this using a callback.
UPDATE: This code is now part of grocery CRUD version >= 1.4 and you don't need to use an extension anymore. For more see the documentation for unique_fields
I will try to explain it as easy as I can:
1. First of all for those who have grocery CRUD lower or equal to 1.3.3 has to use this small change: https://github.com/scoumbourdis/grocery-crud/commit/96ddc991a6ae500ba62303a321be42d75fb82cb2
2. Second create a file named grocery_crud_extended.php at application/libraries
3. Copy the below code at your file application/libraries/grocery_crud_extended.php
<?php
class grocery_CRUD_extended extends grocery_CRUD
{
protected $_unique_fields = array();
public function unique_fields()
{
$args = func_get_args();
if(isset($args[0]) && is_array($args[0]))
{
$args = $args[0];
}
$this->_unique_fields = $args;
return $this;
}
protected function db_insert_validation()
{
$validation_result = (object)array('success'=>false);
$field_types = $this->get_field_types();
$unique_fields = $this->_unique_fields;
$add_fields = $this->get_add_fields();
if(!empty($unique_fields))
{
$form_validation = $this->form_validation();
foreach($add_fields as $add_field)
{
$field_name = $add_field->field_name;
if(in_array( $field_name, $unique_fields) )
{
$form_validation->set_rules( $field_name,
$field_types[$field_name]->display_as,
'is_unique['.$this->basic_db_table.'.'.$field_name.']');
}
}
if(!$form_validation->run())
{
$validation_result->error_message = $form_validation->error_string();
$validation_result->error_fields = $form_validation->_error_array;
return $validation_result;
}
}
return parent::db_insert_validation();
}
protected function db_update_validation()
{
$validation_result = (object)array('success'=>false);
$field_types = $this->get_field_types();
$unique_fields = $this->_unique_fields;
$add_fields = $this->get_add_fields();
if(!empty($unique_fields))
{
$form_validation = $this->form_validation();
$form_validation_check = false;
foreach($add_fields as $add_field)
{
$field_name = $add_field->field_name;
if(in_array( $field_name, $unique_fields) )
{
$state_info = $this->getStateInfo();
$primary_key = $this->get_primary_key();
$field_name_value = $_POST[$field_name];
$ci = &get_instance();
$previous_field_name_value =
$ci->db->where($primary_key,$state_info->primary_key)
->get($this->basic_db_table)->row()->$field_name;
if(!empty($previous_field_name_value) && $previous_field_name_value != $field_name_value) {
$form_validation->set_rules( $field_name,
$field_types[$field_name]->display_as,
'is_unique['.$this->basic_db_table.'.'.$field_name.']');
$form_validation_check = true;
}
}
}
if($form_validation_check && !$form_validation->run())
{
$validation_result->error_message = $form_validation->error_string();
$validation_result->error_fields = $form_validation->_error_array;
return $validation_result;
}
}
return parent::db_update_validation();
}
}
4. Now you will simply have to load the grocery_CRUD_extended like that:
$this->load->library('grocery_CRUD');
$this->load->library('grocery_CRUD_extended');
and then use the:
$crud = new grocery_CRUD_extended();
instead of:
$crud = new grocery_CRUD();
5. Now you can simply have the unique_fields that it works like this:
$crud->unique_fields('field_name1','field_name2','field_name3');
In your case:
$crud->unique_fields('equip_type_id','site_id');
Pretty easy right?
This is checking if the field is unique or not without actually change the core of grocery CRUD. You can simply use the grocery_CRUD_extended instead of grocery_CRUD and update grocery CRUD library as normal. As I am the author of the library I will try to include this to grocery CRUD version 1.4, so you will not have to use the grocery_CRUD_extended in the future.
I have done this using a callback:
$crud->set_rules('name','Name','callback_unique_equip_item_check['.$this->uri->segment(4).']');
function unique_equip_item_check($str, $edited_id)
{
$var = $this->Equip_Item_model->is_unique_except(
$edited_id,
$this->input->post('site_id'),
$this->input->post('equip_type_id'),
$this->input->post('name'));
if ($var == FALSE) {
$s = 'You already have an equipment item of this type with this name.';
$this->form_validation->set_message('unique_equip_item_check', $s);
return FALSE;
}
return TRUE;
}

Categories