How to get Phalcon to not reload the relation each time I want to access it - phalcon

I am using Phalcon and have a model Order that has a one-to-many relationship with model OrderAddress. I access those addresses through the following function:
public function getAddresses($params = null) {
return $this->getRelated("addresses", array(
"conditions" => "[OrderAddress].active = 'Y'"
));
}
The OrderAddress model has a public property errors that I do not want persisted to the database. The problem I am having is that everytime I access the getAddresses function, it reloads the object from MySQL which completely wipes the values that I set against that property.
I really only want the OrderAddress models to be loaded once, so that each call to getAddresses doesn't make another trip to the DB- it just iterates over the collection that was already loaded.
Is this possible?

I suppose there's no such option in phalcon, so it has to be implemented in your code.
You could create an additional object property for cached addresses, and return it if it's already been initialized:
protected $cachedAddresses = null;
public function getAddresses($params = null) {
if ($this->cachedAddresses === null) {
$this->cachedAddresses = $this->getRelated("addresses", array(
"conditions" => "[OrderAddress].active = 'Y'"
));
}
return $this->cachedAddresses;
}
This could be a quick solution, but it will be painful to repeat it if you have other relations in your code. So to keep it DRY, you could redefine a 'getRelated' method in base model so it would try to return cached relations, if they already were initialized.
It may look like this:
protected $cachedRelations = [];
public function getRelated($name, $params = [], $useCache = true) {
//generate unique cache object id for current arguments,
//so different 'getRelated' calls will return different results, as expected
$cacheId = md5(serialize([$name, $params]));
if (isset($this->cachedRelations[$cacheId]) && $useCache)
return $this->cachedRelations[$cacheId];
else {
$this->cachedRelations[$cacheId] = parent::getRelated($name, $params);
return $this->cachedRelations[$cacheId];
}
}
Then, you can leave 'getAddresses' method as is, and it will perform only one database query. In case you need to update cached value, pass false as a third parameter.
And, this is completely untested, but even if there're any minor errors, the general logic should be clear.

Related

Returning distinct data for a dropdownlist box with selectlistItem

I have a field in my database with duplicates. I want to use it in a dropdown list, which has to return distinct data.
Here is the method that I created to do this:
public IEnumerable<SelectListItem> GetBranches(string username)
{
using (var objData = new BranchEntities())
{
IEnumerable<SelectListItem> objdataresult = objData.ABC_USER.Select(c => new SelectListItem
{
Value = c.BRANCH_CODE.ToString(),
Text = c.BRANCH_CODE
}).Distinct(new Reuseablecomp.SelectListItemComparer());
return objdataresult;
}
}
Here is the class I am using:
public static class Reuseablecomp
{
public class SelectListItemComparer : IEqualityComparer<SelectListItem>
{
public bool Equals(SelectListItem x, SelectListItem y)
{
return x.Text == y.Text && x.Value == y.Value;
}
public int GetHashCode(SelectListItem item)
{
int hashText = item.Text == null ? 0 : item.Text.GetHashCode();
int hashValue = item.Value == null ? 0 : item.Value.GetHashCode();
return hashText ^ hashValue;
}
}
}
Nothing is returned and I get the error below. When I try a basic query without Distinct, everything works fine.
{"The operation cannot be completed because the DbContext has been disposed."}
System.Exception {System.InvalidOperationException}
Inner exception = null
How can I return distinct data for my dropdown?
Technically, your problem can be solved simply by appending .ToList() after your Distinct(...) call. The problem is that queries are evaluated JIT (just in time). In other words, until the actual data the query represents is needed, the query is not actually sent to the database. Calling ToList is one such thing that requires the actual data, and therefore will cause the query to be evaluated immediately.
However, the root cause of your problem is that you are doing this within a using statement. When the method exits, the query has not yet been evaluated, but you have now disposed of your context. Therefore, when it comes time to actually evaluate that query, there's no context to do it with and you get that exception. You should really never use a database context in conjuction with using. It's just a recipe for disaster. Your context should ideally be request-scoped and you should use dependency injection to feed it to whatever objects or methods need it.
Also, for what it's worth, you can simply move your Distinct call to before your Select and you won't need a custom IEqualityComparer any more. For example:
var objdataresult = objData.ABC_USER.Distinct().Select(c => new SelectListItem
{
Value = c.BRANCH_CODE.ToString(),
Text = c.BRANCH_CODE
});
Order of ops does matter here. Calling Distinct first includes it as part of the query to the database, but calling it after, as you're doing, runs it on the in-memory collection, once evaluated. The latter requires, then, custom logic to determine what constitutes distinct items in an IEnumerable<SelectListItem>, which is obviously not necessary for the database query version.

Yii2 REST API relational data return

