Symfony 4 with Hateoas: Relations with Child class - rest

I have a parent class MapItem, and a child class, MapExhibit. My MapExhibit class has a property, $builiding, which ties the exhibit to a particular MapBuilding entity. When the API calls for the JSON, the $building should not appear for MapExhibit entities in particular.
Note I am using the willdurand/Hateoas bundle
Here is my current setup:
/**
* #ORM\Entity(repositoryClass="App\Repository\Map\MapItemRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"item" = "MapItem", "bathroom" = "MapBathroom", "building" = "MapBuilding", "bus" = "MapBus", "emergency" = "MapEmergency", "exhibit" = "MapExhibit", "parking" = "MapParking"})
* #Serializer\XmlRoot("mapItem")
* #Hateoas\Relation("self", href = "expr('/api/mapitems/' ~ object.getId())")
*/
abstract class MapItem
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Serializer\XmlAttribute
*/
private $id;
...
}
/**
* #ORM\Entity(repositoryClass="App\Repository\Map\MapExhibitRepository")
*
* #Hateoas\Relation(
* "building",
* exclusion = #Hateoas\Exclusion()
* )
*/
class MapExhibit extends MapItem
{
...
/**
* Many emergency devices can belong to one building.
* #ORM\ManyToOne(targetEntity="MapBuilding", inversedBy="emergencyDevices")
* #ORM\JoinColumn(name="building_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $building;
...
public function getBuilding(): ?MapBuilding
{
return $this->building;
}
}
The result is a JSON object that includes the relation from the parent MapItem
"_links":{"self":{"href":"\/api\/mapitems\/29"}}}]
But also includes the data from the exhibit's building. This part should be ignored.

Take a look on exclusion strategies in documentation.
If you would like to always expose, or exclude certain properties. Then, you can do this with the annotations #ExclusionPolicy, #Exclude, and #Expose.
The default exclusion policy is to exclude nothing. That is, all properties of the object will be serialized. If you only want to expose a few of the properties, then it is easier to change the exclusion policy, and only mark these few properties: (...)
So basically, put #Serializer\Exclude() above private $builidng. Don't forget to add use JMS\Serializer\Annotation as Serializer; on top.

Related

Persisting a document suddenly stops returning next identifier value (ALNUM strategry)

I have Symfony 2.6 application and using doctrine-odm-bundle 3.0.2 (doctrine-odm: 1.1.2, mongodb: 1.4.0).
My document has referenceMany and referenceOne in attributes and when I create new instance of it, fill fields and persist - it goes fine in the begining. I can create few nearly empty documents, with referenced document(s) or without and it works fine. At some point I am trying to add new item in the database and getting an error:
E11000 duplicate key error collection: test.Product index: _id_ dup key: { : 0 }
The message is clear - I can see that there was a document added to the collection with id = 0, therefore second one can't go -> duplicate entry. But why it suddenly starts to return "0" for id? Even though, I checked doctrine_increment_ids collection - counter for id is being incremented. But $product->getId() becomes "0" after persist.
If I drop the database and start all over - it works, I can still add new products in the collection. Let's say I successfully created 12 products. Creating 13th resulting a document with id=0 being persisted in the collection. 14th fails with duplicate error.
Can you please help to troubleshoot or suggest an idea where does it go wrong?
P.S> I am not considering an upgrade of Symfony2 (at this point) neither as doctrine-odm-bundle (it depends on newer Symfony2 as well) - migration efforts are quite high and I am not sure it will fix the issue. First I want to find out the root cause.
// Document Product
/**
* #MongoDB\Document
* #MongoDB\HasLifecycleCallbacks
*/
class Product
{
/** #MongoDB\Id(strategy="ALNUM", type="int") */
protected $id;
/**
* #Gedmo\ReferenceOne(type="entity", class="Entity\User", inversedBy="products", identifier="userId")
*/
protected $user;
/**
* #MongoDB\Field(name="user_id", type="int")
*/
protected $userId;
/**
* #MongoDB\ReferenceMany(
* targetDocument="Picture",
* discriminatorMap={"file" = "File", "picture" = "Picture"},
* discriminatorField="discr",
* defaultDiscriminatorValue="picture"
* )
* #Assert\Valid
*/
protected $pictures;
...
}
// Entity User
/**
* User entity
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection $textures
*
* #Gedmo\ReferenceMany(type="document", class="Document\Product", mappedBy="user")
*/
protected $products;
...
}
// Document Picture
/**
* #MongoDB\Document
* #MongoDB\InheritanceType("SINGLE_COLLECTION")
* #MongoDB\DiscriminatorField("discr")
* #MongoDB\DiscriminatorMap({"file" = "File", "picture" = "Picture"})
* #MongoDB\DefaultDiscriminatorValue("picture")
* #MongoDB\HasLifecycleCallbacks
*/
class Picture
{
/**
*
* #MongoDB\Id(strategy="ALNUM", type="int")
*/
protected $id;
/**
* #MongoDB\ReferenceOne(targetDocument="Product")
*
* #var Product $product
*/
protected $product;
...
}
Documentation reading always helps (generation strategies). Basically, strategy="ALNUM" and type="int" just can't go together :)
Change strategy to INCREMENT and remove type="int" if you want to have integers in your _id.
Or you can change type to string to continue with _id being an alphanumeric string.

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;
}

symfony api-platform depth

