JMSSerializerBundle - preserve relation name - symfony

I'm using Symfony2 with JMSSerializerBundle. And I'm new with last one =) What should I do in such case:
I have Image model. It contains some fields, but the main one is "name". Also, I have some models, which has reference to Image model. For example User and Application. User model has OneToOne field "avatar", and Application has OneToOne field "icon". Now, I want to serialize User instance and get something like
{
...,
"avatar": "http://example.com/my/image/path/image_name.png",
....
}
Also, I want to serialize Application and get
{
...,
"icon": "http://example.com/my/image/path/another_image_name.png",
...
}
I'm using #Inline annotation on User::avatar and Application::icon fields to reduce Image object (related to this field) to single scalar value (only image "name" needed). Also, my Image model has ExclusionPolicy("all"), and exposes only "name" field. For now, JMSSerializer output is
(For User instance)
{
...,
"name": "http://example.com/my/image/path/image_name.png",
...
}
(For Application instance)
{
...,
"name": "http://example.com/my/image/path/another_image_name.png",
...
}
The question is: How can I make JMSSerializer to preserve "avatar" and "icon" keys in serialized array instead of "name"?

Finally, I found solution. In my opinion, it is not very elegant and beautiful, but it works.
I told to JMSSerializer, that User::avatar and Application::icon are Images. To do that, I used annotation #Type("Image")
//src\AppBundle\Entity\User.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="avatar", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $avatar;
//...
//src\AppBundle\Entity\Application.php
//...
/**
* #var integer
*
* #ORM\OneToOne(targetEntity="AppBundle\Entity\Image")
* #ORM\JoinColumn(name="icon", referencedColumnName="id")
*
* #JMS\Expose()
* #JMS\Type("Image")
*/
private $icon;
//...
I implemented handler, which serializes object with type Image to json.
<?php
//src\AppBundle\Serializer\ImageTypeHandler.php
namespace AppBundle\Serializer;
use AppBundle\Entity\Image;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Symfony\Component\HttpFoundation\Request;
class ImageTypeHandler implements SubscribingHandlerInterface
{
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
static public function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'Image',
'method' => 'serializeImageToWebPath'
]
];
}
public function serializeImageToWebPath(JsonSerializationVisitor $visitor, Image $image = null, array $type, Context $context)
{
$path = $image ? "http://" . $this->request->getHost() . "/uploads/images/" . $image->getPath() : '';
return $path;
}
}
And the last step is to register this handler. I also injected request service to generate full web path to image in my handler.
app.image_type_handler:
class: AppBundle\Serializer\ImageTypeHandler
arguments: ["#request"]
scope: request
tags:
- { name: jms_serializer.subscribing_handler }
Also, you can use this workaround, to modify serialized data in post_serialize event.

Related

Return array of Strings in API Platform