I've set up Yii2 REST API with custom actions and everything is working just fine. However, what I'm trying to do is return some data from the API which would include database relations set by foreign keys. The relations are there and they are actually working correctly. Here's an example query in one of the controllers:
$result = \app\models\Person::find()->joinWith('fKCountry', true)
->where(..some condition..)->one();
Still in the controller, I can, for example, call something like this:
$result->fKCountry->name
and it would display the appropriate name as the relation is working. So far so good, but as soon as I return the result return $result; which is received from the API clients, the fkCountry is gone and I have no way to access the name mentioned above. The only thing that remains is the value of the foreign key that points to the country table.
I can provide more code and information but I think that's enough to describe the issue. How can I encode the information from the joined data in the return so that the API clients have access to it as well?
Set it up like this
public function actionYourAction() {
return new ActiveDataProvider([
'query' => Person::find()->with('fKCountry'), // and the where() part, etc.
]);
}
Make sure that in your Person model the extraFields function includes fKCountry. If you haven't implemented the extraFields function yet, add it:
public function extraFields() {
return ['fKCountry'];
}
and then when you call the url make sure you add the expand param to tell the action you want to include the fkCountry data. So something like:
/yourcontroller/your-action?expand=fKCountry
I managed to solve the above problem.
Using ActiveDataProvider, I have 3 changes in my code to make it work.
This goes to the controller:
Model::find()
->leftJoin('table_to_join', 'table1.id = table_to_join.table1_id')
->select('table1.*, table_to_join.field_name as field_alias');
In the model, I introduced a new property with the same name as the above alias:
public $field_alias;
Still in the model class, I modified the fields() method:
public function fields()
{
$fields = array_merge(parent::fields(), ['field_alias']);
return $fields;
}
This way my API provides me the data from the joined field.
use with for Eager loading
$result = \app\models\Person::find()->with('fKCountry')
->where(..some condition..)->all();
and then add the attribute 'fkCountry' to fields array
public function fields()
{
$fields= parent::fields();
$fields[]='fkCountry';
return $fields;
}
So $result now will return a json array of person, and each person will have attribute fkCountry:{...}

Yii form and model for key-value table

