Object wrappers and forms - oop

I am developing an ecommerce platform and came across a difficulty. Basically, I have in my scenario a Product, Option and OptionValue. A product might have multiple options which might have multiple values. The problem is how to store it in a way that is easy to create and edit.
The problem is I've a direct reference to the OptionValue, which is mutable. I need to keep immutable information about the Option and OptionValue (for example, if a order was made and the color was green, even if this option is changed to lime green, the order must keep showing as green). In that case, I need to save some properties of Option (the option name - "Colors" for example) and of each OptionValue (the value of each option - "red" for example). The way I thought, it would require a structure very similar to the existing structure: a new class ItemOption referencing Option and a ItemOptionValue referencing OptionValue.
So, this was my attempt:
To create a form to display my options I have:
class OptionSelectorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach ($options['product']->getOptions() as $option) {
$builder->add($option->getId(), 'choice', array('choice_list' => new ObjectChoiceList($option->getValues());));
}
}
}
I'm using a DataTransformer to convert a collection of OptionValue in a collection of OrderItemOption:
class OrderItemOptionToOptionValueTransformer implements DataTransformerInterface
{
public function transform($lineOptions)
{
if(!$lineOptions) {
return array();
}
$values = array();
foreach($lineOptions as $lineOption) {
$lineOption->getOption()->getId();
$values[$id] = array();
foreach($lineOption->getValues() as $lineOptionValue) {
$values[$id][] = $lineOptionValue->getOptionValue();
}
}
return $values;
}
public function reverseTransform($values)
{
$collection = new ArrayCollection();
foreach($values as $optionId => $optionValues) {
if(!$optionValues) {
continue;
}
$lineOption = new OrderItemOption();
$optionValues = is_array($optionValues) ? $optionValues : array($optionValues);
foreach($optionValues as $optionValue) {
$lineOptionValue = new OrderItemOptionValue();
$lineOptionValue->setOptionValue($optionValue);
$lineOption->addValue($lineOptionValue);
}
$lineOption->setOption($optionValue->getOption());
$collection->add($lineOption);
}
return $collection;
}
}
Finally, my OrderItemType form:
class OrderItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('options', 'option_selector', array('options' => $options['options']))
->addModelTransformer(new OrderItemOptionToOptionValueTransformer())
);
}
}
It works but doesn't seem to me a good approach, once the OptionValue will be always recreated, never updated.
How would you do it?

I will keep it simple. You can stick with your structure but instead of transforming things on the fly, keep them at the same state.
What do I mean?
Whenever an Option is created, create the corresponding ItemOption. The same goes with the OptionValue and ItemOptionValue. The relation between the two is a one-to-one connection, whereas the Option and OptionValue don't know the connected ItemOption and ItemOptionValue.
Now if a change occurs to the OptionValue, you can query for the connected ItemOptionValue and change the things you need to change (depends on internal structure).
How to store the connection?
Use whatever persistence method you already use. For the case of a database, store the connection in one table like this:
CREATE TABLE item_option_to_option (
optionID INT(10) NOT NULL,
itemOptionID INT(10) NOT NULL,
UNIQUE KEY (optionID, itemOptionID)
);
If possible I would use Foreign Keys to link the both columns to the corresponding columns in the tables item_option and option. It works the same with OptionValue and ItemOptionValue.
How to handle the change?
Whenever the controller for the OptionValue change is called, simply update the Item* models as well.
If an Option or OptionValue is deleted, it is up to you, if you delete the ItemOption or ItemOptionValue as well.

Related

Grails 3: using findAll with join tables

