Construct object during deserialization on JMS Serializer - symfony

I try to load object from database (Symfony, Doctrine) during deserialization using JMS Serializer. Lets say that I have a simple football api application, two entities Team and Game, teams with id 45 and 46 are already in db.
When creating a new game from json:
{
"teamHost": 45,
"teamGues": 46,
"scoreHost": 54,
"scoreGuest": 42,
}
Game entity:
class Game {
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Team")
* #ORM\JoinColumn(nullable=false)
*/
private $teamHost;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Team")
* #ORM\JoinColumn(nullable=false)
*/
private $teamGuest;
I would like to create a Game object that has already loaded teams from the database.
$game = $this->serializer->deserialize($requestBody, \App\Entity\Game::class, 'json');
Looking for a solution I found something like jms_serializer.doctrine_object_constructor but there are no specific examples in the documentation.
Are you able to help me with the creation of an object from the database during deserialization?

You need to create a custom handler:
https://jmsyst.com/libs/serializer/master/handlers
A simple example:
<?php
namespace App\Serializer\Handler;
use App\Entity\Team;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
class TeamHandler implements SubscribingHandlerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public static function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => Team::class,
'method' => 'deserializeTeam',
],
];
}
public function deserializeTeam(JsonDeserializationVisitor $visitor, $id, array $type, Context $context)
{
return $this->em->getRepository(Team::class)->find($id);
}
}
Altough I would recommend universal approach to handle any entity you want by a single handler.
Example: https://gist.github.com/Glifery/f035e698b5e3a99f11b5
Also, this question has been asked before:
JMSSerializer deserialize entity by id

Related

Symfony 5 Object Serialization with ManyToMany Relation Times Out

In my Symfony 5 application, I have an entity class Product which has two properties $categories and $bundles. The product class has a ManyToMany relation with both the properties. When I comment out either one of the properties the Product serialization works perfectly. But incase both properties are present the serialization times out.
The code excerpt from Product class.
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #MaxDepth(1)
*/
private $categories;
}
The code for the serialization is below.
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', [
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}
]);
I have tried using the #ORM/JoinTable annotation suggested on some other Stackoverflow answers and #MaxDepth as well but no luck. The code works if any of the properties are commented out. Would be grateful for any advice on this.
okay, 20 products is actually not much. so I guess you're outputting the same objects over and over again if you let the relations be serialized unhindered.
I actually don't know how to achieve this reliably with the serializer. But the standard ways would just be enough probably. I like serializing via the JsonSerializable interface on your entities like this (omitting the ORM stuff for brevity):
class Product implements \JsonSerializable {
public $name;
public $categories; // relation
// getters + setters omitted
// this implements \JsonSerializable
public function jsonSerialize() {
return [
'name' => $this->name,
'categories' => array_map(function($category) {
return $category->jsonSerializeChild();
}, $this->categories),
];
}
// this function effectively stops recursion by leaving out relations
public function jsonSerializeChild() {
return [
'name' => $this->name,
];
}
}
If you implement this on all your entities you can very effectively limit the depth of serialization to two (i.e. the "base" entities and their connected entities).
also, the symfony serializer will use the JsonSerializable interface if it's defined if your serializing to JSON. Obviously, this is not as elegant as some fancy annotation-based serialization or a "smart" serializer, that actually manages to stop ... but it'll probably work better...
Pointed out by #Jakumi the serializer was looping over and over the object properties $categories and $bundles. I avoided that by using the Serialization groups.
The product class
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $categories;
}
The category class
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups("product_listing:read")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("product_listing:read")
*/
private $name;
}
The call to serializer
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', ['groups' => 'product_listing:read']);
I hope this helps someone in future.

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 =)

Trying Multiple User Auth, it keep saying wrong instance of argument passed