I'm new to API Platform and am trying to clone an existing API. I have an entity Tag that has properties which are an id and a name. The default behaviour for /api/tags is to return an array of objects. Something like:
[
{
"id": 1,
"name": "tag1"
},
{
"id": 2,
"name": "tag2"
}
]
What I actually want in the output is just a simple array of strings:
[
"tag1",
"tag2"
]
From the documentation, it sounds like I need to register a data transformer:
final class TagOutputTransformer implements DataTransformerInterface
{
/**
* {#inheritdoc}
*/
public function transform($data, string $to, array $context = [])
{
return $data->name;
}
/**
* {#inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return $data instanceof Tag;
}
}
services.yaml:
services:
'App\DataTransformer\TagOutputTransformer': ~
And annotate my entity:
/**
* #ApiResource(
* collectionOperations={
* "get"={
* "method"="GET",
* "output"="string"
* }
* }
* )
* #ORM\Entity(repositoryClass=TagRepository::class)
*/
The trouble is that string isn't a valid thing to put there in the annotation. I get Class string does not exist. If I remove the quotes, I get Couldn't find constant string.
Is this the right approach? What am I doing wrong?
By specifying "output"="string", you tell API Platform to transform your Tag resource to a string instance for output. string, however, is not a class and can not be instantiated (or transformed to).
What you want is to tell API Platform to transform Tag into a an object that represents your desired data representation.
For example, implement a custom TagCollectionOutput class (holding an array of Tag names) and add "output"=TagCollectionOutput::class to Tag. Then have TagOutputTransformer transform $data into a TagCollectionOutput instance.

How to assign an array from request to a class property with ParamConverter in Symfony 5

I use Symfony 5 and have a trouble with the ParamConverter bundle when I try to map request to my DTO class.
In my controller I use create method:
/**
* #Rest\Post("/project/{projectId}/blogger-mix/")
* #ParamConverter("command", class=CreateBloggerMixCommand::class, converter="fos_rest.request_body")
*/
public function create(string $projectId, CreateBloggerMixCommand $command, CreateBloggerMixHandler $handler): View
{
$command->projectId = $projectId;
try {
$bloggerSetItem = $handler->handle($command);
return new View(['id' => $bloggerSetItem->getId()], Response::HTTP_OK);
} catch (\Throwable $exception){
return $this->handleErrors($exception);
}
}
The DTO looks like:
class CreateBloggerMixCommand implements CommandInterface
{
/**
* #var array|string[]
*/
public array $bloggerSetItems;
}
When I send request with an array:
{
"bloggerSetItems": [
"f04a76e0-d70e-41df-a926-e180c78b34fc",
"07f6d304-9c97-41e9-8f2d-4a993019280c"
]
}
I receive an error:
{
"success": false,
"status": 500,
"errors": "You must define a type for App\\Project\\Api\\Command\\CreateBloggerMix\\CreateBloggerMixCommand::$bloggerSetItems."
}
In a nutshell, I can't figure out why ParamConverter can't resolve property array. If I change array to string, then ParamConverter responds that can't convert an array to a string it means that ParamConverter eventualy can see the property, but can't resolve array exactly...
Any idea welcome!
Thanx to our partner we found the following solution:
In DTO add:
use JMS\Serializer\Annotation as Serializer;
And in annotation:
/**
* #Assert\NotBlank
* #Assert\Type (type="array")
* #Serializer\Type(name="array<string>")
* #var array|string[]
*/
public array $bloggerSetItems;
I think "You must define a type" refers to database column type. You should just need to add the column definition and then update the database. There are three options for storing an array, choose one:
/**
* #var array|string[]
*
* #Column(type="array") // one of these lines
* #Column(type="simple_array") // one of these lines
* #Column(type="json_array") // one of these lines
*/
public array $bloggerSetItems;
https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types

How to return specific data using urls and routing in symfony 4 when making an API GET request?

I'm new to Symfony and trying to learn the basics. I recently saw this question and I wanted to learn how routing works. So I copied the Controller1.php from the question and changed it to UserController.php this:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UsersController extends AbstractController
{
/**
* #Route("/listOf/Users", methods={"GET"})
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App\Entity\User')->findAll());
}
}
Which indeed, as OP claims, works fine and return the following (manually added data using Sequel Pro) list:
[
{
"id": 14,
"name": "user1 Name"
},
{
"id": 226,
"name": "user2 Name"
},
{
"id": 383,
"name": "user3 Name"
}
]
So my next step was to learn how to adjust this list of users to return a specific user with a given id. So I followed the official Symfony Docs on Routing. So I changed the code to the following:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UsersController extends AbstractController
{
/**
* #Route("/listOf/Users/{IdUser}", requirements={"IdUser"="\d+"}, methods={"GET"})
* #param Request $request
* #param int $IdUser
* #return JsonResponse
*/
public function list(Request $request, int $IdUser)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App\Entity\User\{IdUser}')->findAll());
}
}
and tried to request the data of the user with the id 14, but this didn't work and yielded the following error:
Class App\Entity\User{IdUser} does not exist (500 Internal Server Error)
What more changes do I need to do to be able to do what I'm trying to do?
This is my User.php entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements \JsonSerializable
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function jsonSerialize()
{
return get_object_vars($this);
}
}
And my UserRepository.php has nothing beside the automatically generated code in it.
Edit: My first request which worked was of the form: http://domainName.local:80/listOf/Users and my second one was: http://domainName.local:80/listOf/Users/14
As promised earlier - here's why it does not work and how to make it work.
Let's examine the code blow:
$this->getDoctrine()->getRepository('App\Entity\User\{IdUser}')->findAll();
Basically you're saying: doctrine, give me the repository that is responsible for handling
the entity App\Entity\User\{IdUser} literally and ofc there is no such entity class.
What you really want is the repo for App\Entity\User.
The string you pass to the getRepository() method always has to be the fully qualified class name of an entity - period.
To ensure you never have any typos here, it's quite helpful to use the class constant of the entity, which looks like so
$repo = $this->getDoctrine()->getRepository(App\Entity\User::class);
Once you have the repository, you can call it's different methods as shown in the doctrine documentation here https://www.doctrine-project.org/api/orm/latest/Doctrine/ORM/EntityRepository.html
In your case, you have the variable $IdUser, which you want to be mapped to the db column/entity property id of the user class.
Since you know that you want exactly this one user with the id 14, all you have to do is tell the repo to find exactly one which looks like this.
// here's the example for your specific case
$user = $repo->findOneBy(['id' => $IdUser]);
// another example could be e.g. to search a user by their email address
$user = $repo->findOneBy(['email' => $email]);
// you can also pass multiple conditions to find*By methods
$user = $repo->findOneBy([
'first_name' => $firstName,
'last_name' => $lastName,
]);
Hopefully, this was more helpful than confusing =)

