Ask your Symfony questions! Pay money and get answers fast! (more info)

EAV Entity Attribute Value sfForm Symfony

  • SOLVED

Hi,

I have a need to have some forms user-editable in an application. There are different types of customers, and each customer gets assigned an attribute set, composed of multiple attributes. The closest example I could provide is the way magento handles product attributes. I'm using symfony 1.4 and doctrine.

For my purposes, I'd need to have various types of inputs (radio, text, dropdown select), and for select fields I'd need to have values in the database.

So, here's a basic schema I envisioned:

AttributeSet:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(255), notnull: true }
relations:
CustomerAttribute: { refClass: AttributeSetCustomerAttribute, local: attribute_set_id, foreign: customer_attribute_id }

AttributeSetCustomerAttribute:
options:
symfony:
form: false
filter: false
columns:
attribute_set_id: { type: integer(4), primary: true }
customer_attribute_id: { type: integer(4), primary: true }

Customer:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(255), notnull: true }
relations:
AttributeSet: { local: attribute_set_id, foreign: id }

CustomerAttribute:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(100), notnull: true }
label: { type: string(100) }
input_type: { type: enum, values: [Dropdown, TextField, TextArea, Date, Price, MultiSelect] }
visible: { type: boolean }
required: { type: boolean }
searchable: { type: boolean }
comparable: { type: boolean }
layered_use: { type: boolean }
relations:
AttributeSet: { refClass: AttributeSetProductAttribute, local: customer_attribute_id, foreign: attribute_set_id }

CustomerAttributeOption:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
attribute_id: { type: integer(4) }
value: { type: string(100) }
relations:
CustomerAttribute: { local: id, foreign: id }

CustomerAttributeValue:
actAs: [ Softdelete ]
columns:
customer_id: { type: integer(4), primary: true }
attribute_id: { type: integer(4), primary: true }
value: { type: string(150) }


How would I make this happen using the sfForms in symfony? I would like a specific example of making a form that creates a customer form from an attribute set, and builds those attributes with at least one dropdown select built from options in the database.

Answers (2)

2010-05-13

Jakub Zalas answers:

I'm not sure if your schema is perfect. However, I tried to make it working and needed to change it a bit.

I wrote one action which uses the eav form. It looks up customer and passes it to the form. Attribute values are loaded. Once you save values are saved in the database.

This is a proof of concept and I strongly advice you to refactor it before putting it on production. Some methods need to be moved to model, other need to be merged. Generally cleanups are needed :)

You can download the source of project I used: [[LINK href="http://www.zalas.eu/uploads/eav.tgz"]]eav.tgz[[/LINK]]


AttributeSet:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(255), notnull: true }
relations:
CustomerAttribute: { refClass: AttributeSetCustomerAttribute, local: attribute_set_id, foreign: customer_attribute_id }

AttributeSetCustomerAttribute:
options:
symfony:
form: false
filter: false
columns:
attribute_set_id: { type: integer(4), primary: true }
customer_attribute_id: { type: integer(4), primary: true }

Customer:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(255), notnull: true }
attribute_set_id: { type: integer(4), primary: false }
relations:
AttributeSet: { local: attribute_set_id, foreign: id }

CustomerAttribute:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
name: { type: string(100), notnull: true }
label: { type: string(100) }
input_type: { type: enum, values: [Dropdown, TextField, TextArea, Date, Price, MultiSelect] }
visible: { type: boolean }
required: { type: boolean }
searchable: { type: boolean }
comparable: { type: boolean }
layered_use: { type: boolean }
relations:
AttributeSet: { refClass: AttributeSetCustomerAttribute, local: customer_attribute_id, foreign: attribute_set_id }

CustomerAttributeOption:
columns:
id: { type: integer(4), primary: true, autoincrement: true }
attribute_id: { type: integer(4) }
value: { type: string(100) }
relations:
CustomerAttribute: { local: attribute_id, foreign: id }

CustomerAttributeValue:
actAs: [ SoftDelete ]
columns:
customer_id: { type: integer(4), primary: true }
attribute_id: { type: integer(4), primary: true }
value: { type: string(150) }
relations:
Customer: { local: customer_id, foreign: id, type: one }
CustomerAttribute: { local: attribute_id, foreign: id, type: one }


Here is action:


class eavActions extends sfActions
{
public function executeIndex(sfWebRequest $request)
{
$customer = Doctrine_Core::getTable('Customer')->findOneByName('Zend');
$this->forward404Unless($customer);

$this->form = new CustomerEavForm(array(), array('customer' => $customer));

if ($request->isMethod('post'))
{
$this->form->bind($request->getParameter($this->form->getName()));

if ($this->form->isValid())
{
$this->form->save();
}
}
}
}


The form:


