I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.
Related
I am trying to serialize an entity class which has some \DateTime fields. Everything works fine, but \DateTime objects are converted to string using the following format: "2019-10-21T01:05:12+00:00", while I would like to get just the date part: "2019-10-21".
Symfony documentation mentions default format but doesn't explain how to configure it:
DateTimeNormalizer This normalizer converts DateTimeInterface objects (e.g. DateTime and DateTimeImmutable) into strings. By default, it uses the RFC3339 format.
Is it possible to change the default DateTime normalization format and how?
Entity class:
class Fact
{
/**
* #ORM\Column(type="datetime", options={"default": "CURRENT_TIMESTAMP"})
* #Groups({"api"})
*/
private $created_on;
}
Normalization example:
use Symfony\Component\Serializer\SerializerInterface;
class FactController extends AbstractController
{
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api']),
);
...
}
}
It turns out that '$context' array is passed down to normalize() function of each supported Normalizer. Built-in Normalizers define array keys they accept and their default values.
The relevant key in my case is 'datetime_format', which defaults to \DateTime::RFC3339. Format must be the one accepted by \DateTime::format() and \DateTime::createFromFormat() methods - these functions are used for normalization / denormalization.
Correct usage in my case is:
public function view($id)
{
....
$data = array(
'fact' => $this->serializer->normalize($fact, null, ['groups'=> 'api',
'datetime_format' => 'Y-m-d']),
);
...
}
Here is an answer in another question. Solution is:
services:
serializer.normalizer.datetime:
class: ‘Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
arguments
-
!php/const Symfony\Component\Serializer\Normalizer\DateTimeNormalizer::FORMAT_KEY: 'Y-m-d\TH:i:s.uP’
tags:
- { name: serializer.normalizer, priority: -910 }
Priority is taken from the original service, so, this solution will not have any side effects.
Trying to use the serializer component in Symfony 3.3. I struggle with entities having 'DateTime' members.
My config.yml serializer init:
serializer:
enable_annotations: true
Added this in service.yml:
datetime_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
public: false
tags: [serializer.normalizer]
The deserialized code looks like this:
$yml = [...] // It was created by serializer->serialize()
$serializer = $this->get('serializer');
$myObject = $serializer->deserialize($yml, MyObject::class, "yaml");
The error is get is: Expected argument of type "DateTime", "string" given in in vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php (line 204)
I think the DateTimeNormalizer::denormalize never gets called. Any idea how to bring it back to life?
Info: DateTimeNormalizer::__constructor() is called.
As DateTime is a nested object, you should use PropertyInfo Component as described here —
https://symfony.com/doc/current/components/serializer.html#recursive-denormalization-and-type-safety
The extraction of property information is performed by extractor
classes.
https://symfony.com/doc/current/components/property_info.html#extractors
There are 4 types of extractors:
ReflectionExtractor
PhpDocExtractor
SerializerExtractor
DoctrineExtractor
For example, using ReflectionExtractor you need to specify type hint for either params or return type. It also looks for constructor params (requires to be enabled explicitly)
class Item {
protected $date;
public function setDate(\DateTime $date) {...}
public function getDate() : \DateTime {...}
}
Property Info is registered automatically when option set:
# config/packages/framework.yaml
framework:
property_info: ~
After that you need to override serializer service to use it, or define a custom one. And the last part — add DateTimeNormalizer, so DateTime can be processed by serializer.
app.normalizer.item_normalizer:
class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
arguments:
- null
- null
- null
- '#property_info.reflection_extractor'
tags: [ 'serializer.normalizer' ]
app.serializer.item:
class: Symfony\Component\Serializer\Serializer
public: true
arguments:
- [
'#serializer.normalizer.datetime',
'#app.normalizer.item_normalizer',
]
- [ '#serializer.encoder.json' ]
That's it.
This question break my brain recently, and I've two entities with dateTime property, the solution is custom denormalizer like this:
<?php
namespace MyBundle\Serializer\Normalizer;
use MyBundle\Entity\MyEntity1;
use MyBundle\Entity\MyEntity2;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* DateTime hook normalizer
*/
class DateTimeHookNormalizer implements DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
if (isset($data['MyDateTime1']) && is_string($data['MyDateTime1']))
{
$data['MyDateTime1'] = new \DateTime($data['MyDateTime1']);
}
if (isset($data['MyDateTime2']) && is_string($data['MyDateTime2']))
{
$data['MyDateTime2'] = new \DateTime($data['MyDateTime2']);
}
And more ...
$normalizer = new ObjectNormalizer();//default normalizer
return $normalizer->denormalize($data, $class, $format, $context);
}
}
/**
* {#inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return is_array($data) && ($type === MyEntity1::class || $type === MyEntity2::class);
}
And declare service like this :
# DateTime Hook Normalizer
Mybundle.normalizer.dateTimeHook:
class: 'MybundleBundle\Serializer\Normalizer\DateTimeHookNormalizer'
public: false
tags: [serializer.normalizer]
It's ok for me, that work !
The only official way is seems to declare a callback:
$callback = function ($dateTime) {
return $dateTime instanceof \DateTime
? $dateTime->format(\DateTime::ISO8601)
: '';
};
$normalizer->setCallbacks(array('createdAt' => $callback));
$serializer = new Serializer(array($normalizer), array($encoder));
https://symfony.com/doc/current/components/serializer.html#using-callbacks-to-serialize-properties-with-object-instances
I have a controller that renders a form that is suppose to have a dropdown with titles mapped against a client_user entity. Below is code I use in my controller to create the form:
$builder = $this->get(form.factory);
$em = $this->get('doctrine.entity_manager');
$form = $builder->createBuilder(new ClientUserType($em), new ClientUser())->getForm();
Below is my ClientUserType class with a constructor that I pass the entity manager on:
<?php
namespace Application\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class ClientUserType extends AbstractType
{
protected $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', EntityType::class, array(
'class' => '\\Application\\Model\\Entity\\Title',
'em' => $this->entityManager
))
->add('name')
->add('surname')
->add('contact')
->add('email');
}
public function getName()
{
return 'client_user_form';
}
}
I keep on getting this catchable fatal error below and have no idea what I need to do in order to get a dropdown with titles from a database with doctrine.
Catchable fatal error: Argument 1 passed to Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must be an instance of Doctrine\Common\Persistence\ManagerRegistry, none given, called in D:\web\playground-solutions\vendor\symfony\form\FormRegistry.php on line 90 and defined in D:\web\playground-solutions\vendor\symfony\doctrine-bridge\Form\Type\DoctrineType.php on line 111
Reading from that error I have no idea where I need to create a new instance of ManagerRegistry registry as it appears that the entity manager does not work. I am also thinking perhaps I need to get the ManagerRegistry straight from the entity manager itself.
Can someone please help explain the simplest way to get this to work? What could I be missing?
Seems that doctrine-bridge form component is not configured.
Add class
namespace Your\Namespace;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
class ManagerRegistry extends AbstractManagerRegistry
{
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container;
}
}
and configure doctrine-bridge form component
$application->register(new Silex\Provider\FormServiceProvider(), []);
$application->extend('form.extensions', function($extensions, $application) {
if (isset($application['form.doctrine.bridge.included'])) return $extensions;
$application['form.doctrine.bridge.included'] = 1;
$mr = new Your\Namespace\ManagerRegistry(
null, array(), array('em'), null, null, '\\Doctrine\\ORM\\Proxy\\Proxy'
);
$mr->setContainer($application);
$extensions[] = new \Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($mr);
return $extensions;
});
array('em') - em is key for entity manager in $application
For others that may find this: If you want to use the EntityType and you're not using a framework at all, you need to add the DoctrineOrmExtension to your FormFactoryBuilder like so:
$managerRegistry = new myManagerRegistry(
'myManager',
array('connection'),
array('em'),
'connection',
'em',
\Doctrine\ORM\Proxy\Proxy::class
);
// Setup your Manager Registry or whatever...
$doctrineOrmExtension = new DoctrineOrmExtension($managerRegistry);
$builder->addExtension($doctrineOrmExtension);
When you use EntityType, myManagerRegistry#getService($name) will be called. $name is the name of the service it needs ('em' or 'connection') and it needs to return the Doctrine entity manager or the Doctrine database connection, respectively.
In your controller, try to call the service like that:
$em = $this->get('doctrine.orm.entity_manager');
Hope it will help you.
Edit:
Sorry, I thought you was on Symfony... I have too quickly read...
I have some entity, but i want to validate a maximum of X entries in db.
For example, no more than 5 categories in DB are allowed.
I try to see if some constrain validation solve my trouble, but i dont find nothing useful.
Thank in advance
If you need to validate your entity in multiple places or do this type of validation for more than one entity class you could write a custom validation constraint to do this. It does feel a bit like overkill but it is technically the 'correct' solution. This is documented in the Symfony Manual in How to create a Custom Validation Constraint. For example:
Constraint class:
<?php
namespace Your\Bundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class MaxEntries extends Constraint
{
public $message = 'The maximum %max% %name% entries already exist.';
public $max = 5;
public function validatedBy()
{
return 'maxEntries';
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
ConstraintValidator class:
<?php
namespace Your\Bundle\Validator\Constraints;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class MaxEntriesValidator extends ConstraintValidator
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function validate($protocol, Constraint $constraint)
{
/** #var MaxEntries $constraint */
$protocolClass = new \ReflectionClass(get_class($protocol));
$entityName = $protocolClass->getShortName();
// alternative to next 2 lines - you could use dql to do a select count
$repository = $this->entityManager->getRepository('YourBundle:' . $entityName);
$entities = $repository->findAll();
if (count($entities) == $constraint->max)
{
$this->context->addViolation($constraint->message, array(
'%max%' => $constraint->max,
'%name%' => $entityName,
));
}
}
}
services.yml:
validator.max_entries:
class: Bullitt\TargetNeutral\SpectatorBundle\Validator\Constraints\MaxEntriesValidator
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constraint_validator, alias: maxEntries }
validation.yml:
Your\Bundle\YourEntity:
constraints:
- Your\Bundle\Validator\Constraints\MaxEntries:
# You can override the default constraint attributes here
message: "Failed! The maximum %max% %name% entries already exist."
max: 42
Note, you might be able to come up with a better name than MaxEntries (I normally spend more time thinking of the most meaningful name). Also, the validator class currently contains no protection for applying it to a class that is not a doctrine entity.
You will probably have to implement this yourself. I haven't seen any such thing, but it wouldn't be difficult to do one your own. Just add a function to check how many entries you have in the db, and call this function before you insert anything.
Something like this can be done:
public function newAction(){
$isAllowed = $this->checkMaxRecords();
if($isAllowed){
//save to the database
}
}
private function checkMaxRecords(){
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT c
FROM AcmeStoreBundle:Category c'
);
$category = $query->getResult();
if(count($category) >= 5){
return false;
}
return true;
}
This is untested code, but something along these lines should get you on the right track.
I try to pass my variable to constraint in form validator, but can't.
i'm doing that:
$payForm = $this->createForm(new CableTVPayType(), null, array('balance' => $balance));
And in CableTVPayType:
public function getDefaultOptions(array $options)
{
$maxSumm = $options['balance'] - 100;
[...]
It works fine, my maxSumm is what i want, but Symfiony checks $options array. 'balance' isn't a default option, and complain about this:
The option "balance" does not exist
Is there another, more right way to pass custom variable to validation?
Use the constructor for stuff to be used by all instances of a type. For example, your type might need an entity manager for it to work. It will be reused across all the form instances.
For instance specific stuff use options. If you use the constructor for instance specific stuff, all the instances will get the value you pass to the constructor of the first instance.
/**
* #FormType
*/
class PayType extends AbstractType {
private $someService;
/**
* #InjectParams
*/
public function __construct(SomeService $someService)
{
$this->someService = $someService;
}
public function getDefaultOptions(array $options)
{
return array(
'balance' => 0
);
}
public function getName()
{
return 'pay';
}
}
$form = $this->createForm('pay', null, array('balance' => $balance));
Note that the #FormType annotation registers the type as a service. It allows you to use the type's name instead of creating an instance manually. It gets even more convenient when a type needs a service to be injected into it. You use just the name — pay in this case — instead of something like this:
$form = $this->createForm(new PayType($this->get('some_service')), null, array(
'balance' => $balance
));
Done with this!
Crate variable for a class, and passing value to it through construct method
class CableTVPayType extends AbstractType {
private $maxSumm;
public function __construct($maxSumm) {
$this->maxSumm = $maxSumm;
}
Create form with argument
$payForm = $this->createForm(new CableTVPayType($someValue));
Now i can use this variable as i want in my form.