I'm getting this error while trying to log in multiple users with guards and unable to understand what instance it needs to be passed:
Argument 1 passed to
Illuminate\Auth\EloquentUserProvider::validateCredentials() must be an
instance of Illuminate\Contracts\Auth\Authenticatable, instance of
App\Employs given, called in /var/www/html/crmproject/vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php on line 379
This is my Auth Controller:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class EmploysLoginController extends Controller
{
use AuthenticatesUsers;
protected $guard = 'Employs';
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/Employs';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
public function showLoginForm()
{
return view('auth.employe-login');
}
public function login(Request $request)
{
if (auth()->guard('Employs')->attempt(['email' => $request->email, 'password' => $request->password])) {
dd(auth()->guard('Employs')->user());
}
return back()->withErrors(['email' => 'Email or password are wrong.']);
}
}
This is my Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Authenticatable;
// use Illuminate\Contracts\Auth\Authenticatable as
AuthenticatableContract;
class Employs extends Model// implements AuthenticatableContract
{
protected $primaryKey = 'employ_id';
}
i tried many solution provided online/stackoverflow but i'm constantly getting this error, and if you find this question has ambiguity please ask before doing down vote i'm trying this out last time here.
You should create a model like this:
Model
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Employs extends Authenticatable
{
use Notifiable;
protected $guard = 'Employs';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
I hope this work for you.

Use deserialize or getters and setters in controller actions

When implementing a rest json api with Symfony, one can deserialize the data for a create route with Jms Serializer:
$user = $serializer->deserialize($data, 'AppBundle\Entity\User', 'json');
but this makes all parameters of the User Entity available to set from the POST request, which might not be that good.
An alternative to this is to use setters in the controller:
$user = new User();
$user->setUsername($request->request->get('username'));
$user->sePassword($request->request->get('password'));
...
The latter option makes it more clear which parameters are actually able to set, but it requires a lot of code for a large entity.
What is the preferred way here?
Is it a third option?
You can serialize json data from your controller natively in Symfony once you have the Serializer component installed.
$user = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json');
When your object is created via this method, using the json from your request (decoded and then denormalized), the setters of your object are utilized to populate the properties of your object.
Could you post your User Entity?
Alternatively you can use Form Classes to perform this task.
Modification in relation to the comment on your question.
Annotation Groups in your entities works for serialization and deserialization.
class Item
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"first", "second"})
*/
private $id;
/**
* #ORM\Column(type="string", name="name", length=100)
* #Groups({"first"})
*/
private $name;
/**
* #ORM\Column(type="string", name="name", length=200)
* #Groups({"second"})
*/
private $description;
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
}
If you had both "name" and "description" in your POST data, you could insert either into your entity with the following:
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['first']]);
Or
$object = $this->get('serializer')->deserialize($data, 'AppBundle\Entity\User', 'json', ['groups' => ['second']]);
In the first case, only the name property would be populated and only the description property in the second case.

Validation of fields not present in form but in Entity

I have a form for user registration, and only username field is present in the form. And in my form, I wish to allow user input the username only. Nicename would be same as username on registration.
This form is bind to a User entity, i.e., in my form type class:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
));
}
entity User, which has a NotBlank constraint set for both username and nicename.
namespace Some\Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Constraints;
//...
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank()
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank()
*/
private $nicename;
//...
However, if I build a form with only username but not nicename, on validation i.e. $form->isValid() it fails to validate.
To bypass this, I come up with the following:
namespace Some\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Some\Bundle\Form\Type\RegisterType;
//...
class UserController extends Controller
{
//...
public function registerAction()
{
//...
$request = $this->getRequest();
$form = $this->createForm(new RegisterType());
if ($request->getMethod() == 'POST') {
// force set nicename to username.
$registerFields = $request->request->get('register');
$registerFields['nicename'] = $registerFields['username'];
$request->request->set('register', $registerFields);
$form->bind($request);
if ($form->isValid()) {
$user = $form->getData();
//persist $user, etc...
And in form type I add this to my buildForm method:
$builder->add('nicename', 'hidden');
But I find this very inelegant, leave some burden to the controller (extract from the request object, put in data, and put it back into the request object, ouch!), and user can see the hidden field if he were to inspect the source code of generated HTML.
Is there anyway that can at least any controller using the form type does not need do things like above, while retaining the entity constraints?
I cannot change the table schema which backs up the User entity, and I would like to keep the NotBlank constraint.
EDIT: After long hassle, I decided to use Validation groups and it worked.
class User
{
//...
/**
* #var string $username
*
* #ORM\Column(name="user_login", type="string", length=60, unique=true)
* #Constraints\NotBlank(groups={"register", "edit"})
*/
private $username;
/**
* #var string $nicename
*
* #ORM\Column(name="user_nicename", type="string", length=64)
* #Constraints\NotBlank(groups={"edit"})
*/
private $nicename;
Form Type:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Some\Bundle\Entity\User',
'validation_groups' => array('register', 'Default')
));
}
That 'Default' is needed or it ignores all other constraints I added in the form type buildForm method... Mind you, its case sensitive: 'default' does not work.
Though, I find that it is not enough (and sorry I didn't put it in my original question), because when I persist, I need to do this in my controller:
$user->setNicename($user->getUsername());
As a bonus, I move this from controller to Form Type level by adding a Form Event Subscriber
In form type buildForm method:
$builder->addEventSubscriber(new RegisterPostBindListener($builder->getFormFactory()));
And the RegisterPostBindListener class
<?php
namespace Some\Bundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
class RegisterPostBindListener implements EventSubscriberInterface
{
public function __construct(FormFactoryInterface $factory)
{
}
public static function getSubscribedEvents()
{
return array(FormEvents::POST_BIND => 'setNames');
}
public function setNames(DataEvent $event)
{
$data = $event->getData();
$data->setNicename($data->getUsername());
}
}
I think you should use validation groups.
In your User entity you can tell which field can be nullable:
/**
*#ORM\Column(type="string", length=100, nullable=TRUE)
*/
protected $someVar;
This way your view controllers don't need to do anything.
Forgot to mention. You can also define a PrePersist condition that initialises your nicename variable:
// you need to first tell your User entity class it has LifeCycleCallBacks:
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class User
{
...
/**
*#ORM\PrePersist
*/
public function cloneName()
{
$this->nicename = $this->username;
}
}
In this case, you should use a Callback assertion to create a custom validation rule.

Resources