JMS Serializer ignoring non-persisted property - json

I've been struggling with a property on one of my Symfony2/Doctrine objects that isn't persisted in the database. It's an array of objects that contain properties like label, key and value, but even if it is defined as a simple array of arrays, it doesn't show up.
Here is the way that the array is defined in the object that is normally persisted:
/*
* #Accessor(getter="getReceipt",setter="setReceipt")
* #Type("ArrayCollection<MyProject\ReceiptProperty>")
* #Expose
* #Groups({"details"})
*/
protected $receipt;
public function setReceipt($receipt) {
$this->receipt = $receipt;
}
public function getReceipt() {
return $this->receipt;
}
And here is how the object in the array is defined
/**
* #ExclusionPolicy("all")
*/
class ReceiptProperty extends APIObject {
/**
* #Type("string")
* #Expose
* #Groups({"basic"})
*/
public $label;
/**
* #Type("string")
* #Expose
* #Groups({"basic"})
*/
public $type;
/**
* #Type("string")
* #Expose
* #Groups({"basic"})
*/
public $key;
/**
* #Expose
* #Groups({"basic"})
*/
public $value;
public function __construct($data) {
$this->label = $data['label'];
$this->type = $data['type'];
$this->key = $data['key'];
$this->value = $data['value'];
}
}
By straight printing the objects before going into serialization with print_r, I can see that the data is there, but no matter what the configuration is, that field is never shown.

In an effort to help those that might have similar issues going forward, my problem was essentially an incorrect annotation.
In the first code block you'll notice that the annotation comment begins with /*
It turns out that JMS Serializer does not process comments that do not start with /** so effectively it was ignoring my commands. After updating the annotation comment, it worked as expected. It's always the little things...

Related

how to serialize only one level deep of an entity with symfony serializer?

I would like to serialize my objects into text fields in order to store a representation of them for traceability.
What I really want is a JSON representation of the entity's properties, and whenever there is an object, I would like a JSON representation of that as well, but only on that first level, I don't want it to dig deeper into what possible objects and relations there is below that, I'm happy with the object ID's
Many objects reference other objects, and from this documentation https://symfony.com/doc/current/components/serializer.html#handling-circular-references it appears this can be easily handled by just storing the object's ID rather than serializing the entire object (again). But in my case it doesn't work =) Am I missing something critical here?
Entity
<?php
namespace App\Entity;
use App\Repository\RegularServiceHoursRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RegularServiceHoursRepository::class)
*/
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
*/
protected $ACRGroup;
public function getId(): ?int
{
return $this->id;
}
public function getOpen(): ?\DateTimeInterface
{
return $this->open;
}
public function setOpen(\DateTimeInterface $open): self
{
$this->open = $open;
return $this;
}
public function getClose(): ?\DateTimeInterface
{
return $this->close;
}
public function setClose(\DateTimeInterface $close): self
{
$this->close = $close;
return $this;
}
/**
* Set aCRGroup
*
* #param \App\Entity\ACRGroup $aCRGroup
*
* #return DebitPeriod
*/
public function setACRGroup(\App\Entity\ACRGroup $aCRGroup)
{
$this->ACRGroup = $aCRGroup;
return $this;
}
/**
* Get aCRGroup
*
* #return \App\Entity\ACRGroup
*/
public function getACRGroup()
{
return $this->ACRGroup;
}
/**
* Get debitTimeSeconds
*
* #return int
*/
public function getTimeSeconds()
{
$open = $this->getOpen();
$close = $this->getClose();
$r = $close->format('U') - $open->format('U');
return $r;
}
}
Controller
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Serializer;
public function log($type,$message,$unit=null,$previous=null,$current=null) {
//We only log successful operations.
//If you want to log also errors, be very careful to not have anything persisted already, in que to be flushed. The flush below will store possible entities under conflict check and entities that sent you here due to a failed validation/conflict check, entities that weren't meant to be saved but rejected and forgotten.
if ($type == 'success') {
$encoder = new JsonEncoder();
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getId();
},
];
$normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext);
$serializer = new Serializer([$normalizer], [$encoder]);
dd($serializer->serialize($current, 'json'));
It never gets to the dump action, it chews for 30 seconds until memory is exhausted, and then it says:
Error: Maximum execution time of 30 seconds exceeded
Have I somehow missed where to set a default depth of 1? (I understood depth 1 to be the default when nothing was set).
Pay attention to Serialization Groups Attributes. With this attribute you can select the desired data when serializing an object.
Specify groups to the desired object properties:
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
* #Groups({"default"})
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
* #Groups({"default"})
*/
private $ACRGroup;
}
Also specify a group for the ACRGroup identifier
class ACRGroup
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
}
And just specify the group when serializing the object
$serializer->serialize($current, 'json', ['groups' => ['default']]);
I think that should solve your problem

