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;
}
}
Related
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.
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];
}
}
We are using the ZF2 forms as a standalone plugin inside another PHP application. This is working fine, except I'm not able to use some custom view helpers. The ServiceManager (?) doesn't know where my helpers are located and crashes with a fatal error.
Is there a way to register some custom view helpers without a modules.config.php? I haven't found a way to pass an array/config to the HelperConfig below. As a ZF2 rookie I'm a bit lost here.
$form = new \My\Custom\Form\ContactForm();
$renderer = new \Zend\View\Renderer\PhpRenderer();
$config = new \Zend\Form\View\HelperConfig();
$config->configureServiceManager($renderer->getHelperPluginManager());
if(isset($_POST['submit'])) {
$form->setData($_POST);
if($form->isValid()) {
$data = (OBJECT) $form->getData();
// ...
} else {
return $form->render($renderer); // Inside the render() method we use the form-view-helpers to render the form.
}
} else {
return $form->render($renderer);
}
I have also tried to use the HelperPluginManager (Btw. whats the difference between these two?). But there isn't a method to pass a config-array either.
$renderer = new \Zend\View\Renderer\PhpRenderer();
$plugins = $renderer->getHelperPluginManager();
$renderer->setHelperPluginManager($plugins);
i have small trouble...
class Controller {
init() {
// initializing...
// render header && footer
$header = (new HeaderAction)->run();
$footer = (new FooterAction)->run();
// redirect to called action, what renders all the content
}
}
What i can detect diff between ->run() and called action?
Answer found in:
AfterRender -> parse Route -> compare action names. If match - echo, if not match - Return.
Yii is SHIT!!!
I will write new class MyAction with method AddData, what can render for me some viewfile. Creating class CAction for this, what can't rendering? Maybe i must create controller? Are you noob, Quang, ha?
Lol, i can't create the header with action. I must create it in Controller. Controller file now is 1200 lines. >_<
class MyAction {
public $data = array();
public function addData($name, $val) {
$this->data[$name] = $val;
}
public function render($file) {
ob_start;
// ... something
return ob_get_clean;
};
}
/// ITS ALL WHAT NEED ALL THE DEVELOPERS>>>>
BEHAVIORS? EVENTS? FILTERS? WIDGETS? MODULES? MAYBE WE NEED "CAMOBAP" AS AUTOCAR?
REASOOOONS????
===
Lol, there is model cannot to render at all. I have products with views as "tr-tr", and i must create controller, create action, create route for rendering funcking 10 SYMBOLS.... Its Rage. About u, Quang.
Russian Bear will kill you.
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.