I'm using JMS serializer in my Symfony projuect and i have a question about it. I want ot expose property from entity for only one specific method (one route), in other cases i dont want this property to be exposed. I would be appreciate for any advices)
You can probably achieve this using the #Groups annotation on your properties and then tell the serializer which groups to serialize in your controller.
use JMS\Serializer\Annotation\Groups;
class BlogPost
{
/** #Groups({"list", "details"}) */
private $id;
/** #Groups({"list", "details"}) */
private $title;
/** #Groups({"list"}) */
private $nbComments;
/** #Groups({"details"}) */
private $comments;
private $createdAt;
}
And then:
use JMS\Serializer\SerializationContext;
$serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('list')));
//will output $id, $title and $nbComments.
$serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('Default', 'list')));
//will output $id, $title, $nbComments and $createdAt.
More info here.
Related
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"
}
I’m developing a form in zf2 and I want to calculate a value based upon user input and set it in a field after the form has validated. In the form, there is a firstName field and a lastName field; and I want to use the validated input to calculate a value to populate in a fullName field.
I assume I want to set the value something like this, but haven’t found the right code for setting the "element" that gets sent to the database:
public function addAction()
{
$objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$form = new AddMemberForm($objectManager);
$member = new Member();
$form->bind($member);
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
// develop full name string and populate the field
$calculatedName = $_POST['firstName'] . " " . $_POST['lastName'];
$member->setValue('memberFullName', $calculatedName);
$this->getEntityManager()->persist($member);
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('admin-members');
}
}
return array('form' => $form);
}
Doctrine's built-in Lifecycle Callbacks are perfectly fits for handling such requirement and I strongly recommend to use them.
You just need to correctly annotate the entity.
For example:
<?php
/**
* Member Entity
*/
namespace YourNamespace\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* #ORM\Table(name="members")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Member
{
// After all of your entity properies, getters and setters... Put the method below
/**
* Calculate full name on pre persist.
*
* #ORM\PrePersist
* #return void
*/
public function onPrePersist(LifecycleEventArgs $args)
{
$this->memberFullName = $this->getFirstName().' '.$this->getLastName();
}
}
With this way, memberFullName property of the entity will be automatically populated using first and last names on entity level just before persisting.
Now you can remove the lines below from your action:
// Remove this lines
$calculatedName = $_POST['firstName'] . " " . $_POST['lastName'];
$member->setValue('memberFullName', $calculatedName);
Foozy’s excellent answer provides a solution that works for an add action, and the response to my comment directed me to the following solution that works for both add and edit actions:
<?php
/**
* Member Entity
*/
namespace YourNamespace\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Event\PreFlushEventArgs;
/**
* #ORM\Table(name="members")
* #ORM\Entity
*/
class Member
{
// After all of your entity properies, getters and setters... Put the method below
/**
* Calculate full name on pre flush.
*
* #ORM\PreFlush
* #return void
*/
public function onPreFlush(PreFlushEventArgs $args)
{
$this->memberFullName = $this->getFirstName().' '.$this->getLastName();
}
}
I'm trying to make an API Rest in Symfony2 using Parse as cloud database.
If I try to retrieve the Parse users it works fine and returns the expected data.
Local url example: http://www.foo.local/app_dev.php/getUsers/
Here is the code I use in the Users controller (I use annotations in order to set the routes in the controller):
namespace Foo\ApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations\View;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseUser;
class UsersController extends Controller
{
/**
* #return array
* #View()
* #Route("/getUsers/")
*/
public function getUsersAction(Request $request) {
ParseClient::initialize(<my Parse keys>);
$query = ParseUser::query();
$results = $query->find();
return array('users' => $results);
}
}
However if I try the same with my Products ParseObjects, I get the following error message:
error code="500" message="Internal Server Error" exception
class="Doctrine\Common\Annotations\AnnotationException"
message="[Semantical Error] The annotation "#returns" in method
Parse\ParseFile::getData() was never imported. Did you maybe forget to
add a "use" statement for this annotation?"
Local url example: http://www.foo.local/app_dev.php/getProducts/
The Products controller code:
namespace Foo\ApiBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations\View;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Parse\ParseClient;
use Parse\ParseObject;
use Parse\ParseQuery;
use Parse\ParseUser;
use Parse\ParseFile;
class ProductsController extends Controller
{
/**
* #return array
* #View()
* #Route("/getProducts/")
*/
public function getProductsAction(Request $request) {
ParseClient::initialize(<my Parse keys>);
$query = new ParseQuery("Products");
$results = $query->find();
return array('products' => $results);
}
}
If instead of returning $results I return other dummy data, like return array('products' => 'fooProducts'), I no longer get the error message.
Also if I make a var_dump of the $results variable, I get the expected array of ParseObjects.
Here is my routing.yml file in case there is something wrong with it:
api:
resource: "#FooApiBundle/Controller/"
type: annotation
prefix: /
users:
type: rest
resource: Foo\ApiBundle\Controller\UsersController
products:
type: rest
resource: Foo\ApiBundle\Controller\ProductsController
By the error message it seems that the problem is related to Doctrine, but since I'm not using it, I don't know exactly how there can be a conflict or how to fix it. Any suggestions?
There are a few DocBlock typos of #returns in the Parse\ParseFile class that is causing Doctrine's Annotations class to attempt to identify them as a class. This is not your fault but a bug in the Parse PHP SDK library.
I've made a fix in this commit and submitted a pull request back to the original devs, so it should be a simple matter of eventually running composer update to bring your Parse library to the latest correct version.
You can read more about DocBlock and the part specifically on Annotations here
Here is a copy/paste of the resulting diff for src/Parse/ParseFile.php:
## -31,7 +31,7 ## class ParseFile implements \Parse\Internal\Encodable
/**
* Return the data for the file, downloading it if not already present.
*
- * #returns mixed
+ * #return mixed
*
* #throws ParseException
*/
## -50,7 +50,7 ## public function getData()
/**
* Return the URL for the file, if saved.
*
- * #returns string|null
+ * #return string|null
*/
public function getURL()
{
## -112,7 +112,7 ## public function getMimeType()
* #param string $name The file name on Parse, can be used to detect mimeType
* #param string $mimeType Optional, The mime-type to use when saving the file
*
- * #returns ParseFile
+ * #return ParseFile
*/
public static function createFromData($contents, $name, $mimeType = null)
{
## -132,7 +132,7 ## public static function createFromData($contents, $name, $mimeType = null)
* #param string $name Filename to use on Parse, can be used to detect mimeType
* #param string $mimeType Optional, The mime-type to use when saving the file
*
- * #returns ParseFile
+ * #return ParseFile
*/
public static function createFromFile($path, $name, $mimeType = null)
{
The correct way to initialize Parse using Symfony, is on the setContainer method of your controller:
class BaseController extends Controller
{
....
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer( $container );
ParseClient::initialize( $app_id, $rest_key, $master_key );
}
}
Depending of your needs, you can create a BaseController and extend it in your rest of controllers.
class UsersController extends Controller
In addition, you could add your keys in the parameters.yml file.
parameters:
#your parameters...
ParseAppId: your_id
ParseRestKey: your_rest_key
ParseMasterKey: your_master_key
TIP: Note you can add have different Parse projects (dev and release
version). Add your parameters in your different parameters
configuration provides an easy way to handle this issue.
class BaseController extends Controller
{
....
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer( $container );
$app_id = $container->getParameter('ParseAppId');
$rest_key = $container->getParameter('ParseRestKey');
$master_key = $container->getParameter('ParseMasterKey');
ParseClient::initialize( $app_id, $rest_key, $master_key );
}
}
I'm developing RESTful API for a web service. And I need to expose some properties that do not belong to an entity itself.
For example I have a Pizza entity object, it has it's own size and name properties. I'm outputting it in JSON format with FOSRestBundle and JMSSerializer. I've setup properties annotations for this entity to expose needed properties via serialization groups and it's working great.
But I need to add some properties that do not belong to the entity itself. For example I want my pizza to have property: isFresh that is determined by some PizzaService::isFresh(Pizza $pizza) service. How do I do this?
Should I inject some additional logic to serialization process (if so how)?
Should I create a wrapper entity with properties that I want to expose from original entity plus additional external properties?
Should I add property isFresh to the original Pizza entity and populate in in the controller before serialization?
Should I return additional data independent of entity data (in a sibling JSON properties for example)?
In other words: what are the best practices around this issue? Could you provide examples? Thank you.
I think you can do that with the VirtualProperty annotation :
/**
* #JMS\VirtualProperty
* #return boolean
*/
public function isFresh (){
...
}
Edit : another solution with the Accessor annotation
/** #Accessor(getter="getIsFresh",setter="setIsFresh") */
private $isFresh;
// ...
public function getIsFresh()
{
return $this->isFresh;
}
public function setIsFresh($isFresh)
{
$this->isFresh= $isFresh;
}
In your controller, you call the setIsFresh method
(See http://jmsyst.com/libs/serializer/master/reference/annotation)
I've decided to create my own class to serialize an entity.
Here's the example:
class PizzaSerializer implements ObjectSerializerInterface
{
/** #var PizzaService */
protected $pizzaService;
/**
* #param PizzaService $pizzaService
*/
public function __construct(PizzaService $pizzaService)
{
$this->pizzaService = $pizzaService;
}
/**
* #param Pizza $pizza
* #return array
*/
public function serialize(Pizza $pizza)
{
return [
'id' => $pizza->getId(),
'size' => $pizza->getSize(),
'name' => $pizza->getName(),
'isFresh' => $this->pizzaService->isFresh($pizza),
];
}
}
You just have to configure DC to inject PizzaService into the object serializer and then just call it like this from the controller:
$pizza = getPizzaFromSomewhere();
$pizzaSerializer = $this->get('serializer.pizza');
return $pizzaSerializer->serialize($pizza);
The object serializer will return an array that can be easily converted to JSON, XML, YAML or any other format by using real serializer like JMS Serializer. FOSRestBundle will do this automatically if you configured it so.
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!