Symfony constraint to prevent duplicates in overlapping time frame

How can I enforce uniqueness of a value within overlapping date range in Symfony using Doctrine ORM.
I have the following entity
<?php
/**
* #ORM\Entity
* #ORM\Table("tax_component")
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;
/**
* #ORM\Column(name="tax_component_rate", type="integer")
* #Assert\GreaterThanOrEqual(0)
*/
private int $rate;
/**
* #ORM\Column(name="tax_component_applicable_from", type="datetime_immutable")
*/
private DateTimeInterface $applicableFrom;
/**
* #ORM\Column(name="tax_component_applicable_to", type="datetime_immutable")
*/
public function __construct(string $name, int $rate, ?DateTimeImmutable $applicableFrom = null, ?DateTimeImmutable $applicableTo = null)
{
...
}
}
I want to make $name unique withing overlapping time frames of $applicableFrom and $applicableTo. For example,
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2018-04-01:00:00:00'), new DateTime('2019-03-31T23:59:59'))
);
// The following should be allowed since there is no overlap between the two time ranges using the name 'inter-state'
$repository->save(
new TaxComponent('inter-state', 1200, new DateTime('2019-04-01:00:00:00'), new DateTime('2020-03-31T23:59:59'))
);
// The following should fail since 'inter-state' is ambiguous during the period 2019-09-01:00:00:00 to 2020-03-31T23:59:59
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2019-09-01:00:00:00'), new DateTime('2020-09-31T23:59:59'))
);
Is there a constraint to enforce this is Symfony?
I am currency planning to check for existing entities from within TaxComponentRepository::save, before calling $this->entityManager->persist. Is there a better solution?
The cleaner way would be to create your own custom assert.
Starting by creating your constraint :
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class TaxComponentConstraint extends Constraint
{
public $message = 'Another tax component overlap this one: {{ taxComponent}}';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'App\Validator\Constraints\TaxComponentValidator';
}
}
And now you have to create a validator that will check if there exist an overlap with two tax component.
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TaxComponentValidator extends ConstraintValidator
{
public function validate($taxComponentObject, Constraint $constraint)
{
//Check however you want if the tax component can be created (So no overlap between two existing TaxComponent)
if($overlappingTaxComponent){
$this->context->buildViolation($constraint->message)
->setParameter('{{ taxComponent }}', $overlappingTaxComponent->__toString())
->addViolation();
}
}
}
Here, $overlappingTaxComponent is a TaxComponent preventing us from making one because of your constraint.
If the constraint is properly done, you can now use it easily in your entity so that it check automatically when submitting the form :
<?php
//...
use App\Validator\Constraints as CustomAssert;
/**
* #ORM\Entity
* #ORM\Table("tax_component")
* #CustomAssert\TaxComponentConstraint
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;

doctrine2 , retrieve associated data from inverse side

i have 2 entities.
the simple logic is that a user has many delivery address, so in the future he will be able to choose one of them for their deliverys.
first entity, Direccion (address).
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* Direccion
*
* #ORM\Table(name="direccion", indexes={#ORM\Index(name="id_usuario",
columns={"id_usuario"})})
* #ORM\Entity
*/
class Direccion
{
/**
* #var string
*
* #ORM\Column(name="Calle", type="string", length=100, nullable=false)
*/
private $calle;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var \Usuario
*
* #ORM\ManyToOne(targetEntity="Usuario", inversedBy="direcciones", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_usuario", referencedColumnName="id" )
* })
*/
private $idUsuario;
/**
* Set calle
*
* #param string $calle
* #return Direccion
*/
public function setCalle($calle)
{
$this->calle = $calle;
return $this;
}
/**
* Get calle
*
* #return string
*/
public function getCalle()
{
return $this->calle;
}
/**
* Set idUsuario
*
* #param \Usuario $idUsuario
* #return Direccion
*/
public function setIdUsuario(\Usuario $idUsuario = null)
{
$this->idUsuario = $idUsuario;
return $this;
}
/**
* Get idUsuario
*
* #return \Usuario
*/
public function getIdUsuario()
{
return $this->idUsuario;
}
}
and the second entity is, Usuario (User)
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Usuario
*
* #ORM\Table(name="usuario")
* #ORM\Entity
*/
class Usuario
{
/**
* #var string
*
* #ORM\Column(name="nombre", type="string", length=255, nullable=true)
*/
private $nombre;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Direccion", mappedBy="id_usuario", cascade={"persist"})
*/
private $direcciones;
public function __construct()
{
$this->direcciones = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set nombre
*
* #param string $nombre
* #return Usuario
*/
public function setNombre($nombre)
{
$this->nombre = $nombre;
return $this;
}
/**
* Get nombre
*
* #return string
*/
public function getNombre()
{
return $this->nombre;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function getDirecciones()
{
return $this->direcciones;
}
public function setDirecciones($direcciones)
{
$this->direcciones = $direcciones;
return $this;
}
}
i have already readed many blogs and similar questions , but i can't get the property $direcciones hydrated or filled with the associated data, when i try to achieve that on the inverse side of the relation (OneToMany), with $usuario->getDirecciones();
i'm working in with this code in a non MVC architecture, to this point everything works like charm in getting creating and updating through a DAO wich uses the Doctrine sentences,so i know that in the ending points (updating, creating,persist and flush,retrieving data with find, findBy,etc), everything works fine.
when i try to fill the ArrayCollection calling from the Service layer where i use both classes (Usuario-InverseSide and Direccion-OwnerSide), nothing happens, the $direcciones arrayCollection property on Usuario instance, doesn't fetch anything, even when i tried establishing fetch="EAGER"on the annotations.
for the rest of the data or properties, everything works fine, all the other attributes of the user get filled just fine.
For some reason, the annotations are not being considered, donĀ“t know why.
i've spend a few days trying to figure out how to acomplish this way of accessing the associated data, didn't wanted to use DQL, but at this point i think i will take that road.
I tried something hard-coded to test and, the result is the same, $direcciones doesn't get his data.Obvious are already discarted, that exact ID for a Usuario(user) has his related direcciones(address) setted.
$usuario = $this->usuarioDAO->find(20);
$direcciones = $usuario->getDirecciones();
var_dump($direcciones);
return $usuario;
the relation was only defined with a FK called id_usuario on Direccion on a mysql InnoDb table.
don't know if i should be setting something else on the Usuario table.
or if there is something wrong in the way i store the entities on my project.
please help me, any recommendation will be appreciated.
doctrine 2, hwo do get data from the inverse side (many to one)
Provided that your bidirectional mapping between Direccion and Usuaro is correct, you need to use setter methods on both sides.
And the other thing, when using ArrayCollection, an easier (perhaps the only way) is to add elements one by one.
Therefore, instead of setDirecciones() you add this method to Usuario class:
public function addDireccion(Direccion $direccion) {
$direccion->setIdUsuario($this);
$this->direcciones->add($direccion);
}
Hope that helps.
And, it would be better to name it $usuario and setUsuario instead od setIdUsuario because you work with objects and PHP should not be concerned about actual database field names (to put it that way).
i resolved it.
happens that for some reason, the php annotations were not being actually readed, or on a different way to say it, it was listening to the XML annotations first, so i put the xml sentence on the inverse side of the relation (usuario) inside the tags
<one-to-many field="direcciones" target-entity="Direccion" mapped-by="idUsuario" />
and it worked, now i got polish the data result of the consult so i can send a cleaner/clearer object to the front-end.

Symfony2 - Entity findAll return large response on API

I have an API developed in Symfony2 but when i send a request to it the response returns with 204Mb for only 40 rows... This is the code:
$em = $this->getDoctrine()->getManager();
$themes = $em->getRepository("KlickpagesAdminBundle:Theme")->findAll();
return $themes;
Im use FOSRestBundle to serialize and returns the json.
How i can resolve this?
Aa #Cerad said it is very like because of relations to other entities and lazy loading going in circles
For a quick test exclude all fields from the serilazition, except few scalar ones like so:
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\ExclusionPolicy;
/**
* Group
*
* #ExclusionPolicy("all")
*/
class Group implements GroupInterface
{
/**
* #Expose
* #var integer
*/
private $id;
/**
* #Expose
* #var string
*/
private $title;
/**
* Relation to privilegesis not explicitly exposed.
* #var Privilege[]
*/
private $privileges;
/**
* Relation to Users not explicitly exposed.
* #var User[]
*/
private $users;
...
The important parts are exclusionStrategy and expose antations.
If this will help, you got for sure a circles serialization of your annotations and the right solution is to define serialization groups, lets say like this:
/**
* #Expose
* #Groups({"groupDetail", "userAuthenticate"})
*
* #var Privilege[]
*/
private $privileges;
/**
* #Expose
* #Groups({"groupDetail"})
*
* #var User[]|ArrayCollection
*/
private $users;
You can then define by which group should be the response serialized on your controller or programatically.
// controllerAction
/*
* #Annotations\View(serializerGroups={"Default","groupDetail"})
*/
public function getGroupAction($groupId) { ... }
// programatically
...
/** #var $context SerializationContext */
$context = SerializationContext::create();
$serializationGroups = ['Default', 'GroupDetail'];
$context->setGroups($serializationGroups);
$view = $this->view($collection, 200);
$view->setSerializationContext($context);
return $this->handleView($view);
...
Resources: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies

symfony2: JMSSerializerBundle changes the attribute name from "className" to "class_name"

I'm using the JMSSerializerBundle to serialize my entity.
but I have the following problem: the attribute name is "className" but in my Json object I get a "class_name".
this is my entity:
/**
* Events
*
* #ORM\Table()
* #ORM\Entity
*/
class Events
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
...
/**
* #var string
*
* #ORM\Column(name="className", type="string", length=255)
*/
private $className;
/**
* Set className
*
* #param string $className
* #return Events
*/
public function setClassName($className)
{
$this->className = $className;
return $this;
}
/**
* Get className
*
* #return string
*/
public function getClassName()
{
return $this->className;
}
...
}
this is my controller
class myController extends Controller{
public function loadAction($action){
$request=$this->get('request');
if($request->isXmlHttpRequest())
{
switch( $action ) {
case 'load':
$resultat=$this->getDoctrine()->getManager()->getRepository('ECMUserBundle:Events')
->findAll();
$serializer = $this->get('jms_serializer');
$resultat=$serializer->serialize($resultat, 'json');
echo $resultat;
exit();
break;
...
and this my Json
[{"id":90,"title":"holliday","start":"2014-03-25T01:00:00+0000","end":"2014-03-25T01:00:00+0000","class_name":"label-orange","allday":"true"}]
is this the logical behaviors?
As #mike said, you can use #SerializedName annotation to change serialized property name to arbitrary string.
Also, if you want to change naming strategy on application level. You can use the following workaround:
config.yml
parameters:
jms_serializer.serialized_name_annotation_strategy.class: JMS\Serializer\Naming\IdenticalPropertyNamingStrategy
Also, check this issue.
Check the documentation for the #SerializedName annotation:
http://jmsyst.com/libs/serializer/master/reference/annotations
#SerializedName:
This annotation can be defined on a property to define the serialized name for a property. If this is not defined, the property will be translated from camel-case to a lower-cased underscored name, e.g. camelCase -> camel_case.
If you just want to use the camel case version once, without annotations, use the IdenticalPropertyNamingStrategy:
$serializer = SerializerBuilder::create()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())->build();
Inside Symfony, it make way more sense to use a compiler pass, as it avoid losing the #SerializedName annotation.
<?php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class JMSSerializerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->getDefinition('jms_serializer.serialized_name_annotation_strategy')
->replaceArgument(0, new Reference('jms_serializer.identical_property_naming_strategy'));
}
}