Symfony form POST handling - php

How does Symfony handle POST method towards controllers? For example, this code in ASP.NET makes it possible to use an exact similar name for a controller, in a different manner:
public ActionResult Create()
{
return View();
}
// POST: Objects/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create()
{
//Code here that only runs on POST method of a form
return View();
}
I have seen something like:
public function createAction()
{
return $this->render('formPage.html.twig');
}
/**
* #Method({"POST"})
*/
public function createAction()
{
//Some code...
return new Response('Added item with id: ' . $item->getId() . 'to database');
}
Is the latter possible and similar to the former? Is it necessary to use such annotation like this or can it also be added to routing and if so, should I make different routing names or something?
movie_create:
path: /movies/create
defaults: { _controller: AppBundle:Movie:create }

You can define what methods a route will accept, either by annotation as per your example or in the routing.yml;
my_route:
path: /foo/bar/{id}
defaults: { _controller: AppBundle:Fubar:foo }
methods: [POST]
this will acept only POST.
You can't define two methods with the same name, that is a limitation of PHP in general. In your case you could have one route to show the form and another to handle the request.

Controllers in Symfony2 are classes, in which you cannot re-defined methods with the same name.
I'm afraid you'll need to either split your controller into two:
class ViewController extends Controller {
public function createAction()
{
return $this->render('formPage.html.twig');
}
}
class CreateController extends Controller {
/**
* #Method({"POST"})
*/
public function createAction()
{
//Some code...
return new Response('Added item with id: ' . $item->getId() . 'to database');
}
}
Or a much simpler solution, simply re-name your methods in your routing:
movie_view:
path: /movies/view
defaults: { _controller: AppBundle:Movie:view }
movie_create:
path: /movies/create
defaults: { _controller: AppBundle:Movie:create }
requirements:
_method: POST

Related

Catch-all route in Symfony 3

