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;
}
Related
i was getting in a question when i got this scenario:
I have to make a history log about what the user does and of course the user can do a lots different action.
i thought two different 2 way for make it i just need someone that can help me to follow the right way.
First way:
Create 2 different tables
History_user
History_type
History_user table
id | user_id | history_type (int)
1 1 1
1 3 2
History_type
id | name_action (string)
1 The user has posted on the wall
2 The user has change his profile picture
and then just join on the query with History_user.history_type = History_type.id
Second way:
is create the History_user table and an helper example called Converter.
<?php
class Converter {
function history($type_history) {
switch($type_history) {
case 1:
$human_history = "The user has posted on the wall";
break;
case 2:
$human_history = "The user has change his profile picture";
break;
}
return $human_history;
}
}
$converter = new Converter();
$converter->history(1);
I was looking for the better way for do that, in terms of performance and maintainability. Thank you.
Both helper and History_type table are necessary for information representation. In terms of performance it doesn't really matter, because you will insert only in one table on user action. If you need to represent data, you will need just one more query to get descriptions of actions (without joins, ofc, if you want some performance). So 2 tables way is more flexible and extendable.
You still could do that helper function which lets say will have static cache variable - array of id => name of actions, which will be lazy loaded on history() function like this:
class Converter {
protected static $_cache;
protected static function _loadCache() {
if (null !== self::$_cache) {
return;
}
self::$_cache = array();
$query = "SELECT * FROM `History_type`";
$res = mysql_query($query);
while ($row = mysql_fetch_assoc($res)) {
self::$_cache[(int) $row['id']] = $row['action'];
}
}
public static function history($id) {
self::_loadCache();
return isset(self::$_cache[$id]) ? self::$_cache[$id] : 'Undefined action';
}
}
Converter::history(1);
I don't want to get any information from the fields, all I want to get are the field machine names attached to a specific bundle (instance of an entity).
I'm looking into entityfieldquery, entity_load, and entity_get_info, and and I'm leaning towards entity_get_info, but now I'm reading that use is deprecated.
function multi_reg_bundle_select() {
$query = entity_get_info('registration');
}
How do I get information from the attached bundle? ('registration['bundlename']')? Ultimately I just want to get the fields attached to a particular bundle. Preferably in an array of strings.
You can find the answer at https://drupal.stackexchange.com/questions/14352/listing-entity-fields
Short answer: use
$fields = field_info_instances();
to get all info about all entity types and bundles, or use
$fields = field_info_instances('node', 'article');
to get only the fields of the node type "article".
The easiest way to get only the field machine names attached to a specific bundle would be this:
$field_names = array_keys(field_info_instances('node', 'article'));
Using the function already mentioned; a disadvantage of field_info_instances() in some circumstances is that it does not provide the field type. The lightest weight function for that in Drupal 7 is field_info_field_map(). It can be put in a helper function like this:
/**
* Helper function to return all fields of one type on one bundle.
*/
function fields_by_type_by_bundle($entity_type, $bundle, $type) {
$chosen_fields = array();
$fields = field_info_field_map();
foreach ($fields as $field => $info) {
if ($info['type'] == $type &&
in_array($entity_type, array_keys($info['bundles'])) &&
in_array($bundle, $info['bundles'][$entity_type]))
{
$chosen_fields[$field] = $field;
}
}
return $chosen_fields;
}
And use it like so, to get all taxonomy fields on the article content type:
$fields = fields_by_type_by_bundle('node', 'article', 'taxonomy_term_reference');
Note that field_info_field_map() gives only the machine name (as the original poster requested), but you'd have to load the field object with field_info_field() to get the field label (human-readable name).
I believe that field_info_bundles() may be what I am looking for. I'll let people know when I've tested it (but still, if you have suggestions, I'm happy to hear them!)
https://api.drupal.org/api/drupal/modules!field!field.info.inc/function/field_info_bundles/7
I'm really new in PHP, our instructor just teaching us C++ OOP and I want to try it on PHP.
I'm creating objects with my class.
class TwitterUser {
private $twittername;
public function TwitterUser($a)
{
$this->twittername = $a;
// echo $this->twittername;
}
}
$reader = new Spreadsheet_Excel_Reader($target_path);
$veriler = $reader->sheets[0]['cells'];
foreach($veriler as $veri)
{
if(!empty($veri[$sutun]) and $veri[$sutun]!="Twitter")
{
$kisiler[] = new TwitterUser(temizle($veri[$sutun]));
}
}
What I want is, if one object has same string with other object in $twittername data member, don't create new object.
This task is usually done using some kind of Model -> database approach (such as Doctrine), in which case you save the model data into database. The database table should be designed to not allow the same name for more than one record and the logic to enforce and error handle this can be built into the model class.
You can achieve the same by pure PHP, but it requires existing instances to be stored somehow so when creating new instances, existing ones can be checked for uniqueness.
You don't want to add the object if the username is test? Basically you can't back out of a constructor. Just add a simple flag to only add "test" user once.
Using your code sample:
$testuserexists = false;
foreach($veriler as $veri)
{
if(!empty($veri[$sutun]) and $veri[$sutun]!="Twitter" && $testuser == false)
{
$kisiler[] = new TwitterUser(temizle($veri[$sutun]));
if ($veri[$sutun] == "Test")
$testuserexists = true;
}
}
Or if you are trying to not have duplicates:
foreach($veriler as $veri)
{
if(!empty($veri[$sutun]) and $veri[$sutun]!="Twitter" && !isset($kisiler[$veri[$sutun]]))
{
$kisiler[$veri[$sutun]] = new TwitterUser(temizle($veri[$sutun]));
}
}
I don't know what the temizle function is supposed to do, but basically you can assign the username as the associative array key and prevent duplicates by adding an isset() to your conditional.
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.
I'm using Doctrine with Symfony in a couple of web app projects.
I've optimised many of the queries in these projects to select just the fields needed from the database. But over time new features have been added and - in a couple of cases - additional fields are used in the code, causing the Doctrine lazy loader to re-query the database and driving the number of queries on some pages from 3 to 100+
So I need to update the original query to include all of the required fields. However, there doesn't seem an easy way for Doctrine to log which field causes the additional query to be issued - so it becomes a painstaking job to sift through the code looking for usage of fields which aren't in the original query.
Is there a way to have Doctrine log when a getter accesses a field that hasn't been hydrated?
I have not had this issue, but just looked at Doctrine_Record class. Have you tried adding some debug output to the _get() method? I think this part is where you should look for a solution:
if (array_key_exists($fieldName, $this->_data)) {
// check if the value is the Doctrine_Null object located in self::$_null)
if ($this->_data[$fieldName] === self::$_null && $load) {
$this->load();
}
Just turn on SQL logging and you can deduce the guilty one from alias names. For how to do it in Doctrine 1.2 see this post.
Basically: create a class which extends Doctrine_EventListener:
class QueryDebuggerListener extends Doctrine_EventListener
{
protected $queries;
public function preStmtExecute(Doctrine_Event $event)
{
$query = $event->getQuery();
$params = $event->getParams();
//the below makes some naive assumptions about the queries being logged
while (sizeof($params) > 0) {
$param = array_shift($params);
if (!is_numeric($param)) {
$param = sprintf("'%s'", $param);
}
$query = substr_replace($query, $param, strpos($query, '?'), 1);
}
$this->queries[] = $query;
}
public function getQueries()
{
return $this->queries;
}
}
And add the event listener:
$c = Doctrine_Manager::connection($conn);
$queryDbg = new QueryDebuggerListener();
$c->addListener($queryDbg);