PUT request with FOSRest BodyConverter - rest

I'm having troubles using BodyConverter, usually I use FOSRest with Symfony Forms but on a new project I'm trying to use BodyConverter. POST requests works fine but I can't get PUT requests to work.
/**
* #Rest\View(serializerGroups={"Default"})
* #Rest\Put("/{id}", name="put")
* #ParamConverter("mission", converter="fos_rest.request_body")
*
* #param Mission $mission
* #param ConstraintViolationList $violations
* #return Mission | View
*/
public function put(Mission $mission, ConstraintViolationList $violations)
{
if(count($violations)){
return $this->view($violations, Response::HTTP_BAD_REQUEST);
}
return $this->manager->post($mission);
}
With BodyConverter and ParamConverter I would like to update the ressource which ID was passed in the URL but BodyConverter just override the ressource and create a new one. For exemple :
PUT /missions/1 should update mission 1 but it creates a new mission with a new ID.

return $this->manager->post($mission);
The post seems odd when you want to update (put) an object.

Related

TYPO3 tx_form conditional skip steps

I using TYPO3 8 LTS and i want to extend the form.
Right now I am trying to add a condition in my form that skips all other steps and runs my finishers. In the documentation it is written that you have to use the afterInitializeCurrentPage function:
/**
* #param FormRuntime $formRuntime
* #param CompositeRenderableInterface $currentPage
* #param null|CompositeRenderableInterface $lastPage
* #param mixed $requestArguments submitted value of the element *before post processing*
* #return CompositeRenderableInterface
*/
public function afterInitializeCurrentPage(
FormRuntime $formRuntime,
CompositeRenderableInterface $currentPage,
CompositeRenderableInterface $lastPage = null,
array $requestArguments = []
): CompositeRenderableInterface {
if ($requestArguments['personalized'] === '0') {
// code here ...
}
return $currentPage;
}
My problem is i do not know how i execute the finishers out of this function..
i hope someone can give me a hint or something else..
[EDIT]
next problem is if i use the afterInitializeCurrentPage method i get an exception for other forms in my site:
Argument 2 passed to VENDOR\YourNamespace\YourClass::afterInitializeCurrentPage() must implement interface TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface, null given, called in [..]/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php on line 254
Many Thanks!
You can call the finisher Class Like below.
You need to add below line in ext_localconf.php file. like this
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'][]
= \VENDOR\YourNamespace\Hooks\YourClass::class;
After added this function like below on path Classes/Hooks/YourClass.php.
<?php
namespace \VENDOR\YourNamespace\Hooks;
class YourClass
{
/**
* #param \TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime
* #param \TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface $currentPage
* #param null|\TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface $lastPage
* #param mixed $elementValue submitted value of the element *before post processing*
* #return \TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface
*/
public function afterInitializeCurrentPage(\TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime, \TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface $currentPage, \TYPO3\CMS\Form\Domain\Model\Renderable\CompositeRenderableInterface $lastPage = null, array $requestArguments = []): CompositeRenderableInterface
{
return $currentPage;
}
}

Symfony 3: Default URI path / prefix for FOSRestController