So far we've been struggling with Symfony, Doctrine, and Serializer depth.
I'd like to be able to provide just one-level-depth JSON REST API with Symfony, allowing me to manage my "foreign key" and relation logic directly from the view.
GET /people/1
{
id:1,
name:"theonewhoknocks",
friends: [3, 12, 25]
}
Using FosRESTBundle, we've been strugling at succeeding on that. (we've seen "depth" anotations and "groups" views for models, but none of this fit our need).
The question is simple, before we make a choice for our future API, we have to know:
is api-platform able to provide a dead simple one level (with apparent foreign keys) REST API ?
API Platform can handle that using the Serializer Symfony bundle and its annotation set.
To define what will be returned by an operation we use a normalizationContext which define group(s) of property to include in result of an api operation. Property to include have then this group name linked to #Groups serializer annotation
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity()
* #ApiResource(normalizationContext={"groups"={"read"}}
*/
class Book {
/**
* #ORM\Column()
* #Groups({"read"})
*/
private $title;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="books")
* #Groups({"read"})
*/
private $author;
/**
* Will not be included in result
*/
private $secret_comment;
}
If a relation column is in a Group as $author here, properties defined in a group in the child class will be included in the result
/**
* #ORM\Entity()
* #ApiResource(normalizationContext={"groups"={"read"}})
*/
class User {
/**
* #ORM\Column()
* #Groups({"read"})
*/
private $username;
}
In order to avoid cyclic recursion you can specify the max depth of child relation joins with annotation #MaxDepth(n) where n is the max depth (1 in your case). This annotation has to be enabled with enable_max_depth property in serializer context of the #ApiResource annotation
/**
* #ApiPlatform(normalizationContext={"groups"={"read"}, "enable_max_depth"=true})
*/
class Book {
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="books")
* #Groups({"read"})
* #MaxDepth(1)
*/
private $author;
}
Please note that API Platform is in this case an agregation of existing bundles and features. Refer to the main bundles for detailed informations (here the Symfony Serializer bundle)
Be aware that the MaxDepth on the symfony serializer might not behave as expected, see https://github.com/symfony/symfony/issues/33466
The annotation basically says "from here on, render max N instances of THE SAME CLASS into the graph"
So given a pseudo structure like
Class A:
#MaxDepth(1)
Class B:
Class C:
Class D:
would render the whole thing A.B.C.D, while
Class A:
#MaxDepth(1)
Class B:
Class B:
Class B:
would only render A.B.B
Which is quite different from what e.g. JMS serializer is doing, where MaxDepth really means "From here on, max N steps into the relation graph".
Bad thing is that JMS serializer is not supported by api-platform: https://github.com/api-platform/api-platform/issues/753
So the answer to your question, atm, is: no. :/
As #leberknecht indicated, you might not get the results you are looking for using MaxDepth.
This script:
#[ApiResource(
normalizationContext: ['enable_max_depth'=>true],
)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180)]
private ?string $someProperty = null;
#[ORM\ManyToOne(targetEntity: UserClass::class)]
#[SymfonyMaxDepth(1)]
private ?User $createdBy = null;
}
will return:
{
"id": 123,
"someProperty": "objectProperty",
"createdBy": {
"id": 20,
"someProperty": "parentProperty",
"createdBy": "users/5"
}
}
and this script:
#[ApiResource]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180)]
private ?string $someProperty = null;
#[ORM\ManyToOne(targetEntity: UserClass::class)]
#[ApiProperty(readableLink: false, writableLink: false)]
private ?User $createdBy = null;
}
will return
{
"id": 123,
"someProperty": "objectProperty",
"createdBy": "users/20"
}

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!

zend2 + doctrine 2 Uncaught exception 'Doctrine\Common\Annotations\AnnotationException' $jobId does not exist

I am new to zend 2 and Doctrine 2. I tried to create an entity class but got the following message:
Fatal error: Uncaught exception
'Doctrine\Common\Annotations\AnnotationException' with message
'[Semantical Error] The annotation "#Doctrine\ORM\Mapping\jobId" in
property Workers\Entity\Jobsought::$jobId does not exist, or could not
be auto-loaded
Below is the entity class
namespace Workers\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Factory as InputFactory;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
/**
*
*
* #ORM\Entity
* #ORM\Table(name="worker_main_jobsort")
* #property int $jobId
*/
class Jobsought implements InputFilterAwareInterface
{
protected $inputFilter;
/**
* #ORM\jobId
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $jobId;
/**
* Magic getter to expose protected properties.
*
* #param string $property
* #return mixed
*/
public function __get($property)
{
return $this->$property;
}
/**
* Magic setter to save protected properties.
*
* #param string $property
* #param mixed $value
*/
public function __set($property, $value)
{
$this->$property = $value;
}
}
Any ideas why the ORM cannot map it? The table exist in my database.
Also just started out using the two of these combined - but think I know what your issue is.
First off, you can't try specifying any "strange" (according to doctrine strange) annotations without using the #ignore directive.
Secondly, I think you're trying to say with #property int $jobId that "$jobId" is your PK? Well you're already doing so when you say #ORM\GeneratedValue(strategy="AUTO"), telling doctrine to map jobid to be your PK. Also, I read somewhere that adding name="job_id" to your #Column annotation is good practice, but don't quote me on that. Guess it doesn't really matter.
Hope this helps!
Edit -
My bad, also missed that you need to remove #ORM\jobId as it's not a valid doctrine annotation (jobId that is). Just specify it as #ORM\Id and you should be fine.