I have a table which has only two column key-value. I want to create a form which allow user insert 3 pair of key-value settings.
Do I need pass 3 different models to the view? Or is there any possible way to do this?
Check out this link:
http://www.yiiframework.com/doc/guide/1.1/en/form.table
This is considered best form in Yii for updating for creating multiple models.
In essence, for creation you can create a for loop generate as many inputs a you wish to have visible, and in your controller loop over the inputs to create new models.
View File:
for ( $settings as $i=>$setting ) //Settings would be an array of Models (new or otherwise)
{
echo CHtml::activeLabelEx($setting, "[$i]key");
echo CHtml::activeLabelEx($setting, "[$i]key");
echo CHtml::error($setting, "[$i]key");
echo CHtml::activeTextField($setting, "[$i]value");
echo CHtml::activeTextField($setting, "[$i]value");
echo CHtml::error($setting, "[$i]value");
}
Controller actionCreate:
$settings = array(new Setting, new Setting, new Setting);
if ( isset( $_POST['Settings'] ) )
foreach ( $settings as $i=>$setting )
if ( isset( $_POST['Setttings'][$i] ) )
{
$setting->attributes = $_POST['Settings'][$i];
$setting->save();
}
//Render View
To update existing models you can use the same method but instead of creating new models you can load models based on the keys in the $_POST['Settings'] array.
To answer your question about passing 3 models to the view, it can be done without passing them, but to validate data and have the correct error messages sent to the view you should pass the three models placed in the array to the view in the array.
Note: The example above should work as is, but does not provide any verification that the models are valid or that they saved correctly
I'm going to give you a heads up and let you know you could potentially make your life very complicated with this.
I'm currently using an EAV patterned table similar to this key-value and here's a list of things you may find difficult or impossible:
use CDbCriteria mergeWith() to filter related elements on "value"s in the event of a search() (or other)
Filtering CGridView or CListView
If this is just very straight forward key-value with no related entity aspect ( which I'm guessing it is since it looks like settings) then one way of doing it would be:
create a normal "Setting" CActiveRecord for your settings table (you will use this to save entries to your settings table)
create a Form model by extending CFormModel and use this as the $model in your form.
Add a save() method to your Form model that would individually insert key-value pairs using the "Setting" model. Preferably using a transaction incase a key-value pair doesn't pass Settings->validate() (if applicable)
optionally you may want to override the Form model's getAttributes() to return db data in the event of a user wanting to edit an entry.
I hope that was clear enough.
Let me give you some basic code setup. Please note that I have not tested this. It should give you a rough idea though.:
Setting Model:
class Setting extends CActiveRecord
{
public function tableName()
{
return 'settings';
}
}
SettingsForm Model:
class SettingsForm extends CFormModel
{
/**
* Load attributes from DB
*/
public function loadAttributes()
{
$settings = Setting::model()->findAll();
$this->setAttributes(CHtml::listData($settings,'key','value'));
}
/*
* Save to database
*/
public function save()
{
foreach($this->attributes as $key => $value)
{
$setting = Setting::model()->find(array('condition'=>'key = :key',
'params'=>array(':key'=>$key)));
if($setting==null)
{
$setting = new Setting;
$setting->key = $key;
}
$setting->value = $value;
if(!$setting->save(false))
return false;
}
return true;
}
}
Controller:
public function actionSettingsForm()
{
$model = new Setting;
$model->loadAttributes();
if(isset($_POST['SettingsForm']))
{
$model->attributes = $_POST['SettingsForm'];
if($model->validate() && $model->save())
{
//success code here, with redirect etc..
}
}
$this->render('form',array('model'=>$model));
}
form view :
$form=$this->beginWidget('CActiveForm', array(
'id'=>'SettingsForm'));
//all your form element here + submit
//(you could loop on model attributes but lets set it up static for now)
//ex:
echo $form->textField($model,'fieldName'); //fieldName = db key
$this->endWidget($form);
If you want further clarification on a point (code etc.) let me know.
PS: for posterity, if other people are wondering about this and EAV they can check the EAV behavior extention or choose a more appropriate DB system such as MongoDb (there are a few extentions out there) or HyperDex

How to pass a parameter in my method to return using a stateless session? minimize code duplication

I want to pass a parameter in my method call, if set (its a boolean), then return a Stateless session.
I don't want to duplicate the QueryOver code, is there a way to have it like:
public virtual IList<User> GetAllUsers(bool isStateless)
{
var query = QueryOver<User>().Where(x => x.UserType == 1).ToList();
if(isStateless)
return NHibernateHelper.Session(query);
else
return NHibernateHelper.StatelessSession(query);
}
I know the above won't work, but I hope it is clear what I am after.
The only way I know is to basically duplicate the entire queryover code, and the only different between the code blocks will be that one will use .Session and the other will use .StatelessSession.
Hoping there is a cleaner way.
var query = QueryOver.Of<User>().Where(x => x.UserType == 1);
IQueryOver<User, User> executableQuery;
if(isStateless)
executableQuery = query.GetExecutableQueryOver(NHibernateHelper.Session);
else
executableQuery = query.GetExecutableQueryOver(NHibernateHelper.StatelessSession);
return executableQuery.ToList();

How to validate >1 field at a time, in a Zend sub-form?

I've created a 3 screen "wizard" using the Zend_Form_SubForm example from the online reference documentation.
The requirement I'm having trouble meeting is this:
If fields 1, 2, & 3 of the first screen are already in the database, notify the user that they are trying to add a duplicate record. Each of those fields has their own validators. Somehow I need to add this "group validator".
So, at its most basic level, I'm trying to do:
if($field_1_not_in_db && $field_2_not_in_db && $field_3_not_in_db){
return true;//validation OK
} else {
return false;//invalid data
}
I am coming up against several issues, though:
1) Because it applies to multiple fields, I don't know which field to attach it to. Error messages appear beside the field they are attached to, so this is important... unless I can get these "multi-field validator" errors to appear at the top of the screen, which would be ideal.
2) My validator is only receiving a single value (the value of the field I attach it to, not the values of the multiple fields it is supposed to validate).
3) I provide a link to the original (non-duplicate) record in the error message, but it escapes the link, and I can't figure out how to get around that.
The setup I'm currently using (below) actually executes fine, but NewPlace validator receives $_POST['city_fk'] as $fields, instead of the desired group of posted values.
$city_fk = new Zend_Form_Element_Select('city_fk');
$cities = array();
$city_fk->setMultiOptions($cities)
->setLabel('City')
->setDescription('The city this place is in')
->setRequired(true);
$v = array(
'place_is_unique' => array(
'NewPlace',
'fields' => array('place_name','phone_number','phone_extension','street','post_code_name'),
)
);
$city_fk->addValidators($v);
$addressSubForm->addElement($city_fk);
class My_Validate_NewPlace extends Zend_Validate_Abstract
{
public function isValid($fields)
{
$result = false;
if(!$result)
{
$this->_error('sorry, this is duplicate data. see it here');
return false;
}
return true;
}
}
This won't help you decide which field to attach the validation to, but...
There is a thing called a "validation context" that helps.
When you create your custom validator or form IF you specify a second optional parameter ($context = null), then Zend will auto-populate this with the entire array of posted data, which you can use to incorporate other fields values into your validation. Here's a very basic example:
$city_name = new Zend_Form_Element_Text('city_name');
$place_name = new Zend_Form_Element_Text('place_name');
$place_name->addValidator('NewPlace');
class My_Validate_NewPlace extends Zend_Validate_Abstract
{
public function isValid($value, **$context = null**)
{
if(trim($value)!='' && trim($context['city_name']) != '')
{
return true;
}
return false;
}
}