I am building a Symfony data transformer class called EventDataMapper. It handles two fields: A TextType field called My mapDataToForms() definition looks like this:
public function mapDataToForms($data, $forms)
{
$existingTitle = $data->getTitle();
$existingAttendees = $data->getAttendees();
$this->propertyPathMapper->mapDataToForms($data, $forms);
foreach ($forms as $index => $form) {
if ($form->getName() === 'title' && !is_null($existingTitle)) {
$form->setData($existingTitle);
}
if ($form->getName() === 'attendees' && !is_null($existingAttendees)) {
$form->setData($existingAttendees);
}
}
}
The problem is that I'm setting data before validation runs, so if I submit a form with a non-numeric string in the "attendees" field, I get an ugly TransformationFailedException ('Unable to transform value for property path "attendees": Expected a numeric'). And if I try to do a check for whether my field is valid by adding a call to $form->isValid() in the line before I call $form->setData(), I get a LogicException. ('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().')
Is there any way for my to preemptively call a validator on this specific field from within my DataMapper?
(Yes, this can be somewhat prevented with frontend logic. But I don't want to rely too much on that.)
Closing the loop on this. Here's what we did.
A colleague made a new form type corresponding to a new adapter class that wraps our two previous classes, providing a uniform set of wrapper methods for interacting with them.
We passed Symfony's validator service into our new form type using the constructor.
In that form type, we're using $builder->addEventListener() to add a callback/listener on the POST_SUBMIT event. Here's the callback:
function(FormEvent $event): void {
$adapter = $event->getData();
$form = $event->getForm();
$errors = $adapter->propagate($this->validator);
foreach ($errors as $error) {
$formError = new FormError($error->getMessage());
$targetPath = self::mapPropertyPath($error->getPropertyPath());
$target = $targetPath !== null ? $form->get($targetPath) : $form;
$target->addError($formError);
}
}
The adapter, in turn, has some logic that does various translations of data into a form that can be used in our legacy classes, followed by this:
return $validator->validate($this->legacyObject);
This works well for us. I hope it helps somebody else out too.
Related
Have run into a curious behavior a few times over the years, have always meant to ask about it.
It has to do with a behavior I don't understand around binding objects to forms in Zend Framework.
Consider this factory which builds a form, loads a Doctrine entity from database, and attempts to bind it to the form (so that the values display on render):
class TermsConfigFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
$termsConfig = $container->get(TermsConfigMapper::class)->get($options['locale']);
if (!$termsConfig) {
$termsConfig = new TermsConfig($options['locale']);
}
// A. If we do just this, the form doesn't print data
$form->bind($termsConfig);
return $form;
}
}
The form is well wired through form_elements, and so forth. We then attempt to use it inside a Controller like so:
$termsForm = $this->formElementManager->get(TermsConfigForm::class, ['locale' => $this->locale()]);
$viewModel->setVariable('termsForm', $termsForm);
Interestingly, we find that the object will not show the bound data in the ViewModel. Now, even more curious, is if we remove the call to "bind" in the factory and do this in the controller instead, the values are properly displayed!!
$termsForm = $this->formElementManager->get(TermsConfigForm::class, ['locale' => $this->locale()]);
// B. You have to do this in the controller, here, for it to print data!
$termsForm->bind($termsForm->getObject());
$viewModel->setVariable('termsForm', $termsForm);
Why does it not work within the factory?
From this vantage point, the call to bind in the controller, is analogous to the call to bind in the Factory. I'd like to keep this stuff in the factory, but seems I cannot!
Looks like the intricacy lies with the FormElementManager that stacks the factory and the init cycle in order. In other words, init is not called until the factory has completed its work.
Paraphrasing, calling "bind" within a form's factory does not exhibit the same behavior as calling "bind" after the factory returns the form because the Factory calls init (automatically) in between.
Be careful of this 'order of operations' trap.
If you look at the Factory pattern you'll notice that the job of the factory pattern is to return the desired object only. It has nothing to do with the manipulation of the object. So, with that said all the things you want to do with the desired object should be done in the model object. As the model object can be as fat as it can be. Therefore, I would suggest moving your binding of the desired object out from the factory and move it to the model.
I'll quote something which I saw in a video and Marco Pivetta showed something in a very abstract way and that was something like below:
// The below code is my understanding of Marco Pivetta explaining in a youtube video.
use Psr\Container\ContainerInterface;
class TermsConfigFormFactory implements FactoryInterface
{
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
/*
This should have been the first line in the method, as it looks like. Because it seems you want locale to be given.
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
//From here till binding should be moved to a model.
$termsConfig = $container->get(TermsConfigMapper::class)->get($options['locale']);
if (!$termsConfig) {
$termsConfig = new TermsConfig($options['locale']);
}
// A. If we do just this, the form doesn't print data
$form->bind($termsConfig);
*/
return $form;
}
This is what Marco Pivetta would have done in my opinion according to his video and what I've abstracted.
class TermsConfigFormFactory implements FactoryInterface
{
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
return $form;
}
// Some model
namespace Mynacespace\Model;
class SomeModel extends /* I don't remember the actual FQCN*/DoctirneRepository
{
public function dosomething(Form $form ){
$request = $this->getServiceContainer()->getRequest();
$data = $request->getPost();
$form->bind($data);
// do more of your work here.
// This array returning was not mentioned in Marco video.
return ['status' => 200, 'message' => 'Success', 'data' => 'form' => $form];
}
}
What I'm using:
I'm using the JMSSerializerBundle to deserialize my JSON object from a POST request.
Description of the problem:
One of the vaules in my JSON is an Id. I'd like to replace this Id with the correct object before the deserialization occurs.
Unfortunately, JMSSerializerBundle does not have a #preDeserializer annotation.
The problem I'm facing (and that I would have faced if there was an #preDeserializer annotation anyway) is that I would like to create a generic function for all my entities.
Question:
How do I replace my Id with the corresponding object in the most generic way possible ?
You also do your own hydratation as I did (with Doctrine):
Solution
The IHydratingEntity is an interface which all my entities implement.
The hydrate function is used in my BaseService generically. Parameters are the entity and the json object.
At each iteration, the function will test if the method exists then it will call the reflection function to check if the parameter's method (setter) also implements IHydratingEntity.
If it's the case, I use the id to get the entity from the database with Doctrine ORM.
I think it's possible to optimize this process, so please be sure to share your thoughts !
protected function hydrate(IHydratingEntity $entity, array $infos)
{
#->Verification
if (!$entity) exit;
#->Processing
foreach ($infos as $clef => $donnee)
{
$methode = 'set'.ucfirst($clef);
if (method_exists($entity, $methode))
{
$donnee = $this->reflection($entity, $methode, $donnee);
$entity->$methode($donnee);
}
}
}
public function reflection(IHydratingEntity $entity, $method, $donnee)
{
#->Variable declaration
$reflectionClass = new \ReflectionClass($entity);
#->Verification
$idData = intval($donnee);
#->Processing
foreach($reflectionClass->getMethod($method)->getParameters() as $param)
{
if ($param->getClass() != null)
{
if ($param->getClass()->implementsInterface(IEntity::class))
#->Return
return $this->getDoctrine()->getRepository($param->getClass()->name)->find($idData);
}
}
#->Return
return $donnee;
}
I am using codeigniter 2.xx, I found out that on passing Arrays as Field Names in the html form it throws php and database errors. This is a serious vulnerability and what if an user does the same using chrome html debugging tool? It can cause security breach in the website! So, I was looking for a method to turn off the acceptance of Arrays as Field Names in codeigniter by default, but couldn't find it anywhere!
Is there any workaround with the core functionality so that form_validation, set_value() function and database drivers won't accept an array from the field name unless it is programmed for it?
Okay guys I didn't get any response for my question, So I came up with a work around. I made this small function which may get rid of input array injection.
1) Create a library file or a model. import it into the controller you wish to conduct this validation.
2) paste the following code into the model or library you just made:
/*
* Description: Checks for the array attacks in the input forms.
* #param $allowedArrayInputs, the input field name which may contain array values
* #param $method, post, get or request.
* #return boolean, in case no conflicts is detected else invoke error.
* */
public function array_inputs ($allowedArrayInputs = array (), $method = 'post') {
if ($method == 'request') {
$method_param = $_REQUEST;
}
elseif ($method == 'get') {
$method_param = $_GET;
}
else {
$method_param = $_POST;
}
foreach ($method_param as $key => $val2) {
if (is_array($val2)) {
if ($allowedArrayInputs && is_array($allowedArrayInputs)) {
if (in_array($key, array ( 'confirm_password' ))) {
continue;
}
}
show_error('User input was invalid. Try again!');
exit;
}
}
return true;
}
Hope this helps someone who is facing this similar problem!
Good day to all!
I have literally tried everything to try and extend the Symfony2 Form class.
I want to add a new method to Form and call it in a controller:
$form = $this->createForm($this->get('my_form_service'), $entity);
$form->myNewMethod();
Because the Form class isn't defined as a service and is instantiated in another class (FormBuilder, line 221) then I can't see what I can do. I don't want to hack the core.
I could extend the controller class, e.g. the createForm() method returns the instantiated Form object:
// Extend Controller and do something with Form before returning
public function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
In fact How to add a new method to a php object on the fly? shows how I could do the above and add my new method that way, but my understanding is that you need to at least add __call to the Form class - which I can't do - or the added method won't be able to use $this (which I need).
I only need to add this for when the form is used in a controller. I don't need it for any CLI processing.
Any ideas how I can add a method to the Form class without hacking the core?
====================================================================
EDIT: Why am I doing this?
I want to 'silently fail' a form submit if a certain criteria is met. I believe the best way to do this would be to do this in a controller:
if ($form->isValid()) {
if ($form->requiresSilentFail()) {
// Silently fail
} else {
// As normal, add to DB etc.
}
}
I suppose I could just do something like this:
if ($form->isValid()) {
if ($this->get('check_my_form')->requiresSilentFail($form)) {
// Silently fail
} else {
// As normal, add to DB etc.
}
}
.... but I need to also perform a little bit of extra logic in Form::handleRequest() first, so I believe extending Form is the only option.
I don't really understand why you need to extend the form class simply on your controller/service where you process the form call your method under is valid check, see below:
$form->handleRequest($request);
if ($form->isValid()) {
$this->get($apiResource->getResourceHandlerName())->update($resourceData);
$response = new Response();
$response->setStatusCode($statusCode);
if ($this->isSuccessFullStatusCode($statusCode)) {
$serializedObject = $this->getSerializer()->serialize($resourceData, 'json',$serializationContext);
$response->setStatusCode($responseStatusCode);
$response->setContent($serializedObject);
return $response;
}
}
I'm using PHP 5.3's class_alias to help process my Symfony 1.4 (Doctrine) forms. I use a single action to process multiple form pages but using a switch statement to choose a Form Class to use.
public function executeEdit(sfWebRequest $request) {
switch($request->getParameter('page')) {
case 'page-1':
class_alias('MyFormPage1Form', 'FormAlias');
break;
...
}
$this->form = new FormAlias($obj);
}
This works brilliantly when browsing the website, but fails my functional tests, because when a page is loaded more than once, like so:
$browser->info('1 - Edit Form Page 1')->
get('/myforms/edit')->
with('response')->begin()->
isStatusCode(200)->
end()->
get('/myforms/edit')->
with('response')->begin()->
isStatusCode(200)->
end();
I get a 500 response to the second request, with the following error:
last request threw an uncaught exception RuntimeException: PHP sent a warning error at /.../apps/frontend/modules/.../actions/actions.class.php line 225 (Cannot redeclare class FormAlias)
This makes it very hard to test form submissions (which typically post back to themselves).
Presumably this is because Symfony's tester hasn't cleared the throughput in the same way.
Is there a way to 'unalias' or otherwise allow this sort of redeclaration?
As an alternate solution you can assign the name of the class to instantiate to a variable and new that:
public function executeEdit(sfWebRequest $request) {
$formType;
switch($request->getParameter('page')) {
case 'page-1':
$formType = 'MyFormPage1Form';
break;
...
}
$this->form = new $formType();
}
This doesn't use class_alias but keeps the instantiation in a single spot.
I do not know for sure if it is possible, but judging from the Manual, I'd say no. Once the class is aliased, there is no way to reset it or redeclare it with a different name. But then again, why do use the alias at all?
From your code I assume you are doing the aliasing in each additional case block. But if so, you can just as well simply instantiate the form in those blocks, e.g.
public function executeEdit(sfWebRequest $request) {
switch($request->getParameter('page')) {
case 'page-1':
$form = new MyFormPage1Form($obj);
break;
...
}
$this->form = $form;
}
You are hardcoding the class names into the switch/case block anyway when using class_alias. There is no advantage in using it. If you wanted to do it dynamically, you could create an array mapping from 'page' to 'className' and then simply lookup the appropriate class.
public function executeEdit(sfWebRequest $request) {
$mapping = array(
'page-1' => 'MyFormPage1Form',
// more mappings
);
$form = NULL;
$id = $request->getParameter('page');
if(array_key_exists($id, $mapping)) {
$className = $mapping[$id];
$form = new $className($obj);
}
$this->form = $form;
}
This way, you could also put the entire mapping in a config file. Or you could create FormFactory.
public function executeEdit(sfWebRequest $request) {
$this->form = FormFactory::create($request->getParameter('page'), $obj);
}
If you are using the Symfony Components DI Container, you could also get rid of the hard coded factory dependency and just use the service container to get the form. That would be the cleanest approach IMO. Basically, using class_alias just feels inappropriate here to me.
function class_alias_once($class, $alias) {
if (!class_exists($alias)) {
class_alias($class, $alias);
}
}
This doesn't solve the problem itself, but by using this function it is ensured that you don't get the error. Maybe this will suffice for your purpose.