In my Grails webapp I have the following domain classes:
class Customer {
static hasMany = [
addresses: Address
]
static mapping = {
addresses (cascade: "all-delete-orphan", joinTable: [name: "bs_core_customer_addresses"])
}
}
class Address {
...
}
Now I want to implement the abillity to filter the 1:n relation like addresses for things like country (should be dynamic, that means the user can add different filters by itself).
What is the best way to accomplish this?
Filtering the collection from the Customer object? e.g. customer.addresses.findAll{...}
Direct query from the database? How can I add the restriction for the Customer<->Address relation. belongsTo at the Address domain class is no option because the Address object is used in several 1:n relations. e.g. Customer.findAll(...)
Any other option?
you should be able to get away with
static constraints = {
addresses(validator: checkAddress)
}
// This is a static method which is used for validation
// and can be used for when inserting a record to check how many
// existing addresses exist for the end user that has countryCode of US
// This is directly bound to all the object the user and will
// will not be searching the entire DB (A local find limited to user records)
static def checkAddress={val,obj,errors->
if (!obj?.addresses.findAll{it.countryCode=='US'}?.size() >= 2) {
return errors.rejectValue('adress','exceeds limit')
}
}
The above should be self explanatory, but having read through your post a few times now I think I have a better understanding of what you are trying to achieve and there are probably a few different ways of doing it. So let's explore some of them:
Using HQL query, you could change this to another method, I prefer HQL.
class Customer {
def userService
//UserAddress does not have setter transients not essential
static transients = ['userAddress','userService']
//This is a protected function that will return an address
// object given a countryCode
// Execute like this:
// Customer cm = Customer.get(customer.id as Long)
//Address usa = cm.getCountry('US')
protected Address getUserAddress(String countryCode) {
return userService.findAddress(countryCode, this.id)
}
}
Now the service but actually you don't need to execute in domain class unless there is some other need, for displaying etc you could always call this sort of service from within a controller call to render for display purposes
class UserSerice {
// This should return the entire address object back to the domain class
// that called it and will be quicker more efficient than findAll
Address findAddress(String countryCode, Long customerId) {
String query="""
select address from Address a
where a.id :id and countryCode = :code
"""
def inputParams=[code:countryCode, id:customerId]
return Address.executeQuery(query,inputParams,[readOnly:true,timeout:15])
}
Another approach could be a 3rd table that gets updated upon each address added that would give a quick lookup:
class Customer {
static hasMany = [
addresses: Address
//probably don't even need this
//, userCountries:UserCountries
]
}
Class UserCountries {
// register customer
Customer customer
String CountryCode
//maybe address object or Long addressId - depending on if how plain you wanted this to be
Address address
}
Then register the address id and countryCode to this domainclass each time you add a new address and I guess you would need to write some backward compatible code to add existing records to this table for it to work properly.
I left a comment and then removed it for you to expand further on what or how the filtering was taking place. since although you talk of countryCode there is no actual code to show how it all fits in.
I still think something as simple as this would work
//This would only be doing a find with all the bound objects of addresses bound to this customer. so a find within the hasMany relationship elements of this specific customer
protected def getCustomAddress(String countryCode) {
return addresses.findAll{it.code==countryCode}
}
Other far out ideas could be something like this
class Customer {
String _bindAddress
List bindAddress=[]
static transients = [ 'bindAddress' ]
static constraints = {
_bindAddress(nullable:true)
}
//you store a flat CSV as _bindAddress
//you need to work out your own logic to ammend to existing CSV each time address is added
// you will also update _bindAddress of this domainClass each time customer gets a hasMany address added
// so no need for setBindAddress
// void setBindAddress(String b) {
// bindAddress=b.split(',')
// }
//Inorder to set a list back to flat file
//pass in list object
void setBindAddress(List bindAddress) {
_bindAddress=bindAddress.join(',')
/for 1 element this captures better
//_bindAddress=bindAddress.tokenize(',').collect{it}
}
//This is now your object as a list that you can query for what you are querying.
List getBindAdress() {
return _bindAddress.split(',')
}
}
If your actual csv list contained a listing of 'COUNTRY_CODE-ADDRESS_ID' then you could query like this
def found = customer.bindAddress.find{it.startsWith('US-')}
Address usaAddress= Address.get(found.split('-')[1] as Long)
//Slightly longer explaining above:
def found = customer.bindAddress.find{it.startsWith('US-')}
def record = found.split('-')
String countryCode=record[0]
Long addressId=record[1] as Long
Address usaAddress= Address.get(addressId)

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

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.

Yii Behaviors and scenario

i have a behavior for my models, the behavior has beforeFind, beforeSave, in methods i override user_id, something like:
...
public functio beforeSave() {
$this->owner->user_id = Yii::app()->user->id
}
I have model User, how can i disable behavior for registration new user?
Saving code:
$user = new User();
$user->id = 1332;
$user->field1 = 'data';
$user->save();
but on save i have null in $user->id (because work behavior).
i tried
$user->disableBehaviors();
$user->detachBehavior();
Without result.
Maybe its not right way? I create behaviors for identify users in system (find only user something, save only with user id...), but that if i have new user with full previegies, i should again detach behaviors?
If condition can be changed in future I just pass it as callback parameter into behavior from model.
This give you a bit more control over the condition. Hence, behavior becomes more reusable - if it is used by several models this condition can be unique for each.
Example below is a bit simplified, but you should get the idea.
Behavior:
class SomeBehavior extends CActiveRecordBehavior
{
public $trigger;
public function beforeSave($event)
{
if(!call_user_func($this->trigger))
return;
// do what you need
$this->owner->user_id = Yii::app()->user->id;
}
}
Model:
class SomeModel extends CActiveRecord
{
public function behaviors()
{
$me=$this;
return array(
'some'=>array(
'class'=>'SomeBehavior',
'trigger'=>function() use($me){
return $me->scenario=='some-scenario';
}
)
);
}
}
Also I use PHP 5.3. So, I use closure for trigger callback.
If your PHP version is less than 5.3 - anything callable can be used instead. Check here http://www.php.net/manual/en/function.is-callable.php
Because of behavior is a method, you can declare your own logic inside.
The model knows about its scenario, so there is no problem to return different arrays for different conditions:)
Hope it be helpful for somebody.
You can check Yii::app()-user->isGuest to determine if the user is logged in or not. or you can just try looking for the null. Like this:
if (!Yii::app()->user->isGuest)
$this->owner->user_id = Yii::app()->user->id;
or
if (null !== Yii::app()->user->id)
$this->owner->user_id = Yii::app()->user->id;

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