class CustomerEavForm extends sfForm
{
public function __construct($defaults = array(), $options = array(), $CSRFSecret = null)
{
parent::__construct($defaults, $options, $CSRFSecret);

if (!isset($options['customer']))
{
throw new Exception('customer option is required');
}

$this->createWidgetsAndValidators();

$this->loadDefaults();

$this->getWidgetSchema()->setNameFormat('customer[%s]');
}

private function loadDefaults()
{
$customer = $this->getOption('customer');
$customerAttributes = $customer->getAttributeSet()->getCustomerAttribute();

foreach ($customerAttributes as $customerAttribute)
{
$customerAttributeValues = $customerAttribute->getCustomerAttributeValue();
foreach ($customerAttributeValues as $customerAttributeValue)
{
if ($customerAttributeValue->getCustomerId() == $customer->getId())
{
$this->setDefault($customerAttribute->getName(), $customerAttributeValue->getValue());
}
}
}
}

private function createWidgetsAndValidators()
{
$customer = $this->getOption('customer');
$customerAttributes = $customer->getAttributeSet()->getCustomerAttribute();

foreach ($customerAttributes as $customerAttribute)
{
$this->createWidgetAndValidatorForAttributeSet($customerAttribute);
}
}

private function createWidgetAndValidatorForAttributeSet(CustomerAttribute $customerAttribute)
{
$inputType = $customerAttribute->getInputType();
$method = sprintf('createWidgetAndValidatorFor%sInputType', $inputType);
$this->$method($customerAttribute);
}

private function createWidgetAndValidatorForDropdownInputType(CustomerAttribute $customerAttribute, $multiple = false)
{
$query = Doctrine_Core::getTable('CustomerAttributeOption')
->createQuery('cao')
->addWhere('cao.attribute_id = ?', $customerAttribute->getId());

$this->setWidget(
$customerAttribute->getName(),
new sfWidgetFormDoctrineChoice(array(
'query' => $query,
'model' => 'CustomerAttributeOption',
'method' => 'getValue',
'multiple' => $multiple
))
);
$this->setValidator(
$customerAttribute->getName(),
new sfValidatorDoctrineChoice(array(
'query' => $query,
'model' => 'CustomerAttributeOption',
'multiple' => $multiple,
'required' => $customerAttribute->getRequired()
))
);
}

private function createWidgetAndValidatorForTextFieldInputType(CustomerAttribute $customerAttribute)
{
$this->setWidget($customerAttribute->getName(), new sfWidgetFormInput());
$this->setValidator($customerAttribute->getName(), new sfValidatorString(array('required' => $customerAttribute->getRequired())));
}

private function createWidgetAndValidatorForTextAreaInputType(CustomerAttribute $customerAttribute)
{
$this->setWidget($customerAttribute->getName(), new sfWidgetFormTextarea());
$this->setValidator($customerAttribute->getName(), new sfValidatorString(array('required' => $customerAttribute->getRequired())));
}

private function createWidgetAndValidatorForDateInputType(CustomerAttribute $customerAttribute)
{
$this->setWidget($customerAttribute->getName(), new sfWidgetFormDate());
$this->setValidator($customerAttribute->getName(), new sfValidatorDate(array('required' => $customerAttribute->getRequired())));
}

private function createWidgetAndValidatorForPriceInputType(CustomerAttribute $customerAttribute)
{
$this->setWidget($customerAttribute->getName(), new sfWidgetFormInput());
$this->setValidator($customerAttribute->getName(), new sfValidatorInteger(array('required' => $customerAttribute->getRequired())));
}

private function createWidgetAndValidatorForMultiSelectInputType(CustomerAttribute $customerAttribute)
{
$this->createWidgetAndValidatorForDropdownInputType($customerAttribute, true);
}
public function save()
{
$customer = $this->getOption('customer');
$customerAttributes = $customer->getAttributeSet()->getCustomerAttribute();

foreach ($customerAttributes as $customerAttribute)
{
$customerAttributeValues = $customerAttribute->getCustomerAttributeValue();
foreach ($customerAttributeValues as $customerAttributeValue)
{
if ($customerAttributeValue->getCustomerId() == $customer->getId())
{
$customerAttributeValue->setValue($this->getValue($customerAttribute->getName()));
$customerAttributeValue->save();
}
}
}
}
}


Fixtures:


AttributeSet:
set_1:
name: Attribute set 1
set_2:
name: Attribute set 2

Customer:
customer_zend:
name: Zend
AttributeSet: set_1
customer_ibm:
name: IBM
AttributeSet: set_2

AttributeSetCustomerAttribute:
asca_01:
AttributeSet: set_1
CustomerAttribute: ca_title
asca_02:
AttributeSet: set_1
CustomerAttribute: ca_type
asca_03:
AttributeSet: set_1
CustomerAttribute: ca_description
asca_04:
AttributeSet: set_1
CustomerAttribute: ca_created_at
asca_05:
AttributeSet: set_1
CustomerAttribute: ca_price

CustomerAttribute:
ca_title:
name: title
label: Title
input_type: TextField
required: true
ca_type:
name: type
label: Type
input_type: Dropdown
required: true
ca_description:
name: description
label: Description
input_type: TextArea
required: true
ca_created_at:
name: created_at
label: Created at
input_type: Date
required: true
ca_price:
name: price
label: Price
input_type: Price
required: true

CustomerAttributeOption:
cao_type_01:
CustomerAttribute: ca_type
value: Simple
cao_type_02:
CustomerAttribute: ca_type
value: Extended
cao_type_03:
CustomerAttribute: ca_type
value: Full

CustomerAttributeValue:
cav_01:
Customer: customer_zend
CustomerAttribute: ca_title
value: Title
cav_02:
Customer: customer_zend
CustomerAttribute: ca_type
value: Extended
cav_03:
Customer: customer_zend
CustomerAttribute: ca_description
value: Lorem lipsum
cav_04:
Customer: customer_zend
CustomerAttribute: ca_created_at
value: 2010-05-13
cav_05:
Customer: customer_zend
CustomerAttribute: ca_price
value: 200

2010-05-13

Jarret Minkler answers:

So, basically what you want to use is the widget that has 2 select boxes with an arrow between them to select multiple attributes that a customer can have. I believe if your schema is setup correctly the admin generator may do this for you.

Also you may want to look at embedding the form

$this->embedRelation('AttributeSet');

Which will embed multiple AtributeSet forms on the creation of the Customer, you can then setup the single AttributeSet form or extend it to suite your needs.


webguy comments:

I will not be using this within the admin generator.

The attribute sets and attribute options will be managed in an independent form.

When creating a customer, the user would select an attribute set and then the attributes would be output.