I am developing a website that offers a REST service. All the GET actions are OK and rendered using a .json.twig template, but I am having a hard time understanding how to output form errors if the query made to create a new record is not valid.
If I try to do a simple
return $form;
I get the following exception from SF:
"exception":[{"message":"Unable to find template \"SomeBundle:Customers:postCustomer.json.twig\"}]
The template does not exist, that's true, but I have no idea how to create one in JSON format to tell the requestor that his query is incomplete / malformed.
If I try anything else dealing with views but without specifying a template, the result is the same. Is there a way to do that automatically so that if the form is modified the change are reflected as well in the error ?
Or a way to tell FOSRestBundle / JMSSerializerBundle to deal with the serialization themselves ? Before switching to Twig responses the error was nicely handled, and I'd like to have that back, along with the Twig templates for normal operations.
For information, my current controller's action is:
/**
* #ApiDoc(
* resource=false,
* input="SomeBundle\Form\CustomerType",
* description="Create a new customer",
* section="Customers",
* statusCode={
* 201="Action successful",
* 403="Authorization required but incorrect / missing information or insufficient rights",
* 500="Returned if action failed for unknown reasons"
* }
* )
*
* --View(template="SomeBundle:Customers:add.json.twig", templateVar="form", statusCode=400)
* #View(templateVar="form", statusCode=400)
* #param Request $request
* #return \FOS\RestBundle\View\View
*/
public function postCustomerAction(Request $request) {
$data = json_decode($request->getContent(), true);
$manager = $this->getManager();
$customer = new Customer();
$form = $this->getForm($customer);
//$form->submit($data);
//$manager->create($customer);
// $form->handleRequest($request);
// if ($form->isSubmitted() && $form->isValid()) {
// $manager->create($customer);
//
// return $this->redirectView($this->generateUrl('api_get_customer_internal', ['uuid' => $customer->getInternalUuid()], true),
// 201);
// }
return $form;
//return $this->handleView($this->view($form, 400));
//return \FOS\RestBundle\View\View::create($form, 400);
}
And the FOSRestBundle configuration:
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener:
enabled: true
view:
view_response_listener: 'force'
formats:
json: true
templating_formats:
json: true
force_redirects:
html: true
failed_validation: HTTP_BAD_REQUEST
default_engine: twig
routing_loader:
include_format: false
default_format: json
serializer:
serialize_null: true
sensio_framework_extra:
view:
annotations: true
Thanks to jorge07 at https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1620 I was able to find a way to circumvent that in a rather proper way (at least IMHO), here's the updated Controller action (no change in the fosrestbundle settings required):
/**
* #Route("/customers")
* #ApiDoc(
* resource=false,
* input="NetDev\CoreBundle\Form\CustomerType",
* description="Create a new customer",
* section="Customers",
* statusCode={
* 201="Action successful",
* 403="Authorization required but incorrect / missing information or insufficient rights",
* 500="Returned if action failed for unknown reasons"
* }
* )
*
* #View(template="NetDevRestBundle:Common:form_error.json.twig", templateVar="errors", statusCode=400)
*
* #RequestParam(name="customerName", nullable=false)
* #RequestParam(name="customerIndex", nullable=false)
*
* #return \FOS\RestBundle\View\View
*/
public function postCustomerAction(ParamFetcher $fetcher)
{
$customer = new Customer();
$form = $this->getForm($customer);
$form->submit($fetcher->all(), true);
if ($form->isValid()) {
$manager = $this->getManager();
$manager->create($customer);
return $this->redirectView($this->generateUrl('api_get_customer_internal', ['uuid' => $customer->getInternalUuid()], true), 201);
}
$err = $form->getErrors();
$errorsList = [];
foreach ($err as $it) {
$errorsList[(string)$it->getOrigin()->getPropertyPath()] = $it->getMessage();
}
return $this->view([$errorsList])
->setTemplateVar('errors')
;
}
Related
My question concerns various process of json payload validations.
I have recensed :
- deserialization on a model, calling validator service and validate the hydrated object.
- using FormType (even if there are no forms...just json feeds) and validate the form builder after injecting $datas.
Which one do you prefer ?
Have you a better solution ? Such as maybe a middleware (unique bundle ou app that deals with all in/out-coming payloads - request/response)
Thank You
I validate/deserialize with the native listeners/tools FOSRestBundle provides.
Making use of the bundle you can have native form-validation ... or automatically deserialized and validated models plus a list of validation errors injected as controller arguments.
# app/config/config.yml
# You need SensioFrameworkExtraBundle for body converters to work
sensio_framework_extra:
request: { converters: true }
fos_rest:
zone:
- path: '^/api/(.*)+$'
# [..]
body_listener:
enabled: true
default_format: json
decoders:
json: fos_rest.decoder.jsontoform
# automatically injects query parameters into controller Actions
# see #FOSRest\QueryParam in the example below
param_fetcher_listener: force
# https://symfony.com/doc/master/bundles/FOSRestBundle/request_body_converter_listener.html
body_converter:
enabled: true
validate: true
validation_errors_argument: validationErrors
The body converter can deserialize and validate models automatically for you (without using any forms or manual steps). Example:
/**
* #ParamConverter(
* "post",
* converter = "fos_rest.request_body",
* options = {
* "validator" = {
* "groups" = {
* "validation-group-one",
* "validation-group-two",
* }
* },
* "deserializationContext" = {
* "groups" = {
* "serializer-group-one",
* "serializer-group-two"
* },
* "version"="1.0"
* }
* }
* )
*/
public function putPostAction(Post $post, ConstraintViolationListInterface $validationErrors)
{
if (!empty($validationErrors)) {
// return some 4xx reponse
}
// Do something with your deserialized and valid Post model
The bundle can serialize forms (and form-errors) to JSON, too.
i.e. a form with invalid fields will be rendered as:
{
"code": 400,
"message": "Validation Failed",
"errors": {
"errors": [
"This is a global form error."
],
"children": {
"oldPassword": {
"errors": [
"The old password is not correct."
]
},
"newPassword": [],
"submit": []
}
}
}
FOSRestBundle provides a request body listener that automatically decodes Content-Type: application/json to Content: application/x-www-form-urlencoded within the Request object so you can bind the request to the form with handleRequest as you'd do with normal HTML forms.
Quick tip: if you just want to validate your data asynchronously ... you can send the request with a query param (?validate=true in the following example) and return an early response with HTTP 200 (OK) / 202 (Accepted) before performing any business logic.
The following example shows an endpoint that accepts requests of the form:
{
"oldPassword": "xxxxxxx",
"newPassword": "yyyyyyy"
}
Corresponding controller action:
/**
* #FOSRest\Route(
* "/profile/change-password",
* name="api_put_password",
* methods={
* Request::METHOD_PUT
* }
* )
*
* #FOSRest\QueryParam(
* name="validate",
* allowBlank=false,
* default="false",
* strict=true,
* nullable=true,
* requirements="^(true|false)$"
* )
*/
public function putPasswordAction(Request $request, string $validate = 'false')
{
$validate = filter_var($validate, FILTER_VALIDATE_BOOLEAN);
$form = $this->formFactory->createNamed(null, ChangePasswordType::class, null, [
'action' => $this->router->generateUrl('api_put_password'),
'method' => $request->getMethod(),
]);
$form->handleRequest($request);
if (!$form->isValid()) {
$view = new View();
$view->setStatusCode(Response::HTTP_BAD_REQUEST);
$view->setData($form);
return $view;
}
if ($validate) {
$view = new View();
$responseCode = Response::HTTP_ACCEPTED;
$view->setStatusCode($responseCode);
$view->setData([
'code' => $responseCode,
'message' => 'Data is valid.',
'data' => null
]);
return $view;
}
$user = $this->securityContext->getToken()->getUser();
/** #var PasswordChangeRequest $passwordChangeRequest */
$passwordChangeRequest = $form->getData();
$user->setPassword($this->passwordEncoder->encodePassword($user, $passwordChangeRequest->getNewPassword()));
$this->userManager->persist($user);
$view = new View();
$view->setStatusCode(Response::HTTP_OK);
$view->setData([
'code' => Response::HTTP_OK,
'message' => 'Password changed successfully.',
'data' => $user
]);
$context = new Context();
$context->setGroups([
'profile'
]);
$view->setContext($context);
return $view;
}
I'm starting with FOSRestBundle and when I get the values of an entity without relations and display it in the browser, I have no problem. But when I try to get an entity with relations, it shows me an error with code: 500.
Here is the code:
app/config/config.yml:
fos_rest:
routing_loader:
default_format: json
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
view_response_listener: 'force'
ApiRestBundle/Controller/UserController (this works fine)
/**
* #return array
* #Rest\Get("/users")
* #Rest\View()
*/
public function getUsersAction()
{
$response = array();
$em = $this->getDoctrine()->getManager();
$users = $em->getRepository('CASUsuariosBundle:User')->findAll();
$view = $this->view($users);
return $this->handleView($view);
}
APIRestBunde/Controller/CategoryController (this doesn't works)
/**
* #return array
* #Rest\Get("/categories")
* #Rest\View()
*/
public function getCategoriesAction()
{
$response = array();
$em = $this->getDoctrine()->getManager();
$categories = $em->getRepository('CASEventBundle:Category')->findAll();
$view = $this->view($categories);
return $this->handleView($view);
}
the error code:
{"error":{"code":500,"message":"Internal Server
Error","exception":[{"message":"Notice: Undefined index:
name","class":"Symfony\Component\Debug\Exception\ContextErrorException","trace":[{"namespace":"","short_class":"","class":"","type":"","function":"","file":"C:\xampp\htdocs\CASPruebas\vendor\doctrine\orm\lib\Doctrine\ORM\Persisters\BasicEntityPersister.php","line":1758,"args":[]}...
your problem is a little bit complicated to solve.
This error code can mean a lot of different things!
But I think it is not directly a FOSRestBundle problem. Maybe you have a relation problem on your Category entity...
What is the result of this: doctrine:schema:validate
Edit : Maybe it will be more simple to solve it if you give us the full error code.
I have a test file to test services instantiation and i have made a custom menu with KnpMenuBundle.
Everything is working expect phpunit who return an error when testing my MenuBuilder.
There is the function who test all services instantiation my test file :
class ServiceAvailabilityTest extends KernelTestCase
{
/**
* #dataProvider getServiceIds
*
* #param $serviceId
*/
public function testServiceInstance($serviceId)
{
static::bootKernel();
static::$kernel->getContainer()->get($serviceId);
}
}
On my MenuBuilder i use authorizationChecker to know if the user is granted or not, like this.
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
$menu->addChild('sidebar.front.administration', ['route' => 'sonata_admin_redirect'])
->setExtra('translation_domain', 'navigation')
->setAttribute('icon', 'fa fa-eye');
}
When i'm removing all this, tests are ok
$this->authorizationChecker->isGranted('ROLE_ADMIN')
There is the error i get when i run phpunit
1) Tests\ServiceAvailabilityTest::testServiceInstance with data set #423 ('menu.main')
Symfony\Component\Security\Core\ExceptionAuthenticationCredentialsNotFoundException: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL. /code/vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php:57
/code/src/AppBundle/Menu/MenuBuilder.php:192
/code/src/AppBundle/Menu/MenuBuilder.php:101
/code/app/cache/test/appTestDebugProjectContainer.php:8311
/code/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:312
/code/tests/ServiceAvailabilityTest.php:3
There is my menu services if you have to check them
menu.builder:
class: AppBundle\Menu\MenuBuilder
arguments: [ '#knp_menu.factory', '#doctrine', '#manager.server','#security.authorization_checker', '#request_stack' ]
menu.main:
class: Knp\Menu\MenuItem
factory: [ '#menu.builder', 'createMainMenu' ]
arguments: [ '#request_stack' ]
tags:
- { name: knp_menu.menu, alias: sidebar }
I already search on the internet but they fix this by adding an access control on security.yml like this
- { path: ^/, role: ROLE_USER }
But i don't have any route for a menu.
Does someone already had this phpunit error ?
Thanks,
Try this:
/**
* #param string $firewallName
* #param UserInterface $user
* #param array $options
* #param array $server
*/
protected function loginUser($firewallName, UserInterface $user, array $options = array(), array $server = array())
{
$this->client = static::createClient();
$token = new UsernamePasswordToken($user, null, $firewallName, $user->getRoles());
static::$kernel->getContainer()->get('security.token_storage')->setToken($token);
// <2.8 this may be usefull
//$request = new Request();
//$event = new InteractiveLoginEvent($request, $token);
//static::$kernel->getContainer()->get('event_dispatcher')->dispatch('security.interactive_login', $event);
$session = $this->client->getContainer()->get('session');
$session->set('_security_'.$firewallName, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
in your testCase/setUp for example::
static::bootKernel();
$this->loginUser('admin', $testUser);
$this->assertNotFalse(static::$kernel->getContainer()->get('security.authorization_checker')->isGranted('ROLE_ADMIN'));
I am working in a Restful API using Symfony2 and FOSRestBundle. I have read view layer docs but is not clear to me how to handle output for API. What I want to achieve is simple: display or return or output the result as valid JSON. This is what I have at controller:
<?php
/**
* RestAPI: Company.
*/
namespace PDI\PDOneBundle\Controller\Rest;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Request\ParamFetcherInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\Get;
class CompanyRestController extends FOSRestController
{
/**
* Gets all companies.
*
* #return array
*
* #ApiDoc(
* resource = true,
* https = true,
* description = "Gets all companies",
* statusCodes = {
* 200 = "Returned when successful",
* 400 = "Returned when errors"
* }
* )
* #Get("/api/v1/companies")
*
*/
public function getCompaniesAction()
{
$response = array();
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('PDOneBundle:Company')->findAll();
if ($entities) {
foreach ($entities as $entity) {
$response['companies'][] = [
'id' => $entity->getId(),
'createdAt' => $entity->getCreatedAt(),
'updatedAt' => $entity->getUpdatedAt(),
'name' => $entity->getName(),
'logo_url' => $entity->getLogoUrl(),
'division' => $entity->getDivision(),
'inactive' => $entity->getInactive(),
];
}
$response['status'] = 'ok';
} else {
$response['status'] = 'error';
}
return $response;
}
}
If I try this URL: /app_dev.php/api/v1/companies.json I got 404 error:
{"code":404,"message":"No route found for \"GET\/api\/v1\/companies.json\""}
If I try this URL: https://reptool.dev/app_dev.php/api/v1/companies error turns on:
Unable to find template "". 500 Internal Server Error -
InvalidArgumentException 3 linked Exceptions: Twig_Error_Loader »
InvalidArgumentException » InvalidArgumentException »
I've also check FOSRestBundleByExample but didn't get much help.
What I am missing here? How do I achieve what I need? Any advice?
FOSRest Config
I forgot to add the FOSRestBundle at config.yml:
#FOSRestBundle
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener:
rules:
- { path: ^/, priorities: [ json, html ], fallback_format: ~, prefer_extension: true }
media_type:
version_regex: '/(v|version)=(?P<version>[0-9\.]+)/'
body_converter:
enabled: true
validate: true
view:
mime_types:
json: ['application/json', 'application/json;version=1.0', 'application/json;version=1.1']
view_response_listener: 'force'
formats:
xml: false
json: true
templating_formats:
html: true
exception:
codes:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
messages:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
allowed_methods_listener: true
access_denied_listener:
json: true
I feel your pain. I had troubles getting started as well. One important place to start is the config. Here's what I use in my implementation.
fos_rest:
param_fetcher_listener: true
view:
mime_types:
json: ['application/json', 'application/json;version=1.0', 'application/json;version=1.1']
view_response_listener: 'force'
formats:
xml: false
json: true
templating_formats:
html: true
format_listener:
rules:
- { path: ^/, priorities: [ json, html ], fallback_format: ~, prefer_extension: true }
media_type:
version_regex: '/(v|version)=(?P<version>[0-9\.]+)/'
exception:
codes:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
messages:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
allowed_methods_listener: true
access_denied_listener:
json: true
body_listener: true
In the format_listener if you want JSON to be the default response, make sure it's set first in priorities. Otherwise your header will need to include Accept: application/json every time. This may be why you're getting a twig error as it's trying to use twig to render an HTML output.
Also, make sure you have a serializer like http://jmsyst.com/bundles/JMSSerializerBundle installed and included in your AppKernal.
In your controller I found it easiest to extend the FOSRestController like you did, but also return a view object instead of creating the array yourself. The serializer will handle all of that for you.
/**
* RestAPI: Company.
*/
namespace PDI\PDOneBundle\Controller\Rest;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Request\ParamFetcherInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\Get;
class CompanyRestController extends FOSRestController
{
/**
* Gets all companies.
*
* #return array
*
* #ApiDoc(
* resource = true,
* https = true,
* description = "Gets all companies",
* statusCodes = {
* 200 = "Returned when successful",
* 400 = "Returned when errors"
* }
* )
* #Get("/api/v1/companies")
*
*/
public function getCompaniesAction()
{
$response = array();
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('PDOneBundle:Company')->findAll();
if(!$entities)
{
return $this->view(null, 400);
}
return $this->view($entities, 200);
}
}
I hope this helps a little.
I`m trying to write some functional tests for a REST API, created using FOS Rest Bundle.
The problem is that when I use the Symfony\Component\BrowserKit, symfony throws me the following error:
{"message":"Unable to find template \"AccountBundle:Account:list.html.twig\". .. }
The code that I run is:
$client = static::createClient();
$client->request('GET','/account');
When I run the request from the browser, it works fine.
Here is the controller:
/**
* Get channel by ID
* #Secure(roles="ROLE_USER")
* #RestView()
* #ApiDoc(
* resource=true,
* description="Get channel by id",
* section="Channel",
* output="Channel"
* )
*/
public function getAction(Channel $channel)
{
return array('channel' => $channel);
}
So when in test scenario, instead of returning the JSON tries to load the template.
You should use the $server parameter of the $client-request() method to set the Accept header to application/json. FOSRestBundle has a listener that returns JSON only if the corresponding Accept header is received, otherwise it will search for the template corresponding to the controller.
$client->request('GET', '/account', array(), array(), array('HTTP_ACCEPT' => 'application/json'));