In my update form, I want to add a data attribute on the inputs that will contains the initial value of the entity. This way, I will be able to highlight the input when the user will modify it.
In the end, only the input modified by the users will be highlighted.
I want to use this only in update, not in creation.
To do so, I created a form extension like this:
class IFormTypeExtension extends AbstractTypeExtension
{
...
public static function getExtendedTypes()
{
//I want to be able to extend any form type
return [FormType::class];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_iform' => false,
'is_iform_modification' => function (Options $options) {
return $options['is_iform'] ? null : false;
},
]);
$resolver->setAllowedTypes('is_iform', 'bool');
$resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!$options['is_iform'] && !$this->isParentIForm($form)) {
return;
}
//We need to add the original value in the input as data-attributes
if (is_string($form->getViewData()) || is_int($form->getViewData())) {
$originValue = $form->getViewData();
} elseif (is_array($form->getViewData())) {
if (is_object($form->getNormData())) {
$originValue = implode('###', array_keys($form->getViewData()));
} elseif (is_array($form->getNormData()) && count($form->getNormData()) > 0 && is_object($form->getNormData()[0])) {
$originValue = implode('###', array_keys($form->getViewData()));
} else {
$originValue = implode('###', $form->getViewData());
}
} else {
//There's no value yet
$originValue = '';
}
$view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}
private function isParentIForm(FormInterface $form)
{
if (null === $form->getParent()) {
return $form->getConfig()->getOption('is_iform');
}
return $this->isParentIForm($form->getParent());
}
}
As you can see in the buildView method, I get the originValue from the ViewData.
In a lot of cases, this works well.
But if I have any validation error in my form OR if I reload my form through AJAX, the ViewData contains the new information and not the values of the entity I want to update.
How can I get the values of the original entity?
I don't want to make a DB request in here.
I think I can use the FormEvents::POST_SET_DATA event, then save the entity values in session and use these in the buildView.
I could also give a new Option in my OptionResolver to ask for the initial entity.
Is it possible to have the original data of the entity directly form the buildView? (If I'm not wrong, this means the form before we call the handleRequest method).
Someone wanted to have an example with a controller. I don't think it's really interresting, because with the FormExtension, the code will be added automatically. But anyway, here is how I create a form in my controller :
$form = $this->createForm(CustomerType::class, $customer)->handleRequest($request);
And in the CustomerType, I will add the 'is_iform' key with configureOptions() :
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
"translation_domain" => "customer",
"data_class" => Customer::class,
'is_iform' => true //This line will activate the extension
]);
}
It's probably an opinionated answer. There also might be better approaches.
I'm not a big fan of your form extension, since it's really convoluted and unclear what's happening, at least to my eyes.
What I'm proposing: When the form submit happened, in your controller you should do the following
// ((*)) maybe store customer, see below
$form = $this->createForm(CustomerType::class, $customer);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
// easy case, you got this.
$em->flush();
return $this->redirect(); // or another response
} elseif($form->isSubmitted()) {
// form was submitted with errors, have to refresh entity!
// REFRESH - see discussion below for alternatives
$em->refresh($customer);
// then create form again with original values:
$form = $this->createForm(CustomerType::class, $customer);
}
// other stuff
return $this->render(..., ['form' => $form->createView(), ...]);
so, essentially, when the form validation fails, you refresh the entity and recreate the form, avoiding the problem with the changed state of your entity. I believe this approach ultimately is easier then hacking the form to magically not update values or re-set older values.
Now the question remains: how to refresh an entity? Simplest approach: reload from database:
$em->refresh($customer); // easiest approach, will likely run another query.
Alternatives:
Instead of giving $customer to the form, you create a customer DTO that contains the same values but on change doesn't automatically change the original object. If the form validation fails, you can just re-generate the DTO.
Instead of refresh($customer), which will most likely run another query (except maybe not, if you have a cache), you could cache the customer yourself via a DefaultCacheEntityHydrator, you would have to create your own EntityCacheKey object (not really hard), generate a cache entry (DefaultCacheEntityHydrator::buildCacheEntry() at the ((*)) above) and restore the entry for when you need to restore it. Disclaimer: I don't know if/how this works with collections (i.e. collection properties, the entity might have).
That being said ... if you really really want a form extension for whatever reason, you might want to form event with a PRE_SET_DATA handler that stores the data in the form type object, then on buildView uses those values. I wouldn't store something in the session for I don't see the necessity ... your aversion to db queries is baffling though, if that's your main reason for all the shenanigans
In the end, I managed to make it work BUT I'm not fully convinced by what I did.
It was not possible to get the original data from the form OR add a new property (the form is read only in the form extension).
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
if ('_token' === $form->getName()) {
return;
}
$data = $event->getData();
$this->session->set('iform_'.$form->getName(), is_object($data) ? clone $data : $data);
}
);
}
What I do here, is simply register the form values by its name in the session.
If it's an object, I need to clone it, because the form will modify it later in the process and I want to work with the original state of the form.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_iform' => false,
'is_iform_modification' => function (Options $options) {
return $options['is_iform'] ? null : false;
},
]);
$resolver->setAllowedTypes('is_iform', 'bool');
$resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}
The configure options did not change.
And then, depending on the value type, I create my "data-orig-value" :
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!$options['is_iform'] && !$this->isParentIForm($form)) {
return;
}
$propertyValue = $this->session->get('iform_'.$form->getName());
$originValue = '';
try {
if (null !== $propertyValue) {
//We need to add the original value in the input as data-attributes
if (is_bool($propertyValue)) {
$originValue = $propertyValue ? 1 : 0;
} elseif (is_string($propertyValue) || is_int($propertyValue)) {
$originValue = $propertyValue;
} elseif (is_array($propertyValue) || $propertyValue instanceof Collection) {
if (is_object($propertyValue)) {
$originValue = implode('###', array_map(function ($object) {
return $object->getId();
}, $propertyValue->toArray()));
} elseif (is_array($propertyValue) && count($propertyValue) > 0 && is_object(array_values($propertyValue)[0])) {
$originValue = implode('###', array_map(function ($object) {
return $object->getId();
}, $propertyValue));
} else {
$originValue = implode('###', $propertyValue);
}
} elseif ($propertyValue instanceof DateTimeInterface) {
$originValue = \IntlDateFormatter::formatObject($propertyValue, $form->getConfig()->getOption('format', 'dd/mm/yyyy'));
} elseif (is_object($propertyValue)) {
$originValue = $propertyValue->getId();
} else {
$originValue = $propertyValue;
}
}
} catch (NoSuchPropertyException $e) {
if (null !== $propertyValue = $this->session->get('iform_'.$form->getName())) {
$originValue = $propertyValue;
$this->session->remove('iform_'.$form->getName());
} else {
$originValue = '';
}
} finally {
//We remove the value from the session, to not overload the memory
$this->session->remove('iform_'.$form->getName());
}
$view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}
private function isParentIForm(FormInterface $form)
{
if (null === $form->getParent()) {
return $form->getConfig()->getOption('is_iform');
}
return $this->isParentIForm($form->getParent());
}
Maybe the code sample will help anyone !
If anyone have a better option, don't hesitate to post it !
I am studying Ceylon and have question about it metamodel. I want to create some create some base class 'DataContainer' which allow to instantiate immutable classes with build-in equals-hash implementation:
e.g. Identifier(125, "ab") == Identifier(125, "ab")
So base class should collect all shared non-variable values and use this information in 'hash' an 'equals' methods.
I have wrote this code:
shared abstract class DataContainer(ClassDeclaration declaration) {
value members = {
for (i in declaration.memberDeclarations<ValueDeclaration>())
if (!i.variable, i.name != "hash", i.name != "string") i
};
variable Integer? hashCode = null;
shared actual Boolean equals(Object that) {
if (is DataContainer that) {
for (item in members) {
value thisMember = item.memberGet(this);
value thatMember = item.memberGet(that);
if (exists thisMember, exists thatMember) {
if (thisMember != thatMember) { return false; }
} else if (thisMember exists != thatMember exists) { return false; }
}
return true;
}
return false;
}
shared actual Integer hash => hashCode else (hashCode = calculateHash());
Integer calculateHash() {
variable value result = 0;
for(i in members) {
if (exists member = i.memberGet(this)) {
result = result.xor(member.hash);
}
}
return result;
}
}
class Identifier(shared Integer? id, shared String? name) extends DataContainer(`class`) {}
The Identifier class is the client of DataContainer. I like this solution in whole but I have to pass 'class' into the super class constructor because if I use 'class' inside DataContainer it doesn't see any members of subclass.
How can I obtain actual list of extended class's members in base class methods?
Something like 'this' doesn't work...
I found solution thanks to guys from Ceylon community. The function classDeclaration(this) from ceylon.language.meta should be used instead of 'class'.
This is the final code:
shared abstract class DataContainer() {
variable Integer? _hash = null;
variable ValueDeclaration[]? _members = null;
shared actual Boolean equals(Object that) {
if (is DataContainer that) {
for (i in members) {
value thisMember = i.memberGet(this);
value thatMember = i.memberGet(that);
if (exists thisMember, exists thatMember) {
if (thisMember != thatMember) { return false; }
} else if (thisMember exists != thatMember exists) { return false; }
}
return true;
}
return false;
}
shared actual Integer hash => _hash else (_hash = calculateHash());
ValueDeclaration[] members => _members else (_members = [
for (i in classDeclaration(this).memberDeclarations<ValueDeclaration>())
if (!i.variable, i.name != "string", i.name != "hash") i
]);
Integer calculateHash() {
variable Integer result = 0;
for (i in members) {
if (exists member = i.memberGet(this)) {
result = result.xor(member.hash);
}
}
return result;
}
}
class Identifier(shared Integer? number, shared String? name) extends DataContainer() {}
Adding to Alexander's formidable answer, I took the liberty to implement a string function as well so you can just print(myObj) and it renders nicely:
shared default actual String string {
value s = StringBuilder();
s.append(type(this).declaration.name).append(" { ");
for (i in members) {
value m = i.memberGet(this);
s.append(i.name).append(":");
s.append(if(exists m) then m.string else "<null>");
}
return s.append(" }").string;
}
I have the following QueryBuilder snippet working as expected:
$restaurants = $qb->select('r.id, r.name, r.addressGpsLat, r.addressGpsLng, r.addressZip, r.addressCity')
->from('BackendBundle:Restaurant', 'r')
->orderBy('r.name', 'ASC');
In our Restaurant entity we calculate ratings based on a relation, which looks like this:
public function getRating()
{
$rating = 0;
$votes = count($this->votes);
foreach ($this->votes as $vote) {
$rating += $vote->getRating();
}
if ($votes == 0) {
return null;
}
return number_format(($rating / $votes), 2, ',', '.');
}
So my question: is there a way I get the result of the getRating() property into my QueryBuilder object? We need it to be a seperate attribute in our JSON response. E.g. { …, "rating":2.2, …}
Any advice on this? Thanks!
I have made an extension on the UserDefinedForm (module userforms). This works well, but I cannot figure out how to set validation on this extra fields. This is (a part of) my code:
class UserDefinedPaymentForm_Controller extends UserDefinedForm_Controller {
private static $allowed_actions = array(
"finished",
"complete",
"error"
);
public function getFormFields() {
//Payment fields
$supported_methods = PaymentProcessor::get_supported_methods();
$gateways = array();
foreach ($supported_methods as $methodName) {
$methodConfig = PaymentFactory::get_factory_config($methodName);
$gateways[$methodName] = $methodConfig['title'];
}
$fields = parent::getFormFields();
$fields->add(new NumericField("PaymentAmount", _t('UserDefinedPaymentForm.PAYMENT_AMOUNT', 'Payment Amount')));
$fields->add(new Literalfield("literalfield", _t('UserDefinedPaymentForm.PAY', '<h2>Pay</h2>')));
$fields->add(new Literalfield("literalfield", _t('UserDefinedPaymentForm.PAY_INSTRUCTIONS', '<p>Choose your prefered payment method and click Pay:</p>')));
$fields->add(new DropdownField("PaymentMethod", _t('UserDefinedPaymentForm.PAYMENT_METHOD', 'Payment Method'), $gateways));
return $fields;
}
}
Now I want to validate the field PaymentAmount, the value of this field has to be 2 or more. How can I do this?
I would guess (I haven't tested this) your best bet is to create a subclass of UserFormValidator and override the php($data) method.
Then, in your UserDefinedPaymentForm_Controller, you will also need to override the Form method.
class PaymentAmountUserFormValidator extends UserFormValidator {
public function php($data) {
$result = parent::php($data);
if ($result === true) {
// verify your PaymentAmount here and return true or false, accordingly
}
return $result;
}
class UserDefinedPaymentForm_Controller {
...
public function Form()
{
$form = UserForm::create($this);
// Generate required field validator
$requiredNames = $this
->getController()
->Fields()
->filter('Required', true)
->column('Name');
$validator = new PaymentAmountUserFormValidator($requiredNames);
$form->setValidator($validator);
$this->generateConditionalJavascript();
return $form;
}
...
}
I'm write this function:
public static String QueryString(string queryStringKey)
{
if (HttpContext.Current.Request.QueryString[queryStringKey] != null)
{
if (HttpContext.Current.Request.QueryString[queryStringKey].ToString() != string.Empty)
return HttpContext.Current.Request.QueryString.GetValues(1).ToString();
}
return "NAQ";
}
And i want to get just one value from querystring parameter.
for example i send "page" to my function and url is: "sth.com/?page=1&page=2"
and function return to me: "1,2" ; but i want first value: "1", How?
GetValues returns a string[] if the key exists. An array is zero based, so you get the first element by using array[0], you are using GetValues(1) in your code, i assume that you wanted the first.
You could also use the Enumerable.First extension method:
Request.QueryString.GetValues("page").First();
Since GetValues returns not an empty array but null if the key was not present you need to check that explicitely (FirstOrDefault doesn't work):
public static String QueryString(string queryStringKey)
{
if (HttpContext.Current != null && HttpContext.Current.Request != null)
{
string[] values = HttpContext.Current.Request.QueryString.GetValues("queryStringKey");
if (values != null) return values.First();
}
return "NAQ";
}
A better approach would be -
public static String QueryString(string queryStringKey)
{
if (HttpContext.Current!=null && HttpContext.Current.Request!=null && !string.IsNullOrEmpty(HttpContext.Current.Request.QueryString[queryStringKey])
{
return HttpContext.Current.Request.QueryString.GetValues(queryStringKey).First();
}
return "NAQ";
}