Does NHibernate really deliver transparent persistency

Starting to use Nhibernate for persistency being seduced by the promise that it respects your domain model, I tried to implement a relation manager for my domain objects. Basically, to DRY my code with respect to managing bidirectional one to many and many to many relations, I decided to have those relations managed by a separate class. When a one to many or many to one property is set an entry for the two objects is made in an dictionary, the key is either a one side with a collection value to hold the many sides, or a many side with a value of the one side.
A one to many relation for a specific combination of types looks as follows:
public class OneToManyRelation<TOnePart, TManyPart> : IRelation<IRelationPart, IRelationPart>
where TOnePart : class, IRelationPart
where TManyPart : class, IRelationPart
{
private readonly IDictionary<TOnePart, Iesi.Collections.Generic.ISet<TManyPart>> _oneToMany;
private readonly IDictionary<TManyPart, TOnePart> _manyToOne;
public OneToManyRelation()
{
_manyToOne = new ConcurrentDictionary<TManyPart, TOnePart>();
_oneToMany = new ConcurrentDictionary<TOnePart, Iesi.Collections.Generic.ISet<TManyPart>>();
}
public void Set(TOnePart onePart, TManyPart manyPart)
{
if (onePart == null || manyPart == null) return;
if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
else _manyToOne[manyPart] = onePart;
}
public void Add(TOnePart onePart, TManyPart manyPart)
{
if (onePart == null || manyPart == null) return;
if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
else _manyToOne[manyPart] = onePart;
if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, new HashedSet<TManyPart>());
_oneToMany[onePart].Add(manyPart);
}
public Iesi.Collections.Generic.ISet<TManyPart> GetManyPart(TOnePart onePart)
{
if (!_oneToMany.ContainsKey(onePart)) _oneToMany[onePart] = new HashedSet<TManyPart>();
return _oneToMany[onePart];
}
public TOnePart GetOnePart(TManyPart manyPart)
{
if(!_manyToOne.ContainsKey(manyPart)) _manyToOne[manyPart] = default(TOnePart);
return _manyToOne[manyPart];
}
public void Remove(TOnePart onePart, TManyPart manyPart)
{
_manyToOne.Remove(manyPart);
_oneToMany[onePart].Remove(manyPart);
}
public void Set(TOnePart onePart, Iesi.Collections.Generic.ISet<TManyPart> manyPart)
{
if (onePart == null) return;
if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, manyPart);
else _oneToMany[onePart] = manyPart;
}
public void Clear(TOnePart onePart)
{
var list = new HashedSet<TManyPart>(_oneToMany[onePart]);
foreach (var manyPart in list)
{
_manyToOne.Remove(manyPart);
}
_oneToMany.Remove(onePart);
}
public void Clear(TManyPart manyPart)
{
if (!_manyToOne.ContainsKey(manyPart)) return;
if (_manyToOne[manyPart] == null) return;
_oneToMany[_manyToOne[manyPart]].Remove(manyPart);
_manyToOne.Remove(manyPart);
}
}
On the many side a code snippet looks like:
public virtual SubstanceGroup SubstanceGroup
{
get { return RelationProvider.SubstanceGroupSubstance.GetOnePart(this); }
protected set { RelationProvider.SubstanceGroupSubstance.Set(value, this); }
}
On the one side, so, in this case the SubstanceGroup, the snippet looks like:
public virtual ISet<Substance> Substances
{
get { return RelationProvider.SubstanceGroupSubstance.GetManyPart(this); }
protected set { RelationProvider.SubstanceGroupSubstance.Set(this, value); }
}
Just using my domain objects, this works excellent. In the domain object I just have to reference an abstract factory that retrieves the appropriate relation and I can set the relation from one side, wich automatically becomes thus bidirectional.
However, when NH kicks in the problem is that I get duplicate keys in my dictionaries. Somehow NH sets a relation property with a null value(!) with a new copy(?) of a domain object. So when the domain object gets saved, I have two entries of that domain object in, for example the many side of the relation, i.e. _manyToOne dictionary.
This problem makes me lose my hair, I do not get it what is happening??
To answer your first, very general question: "Does NHibernate really deliver transparent persistency", I just can say: nothing is perfect. NH tries its best to be as transparent as possible, by also trying to keep its complexity as low as possible.
There are some assumptions, particularly regarding collections: Collections and their implementations are not considered to be part of your domain model. NH provides its own collection implementations. You are not only expected to use the interfaces like ISet and IList. You should also take the instances given by NH when the object is read from the database and never replace it with your own. (I don't know what your relation class is actually used for, so I don't know if this is the problem here.)
Domain objects are unique within the same instance of the session. If you get new instances of domain objects each time, you probably implemented the "session-per-call" anti-pattern, which creates a new session for each database interaction.
I don't have a clue what you actually are doing. How is this OneToManyRelation actually used for? What are you doing when NH doesn't behave as expected? This is a very specific problem to your specific implementation.
Besides the comments on 'convoluted code' and 'what the heck are you doing'. The problem was that I was replacing the persistence collections of NH like in the below code snippet:
public void Add(TOnePart onePart, TManyPart manyPart)
{
if (onePart == null || manyPart == null) return;
if (!_manyToOne.ContainsKey(manyPart)) _manyToOne.Add(manyPart, onePart);
else _manyToOne[manyPart] = onePart;
if (!_oneToMany.ContainsKey(onePart)) _oneToMany.Add(onePart, new HashedSet<TManyPart>());
_oneToMany[onePart].Add(manyPart);
}
I create a new Hashed set for the many part. And that was the problem. If just has set the many part with the collection coming in (in case of the persistence collection implementation of NH) than it would have worked.
As a NH newbie, this replacing of collections with a special implementation from NH has been an important source of errors. Just as a warning to other NH newbies.