What I'm using:
I'm using the JMSSerializerBundle to deserialize my JSON object from a POST request.
Description of the problem:
One of the vaules in my JSON is an Id. I'd like to replace this Id with the correct object before the deserialization occurs.
Unfortunately, JMSSerializerBundle does not have a #preDeserializer annotation.
The problem I'm facing (and that I would have faced if there was an #preDeserializer annotation anyway) is that I would like to create a generic function for all my entities.
Question:
How do I replace my Id with the corresponding object in the most generic way possible ?
You also do your own hydratation as I did (with Doctrine):
Solution
The IHydratingEntity is an interface which all my entities implement.
The hydrate function is used in my BaseService generically. Parameters are the entity and the json object.
At each iteration, the function will test if the method exists then it will call the reflection function to check if the parameter's method (setter) also implements IHydratingEntity.
If it's the case, I use the id to get the entity from the database with Doctrine ORM.
I think it's possible to optimize this process, so please be sure to share your thoughts !
protected function hydrate(IHydratingEntity $entity, array $infos)
{
#->Verification
if (!$entity) exit;
#->Processing
foreach ($infos as $clef => $donnee)
{
$methode = 'set'.ucfirst($clef);
if (method_exists($entity, $methode))
{
$donnee = $this->reflection($entity, $methode, $donnee);
$entity->$methode($donnee);
}
}
}
public function reflection(IHydratingEntity $entity, $method, $donnee)
{
#->Variable declaration
$reflectionClass = new \ReflectionClass($entity);
#->Verification
$idData = intval($donnee);
#->Processing
foreach($reflectionClass->getMethod($method)->getParameters() as $param)
{
if ($param->getClass() != null)
{
if ($param->getClass()->implementsInterface(IEntity::class))
#->Return
return $this->getDoctrine()->getRepository($param->getClass()->name)->find($idData);
}
}
#->Return
return $donnee;
}
Related
I am building a Symfony data transformer class called EventDataMapper. It handles two fields: A TextType field called My mapDataToForms() definition looks like this:
public function mapDataToForms($data, $forms)
{
$existingTitle = $data->getTitle();
$existingAttendees = $data->getAttendees();
$this->propertyPathMapper->mapDataToForms($data, $forms);
foreach ($forms as $index => $form) {
if ($form->getName() === 'title' && !is_null($existingTitle)) {
$form->setData($existingTitle);
}
if ($form->getName() === 'attendees' && !is_null($existingAttendees)) {
$form->setData($existingAttendees);
}
}
}
The problem is that I'm setting data before validation runs, so if I submit a form with a non-numeric string in the "attendees" field, I get an ugly TransformationFailedException ('Unable to transform value for property path "attendees": Expected a numeric'). And if I try to do a check for whether my field is valid by adding a call to $form->isValid() in the line before I call $form->setData(), I get a LogicException. ('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().')
Is there any way for my to preemptively call a validator on this specific field from within my DataMapper?
(Yes, this can be somewhat prevented with frontend logic. But I don't want to rely too much on that.)
Closing the loop on this. Here's what we did.
A colleague made a new form type corresponding to a new adapter class that wraps our two previous classes, providing a uniform set of wrapper methods for interacting with them.
We passed Symfony's validator service into our new form type using the constructor.
In that form type, we're using $builder->addEventListener() to add a callback/listener on the POST_SUBMIT event. Here's the callback:
function(FormEvent $event): void {
$adapter = $event->getData();
$form = $event->getForm();
$errors = $adapter->propagate($this->validator);
foreach ($errors as $error) {
$formError = new FormError($error->getMessage());
$targetPath = self::mapPropertyPath($error->getPropertyPath());
$target = $targetPath !== null ? $form->get($targetPath) : $form;
$target->addError($formError);
}
}
The adapter, in turn, has some logic that does various translations of data into a form that can be used in our legacy classes, followed by this:
return $validator->validate($this->legacyObject);
This works well for us. I hope it helps somebody else out too.
Have run into a curious behavior a few times over the years, have always meant to ask about it.
It has to do with a behavior I don't understand around binding objects to forms in Zend Framework.
Consider this factory which builds a form, loads a Doctrine entity from database, and attempts to bind it to the form (so that the values display on render):
class TermsConfigFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
$termsConfig = $container->get(TermsConfigMapper::class)->get($options['locale']);
if (!$termsConfig) {
$termsConfig = new TermsConfig($options['locale']);
}
// A. If we do just this, the form doesn't print data
$form->bind($termsConfig);
return $form;
}
}
The form is well wired through form_elements, and so forth. We then attempt to use it inside a Controller like so:
$termsForm = $this->formElementManager->get(TermsConfigForm::class, ['locale' => $this->locale()]);
$viewModel->setVariable('termsForm', $termsForm);
Interestingly, we find that the object will not show the bound data in the ViewModel. Now, even more curious, is if we remove the call to "bind" in the factory and do this in the controller instead, the values are properly displayed!!
$termsForm = $this->formElementManager->get(TermsConfigForm::class, ['locale' => $this->locale()]);
// B. You have to do this in the controller, here, for it to print data!
$termsForm->bind($termsForm->getObject());
$viewModel->setVariable('termsForm', $termsForm);
Why does it not work within the factory?
From this vantage point, the call to bind in the controller, is analogous to the call to bind in the Factory. I'd like to keep this stuff in the factory, but seems I cannot!
Looks like the intricacy lies with the FormElementManager that stacks the factory and the init cycle in order. In other words, init is not called until the factory has completed its work.
Paraphrasing, calling "bind" within a form's factory does not exhibit the same behavior as calling "bind" after the factory returns the form because the Factory calls init (automatically) in between.
Be careful of this 'order of operations' trap.
If you look at the Factory pattern you'll notice that the job of the factory pattern is to return the desired object only. It has nothing to do with the manipulation of the object. So, with that said all the things you want to do with the desired object should be done in the model object. As the model object can be as fat as it can be. Therefore, I would suggest moving your binding of the desired object out from the factory and move it to the model.
I'll quote something which I saw in a video and Marco Pivetta showed something in a very abstract way and that was something like below:
// The below code is my understanding of Marco Pivetta explaining in a youtube video.
use Psr\Container\ContainerInterface;
class TermsConfigFormFactory implements FactoryInterface
{
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
/*
This should have been the first line in the method, as it looks like. Because it seems you want locale to be given.
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
//From here till binding should be moved to a model.
$termsConfig = $container->get(TermsConfigMapper::class)->get($options['locale']);
if (!$termsConfig) {
$termsConfig = new TermsConfig($options['locale']);
}
// A. If we do just this, the form doesn't print data
$form->bind($termsConfig);
*/
return $form;
}
This is what Marco Pivetta would have done in my opinion according to his video and what I've abstracted.
class TermsConfigFormFactory implements FactoryInterface
{
if (!is_string($options['locale'])) {
throw new \InvalidArgumentException("An illegal locale variable was received by the terms configuration factory");
}
$form = new TermsConfigForm('terms_config_form', $options);
$form->setInputFilter($container->get('InputFilterManager')->get(TermsConfigInputFilter::class, $options));
$form->setHydrator(new DoctrineHydrator($container->get('doctrine.entitymanager.orm_default'), false));
return $form;
}
// Some model
namespace Mynacespace\Model;
class SomeModel extends /* I don't remember the actual FQCN*/DoctirneRepository
{
public function dosomething(Form $form ){
$request = $this->getServiceContainer()->getRequest();
$data = $request->getPost();
$form->bind($data);
// do more of your work here.
// This array returning was not mentioned in Marco video.
return ['status' => 200, 'message' => 'Success', 'data' => 'form' => $form];
}
}
I have below code that save the country information in Database. Below code works fine. There is no problem in that.
private function SaveChanges(\App\Http\Requests\CountryRequest $request) {
if($request['CountryID'] == 0) {
$Country = new \App\Models\CountryModel();
}
else {
$Country = $this->GetCountry($request['CountryID']);
}
$Country->Country = $request['Country'];
$Country->CountryCode = $request['CountryCode'];
$Country->save();
return redirect()->route($this->AllCountries);
}
Now, I decided to shift the working of above method inside a new class like below. Here I am reading the JSON data
class CountryData {
public function CreateCountry($CountryObject) {
$obj = json_decode($CountryObject);
$Country = new \App\Models\CountryModel();
$Country->Country = $CountryObject->Country;
$Country->CountryCode = $CountryObject->CountryCode;
$Country->save();
return true;
}
}
and the original function is changed like below. Sending the Request parameter in the form of JSON.
private function SaveChanges(\App\Http\Requests\CountryRequest $request) {
$data = array(
'Country' => $request['Country'],
'CountryCode' => $request['CountryCode'],
'CountryID' => $request['CountryID']
);
if($request['CountryID'] == 0) {
$result = (new \CountryData())->CreateCountry( json_encode($data) );
}
return redirect()->route($this->AllCountries);
}
Question: Is my approach correct to send converted request object to JSON object and reading in an another Class .
I am doing that so that I can create a new controller and call the CreateCountry from class CountryData to return JSON data for an Android App.
Well, I don't think it's a good approach. Your CountryData class acts as a service, so I think it hasn't have to know anything about JSON, that is part of the interface between your business logic and the external side of your system (Android app, web interface, etc.)
Your new Controller may receive JSON objects and answer with JSON objects, but it must convert the JSON received to your business classes, then pass them to your services, in this case CountryData (not a good name, though).
So the logic should be:
Controller:
- receive request data
- call service and save or whatever
- encode to JSON
- send the response in JSON format
So your business classes don't know anything about JSON.
A not fully code solution is provided as an idea, but it lacks error management, and more work to do. It's based on some Laravel 5 features. Also I don't know if you're using REST or what kind of request are you doing...
use App\Http\Controllers\Controller;
class CountryController() extends Controller {
public function store(\App\Http\Requests\CountryRequest $request) {
// TODO manage errors
$countryModel = $this->createOrUpdateCountry($request);
// Laravel way to response as JSON
return redirect()->json($this->country2Array($countryModel);
}
private function createOrUpdateCountry(\App\Http\Requests\CountryRequest $request) {
$countryId = $request['CountryID'];
if($id == 0) {
$countryModel = new \App\Models\CountryModel();
} else {
$countryModel = $this->GetCountry($countryId);
}
$countryModel->Country = $request['Country'];
$countryModel->CountryCode = $request['CountryCode'];
// You must have an initialised instance of CountryDAO
// TODO manage errors
$countryDAO->saveOrUpdate($countryModel);
return $countryModel;
}
private function country2Array($countryModel) {
$data = array(
'country' => $countryModel->Country,
'countryCode' => $countryModel->CountryCode,
'countryId' => $countryModel->CountryID
);
return $data;
}
}
/**
* Formerly CountryData
*/
class CountryDAO {
public function saveOrUpdate($countryModel) {
// TODO Manage errors or DB exceptions
// I'd put the DB save access/responsability here instead of in CountryModel
$countryModel->save();
return true;
}
}
First of you should not do any conversions to objects and so on.
Second, since the request object should be an array as shown on your example I suggest you to use the "fill" method of Laravel, instead of looping on hand all of the request elements.
Your code for saving the request should be as follows:
class CountryData {
public function CreateCountry($requestData) {
$Country = new \App\Models\CountryModel();
$country->fill($requestData);
$Country->save();
return true;
}
}
The "fill" method loops all of the array keys and tries to set them into the object instance if it has those keys as properties. If there are any extra fields, they are trimmed and you wont get any errors.
Cheers! :)
I'm trying to serialize an XML document containing entities to insert into Doctrine MySQL database.
I got, for example, these two attributes in my entity :
$companyId
$companyName
The problem is that instead of something like this into my XML doc :
<company>
<id>8888</id>
<name>MyCompany</name>
</company>
I got something like this :
<company id="8888" name="MyCompany"/>
The XML is generated by an independant company I work with ; so I can't change it.
So the Symfony2 serializer is creating an empty $company attribute :(
Is there a simple way to costumize the seralizing process like I want ? Or do I have to implement a complete independant method ?
Thanks a lot.
I'd create a simple Denormalizer because attributes are already parsed by default XmlEncoder. It adds a special character # in at the beggining of the key.
Without tweaking alot you could add a context parameter like use_attributes which your custom denormalizer can understand. Here's an example
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class AttributesDenormalizer implements DenormalizerInterface
{
public function __construct(DenormalizerInterface $delegate)
{
$this->delegate = $delegate;
}
public function denormalize($data, $class, $format = null, array $context = array())
{
if (!is_array($data) || !isset($context['use_attributes']) || true !== $context['use_attributes']) {
return $this->delegate->denormalize($data, $class, $format, $context);
}
$attributes = array();
foreach ($data as $key => $value) {
if (0 === strpos($key, '#')) {
$attributes[substr($key, 1)] = $value;
}
}
if (empty($attributes)) {
$attributes = $data;
}
return $this->delegate->denormalize($attributes, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->delegate->supportsDenormalization($data, $type, $format);
}
}
And here is an example of usage
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
$xml = '<company id="8888" name="MyCompany"/>';
$encoders = array(new XmlEncoder());
$normalizers = array(new AttributesDenormalizer(new GetSetMethodNormalizer));
$serializer = new Serializer($normalizers, $encoders);
$serializer->deserialize($xml, 'Company', 'xml', array('use_attributes' => true));
Which results in
class Company#13 (2) {
protected $id =>
string(4) "8888"
protected $name =>
string(9) "MyCompany"
}
It's now much easier to serialize XML attributes by using the #SerializeName annotation with '#'.
In your Company entity, when defining $name, add
/**
* #ORM\Column(type="string", length=255)
* #SerializedName('#name')
*/
private $name;
Now when you serialize to XML, it will come out as a property, as expected.
I know the OP was actually asking about deserialization, but hope this helps someone who is searching.
Okay so finally I tried to use the JMSSerializerBundle but my case is too complicated. I got many entities with several ManyToOne relations ; and I got bot standard and attributes values in my XML.
So I'll use your idea : create my complete whole Denormalizer. It will use the decoded XML and read it line by line, doing what it has to do (creating entities with Doctrine).
It's gonna be a huge process but the most simple one.
Thank you.
[EDIT] I finally found a pretty good solution.
I registered the links between XML and my entity setters into a yaml table
company:
#id: setCompanyId
#name : setCompanyName
address:
#city: setAddressCity
#street: setAddressStreet
...
Thanks to that, I can easily read my whole XML and, for each node/attribute value, find the setter name into the table and then do :
$company = new Company;
$company->setterNameFromTable($value);
Quite old, but I got to a much simpler solution using Serializator + xpath in SerializedName annotation, so this could be useful for someone.
Having for example this entry XML:
<root>
<company id="123456"/>
</root>
Whe deserializing into an object you could use this annotation to populate the company id into "id" property:
/**
* #Serializer\SerializedName(name="company/#id")
*/
public ?int $id = null;
PS: tested on Symfony 5.4
I have a PHP program I'm writing that does a SOAP request, and it returns an Object. I need to write a function where it takes the data from this Object and uses it in various ways, but I don't want it to do a SOAP request each time if the SOAP request for the data in this Object is already resident.
Pseudo-code example:
$price = GetPartPrice("1234");
function GetPartPrice($part_number) {
If Parts_List_Object not found then do SOAP request to get Parts_List_Object.
}
The problem I see is that I don't know where or how to store if the Parts_List_Object is already there. Do I need to set something up to make the StdClass object that gets requested from the SOAP/JSON request global or is there a better method to do all this? Thanks!
One method would be to build a registry of these objects where you store the ones you fetch and look up the ones you need. That allows you to simply grab a reference to the instance that you've already loaded. A very basic example:
class PartListRegistry {
private static $list = array();
// After you do the SOAP request, call this to save a reference to the object
public static function addPartObject($key, $obj) {
self::$list[$key] = $obj;
}
// Call this to see if the object exists already
public static function getPartObject($key) {
if (isset(self::$list[$key])) {
return self::$list[$key];
}
return null;
}
}
function GetPartPrice($part_number) {
$part = PartListRegistry::getPartObject($part_number);
if ($part === null) {
$part = .... // Do your SOAP request here
// Save a reference to the object when you're done
PartListregistry::addPartObject($part_num, $part);
}
// Do your stuff with the part ....
}