I´m implementing a REST API with symfony 3 and FOSRestBundle. I want all REST methods to be available under the endpoint http://tld.com/api/v1/*. So one - not elegant - solution is to provide the complete route / path as annotation:
/**
* #param $id
* #param Request $request
* #return View
* #Rest\Post("/api/v1/order/{id}")
*/
public function updateAction($extId, Request $request)
{
.....
Adding the full route in each annotation generates redundant code. Besides that if I want to change the version of the API to v2 I have to update all methods - not that clever.
My preferred result:
/**
* #param $id
* #param Request $request
* #return View
* #Rest\Post("/order/{id}")
*/
public function updateAction($extId, Request $request)
{
.....
So my question is:
Is there a way to define a general route prefix like '/api/v1' in the FOSRestBundle config?
Thanks for every hint :)
Ciao

Adding customer_balance_amount to Magento 2's Order REST API

By default Magento's Order REST API doesn't submit the store credit amount used for the order(customer_balance_amount col in the DB). I need to expose that to the API interface, but as of now am unable to. I tried two approaches:
http://magehit.com/blog/how-to-get-value-of-custom-attribute-on-magento-2-rest-api/ - using an observer, but that doesn't seem to have any reflection on the API data
and
http://www.ipragmatech.com/extend-magento2-rest-api-easy-steps/ - which I successfully tried, but it concerns actually creating a new ednpoint instead of overriding/extending the current API.
I was actually able to reproduce that by directly altering the OrderInterface and Order model inside the module-sales core module, but I want to achieve that the "proper" way instead of modifying core.
I would be thankful if anyone shares some knowledge how to do that.
Edit: adding the code that made the solution working, but the goal is to make it the proper way, not edit the core files like so:
vendor/magento/module-sales/Api/Data/OrderInterface.php:
/*
* Customer Balance Amount
*/
const CUSTOMER_BALANCE_AMOUNT = 'customer_balance_amount';
/**
* Returns customer_balance_amount
*
* #return float Customer Balance Amount
*/
public function getCustomerBalanceAmount();
/**
* Sets the customer_balance_amount for the order.
*
* #param float $amount
* #return $this
*/
public function setCustomerBalanceAmount($amount);
vendor/magento/module-sales/model/Order.php:
/**
* Returns customer_balance_amount
*
* #return float
*/
public function getCustomerBalanceAmount()
{
return $this->getData(OrderInterface::CUSTOMER_BALANCE_AMOUNT);
}
/**
* Sets the customer_balance_amount for the order.
*
* #param float $amount
* #return $this
*/
public function setCustomerBalanceAmount($amount)
{
return $this->setData(OrderInterface::CUSTOMER_BALANCE_AMOUNT, $amount);
}
Regards,
Alex
It looks like this is actually a bug, since Magento does define the balance columns as extension attributes in vendor/magento/module-customer-balance/etc/extension_attributes.xml
Looking at the GiftMessage module, the way to do this is via a plugin.
vendor/magento/module-gift-message/etc/di.xml
<type name="Magento\Sales\Api\OrderRepositoryInterface">
<plugin name="save_gift_message" type="Magento\GiftMessage\Model\Plugin\OrderSave"/>
<plugin name="get_gift_message" type="Magento\GiftMessage\Model\Plugin\OrderGet"/>
</type>
\Magento\GiftMessage\Model\Plugin\OrderGet
/**
* Get gift message for order
*
* #param \Magento\Sales\Api\Data\OrderInterface $order
* #return \Magento\Sales\Api\Data\OrderInterface
*/
protected function getOrderGiftMessage(\Magento\Sales\Api\Data\OrderInterface $order)
{
$extensionAttributes = $order->getExtensionAttributes();
if ($extensionAttributes && $extensionAttributes->getGiftMessage()) {
return $order;
}
try {
/** #var \Magento\GiftMessage\Api\Data\MessageInterface $giftMessage */
$giftMessage = $this->giftMessageOrderRepository->get($order->getEntityId());
} catch (NoSuchEntityException $e) {
return $order;
}
/** #var \Magento\Sales\Api\Data\OrderExtension $orderExtension */
$orderExtension = $extensionAttributes ? $extensionAttributes : $this->orderExtensionFactory->create();
$orderExtension->setGiftMessage($giftMessage);
$order->setExtensionAttributes($orderExtension);
return $order;
}

How to handle entity update (PUT request) in REST API using FOSRestBundle

I am prototyping a REST API in Symfony2 with FOSRestBundle using JMSSerializerBundle for entity serialization. With GET request I can use the ParamConverter functionality of SensioFrameworkExtraBundle to get an instance of an entity based on the id request parameter and when creating a new entity with POST request I can use the FOSRestBundle body converter to create a new instance of the entity based on the request data. But when I want to update an existing entity, using the FOSRestBundle converter gives an entity without id (even when the id is sent with the request data) so if I persist it, it will create a new entity. And using SensioFrameworkExtraBundle converter gives me the original entity without the new data so I would have to manually get the data from the request and call all the setter methods to update the entity data.
So my question is, what is the preferred way to handle this situation? Feels like there should be some way to handle this using the (de)serialization of the request data. Am I missing something related to the ParamConverter or JMS serializer that would handle this situation? I do realize that there are many ways to do this kind of things and none of them are right for every use case, just looking for something that fits this kind of rapid prototyping you can do by using the ParamConverter and minimal code required to be written in the controllers/services.
Here is an example of a controller with the GET and POST actions as described above:
namespace My\ExampleBundle\Controller;
use My\ExampleBundle\Entity\Entity;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
class EntityController extends Controller
{
/**
* #Route("/{id}", requirements={"id" = "\d+"})
* #ParamConverter("entity", class="MyExampleBundle:Entity")
* #Method("GET")
* #Rest\View()
*/
public function getAction(Entity $entity)
{
return $entity;
}
/**
* #Route("/")
* #ParamConverter("entity", converter="fos_rest.request_body")
* #Method("POST")
* #Rest\View(statusCode=201)
*/
public function createAction(Entity $entity, ConstraintViolationListInterface $validationErrors)
{
// Handle validation errors
if (count($validationErrors) > 0) {
return View::create(
['errors' => $validationErrors],
Response::HTTP_BAD_REQUEST
);
}
return $this->get('my.entity.repository')->save($entity);
}
}
And in config.yml I have the following configuration for FOSRestBundle:
fos_rest:
param_fetcher_listener: true
body_converter:
enabled: true
validate: true
body_listener:
decoders:
json: fos_rest.decoder.jsontoform
format_listener:
rules:
- { path: ^/api/, priorities: ['json'], prefer_extension: false }
- { path: ^/, priorities: ['html'], prefer_extension: false }
view:
view_response_listener: force
If you are using PUT, according to REST, you should use a route for the update with the id of the entity in question in the route itself like /entity/{entity}. FOSRestBundle does it that way too.
In your case this should be something like:
/**
* #Route("/{entityId}", requirements={"entityId" = "\d+"})
* #ParamConverter("entity", converter="fos_rest.request_body")
* #Method("PUT")
* #Rest\View(statusCode=201)
*/
public function putAction($entityId, Entity $entity, ConstraintViolationListInterface $validationErrors)
EDIT: It would actually be even better to have two entities injected. One being the current database state and one being the sent data from the client. You can achieve this with two ParamConverter-annotations:
/**
* #Route("/{id}", requirements={"id" = "\d+"})
* #ParamConverter("entity")
* #ParamConverter("entityNew", converter="fos_rest.request_body")
* #Method("PUT")
* #Rest\View(statusCode=201)
*/
public function putAction(Entity $entity, Entity $entityNew, ConstraintViolationListInterface $validationErrors)
This will load the current db state into $entity and the uploaded data into $entityNew. Now you can merge the data as you see fit.
If it's fine for you to just overwrite the data without merging/checking, then use the first option. But keep in mind that this would allow creating a new entity if the client sends a not yet used id if you do not prevent that.
Seems one way would be to use Symfony Form component (with SimpleThingsFormSerializerBundle) as described in http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/#post-it
Quote from SimpleThingsFormSerializerBundle README:
Additionally all the current serializer components share a common flaw: They cannot deserialize (update) into existing object graphs. Updating object graphs is a problem the Form component already solves (perfectly!).
I also had a problem with the processing of PUT requests using JMS serializer. First of all I would like to automate the processing of queries using the serializer. The put request may not contain the complete data. Part of the data must be map on entity. You can use my simple solution:
/**
* #Route(path="/edit",name="your_route_name", methods={"PUT"})
*
* This parameter is using for creating a current fields of request
* #RequestParam(
* name="id",
* requirements="\d+",
* nullable=false,
* allowBlank=true,
* strict=true,
* )
* #RequestParam(
* name="some_field",
* requirements="\d{13}",
* nullable=true,
* allowBlank=true,
* strict=true,
* )
* #RequestParam(
* name="some_another_field",
* requirements="\d{13}",
* nullable=true,
* allowBlank=true,
* strict=true,
* )
* #param Request $request
* #param ParamFetcher $paramFetcher
* #return Response
*/
public function editAction(Request $request, ParamFetcher $paramFetcher)
{
//validate parameters
$paramFetcher->all();
/** #var EntityManager $em */
$em = $this->getDoctrine()->getManager();
$yourEntity = $em->getRepository('YourBundle:SomeEntity')->find($paramFetcher->get('id'));
//get request params (param fetcher has all params, but we need only params from request)
$data = $request->request->all();
$this->mapDataOnEntity($data, $yourEntity, ['some_serialized_group','another_group']);
$em->flush();
return new JsonResponse();
}
Method mapDataOnEntity you can locate in some trait or in you intermediate controller class. Here is his implementation of this method:
/**
* #param array $data
* #param object $targetEntity
* #param array $serializationGroups
*/
public function mapDataOnEntity($data, $targetEntity, $serializationGroups = [])
{
/** #var object $source */
$sourceEntity = $this->get('jms_serializer')
->deserialize(
json_encode($data),
get_class($targetEntity),
'json',
DeserializationContext::create()->setGroups($serializationGroups)
);
$this->fillProperties($data, $targetEntity, $sourceEntity);
}
/**
* #param array $params
* #param object $targetEntity
* #param object $sourceEntity
*/
protected function fillProperties($params, $targetEntity, $sourceEntity)
{
$propertyAccessor = new PropertyAccessor();
/** #var PropertyMetadata[] $propertyMetadata */
$propertyMetadata = $this->get('jms_serializer.metadata_factory')
->getMetadataForClass(get_class($sourceEntity))
->propertyMetadata;
foreach ($propertyMetadata as $realPropertyName => $data) {
$serializedPropertyName = $data->serializedName ?: $this->fromCamelCase($realPropertyName);
if (array_key_exists($serializedPropertyName, $params)) {
$newValue = $propertyAccessor->getValue($sourceEntity, $realPropertyName);
$propertyAccessor->setValue($targetEntity, $realPropertyName, $newValue);
}
}
}
/**
* #param string $input
* #return string
*/
protected function fromCamelCase($input)
{
preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
$ret = $matches[0];
foreach ($ret as &$match) {
$match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
}
return implode('_', $ret);
}
The best way is using JMSSerializerBundle
The problem is JMSSerializer initializes with the default ObjectConstructor for deserialization (setting the fields that are not in the request as null, and making that merge method will also persist null properties to database). So you need to switch this one with the DoctrineObjectConstructor.
services:
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false
Then just deserialize and persist the entity, and it will be filled with the missing fields. When you save to database only the attributes that have changed will be updated on the database:
$foo = $this->get('jms_serializer')->deserialize(
$request->getContent(),
'AppBundle\Entity\Foo',
'json');
$em = $this->getDoctrine()->getManager();
$em->persist($foo);
$em->flush();
Credits to: Symfony2 Doctrine2 De-Serialize and Merge Entity issue
I'm having the same issue as you described, I just do the entity merging manually:
public function patchMembersAction($memberId, Member $memberPatch)
{
return $this->members->updateMember($memberId, $memberPatch);
}
This calls method that does the validation, and then manually calls all the required setter methods. Anyway, I'm wondering about writing my own param converter for such cases.
Another resource which helped me a lot is http://welcometothebundle.com/symfony2-rest-api-the-best-2013-way/. A step by step tutorial which filled in the blanks I had after the resource in the previous comment. Good luck!

An error occurred while trying to call Controller->createAction()

I am trying to create something with extbase, but the error-message I get is not very helpful. I took the blog_example extension as a guide. A (maybe) important difference is: I don't have a database table because I want to write a custom domain repository that connects to an external servive through REST.
The actual error message (displayed above the plugin, not as an exception message):
An error occurred while trying to call Tx_MyExt_Controller_SubscriptionController->createAction()
Classes/Controller/SubscriptionController:
Stripped down to the important parts.
class Tx_MyExt_Controller_SubscriptionController extends Tx_Extbase_MVC_Controller_ActionController
{
/**
* #var Tx_MyExt_Domain_Repository_SubscriberRepository
*/
protected $subscriberRepository;
/**
* #return void
*/
public function initializeAction()
{
$this->subscriberRepository = t3lib_div::makeInstance('Tx_MyExt_Domain_Repository_SubscriberRepository');
}
/**
* #param Tx_MyExt_Domain_Model_Subscriber $subscriber
* #dontvalidate $subscriber
* #return string The rendered view
*/
public function newAction(Tx_MyExt_Domain_Model_Subscriber $subscriber = null)
{
$this->view->assign('subscriber', $subscriber);
}
/**
* #param Tx_MyExt_Domain_Model_Subscriber $subscriber
* #return string The rendered view
*/
public function createAction(Tx_MyExt_Domain_Model_Subscriber $subscriber)
{ }
}
Classes/Domain/Model/Subscriber
class Tx_MyExt_Domain_Model_Subscriber extends Tx_Extbase_DomainObject_AbstractEntity
{
/**
* #var string
* #dontvalidate
*/
protected $email = '';
/**
* #param string $email
* #return void
*/
public function setEmail($email)
{
$this->email = $email;
}
/**
* #return string
*/
public function getEmail()
{
return $this->email;
}
}
Resources/Private/Templates/Subscription/new
<f:form action="create" controller="Subscription" objectName="Subscriber" object="{subscriber}" method="post">
<f:form.textfield property="email"></f:form.textfield>
<f:form.submit value="submit"></f:form.submit>
</f:form>
Facts
Adding $subscriber = null removes the message. But $subscriber is null then
A var_dump($this->request->getArguments()); displays the form's fields
There is an index action, and it is also the first action defined in ext_localconf.php
The hints and solutions I found aren't working for me, so I hope someone can guide me into the right direction.
I've got the same bug.
If you pass an Model as argument to an method, it will also validate the model fields.
I've had this annotation on my model property:
/**
*
* #var \string
* #validate NotEmpty
*/
It validates the "#validate" annotation.
The field in the database was empty so i got the error message
An error occurred while trying to call ...
It would be good if there was a better error message.
You need to customize the validation annotation or verify that the property is not empty in the database
Hope it helps somebody
In addtion: check any Validations in your Model and your TCA. If a field is marked as #validate NotEmpty in your Model and is not marked appropriately in the TCA, a record can be saved ignoring the #validate settings in the Model. This can happen if you change the Model and/or TCA after creating records.
An example:
Field 'textfield' is set to not validate, both in the TCA and the Model. You create a new record and save it without filling in the field 'textfield' (you can, it is not set to validate). You then change the Model setting 'textfield' to #validate NotEmpty and then try to show the record on the FE, you will get the error.
The solution for that example:
Simply remove the validation in your Model OR check validations in the TCA and Model so that they work together.
--
A German blog post covers this solution: http://www.constantinmedia.com/2014/04/typo3-extbase-an-error-occurred-while-trying-to-call-anyaction/
just override the template method getErrorFlashMessage in yout controller to provide a custom error message...
/**
* A template method for displaying custom error flash messages, or to
* display no flash message at all on errors. Override this to customize
* the flash message in your action controller.
*
* #return string|boolean The flash message or FALSE if no flash message should be set
* #api
*/
protected function getErrorFlashMessage() {
return 'An error occurred while trying to call ' . get_class($this) . '->' . $this->actionMethodName . '()';
}
classic case of "start over from scratch and it works, and if you compare it you have the same code, though".
I updated the code in the question, maybe it helps someone.