Symfony FOSRestBundle add custom header to response

I use FOSRestBundle in Symfony 4 to API project. I use annotations and in controller I have for example
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* #Rest\Get("/api/user", name="index",)
* #param UserRepository $userRepository
* #return array
*/
public function index(UserRepository $userRepository): array
{
return ['status' => 'OK', 'data' => ['users' => $userRepository->findAll()]];
}
config/packages/fos_rest.yaml
fos_rest:
body_listener: true
format_listener:
rules:
- { path: '^/api', priorities: ['json'], fallback_format: json, prefer_extension: false }
param_fetcher_listener: true
view:
view_response_listener: 'force'
formats:
json: true
Now I'd like to add custom header 'X-Total-Found' to my response. How to do it?
You are relying in FOSRestBundle ViewListener, so that gives you limited options, like not being able to pass custom headers. In order to achieve what you want, you will need to call $this->handleView() from your controller and pass it a valid View instance.
You can use the View::create() factory method or the controller $this->view() shortcut. Both take as arguments the array of your data, the status code, and a response headers array. Then, you can set up your custom header there, but you will have to do that for every call.
The other option you have, which is more maintainable, is register a on_kernel_response event listener/subscriber and somehow pass it the value of your custom header (you could store it in a request attribute for example).
Those are the two options you have. You may have a third one, but I cannot come up with it at the minute.
I ran into the same issue. We wanted to move pagination meta information to the headers and leave the response without an envelope (data and meta properties).
My Environment
Symfony Version 5.2
PHP Version 8
FOS Rest Bundle
STEP 1: Create an object to hold the header info
// src/Rest/ResponseHeaderBag.php
namespace App\Rest;
/**
* Store header information generated in the controller. This same
* object is used in the response subscriber.
* #package App\Rest
*/
class ResponseHeaderBag
{
protected array $data = [];
/**
* #return array
*/
public function getData(): array
{
return $this->data;
}
/**
* #param array $data
* #return ResponseHeaderBag
*/
public function setData(array $data): ResponseHeaderBag
{
$this->data = $data;
return $this;
}
public function addData(string $key, $datum): ResponseHeaderBag
{
$this->data[$key] = $datum;
return $this;
}
}
STEP 2: Inject the ResponseHeaderBag into the controller action
public function searchCustomers(
ResponseHeaderBag $responseHeaderBag
): array {
...
...
...
// replace magic strings and numbers with class constants and real values.
$responseHeaderBag->add('X-Pagination-Count', 8392);
...
...
...
}
STEP 3: Register a Subscriber and listen for the Response Kernel event
// config/services.yaml
App\EventListener\ResponseSubscriber:
tags:
- kernel.event_subscriber
Subscribers are a great way to listen for events.
// src/EventListener/ResponseSubscriber
namespace App\EventListener;
use App\Rest\ResponseHeaderBag;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ResponseSubscriber implements EventSubscriberInterface
{
public function __construct(
protected ResponseHeaderBag $responseHeaderBag
){
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => ['addAdditionalResponseHeaders']
];
}
/**
* Add the response headers created elsewhere in the code.
* #param ResponseEvent $event
*/
public function addAdditionalResponseHeaders(ResponseEvent $event): void
{
$response = $event->getResponse();
foreach ($this->responseHeaderBag->getData() as $key => $datum) {
$response->headers->set($key, $datum);
}
}
}

Symfony - FOSRestBundle - show selected fields

I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio

Resources