The user sumbits a form that was build using the symfony 2 framework with abstract type:
<?php
$form = $this->createForm(new MyAbstractType(), new MyEntity());
I receive this post request in an action:
public function receiveFormRequestAction(Request $request){
//How do I get the abstract type from the request?
}
I need to be able to create the AbstractType used on the form using only information in the request.
Is it possible?
How do you do it?
Thanks.
Edit:
Sorry if i wasn't clear enough. in the method "recieveFormRequestAction" i don't know what abstract type i am going to get, so i cant bind the form directly to MyAbstractType.
This action can, in theory, recieve any AbastractType and bind it.
Yes
Like this:
// first, create the very same form
$form = $this->createForm(new MyAbstractType(), new MyEntity());
// bind the form with your request
$form->bind($request);
// Optional step : validate the form
if ($form->isValid()) {
// your object is ready, get it like this:
$object = $form->getData();
} else {
// handle the validation errors.
}
You need to bind the Request object to the form.
$form->bind($request);
Then you can run things like $form->isValid() and $form->getData().
I ended up doing this:
The method getName() on my forms returned exactly the same name as the Form class name
Class MyAbstractType extends AbstractType
[...]
public function getName(){
return "MyAbstractType";
}
Now i can get the type using the hash on the parameter keys
public function myAction(Request $request){
$parameterKeys = $request->request->keys();
$formName = $parameterKeys[0];
Ugly as hell, but i needed a quick solution. Until there is a cleaner one, i'm accepting this.
Related
My Controller has a Factory that gives it a Form
$formManager = $container->get('FormElementManager');
return new MyController(
$formManager->get(MyForm::class)
);
My Form has also a Factory that gives it an AuthenticationService
return new MyForm(
$container->get(AuthenticationService::class)
);
That way I can check in the form if the user has identity.
But how can i redirect him from the form?
Just like in a Controller?
if(!$authService->hasIdentity()) {
return $this->redirect()->toRoute('myRoute);
}
Or how can i redirect from a (Controller and/or Form) Factory?
A possible solution for your issue could be the possibilty of using the build method with the factory call.
You haven 't shown your factories, so I will use some standard examples, which explain the solution.
The first approach is not injecting the whole form to the controller. Instead just inject the form element manager. So you can use the build method of the factory inside your controller.
The controller factory
namespace Application\Controller\Factory;
use Application\Controller\YourController;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$formElementManager = $container->get('FormElementManager');
return new YourController($formElementManager);
}
}
This differs from your original factory. Only the form element manager is injected to the controller. This holds a few advantages for you. One of this is the build method of the manager.
The Controller
namespace Application\Controller;
class YourController extends AbstractActionController
{
protected $formElementManager;
public function __construct($formElementManager)
{
$this->formElementManager = $formElementManager;
}
public function indexAction()
{
$user = $this->currentUser();
if ($user === null) {
$this->redirect('to/somewhere/the/user/belongs');
}
// here 's the magic!
$form = $this->formElementManager->build(YourForm::class, [
'userID' => $user->getUserId(),
]);
// some form stuff follows here
}
}
As the form was not injected directly to your controller but the form element manager, you can use the form element manager instead inside the controller. This offers you the opportunity to use the build function. With this function you can add some options to your form factory. In this case I 'm using the user id for the form factory.
If there 's no valid user, no form will be created because an exception is thrown before.
The Form Factory
The form factory creates a new instance of your form. All needed dependencies should be created in the factory. How the build function works here, I 'll explain later in the answer.
namespace Application\Form\Factory;
use Application\Form\YourForm;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$selectOptions = [];
if ($options !== null) {
if (isset($options['userID])) {
$tablegateway = $container->get(YourTableGateway::class);
$selectOptions = $tablegateway->findOptionsByUserId($options['userID]);
}
}
$form = $container->get(YourForm::class);
if (count($selectOptions))
$form->get('YourSelectElement')->setValueOptions($selectOptions);
return $form;
}
}
This factory does all you need. Via the build method you hand over the user id. If a user id is present a table gateway is created from wich you retrieve select options by the given user id. These options will be set to the form field. This logic is kept in the factory to keep the form class itself clean and simple.
With this solution you don 't need the auth service in your form. Your form is only generated when a valid user id is given. Your form instance will not crash, if there 's no user id given. The only conceivable case could be a form with default or no select options for the specific field.
Hope this helps a bit.
is it possible or good idea to test arguments of the class that has the method mocked ?
Example :
class CarType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventSubscriber(new User('argument example')
}
}
This is my test:
public function testFormBuilder()
{
$builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
->disableOriginalConstructor()
->getMock();
// Test
$type = new CarType();
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->isInstanceOf(User::class)
);
$type->buildForm($this->builder, []);
}
This test work fine, But...
I want to know if the first argument of the User class is a string, but can I do in this same test?
You are not able to check directly because the User is being created directly in your code that you are testing.
However depending if the string can be retrieved via a public method of the User class, then you could do something like this:
$this->builder->expects($this->once())
->method('addEventSubscriber')
->with(
$this->callback(function($user) {
if (! $user instanceof User) {
return False;
}
if ($user->getName() != <Expected String>) {
return False;
}
return True;
})
);
You can use a callback check further details of User instance. You are not verifying that the string was passed to the constructor but you can check that the object is properly created.
Though this isn't the best way to go about this. Your code is not extensible because you are not able to change the User class. It would be better to pass in a User object to the method that gets passed on. This would allow you have different types of User objects in the future.
For example, I have two entities: main (parent) entity (for example User) and dependent entity (Post). I want to serialize User entity using JMS Serializer with additional information of its first post date. The post date stored in DB is timestamp int, but I want to serialize it with my helper (just a service) which converts int to sting with some formatting.
Trying to create a virtual_property with method at entity class, but failed to inject my helper into entity class. The only way to solve it for me is to serialize by myself into controller:
public function someAction()
{
$serializedUser = $this->serializeEntity($user);
}
public function serializeEntity(User $user)
{
// JMS serialization
$user = $this->get('jms_serializer')->serialize($user, 'array');
if ($user->getPosts()->count()) {
$post = $user->getPosts()->first();
$user['first_post_date'] = $this->get('my_helper_service')->dateFormat($post->getDate());
}
return $user;
}
NB: this example is synthetic, in the real world I have more complex methods, not just date formatter. But the main idea is the same.
I feel there should be better way to do this.
Dmtry's solution should work perfectly for your case.
An event listener/subscriber is the best solution in this case.
A bit more general solution, that works even with objects and will trigger the whole event system part of the JMS serializer (Dmtry's solution works only with primitives and only for JSON/YAML, not XML), is:
class MyFormatter implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'class' => 'YourEntity'
),
);
}
public function __construct(MyFormatter $foermatter)
{
$this->formatter = $formatter;
}
public function onPostSerialize(ObjectEvent $event)
{
$visitor = $event->getVisitor();
$context = $event->getContext();
$timestamp = $event->getObject()->getTimestamp();
$formattedTime = $this->formatter->format($timestamp);
$metadata = new StaticPropertyMetadata('stdClass', 'first_post_date', $formattedTime);
$visitor->visitProperty($metadata, $formattedTime, $context);
}
}
stdClass is ignored by the serializer...
Of course, there is a better way. It's called serialization events.
http://jmsyst.com/libs/serializer/master/event_system
You create event subscriber
my_bundle.serializer_subscriber:
class: MyBundle\Serializer\MyEntitySerializerSubscriber
arguments:
- #bundle.time_formatter
tags:
- { name: jms_serializer.event_subscriber }
And then just add the data you need in your listener
public function myOnPostSerializeMethod(ObjectEvent $event)
{
if (!($event->getObject() instance of YourEntity)) {
return;
}
$timestamp = $event->getObject()->getTimestamp();
$visitor = $event->getVisitor();
$visitor->addData('date', $this->formatter->format($timestamp));
}
P.S. I didn't check the code, so maybe I'm mistaken somewhere with name of methods, but the idea is clear, I hope
in controller methods I have something like:
public function showAllCustomersAction(Request $request) {
return $this->render('cus/showAllCustomers.html.twig', $myarray);
}
and in view I can of cource access the array $myarray.
My questin is: how to in test access this array. I do not parse HTML. I want just the array.
class CustomerTest extends WebTestCase {
public function testAllCustomers() {
$client = static::createClient();
$crawler = $client->request('GET', '/cus/showAllCustomers');
// here somehow access the array $myarray
}
}
Thank you in advice :)
you can't, the Crawler will return you a Symfony Response object that are not aware of the data passed by the controller to return this response.
You can of course use the DomCrawler component to search for the content displayed by the array (I guess you use it in your view).
Mickaƫl
in my symfony2 project i need call the same action in many controllers and this action should return a very simple php array that then will be passed to a twig template by these controllers. How can i do it?
A pratical example can explain my situation better.
1.shared controller
// Acme/DemoBundle/Controller/MetasController
class MetasController extends Controller {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
page render controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->forward('AcmeDemoBundle:Metas:metas');
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
when i do this i get an error: the controller must be a response array given.
You should create a service
// Acme/DemoBundle/Controller/MetasController
class MetasController {
public function metasAction() {
$myArray= array();
return $myAarray;
}
}
declare as service in Acme\DemoBundle\Resources\config\services.yml
services:
demo.metas:
class: "Acme\DemoBundle\Controller\MetasController"
Then you can use it in any other controller
// Acme/DemoBundle/Controller/PageController
class PageController extends Controller {
protected $property = "test";
public function indexAction() {
$metas= $this->get('demo.metas')->metas();
return $this->render('AcmeDemoBundle:Page:index.html.twig', array('property'=>property, 'metas'=>$metas));
}
}
In your action controller :
<?php
...
$arrayExample = array();
return $this->render('ExampleBundle:ExampleFolder:exampleTemplate', array('myArray' => $arrayExample));
And in your twig template now you have access to your array using myArray
Example :
{% for data in myArray %}
...
{% endfor %}
Try this :
use Symfony\Component\HttpFoundation\Response;
public function indexAction()
{
...
$content = $this->renderView(
'AcmeDemoBundle:Page:index.html.twig',
array('property'=> $property,
'metas' => $metas
));
return new Response($content);
}
Yes, you can register your controller as a service as it said above but I would recommend to isolate this logic in a different place. It might be a service but not controller.
As I understand you need the same array in several places. So, it might be some class registered as service or some simple class with static method providing this array. In this case your code will be much cleaner.
If you need this array only in view you can define custom twig method which will return array you need. If this array might be different time to time (if it might depend on some data) you can pass entity manager to the service providing this array or to the twig extension.
(The best use of controllers is to be just a proxy between view and data layer. It's not a good idea to use it for such purposes as you described (in my opinion of course).)