I have a catch-all fallback route in Symfony2 that I couldn't get to work in Symfony3. I tried this exact syntax (a verbatim copy of my Symfony2 route) and that didn't work.
fallback:
path: /{req}
defaults: { _controller: MyBundle:Default:catchAll }
requirements:
req: ".+"
How can I get this working in Symfony3? (It's literally the only thing holding me back from using Symfony3 and keeping me at v2.8)
This should help you:
route1:
path: /{req}
defaults: { _controller: 'AppBundle:Default:index' }
requirements:
req: ".+"
Where, my controller is called "DefaultController", and I have a function called "indexAction()".
Here is my code for the DefaultController:
class DefaultController extends Controller
{
/**
* #Route("/", name="homepage")
*/
public function indexAction(Request $request)
...
I actually did try what you said in my environment, and it didn't work until I had the right controller settings specified.
EDIT:
For this to work, it was necessary to add the parameter Request $request (with the type hint) to the action's method signature.
I found the current accepted answer almost useful for Symfony 4, so I'm going to add my solution:
This is what I did to get it working in Symfony 4:
Open /src/Controller/DefaultController.php, make sure there is a function called index(){}
It's not required to add the Request $request as first param as some comment suggest.
This is the method that will handle all urls caught by the routes.yaml
Open /config/routes.yaml, add this:
yourRouteNameHere:
path: /{req}
defaults: { _controller: 'App\Controller\DefaultController::index' }
requirements: # the controller --^ the method --^
req: ".*"` # not ".+"
You can also override Exception controller.
# app/config/config.yml
twig:
exception_controller: app.exception_controller:showAction
# app/config/services.yml
services:
app.exception_controller:
class: AppBundle\Controller\ExceptionController
arguments: ['#twig', '%kernel.debug%']
namespace AppBundle\Controller;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ExceptionController
{
protected $twig;
protected $debug;
public function __construct(\Twig_Environment $twig, $debug)
{
$this->twig = $twig;
$this->debug = $debug;
}
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
// some action
return new Response($this->twig->render('error/template.html.twig', [
'status_code' => $exception->getStatusCode()
]
));
}
}

Symfony nested resource routing not passing ids

Here's the gist of my problem. I'm creating an API in Symfony2 and I cannot seem to get nested routes to work.
This Works
api_v1_role_show:
pattern: /api/v1/roles/{roleId}
defaults: { _controller: rest_v1_roles_controller:showAction }
methods: [GET]
I can prove that it works by doing something like this
function showAction()
{
var_dump(func_get_args()); // array(1)
}
This Does Not Work
api_v1_permission_post:
pattern: /api/v1/roles/{roleId}/permissions
defaults: { _controller: rest_v1_role_permissions_controller:createAction }
methods: [POST]
I end up getting something like this:
function createAction()
{
//This should be array(1)
var_dump(func_get_args()); // array()
}
What am I missing? I've tried looking online for over an hour now and I can't seem to find anything on the subject. I have to wonder if it's a post action security thing.
REST Class Structuring
We have a lot of rest end-points in our application. We created a rest class that allows us to quickly add new end-points called the RestBaseController and it looks something like this:
class RestBaseController {
protected $urlParams;
public function showAction()
{
$this->urlParams = func_get_args();
//Shows the resource based on ID now stored in $this->urlParams;
}
public function createAction()
{
$this->urlParams = func_get_args();
$this->adjustParameters();
//creates the resource from JSON body, essentially
}
protected function adjustParameters()
{
return null;
}
}
Then comes the class that I'm having a problem with:
class RolePermissionsController extends RestBaseController
{
protected function adjustParameters()
{
$role = $this->em()->getRepository('AppBundle:Role')
->find($this->urlParams[0]); //This will give me an error saying offset 0 does not exist.
$this->roleId = $role->getRoleId();
}
}
My Question:
How would I get a nested URL to work in Symfony?
if I'm not mistaken, symfony routing usin Reflection and "named parameters".
For example: route: /api/v1/roles/{roleId}/permissions/{otherId}
public fucntion action($otherId, $roleId) // position here is not important, important name
Also you can:
public fucntion action(Request $request, $otherId, $roleId)
and first argument will be $request.
So man, change your architecture until it's not too late
In the end, I found two ways to fix this issue. Dmitry pointed out that symfony uses Reflection and "named parameters" to break down the routes into passable arguments that are similar to function(Request $request, [ ...$uriExtractedName1, $uriExtractedName2]) which means that I would have been able to something like this in
PHP 5.6+
RestBaseController.php
public function createAction(Request $request, ...$args)
{
$this->request = $request;
$this->urlParams = $args;
//...
}
But I'm currently working on a project in an earlier version so I had to something a little more simplistic.
PHP 5.4
routing.yml
api_v1_permission_post:
pattern: /api/v1/roles/{parentId}/permissions
defaults: { _controller: rest_v1_role_permissions_controller:createAction }
methods: [POST]
RestBaseController.php
public function createAction($parentId = null)
{
$this->parentId = $parentId;
//...
}
This solution has worked out quite elegantly. I don't like having to always call the parent id parentId but it's a small price to pay for having exceptionally small controller classes. I realized that the other routers always worked because we call the passed id id.

Symfony2 - Use same controller but different view based on HTTP host?

Like the title says, I would like to use the same controller, but different views, based on the HTTP host name. Is this possible? What would be the best architecture to accomplish it?
If the controller returns null then the Symfony 2 request handler will dispatch a KernelEvents::VIEW event.
You can make yourself a view listener (http://symfony.com/doc/current/cookbook/service_container/event_listener.html) to catch the event. Your view listener would then need the logic to determine which view to create based on request parameters such as the host name. The view would then create the response object. The listener then sets the response in the event.
Is this the "best" approach. Hard to say. There is no reason why the controller itself could not create the view. On the other hand, with a view listener you can share views with multiple controllers. Really depends on your application.
Here is an example of a view listener which kicks off different views depending on the _format attribute.
namespace Cerad\Bundle\CoreBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ViewEventListener extends ContainerAware implements EventSubscriberInterface
{
const ViewEventListenerPriority = -1900;
public static function getSubscribedEvents()
{
return array(
KernelEvents::VIEW => array(
array('onView', self::ViewEventListenerPriority),
),
);
}
/* =================================================================
* Creates and renders a view
*/
public function onView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->has('_format'))
{
$viewAttrName = '_view_' . $request->attributes->get('_format');
}
else $viewAttrName = '_view';
if (!$request->attributes->has($viewAttrName)) return;
$viewServiceId = $request->attributes->get($viewAttrName);
$view = $this->container->get($viewServiceId);
$response = $view->renderResponse($request);
$event->setResponse($response);
}
# services.yml
cerad_core__view_event_listener:
class: '%cerad_core__view_event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }
# routing.yml
cerad_game__project__schedule_team__show:
path: /project/{_project}/schedule-team.{_format}
defaults:
_controller: cerad_game__project__schedule_team__show_controller:action
_model: cerad_game__project__schedule_team__show_model_factory
_form: cerad_game__project__schedule_team__show_form_factory
_template: '#CeradGame\Project\Schedule\Team\Show\Twig\ScheduleTeamShowPage.html.twig'
_format: html
_view_csv: cerad_game__project__schedule_team__show_view_csv
_view_xls: cerad_game__project__schedule_team__show_view_xls
_view_html: cerad_game__project__schedule_team__show_view_html
requirements:
_format: html|csv|xls|pdf

Redirect Symfony2 LogoutSuccessHandler to original logout target

I need to modify my user object on logout. To do this, I have a security.yml that contains the following (amongst other things) -
#...
logout:
success_handler: my.logout_success_handler
target: /
#...
...this defines a logout success handler, which is defined in services.yml like this -
my.security.logout_success_handler:
class: My\Security\LogoutSuccessHandler
arguments: ["#security.context", "#doctrine.orm.default_entity_manager"]
...finally, the business-end of my handler is like this -
// ...
public function onLogoutSuccess(Request $request)
{
$user = $this->securityContext->getToken()->getUser();
// ... do stuff with the user object....
$this->em->flush();
// now what?
}
// ...
So, where it says "now what?" I understand that I need to return a Response object. Ideally I want that response object to redirect the user to whatever is defined in logout.target in the security.yml.
Is there an easy way I can query that? Or, even better, is there another way of doing this kind of thing that doesn't require me to get involved with the request/response objects at all?
Thanks
You could define your target as a parameter in your parameters.yml or config.yml:
parameters:
logout.target: /
And then reference this value in your security.yml:
logout:
success_handler: my.logout_success_handler
target: %logout.target%
And/or inject it into your logout handler:
my.security.logout_success_handler:
class: My\Security\LogoutSuccessHandler
arguments: ["#security.context", "#doctrine.orm.default_entity_manager", %logout.target%]
And return a RedirectResponse with this value:
// Assign the 3. constructor parameter to the instance variable $logoutTarget
public function onLogoutSuccess(Request $request)
{
// ...
return new RedirectResponse($this->logoutTarget);
}
So, I think I've figured out the right answer -
Rather than implementing LogoutSuccessHandlerInterface and configuring a logout.success_handler, my security.yml now looks like this -
# ...
logout:
handlers: [my.bundle.security.logout_handler]
# ...
...and I'm implementing Symfony\Component\Security\Http\Logout\LogoutHandlerInterface. Confusing naming, but this seems to be the preferred way of doing post-logout operations without having to get involved with the response object. My implementation looks like this -
namespace My\Bundle\Security;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
/**
* Do post logout stuff
*/
class LogoutHandler implements LogoutHandlerInterface
{
/**
* #var EntityManager
*/
protected $em;
/**
* Constructor
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* Do post logout stuff
*/
public function logout(Request $request, Response $response, TokenInterface $authToken)
{
$user = $authToken->getUser();
// do stuff with the user object...
$this->em->flush();
return $response;
}
}
...as you can see, the LogoutHandlerInterface provides a pre-made $response object that I can just return when I'm finished.
You could use composition and inject the default LogoutSuccessHandler into your object and call the onLogoutSucces method on it.
The following pseudu code shows the idea of doing it.
class MyLogoutSuccessHandler implements \LogoutSuccessHandler
{
protected $original;
public function __construct(OriginalLogoutSuccesHandler $original)
{
$this->original = $original;
}
public function onLogoutSuccess(Request $request)
{
// do stuf your want and delegate to the original
return $this->original->onLogoutSuccess($request);
}
}
This is also the way HttpKernelInterface works in StackPHP and when you use HttpCache in your application.
Hopefully this helps, happy coding :)

Error: Call to a member function get() on a non-object

I am trying to send mail with Swift_Message however when I go to send the data it will not send and I get an error of
FatalErrorException: Error: Call to a member function get() on a
non-object in
/vagrant/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
line 252
Here is the Email Controller that I am using.
use Symfony\Component\Finder\Shell\Command;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
class EmailController extends Controller{
public function createMessage($subject, $from, $from_name, $to, $to_name, $body){
// Create the message
$message = \Swift_Message::newInstance()
// Give the message a subject
->setSubject($subject)
// Set the From address with an associative array
->setFrom(array($from => $from_name))
// Set the To addresses with an associative array
->setTo(array($to => $to_name))
// Give it a body
->setBody($body, 'text/html');
return $message;
}
public function sendEmail($message, $urlAlias){
$this->get('mailer')->send($message);
return $this->redirect($this->generateUrl($urlAlias));
}
}
I understand that its unable to access the object which I think is part of the container class but I can seem to get it to pull up. I have tried using $this->container->get(...
but that also does not work. What am I missing. This seems like it should be really straight forward.
I am calling this function from a different bundle using an action to call the current controller. I don't know if that makes a difference.
Ok so when looking in /vagrant/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
The line it errors on is
/**
* Gets a service by id.
*
* #param string $id The service id
*
* #return object The service
*/
public function get($id)
{
return $this->container->get($id);
}
}
Which makes me feel like 'mailer; is not a good $id but it is used in Symfony's examples and in a lot of other private examples.
Don't know if this helps or not but figured it was worth mentioning.
Could this be because of the swiftmailer: setting inside of my config.yml file?
routing.yml file
fuel_form_homepage:
pattern: /hello/{name}
defaults: { _controller: FuelFormBundle:Default:index }
referral_form:
pattern: /form/referral/{hash}
defaults: { _controller: FuelFormBundle:Form:referralForm }
referral_result:
pattern: /form/referral/result
defaults: { _controller: FuelFormBundle:Form:referralResult }
user_form:
pattern: /form/user
defaults: { _controller: FuelFormBundle:Form:userForm }
home:
pattern: /
defaults: { _controller: FuelFormBundle:Default:home}
This is the function that calls
public function userFormAction(request $request){
$user = new User();
$form = $this->createForm('user', $user);
$form->handleRequest($request);
if($form->isValid()){
$user->setTimeCreated();
$user->setTimeUpdated();
$date = $user->getTimeCreated();
$timestamp = $date->format("U");
$hash = $user->getFirstName() . $user->getLastName() . $timestamp ;
$user->setUserHash(md5($hash));
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
print_r($user);
//TODO: #Email: #Body: make sure to replace with correct information.
//Calls a service named email_bundle_controller
$emailController = $this->get('email_bundle_controller');
$fullName = $user->getFirstName() . $user->getLastName();
$body = "please visit the following url to start referring! <a href='http://localhost:8080/app_dev.php/form/referral/" . $user->getUserHash() . "'>Your URL</a>";
$message = $emailController->createMessage('Welcome to Fuel PRM References', 'bsaverino#gmail.com', 'Brad Saverino', $user->getEmail(), $fullName, $body);
$emailController->sendEmail($message, 'user_form');
}
return $this->render('FuelFormBundle:Default:mainForm.html.twig', array('form' => $form->createView(),));
}
This is the service that allows me to call on the other bundle.
services:
fuel_form.form.type.referral:
class: Fuel\FormBundle\Form\Type\ReferralType
tags:
- { name: form.type, alias: referral}
fuel_form.form.type.user:
class: Fuel\FormBundle\Form\Type\UserType
tags:
- { name: form.type, alias: user}
email_bundle_controller:
class: Fuel\EmailBundle\Controller\EmailController
This is the FuelEmailBundle.php
namespace Fuel\EmailBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use \Symfony\Component\DependencyInjection\ContainerInterface;
class FuelEmailBundle extends Bundle
{
private static $containerInstance = null;
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer($container);
self::$containerInstance = $container;
}
public static function getContainer()
{
return self::$containerInstance;
}
}
These are the changes that were made to the sendEmail function
public function sendEmail($message, $urlAlias){
$container = FuelEmailBundle::getContainer();
$mailer = $container->get('mailer');
$mailer->send($message);
return $this->redirect($this->generateUrl($urlAlias));
}
As Cerad had mentioned above, you are getting the error as container is not set. One way of fixing this issue would be to pass a container instance to your bundle so that you can call the container from anywhere in your project.
Edit the class corresponding to your bundle(BundleName.php) to include two methods setContainer and getContainer. See the example below.
namespace Venom\CoreBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use \Symfony\Component\DependencyInjection\ContainerInterface;
class VenomCoreBundle extends Bundle
{
private static $containerInstance = null;
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer($container);
self::$containerInstance = $container;
}
public static function getContainer()
{
return self::$containerInstance;
}
}
Use the appropriate namespaces.
Then, use the namespace for the bundle in classes where you need the container.
You may call the container by
$container = VenomCoreBundle::getContainer();
Then, call the mailer
$mailer = $container->get('mailer');

Categories