Is it possible to validate a property of a model class dependent on another property of the same class?
For example, I have this class:
class Conference
{
/** $startDate datetime */
protected $startDate;
/** $endDate datetime */
protected $endDate;
}
and I want that Symfony 2.0 validates, that $startDate has to be after $endDate.
Is this possible by annotations or do I have to do this manually?
Starting from Symfony 2.4 you can also use Expression validation constraint to achieve what you need. I do believe, that this is the most simple way to do this. It's more convenient than Callback constraint for sure.
Here's example of how you can update your model class with validation constraints annotations:
use Symfony\Component\Validator\Constraints as Assert;
class Conference
{
/**
* #var \DateTime
*
* #Assert\Expression(
* "this.startDate <= this.endDate",
* message="Start date should be less or equal to end date!"
* )
*/
protected $startDate;
/**
* #var \DateTime
*
* #Assert\Expression(
* "this.endDate >= this.startDate",
* message="End date should be greater or equal to start date!"
* )
*/
protected $endDate;
}
Don't forget to enable annotations in your project configuration.
You can always do even more complex validations by using expression syntax.
Yes with the callback validator: http://symfony.com/doc/current/reference/constraints/Callback.html
On symfony 2.0:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContext;
/**
* #Assert\Callback(methods={"isDateValid"})
*/
class Conference
{
// Properties, getter, setter ...
public function isDateValid(ExecutionContext $context)
{
if ($this->startDate->getTimestamp() > $this->endDate->getTimestamp()) {
$propertyPath = $context->getPropertyPath() . '.startDate';
$context->setPropertyPath($propertyPath);
$context->addViolation('The starting date must be anterior than the ending date !', array(), null);
}
}
}
On symfony master version:
public function isDateValid(ExecutionContext $context)
{
if ($this->startDate->getTimestamp() > $this->endDate->getTimestamp()) {
$context->addViolationAtSubPath('startDate', 'The starting date must be anterior than the ending date !', array(), null);
}
}
Here I choose to show the error message on the startDate field.
Another way (at least as of Symfony 2.3) is to use simple #Assert\IsTrue:
class Conference
{
//...
/**
* #Assert\IsTrue(message = "Startime should be lesser than EndTime")
*/
public function isStartBeforeEnd()
{
return $this->getStartDate() <= $this->getEndDate;
}
//...
}
As reference, documentation.
It's even more simple since version 2.4. All you have to do is add this method to your class:
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* #Assert\Callback
*/
public function isStartBeforeEnd(ExecutionContextInterface $context)
{
if ($this->getStartDate() <= $this->getEndDate()) {
$context->buildViolation('The start date must be prior to the end date.')
->atPath('startDate')
->addViolation();
}
}
The buildViolation method returns a builder that has a couple of other methods to help you configure the constraint (like parameters and translation).
A better and cleaner solution https://symfony.com/doc/3.4/validation/custom_constraint.html
is to write
a custom constraint (which is basically the error message)
and its validator (which is like a controller function that does the control
To check that the entity is fine, add to the custom contraint (not the validator)
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
Which allows you to use an instance of that entity instead of just a property value. That make possible to write in the validator:
public function validate($object, Constraint $constraint)
{
#Your logic, for example:
if($value1 = $object->getValue1())
{
if($value2 = $object->getValue2())
{
if($value1 === $value2)
{
# validation passed
return True;
}
else
{
# validation failed
$this->context->buildViolation($constraint->message)
->setParameter('{{ string }}', $value1.' !== '.$value2)
->addViolation();
}
The best part is what you need to write in the entity class:
use YourBundle\Validator\Constraints as YourAssert;
/**
* Yourentity
*
* #ORM\Table(name="yourentity")
* #ORM\Entity(repositoryClass="YourBundle\Repository\YourentityRepository")
*
* #YourAssert\YourConstraintClassName # <-- as simple as this
Hope that helps
For Date validations, we can simply use GreaterThan and GreaterThanOrEqual comparison constraints.
class Conference
{
/**
* #var \DateTime
* #Assert\GreaterThanOrEqual("today")
*/
protected $startDate;
/**
* #var \DateTime
* #Assert\GreaterThan(propertyPath="startDate")
*/
protected $endDate;
}
For more information, see validation constraints
Related
Question
Is it possible in Symfony 2.8+ / 3.x+ to dispatch event before starting entity validation?
Situation:
Let's say we have 100 entities, they have #LifeCycleCallbacks, they have #postLoad Event that do something, but the result of this is only used for valiation of Entity, in 99% of situations result of #postLoad is unimportant for system. So if we have hundrets or thousands of Entities fetched from DB there will be a lot of machine-cycles lose for unimportant data.
It would be nice to run some kind of event, that will run method, that will populate that data for that specific Entity, just before validations starts.
instead of:
$entity->preValidate();
$validator = $this->get('validator');
$errors = $validator->validate($entity);
we could have:
$validator = $this->get('validator');
$errors = $validator->validate($entity);
And in validate() situation, preValidate() will be dispatched autmaticly as Event (of course with check if Entity does have such method).
CaseStudy:
I have a system that stores pages/subpages as entities. There can be 10 or 10000 pages/subpages
Pages/subpages can have file.
Entities stores only files names (becouse we can't store SplFileInfo - resource serialization restriction)
While Entity->file property is type of string, when I want to make it to be instance of File (so we can do validation of type File) I have something like:
/**
* #postLoad()
*/
public function postLoad()
{
//magicly we get $rootPath
$this->file = new File($rootPath . '/' . $this->file);
}
/**
* #prePersist()
* #preUpdate()
*/
public function preSave()
{
if ($this->file instance of File)
$this->file = $this->file->getFilename();
}
}
Ok, but postLoad() will CHANGE the property, and Doctrine will NOTICE that. So in next
$entityManager->flush()
ALL entities will be flushed - even if preSave() will change it back to be just string as it was before.
So if I have any other entity, let's say TextEntity, and I want to remove it
$entityManager->remove($textEntity);
$entityManager->flush();
All other Entities that are somehow changed (change was noticed by Doctrine), are flushed, no matter if value of file property is the same as in DB (and change was only temporary).
It will be flushed.
So we have hundrets, or thousends of pointless sql updates.
Btw.
1. ->flush($textEntity) will throw Exception, becouse ->remove($textEntity) have already "deleted" that entity.
2. Entity property ->file must be of type File for Assert/File, becouse FileValidator can only accept values of File or absolute-path-to-file.
But I will NOT store absolute-path-to-file, becouse it will be completly different on Dev, Stage, and Production environments.
This is problem that occured when I tried to make file uploading as it was described in Symfony cookbook http://symfony.com/doc/current/controller/upload_file.html.
My solution was to, in postLoad(), create File instance in property that is not Doctrine column, and is anoted to have assertion, etc.
That works, but the problem of useless postLoad()s stays, and i thought about events. That could be elastic, and very elegant solution - instead of controllers getting "fat".
Any one have better solution? Or know how to dispatch event if ->validate() happends?
Hello Voult,
Edit: first method is deprecated in symfony 3 as the thread op mentioned in a comment. Check the second method made for symfony 3.
Symfony 2.3+,Symfony < 3
What I do in this cases, since symfony and most other bundles are using parameters for service class definition, is to extend that service. Check the example below and for more information on extending services check this link
http://symfony.com/doc/current/bundles/override.html
First you need to add a some marker to your entities that require pre-validation. I usually use interfaces for stuff like this something like
namespace Your\Name\Space;
interface PreValidateInterface
{
public function preValidate();
}
After this you extend the validator service
<?php
namespace Your\Name\Space;
use Symfony\Component\Validator\Validator;
class MyValidator extends Validator //feel free to rename this to your own liking
{
/**
* #inheritdoc
*/
public function validate($value, $groups = null, $traverse = false, $deep = false)
{
if (is_object($value) && $value instanceof PreValidateInterface) {
$value->preValidate();
}
return parent::validate($value, $groups, $traverse, $deep);
}
}
And final step, you need to add the class value parameter to your 'parameters' config block in config.yml, something like this:
parameters:
validator.class: Your\Name\Space\MyValidator
This is the basic idea. Now you can mix end match this idea with whatever you want to achieve. For instance instead of calling a method on the entity (I usually like to keep business logic outside of my entities), you can look for the interface and if it is there you can launch a pre.validate event with that entity on it, and use a listener to do the job. After that you can keep the result from parent::validate and also launch a post.validate event. You see where i'm going with this. You basically can do whatever you like now inside that validate method.
PS: The example above is the easy method. If you want to go the event route, the service extension will be harder, since you need to inject the dispatcher into it. Check the link I provided at the beginning to see the other way to extend a service and let me know if you need help with this.
For Symfony 3.0 -> 3.1
In this case they managed to make it hard and dirtier to extend
Step 1:
Create your own validator something like this:
<?php
namespace Your\Name\Space;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception;
use Symfony\Component\Validator\MetadataInterface;
use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class myValidator implements ValidatorInterface
{
/**
* #var ValidatorInterface
*/
protected $validator;
/**
* #param ValidatorInterface $validator
*/
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
/**
* Returns the metadata for the given value.
*
* #param mixed $value Some value
*
* #return MetadataInterface The metadata for the value
*
* #throws Exception\NoSuchMetadataException If no metadata exists for the given value
*/
public function getMetadataFor($value)
{
return $this->validator->getMetadataFor($value);
}
/**
* Returns whether the class is able to return metadata for the given value.
*
* #param mixed $value Some value
*
* #return bool Whether metadata can be returned for that value
*/
public function hasMetadataFor($value)
{
return $this->validator->hasMetadataFor($value);
}
/**
* Validates a value against a constraint or a list of constraints.
*
* If no constraint is passed, the constraint
* {#link \Symfony\Component\Validator\Constraints\Valid} is assumed.
*
* #param mixed $value The value to validate
* #param Constraint|Constraint[] $constraints The constraint(s) to validate
* against
* #param array|null $groups The validation groups to
* validate. If none is given,
* "Default" is assumed
*
* #return ConstraintViolationListInterface A list of constraint violations.
* If the list is empty, validation
* succeeded
*/
public function validate($value, $constraints = null, $groups = null)
{
//the code you are doing all of this for
if (is_object($value) && $value instanceof PreValidateInterface) {
$value->preValidate();
}
//End of code
return $this->validator->validate($value, $constraints, $groups);
}
/**
* Validates a property of an object against the constraints specified
* for this property.
*
* #param object $object The object
* #param string $propertyName The name of the validated property
* #param array|null $groups The validation groups to validate. If
* none is given, "Default" is assumed
*
* #return ConstraintViolationListInterface A list of constraint violations.
* If the list is empty, validation
* succeeded
*/
public function validateProperty($object, $propertyName, $groups = null)
{
$this->validator->validateProperty($object, $propertyName, $groups);
}
/**
* Validates a value against the constraints specified for an object's
* property.
*
* #param object|string $objectOrClass The object or its class name
* #param string $propertyName The name of the property
* #param mixed $value The value to validate against the
* property's constraints
* #param array|null $groups The validation groups to validate. If
* none is given, "Default" is assumed
*
* #return ConstraintViolationListInterface A list of constraint violations.
* If the list is empty, validation
* succeeded
*/
public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = null)
{
$this->validator->validatePropertyValue($objectOrClass, $propertyName, $value, $groups);
}
/**
* Starts a new validation context and returns a validator for that context.
*
* The returned validator collects all violations generated within its
* context. You can access these violations with the
* {#link ContextualValidatorInterface::getViolations()} method.
*
* #return ContextualValidatorInterface The validator for the new context
*/
public function startContext()
{
$this->validator->startContext();
}
/**
* Returns a validator in the given execution context.
*
* The returned validator adds all generated violations to the given
* context.
*
* #param ExecutionContextInterface $context The execution context
*
* #return ContextualValidatorInterface The validator for that context
*/
public function inContext(ExecutionContextInterface $context)
{
$this->validator->inContext($context);
}
}
Step 2:
Extend Symfony\Component\Validator\ValidatorBuilder something like this:
namespace Your\Name\Space;
use Symfony\Component\Validator\ValidatorBuilder;
class myValidatorBuilder extends ValidatorBuilder
{
public function getValidator()
{
$validator = parent::getValidator();
return new MyValidator($validator);
}
}
You need to override Symfony\Component\Validator\Validation. This is the ugly/dirty part, because this class is final so you can't extend it, and has no interface to implement, so you will have to pay attention to in on future versions of symfony in case backward compatibility is broken. It goes something like this:
namespace Your\Name\Space;
final class MyValidation
{
/**
* The Validator API provided by Symfony 2.4 and older.
*
* #deprecated use API_VERSION_2_5_BC instead.
*/
const API_VERSION_2_4 = 1;
/**
* The Validator API provided by Symfony 2.5 and newer.
*/
const API_VERSION_2_5 = 2;
/**
* The Validator API provided by Symfony 2.5 and newer with a backwards
* compatibility layer for 2.4 and older.
*/
const API_VERSION_2_5_BC = 3;
/**
* Creates a new validator.
*
* If you want to configure the validator, use
* {#link createValidatorBuilder()} instead.
*
* #return ValidatorInterface The new validator.
*/
public static function createValidator()
{
return self::createValidatorBuilder()->getValidator();
}
/**
* Creates a configurable builder for validator objects.
*
* #return ValidatorBuilderInterface The new builder.
*/
public static function createValidatorBuilder()
{
return new MyValidatorBuilder();
}
/**
* This class cannot be instantiated.
*/
private function __construct()
{
}
}
And last step overwrite the parameter validator.builder.factory.class in your config.yml:
parameters:
validator.builder.factory.class: Your\Name\Space\MyValidation
This is the least invasive way to do it, that i can find. Is not that clean and it could need some maintaining when you upgrade symfony to future versions.
Hope this helps, and happy coding
Alexandru Cosoi
I have some difficulties about applying validation for only one associated entity.
So I have two entities, News and NewsTranslation. A news could be translated in multiple languages. But I would like to apply validation only if locale is en.
// AppBundle/Entity/News.php
class News
{
use ORMBehaviors\Translatable\Translatable;
use ORMBehaviors\Timestampable\Timestampable;
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="status", type="smallint")
* #Assert\NotBlank
*/
private $status;
...
}
// AppBundle/Entity/NewsTranslation.php
class NewsTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\NotBlank
* #Assert\Length(max = 255)
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="text", type="string", nullable=true)
* #Assert\NotBlank
*/
private $text;
}
# AppBundle/Resources/config/validation.yml
AppBundle\Entity\News:
properties:
translations:
- Valid: ~
I tried to use a Closure for the validation_groups form option. But it looks like Symfony do validation on News entity and Valid constraint apply the same groups on NewsTranslation.
I know I could use Callback constraint but that's mean to redo NotBlank, Length and other exiting constraints by myself. And I would like to avoid it if possible.
EDIT:
I'm using Symfony 2.8.*
I try using an en validation group. But looks like the validation is launch on News entity with validation_groups. And with Valid constraint the en validation group is given to validate NewsTranlation. So even it's the en or fr translation the group change nothing in this case.
I also try using the validation medatada through an #Assert\Callback or by using loadValidatorMetadata method into NewsTranslation entity. And the problem stay similar. I can't apply an constraint for a specific entity of collection.
I finally found a way by creating a custom validator.
Like this I could use core constraints easily.
In the translation entity, I could use my validator like this:
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\Length(max = 255)
* #AppAssert\ValidTranslation(locales = {"fr"}, constraints = {
* #Assert\NotBlank
* })
*/
private $title;
And the validator:
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraints\Composite;
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* #author Nicolas Brousse
*/
class ValidTranslation extends Composite
{
public $locales = array();
public $constraints = array();
public function getCompositeOption()
{
return 'constraints';
}
public function getRequiredOptions()
{
return array('locales', 'constraints');
}
}
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* #author Nicolas Brousse
*/
class ValidTranslationValidator extends ConstraintValidator
{
/**
* If property constraint
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidTranslation) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\ValidTranslation');
}
if (false) { // #todo check by interface or trait
throw new UnexpectedTypeException($value, 'not a translation entity');
}
$context = $this->context;
$entity = $this->context->getObject();
if (in_array($entity->getLocale(), $constraint->locales)) {
$context = $this->context;
if ($context instanceof ExecutionContextInterface) {
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
} else {
// 2.4 API
$context->validateValue($value, $constraint->constraints);
}
}
}
}
you form need to return 2 validations_groups, "Default" and the validation group corresponding to the "en" locale
I'm using Symfony 2.5 and my Model class is the following:
/**
* #UserAssert\UserPasswordReset
*/
class ResetPassword {
/**
* #var string
* #Assert\NotBlank()
*/
public $username;
/**
* #var string
* #Assert\NotBlank()
* #Assert\Date
*/
public $birthday;
/**
* #var string
* #Assert\NotBlank()
*/
public $plainSecurityAnswer;
function __toString()
{
return $this->username . $this->birthday->format('Y-m-d H:i:s') . $this->plainSecurityAnswer;
}
}
This Model is mapped to a ResetFormType.
Now my intention: How can i say / configure, that i first want the property constraints to be passed. And if all property constraints are passed (e.g. no field is blank), i want the #UserAssert\UserPasswordReset to be called.
At the moment, it always validates the property AND the class constraints.
Regards ++
I think you can do it using a GroupSequence Validator like this:
/**
* #UserAssert\UserPasswordReset(groups={"PasswordReset"})
* #Assert\GroupSequence({"Default", "PasswordReset"})
*/
class ResetPassword
{
//----
}
In this mode UserPasswordReset will be validated only after the Defaults Asserts.
In the docs you will find some implementations example to use groups sequences..
I'm trying to return a JSON object('Module') with a ManyToOne link to an Sonata\MediaBundle\Entity through FOSRestBundle and JMS Serializer. How should I go about doing that?
Here's a hack that I did, but dont think it's the best practise.
class Module
{
...
/**
* #var Application\Sonata\MediaBundle\Entity\Media
*
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media", inversedBy="module")
* #ORM\JoinColumn(name="hero_image_id", referencedColumnName="id")
* #JMS\Expose()
*/
private $heroImage;
...
}
class Media extends BaseMedia
{
...
/**
* A Quick hack not the best method.
*
* #JMS\VirtualProperty
* #JMS\SerializedName("url")
*
* #return string
*/
public function getUrlMethod()
{
global $kernel;
$imageProvider = $kernel->getContainer()->get('sonata.media.provider.image');
return $imageProvider->generatePublicUrl($this, 'reference');
}
...
}
thank you!
EDIT
Thanks to Tautrimas Pajarskas and the post he mention.
Here's the class.
<?php
namespace AXO\APIBundle\Listener\Serialization;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use JMS\DiExtraBundle\Annotation\Inject;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\GraphNavigator;
/**
* Add data after serialization
*
* #Service("axo.listener.serializationlistener")
* #Tag("jms_serializer.event_subscriber")
*/
class SerializationListener implements EventSubscriberInterface
{
/**
* #inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'class' => 'Application\Sonata\MediaBundle\Entity\Media', 'method' => 'onPostSerialize'),
);
}
public function onPostSerialize(ObjectEvent $event)
{
global $kernel;
$imageProvider = $kernel->getContainer()->get('sonata.media.provider.image');
$event->getVisitor()->addData('url',$imageProvider->generatePublicUrl($event->getObject(), 'reference'));
}
}
You might want to see Add extra fields using JMS Serializer bundle as it has an example, on how to add additional fields to serialized data that depend on external classes.
I'm developing game app and using Symfony 2.0. I have many AJAX requests to the backend. And more responses is converting entity to JSON. For example:
class DefaultController extends Controller
{
public function launchAction()
{
$user = $this->getDoctrine()
->getRepository('UserBundle:User')
->find($id);
// encode user to json format
$userDataAsJson = $this->encodeUserDataToJson($user);
return array(
'userDataAsJson' => $userDataAsJson
);
}
private function encodeUserDataToJson(User $user)
{
$userData = array(
'id' => $user->getId(),
'profile' => array(
'nickname' => $user->getProfile()->getNickname()
)
);
$jsonEncoder = new JsonEncoder();
return $jsonEncoder->encode($userData, $format = 'json');
}
}
And all my controllers do the same thing: get an entity and encode some of its fields to JSON. I know that I can use normalizers and encode all entitities. But what if an entity has cycled links to other entity? Or the entities graph is very big? Do you have any suggestions?
I think about some encoding schema for entities... or using NormalizableInterface to avoid cycling..,
With php5.4 now you can do :
use JsonSerializable;
/**
* #Entity(repositoryClass="App\Entity\User")
* #Table(name="user")
*/
class MyUserEntity implements JsonSerializable
{
/** #Column(length=50) */
private $name;
/** #Column(length=50) */
private $login;
public function jsonSerialize()
{
return array(
'name' => $this->name,
'login'=> $this->login,
);
}
}
And then call
json_encode(MyUserEntity);
Another option is to use the JMSSerializerBundle. In your controller you then do
$serializer = $this->container->get('serializer');
$reports = $serializer->serialize($doctrineobject, 'json');
return new Response($reports); // should be $reports as $doctrineobject is not serialized
You can configure how the serialization is done by using annotations in the entity class. See the documentation in the link above. For example, here's how you would exclude linked entities:
/**
* Iddp\RorBundle\Entity\Report
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Iddp\RorBundle\Entity\ReportRepository")
* #ExclusionPolicy("None")
*/
....
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="reports")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
* #Exclude
*/
protected $client;
You can automatically encode into Json, your complex entity with:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new
JsonEncoder()));
$json = $serializer->serialize($entity, 'json');
To complete the answer: Symfony2 comes with a wrapper around json_encode:
Symfony/Component/HttpFoundation/JsonResponse
Typical usage in your Controllers:
...
use Symfony\Component\HttpFoundation\JsonResponse;
...
public function acmeAction() {
...
return new JsonResponse($array);
}
I found the solution to the problem of serializing entities was as follows:
#config/config.yml
services:
serializer.method:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
serializer.encoder.json:
class: Symfony\Component\Serializer\Encoder\JsonEncoder
serializer:
class: Symfony\Component\Serializer\Serializer
arguments:
- [#serializer.method]
- {json: #serializer.encoder.json }
in my controller:
$serializer = $this->get('serializer');
$entity = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findOneBy($params);
$collection = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findBy($params);
$toEncode = array(
'response' => array(
'entity' => $serializer->normalize($entity),
'entities' => $serializer->normalize($collection)
),
);
return new Response(json_encode($toEncode));
other example:
$serializer = $this->get('serializer');
$collection = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findBy($params);
$json = $serializer->serialize($collection, 'json');
return new Response($json);
you can even configure it to deserialize arrays in http://api.symfony.com/2.0
I just had to solve the same problem: json-encoding an entity ("User") having a One-To-Many Bidirectional Association to another Entity ("Location").
I tried several things and I think now I found the best acceptable solution. The idea was to use the same code as written by David, but somehow intercept the infinite recursion by telling the Normalizer to stop at some point.
I did not want to implement a custom normalizer, as this GetSetMethodNormalizer is a nice approach in my opinion (based on reflection etc.). So I've decided to subclass it, which is not trivial at first sight, because the method to say if to include a property (isGetMethod) is private.
But, one could override the normalize method, so I intercepted at this point, by simply unsetting the property that references "Location" - so the inifinite loop is interrupted.
In code it looks like this:
class GetSetMethodNormalizer extends \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer {
public function normalize($object, $format = null)
{
// if the object is a User, unset location for normalization, without touching the original object
if($object instanceof \Leonex\MoveBundle\Entity\User) {
$object = clone $object;
$object->setLocations(new \Doctrine\Common\Collections\ArrayCollection());
}
return parent::normalize($object, $format);
}
}
I had the same problem and I chosed to create my own encoder, which will cope by themself with recursion.
I created classes which implements Symfony\Component\Serializer\Normalizer\NormalizerInterface, and a service which holds every NormalizerInterface.
#This is the NormalizerService
class NormalizerService
{
//normalizer are stored in private properties
private $entityOneNormalizer;
private $entityTwoNormalizer;
public function getEntityOneNormalizer()
{
//Normalizer are created only if needed
if ($this->entityOneNormalizer == null)
$this->entityOneNormalizer = new EntityOneNormalizer($this); //every normalizer keep a reference to this service
return $this->entityOneNormalizer;
}
//create a function for each normalizer
//the serializer service will also serialize the entities
//(i found it easier, but you don't really need it)
public function serialize($objects, $format)
{
$serializer = new Serializer(
array(
$this->getEntityOneNormalizer(),
$this->getEntityTwoNormalizer()
),
array($format => $encoder) );
return $serializer->serialize($response, $format);
}
An example of a Normalizer :
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PlaceNormalizer implements NormalizerInterface {
private $normalizerService;
public function __construct($normalizerService)
{
$this->service = normalizerService;
}
public function normalize($object, $format = null) {
$entityTwo = $object->getEntityTwo();
$entityTwoNormalizer = $this->service->getEntityTwoNormalizer();
return array(
'param' => object->getParam(),
//repeat for every parameter
//!!!! this is where the entityOneNormalizer dealt with recursivity
'entityTwo' => $entityTwoNormalizer->normalize($entityTwo, $format.'_without_any_entity_one') //the 'format' parameter is adapted for ignoring entity one - this may be done with different ways (a specific method, etc.)
);
}
}
In a controller :
$normalizerService = $this->get('normalizer.service'); //you will have to configure services.yml
$json = $normalizerService->serialize($myobject, 'json');
return new Response($json);
The complete code is here : https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer
in Symfony 2.3
/app/config/config.yml
framework:
# сервис конвертирования объектов в массивы, json, xml и обратно
serializer:
enabled: true
services:
object_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
tags:
# помечаем к чему относится этот сервис, это оч. важно, т.к. иначе работать не будет
- { name: serializer.normalizer }
and example for your controller:
/**
* Поиск сущности по ИД объекта и ИД языка
* #Route("/search/", name="orgunitSearch")
*/
public function orgunitSearchAction()
{
$array = $this->get('request')->query->all();
$entity = $this->getDoctrine()
->getRepository('IntranetOrgunitBundle:Orgunit')
->findOneBy($array);
$serializer = $this->get('serializer');
//$json = $serializer->serialize($entity, 'json');
$array = $serializer->normalize($entity);
return new JsonResponse( $array );
}
but the problems with the field type \DateTime will remain.
This is more an update (for Symfony v:2.7+ and JmsSerializer v:0.13.*#dev), so to avoid that Jms tries to load and serialise the whole object graph ( or in case of cyclic relation ..)
Model:
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Exclude;
use JMS\Serializer\Annotation\MaxDepth; /* <=== Required */
/**
* User
*
* #ORM\Table(name="user_table")
///////////////// OTHER Doctrine proprieties //////////////
*/
public class User
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="FooBundle\Entity\Game")
* #ORM\JoinColumn(nullable=false)
* #MaxDepth(1)
*/
protected $game;
/*
Other proprieties ....and Getters ans setters
......................
......................
*/
Inside an Action:
use JMS\Serializer\SerializationContext;
/* Necessary include to enbale max depth */
$users = $this
->getDoctrine()
->getManager()
->getRepository("FooBundle:User")
->findAll();
$serializer = $this->container->get('jms_serializer');
$jsonContent = $serializer
->serialize(
$users,
'json',
SerializationContext::create()
->enableMaxDepthChecks()
);
return new Response($jsonContent);
If you are using Symfony 2.7 or above, and don't want to include any additional bundle for serializing, maybe you can follow this way to seialize doctrine entities to json -
In my (common, parent) controller, I have a function that prepares the serializer
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
// -----------------------------
/**
* #return Serializer
*/
protected function _getSerializer()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory);
return new Serializer([$normalizer], [new JsonEncoder()]);
}
Then use it to serialize Entities to JSON
$this->_getSerializer()->normalize($anEntity, 'json');
$this->_getSerializer()->normalize($arrayOfEntities, 'json');
Done!
But you may need some fine tuning. For example -
If your entities have circular reference, check how to handle it.
If you want to ignore some properties, can do it
Even better, you can serialize only selective attributes.
When you need to create a lot of REST API endpoints on Symfony,
the best way is to use the following stack of bundles:
JMSSerializerBundle for the serialization of Doctrine entities
FOSRestBundle bundle for response view listener. Also, it can generate definitions of routes based on controller/action name.
NelmioApiDocBundle to auto-generate online documentation and Sandbox(which allows testing endpoint without any external tool).
When you configure everything properly, you entity code will look like this:
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* #ORM\Table(name="company")
*/
class Company
{
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*
* #JMS\Expose()
* #JMS\SerializedName("name")
* #JMS\Groups({"company_overview"})
*/
private $name;
/**
* #var Campaign[]
*
* #ORM\OneToMany(targetEntity="Campaign", mappedBy="company")
*
* #JMS\Expose()
* #JMS\SerializedName("campaigns")
* #JMS\Groups({"campaign_overview"})
*/
private $campaigns;
}
Then, code in controller:
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\View;
class CompanyController extends Controller
{
/**
* Retrieve all companies
*
* #View(serializerGroups={"company_overview"})
* #ApiDoc()
*
* #return Company[]
*/
public function cgetAction()
{
return $this->getDoctrine()->getRepository(Company::class)->findAll();
}
}
The benefits of such a set up are:
#JMS\Expose() annotations in the entity can be added to simple fields, and to any type of relations. Also, there is the possibility to expose the result of some method execution (use annotation #JMS\VirtualProperty() for that)
With serialization groups, we can control exposed fields in different situations.
Controllers are very simple. The action method can directly return an entity or array of entities, and they will be automatically serialized.
And #ApiDoc() allows testing the endpoint directly from the browser, without any REST client or JavaScript code
Now you can also use Doctrine ORM Transformations to convert entities to nested arrays of scalars and back
The accepted answer is correct but if You'll need to serialize a filtered subset of an Entity , json_encode is enough:
Consider this example:
class FileTypeRepository extends ServiceEntityRepository
{
const ALIAS = 'ft';
const SHORT_LIST = 'ft.name name';
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, FileType::class);
}
public function getAllJsonFileTypes()
{
return json_encode($this->getAllFileTypes());
}
/**
* #return array
*/
public function getAllFileTypes()
{
$query = $this->createQueryBuilder(self::ALIAS);
$query->select(self::SHORT_LIST);
return $query->getQuery()->getResult();
}
}
/** THIS IS ENOUGH TO SERIALIZE AN ARRAY OF ENTITIES SINCE the doctrine SELECT will remove complex data structures from the entities itself **/
json_encode($this->getAllFileTypes());
Short note: Tested at least on Symfony 5.1