Symfony Serializer - Deserialize XML with attributes - php

I cant figure out how to deserialize this xml with the symfony serializer
<?xml version="1.0" encoding="UTF-8"?>
<issues>
<issue id="1" name="test">
<page id="0" name="page 0"/>
<page id="1" name="page 1"/>
<page id="2" name="page 2"/>
....
</issue>
</issues>
I created entitys for issue and page like that
class Issue
{
#[ORM\Id]
#[ORM\Column]
#[SerializedName('issue/#id')]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
#[SerializedName('issue/#name')]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'issue', targetEntity: Page::class)]
#[SerializedPath('issue/page')]
private Collection $Pages;
.....
}
And the serializer is set up like that
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$normalizers = [
new ArrayDenormalizer(),
new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter, null, new ReflectionExtractor())
];
$encoders = [new XmlEncoder()];
$serializer = new Serializer($normalizers, $encoders);
$issues = $serializer->deserialize($xml, Issue::class, 'xml', [
'xml_root_node_name' => 'issues',
]);
but i only get back a empty result:
^ App\Entity\Issue {#184 ▼
-id: null
-name: null
-Pages: Doctrine\Common\Collections\ArrayCollection {#140 ▼
-elements: []
}
}

First, remove issue/ in the values passed in the #SerializedName like this in your Issue class :
#[SerializedName('#id')]
private ?int $id = null;
And for the collection of pages :
#[SerializedPath('[page]')]
private Collection $pages;
You also need to tell to the deserialize method you want an array of issues, not just one :
$issues = $serializer->deserialize($xml, Issue::class.'[]', 'xml', [
'xml_root_node_name' => 'issues',
]);
Note that you will need to create a setId() method in your Issue class to populate the id, same for Page.

Related

Symfony array of objects deserialization

I have this class:
use Symfony\Component\Uid\Ulid;
final class Payload
{
/**
* #param Ulid[] $ulidList
*/
public function __construct(
public readonly string $id,
public readonly array $ulidList,
) {
}
}
when serializing it
$this->serializer->serialize($payload, 'json');
I'm receiving this output:
{"id":"XXX","ulidList":["01GP9H0WPW2A2BK9GYV9GQJMAK"]}
but when de-serializing the above
$this->serializer->deserialize($data, Payload::class, 'json');
the $ulidList property is filled with array of strings instead of Ulid objects.
How to make it to fill it with Ulid? I'm using SerializerInterface loaded from Dependency Injection.
I ended up making my own instance of Serializer with this way:
$this->serializer = new Serializer(
normalizers: [
new UidNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(
propertyTypeExtractor: new PropertyInfoExtractor(
typeExtractors: [new PhpDocExtractor(), new ReflectionExtractor()],
),
),
],
encoders: [
new JsonEncoder(),
],
);
What I was missing is the ObjectNormalizer with properly filled propertyTypeExtractor and ArrayDenormalizer.

Getter on a ManyToMany relation - Symfony\ApiPlatform

Describe here your problem or what you are trying to do.
Hello ,
I'm on a small Symfony/ApiPlatform project but I have to admit that I'm really stuck on a (small?) problem..
I have several entities including a #City entity, an #Company entity and an #Manager entity.
Each city can have multiple companies, and each company can have multiple cities;
Each company can have multiple managers, but each manager can only have one company.
So a manager must be linked to a company but also to a city. For instance :
MyCompany Limited is present in New York and Los Angeles, we have a manager for New York and another manager for Los Angeles.
So far, it's very simple. But where things get tricky is that I can't limit a manager to only company cities.
For example, the New York manager of MyCompagny Limited should not be able to change his city to Chicago, given that his company is not present in this city.
I try to do it from my events in PRE_VALIDATE, I manage to recover the manager's company but on the other hand the $company->getCity() method returns my #Company object rather than the city object?
class ManagerCitySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['setCityForManager', EventPriorities::PRE_VALIDATE]
];
}
public function setCityForManager(ViewEvent $event)
{
$result = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if ($result instanceof Manager && ($method === "POST" || $method === "PATCH"))
{
$company= $result->getCompany();
$city= $result->getCity();
$cityCompany = $company->getCity();
if($city instanceof City){
dd(
$company, // dd : return my company object
$city, // dd : returns the city the user wants
$cityCompany // dd : return also my company object ?
);
} else {
dd("This is not an instance of #City ; else");
}
};
}
}
What I want
It's quite simple, at this stage the user (Manager) can assign himself the city he wants, such as the city of Chicago or Boston (provided he knows his IRI), even if his company is not present in this city but only in New York and Los Angeles.
So I would like to limit the possibility only to the same cities as his company.
A manager of the same company can change city, as long as he respects one of the cities of his company.
So I'm trying to retrieve his company, the city he wants to assign himself but also the cities of his company to be able to make a condition:
if($desiredcity == $cityOfMyCompany) { then I assign the city; } else { I return an error: You don't have access to this city; }
what i get
I can't do the above condition because I can't retrieve the cities of the manager's company. However, I manage to get the manager's company, the city he wants to assign himself, but my $company->getCity() method returns my company object:
dd($cityCompany); =
ManagerCitySubscriber.php on line 35:
Doctrine\ORM\PersistentCollection {#1049
#collection: Doctrine\Common\Collections\ArrayCollection {#1051
-elements: []
}
#initialized: false
-snapshot: []
-owner: Proxies\__CG__\App\Entity\Company {#797
-id: 3
-createdAt: DateTimeImmutable #1662842908 {#1052
date: 2022-09-10 20:48:28.0 UTC (+00:00)
}
-company: "MyCompany Limited"
-companyState: "Limited"
-city: Doctrine\ORM\PersistentCollection {#1049}
-Manager: Doctrine\ORM\PersistentCollection {#1056
#collection: Doctrine\Common\Collections\ArrayCollection {#1057
-elements: []
}
#initialized: false
-snapshot: []
-owner: Proxies\__CG__\App\Entity\Company {#797 …2}
-association: array:15 [ …15]
-em: Doctrine\ORM\EntityManager {#642 …11}
-backRefFieldName: "company"
-typeClass: Doctrine\ORM\Mapping\ClassMetadata {#692 …}
-isDirty: false
}
-Clients: Doctrine\ORM\PersistentCollection {#1058
#collection: Doctrine\Common\Collections\ArrayCollection {#1059
-elements: []
}
#initialized: false
-snapshot: []
-owner: Proxies\__CG__\App\Entity\Company{#797 …2}
-association: array:15 [ …15]
-em: Doctrine\ORM\EntityManager {#642 …11}
-backRefFieldName: "company"
-typeClass: Doctrine\ORM\Mapping\ClassMetadata {#796 …}
-isDirty: false
}
+__isInitialized__: true
…2
}
-association: array:20 [ …20]
-em: Doctrine\ORM\EntityManager {#642 …11}
-backRefFieldName: "companies"
-typeClass: Doctrine\ORM\Mapping\ClassMetadata {#770 …}
-isDirty: false
}
Re,
I "found" the solution to my problem, however I don't know if this is the right method to follow. If anyone has a better idea for cleaner code, it's with great pleasure :
public function setCityForManager(ViewEvent $event)
{
$result = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if ($result instanceof Manager && ($method === "POST" || $method === "PATCH"))
{
$company= $result->getCompany();
$city= $result->getCity();
if($city instanceof City && in_array($city, $company->getCity()->toArray())) {
$result->setCity($city);
} else {
die();
}
};
}

Display Laravel Notification (MailMessage) with markdown after sent

I'm saving every email I send to an entity into the database by creating a function storeEmail and make an insert of MailMessage class into EmailMessage model. Everything works fine, and the main goal is to display the message exactly as it was, when the recipient received it and retrieve all the messages I sent as a User, to a page. To be much easier to retrieve a render of each specific Message in foreach loop, I think is better to fetch it from the Model.
This is my Notification class:
class SimpleEmail extends Notification
{
use Queueable;
private $link;
private $user;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($link)
{
$this->link = $link;
$this->user = Auth::user();
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$mail = (new MailMessage)
->from($this->user->email, $this->user->name)
->subject('My Dummy Subject')
->greeting('To: '.$notifiable->email)
->action('Action Button', url($this->link))
->line('Thank you for reading my message')
->salutation('Friendly, '.$this->user->name);
$this->storeEmail($mail,$notifiable);
return $mail;
}
public function storeEmail($mail,$notifiable){
$email = new EmailMessage;
$email->sender_type = 'App\User';
$email->sender_id = $this->user->id;
$email->mail = $mail;
$email->save();
$notifiable->email_messages()->save($email);
}
}
Note:
I'm using Illuminate\Notifications\Messages\MailMessage
My class extends Illuminate\Notifications\Notification
I'm saving (new MailMessage) in the $email->mail = $mail;
I tried to dd($email->mail); and I get this:
^ array:20 [▼
"view" => null
"viewData" => []
"markdown" => "notifications::email"
"theme" => null
"from" => array:2 [▶]
"replyTo" => []
"cc" => []
"bcc" => []
"attachments" => []
"rawAttachments" => []
"priority" => null
"callbacks" => []
"level" => "info"
"subject" => "My Dummy Subject"
"greeting" => "To: Dohn John"
"salutation" => "Friendly, Nikolas Diakosavvas"
"introLines" => array:2 [▶]
"outroLines" => array:1 [▶]
"actionText" => "Action Button"
"actionUrl" => "http://my-example-url.com ▶"
How can I display the Mail Notification, as it was when I sent it ? What is the optimal solution for that ?
Thanks, in advance
EDITED
Managed to render MailMessage using this code works :
$email = EmailMessage::first();
return (new \App\Notifications\SimpleEmail('my-link', $email->recipient->assignto))->toMail($email->recipient);
But this is not exactly what I wanted, because every time I need to find:
Which Notification class used on every email so I can render it.
Variables for each Notification class.
In order to accomplish this:
1. You can create a accessor.
2. Use Markdown's render method.
3. Pass in render method the mail's markdown you saved in storeEmail.
You can see an example above :
use \Illuminate\Mail\Markdown;
public function getRenderAttribute(){
$markdown = new Markdown(view());
return $markdown->render($this->mail['markdown'], $this->mail);
}

Persisting/Flushing a form created an extra row in the database

I have a form with multiple rows created from one table (no relationships to other tables). When I save the form, every change I've made is saved, but I do have an additional empty row in the database.
See below for (hopefully) all neccessary informations.
PropertyAdditionCostFrequency.php
/**
* #ORM\Entity
* #ORM\Table(name="property_addition_cost_frequency")
*/
class PropertyAdditionCostFrequency
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", nullable=true)
*/
private $label = '';
private $costFrequencyCollection;
public function __construct()
{
$this->costFrequencyCollection = new ArrayCollection();
}
// + the getters and setters
}
PropertyAdditionCostFrequencyLabelType.php
class PropertyAdditionCostFrequencyLabelType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label' )
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => PropertyAdditionCostFrequency::class,
));
}
}
PropertyAdditionCostFrequencyForm.php
class PropertyAdditionCostFrequencyForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add( 'cost_frequency_collection', CollectionType::class, array(
'entry_type' => PropertyAdditionCostFrequencyLabelType::class,
'label' => ''
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => PropertyAdditionCostFrequency::class
]);
}
}
AdminCoreDataController.php
public function showCoreDataListAction( Request $request )
{
$PropertyAdditionCostFrequency = new PropertyAdditionCostFrequency();
$repository = $this->getDoctrine()->getRepository('AppBundle:PropertyAdditionCostFrequency');
$cost_frequency = $repository->findAll();
foreach ($cost_frequency as $k => $v) {
$PropertyAdditionCostFrequency->getCostFrequencyCollection()->add($v);
}
$form = $this->createForm( PropertyAdditionCostFrequencyForm::class, $PropertyAdditionCostFrequency);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$propertyAdditionCostFrequency_form = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($propertyAdditionCostFrequency_form);
$em->flush();
$this->addFlash('success', 'successfully changed the data');
return $this->redirectToRoute('admin_core_data');
}
return $this->render('logged_in/content/admin/core_data/core_data.html.twig', [
'propertyCostFrequencyForm' => $form->createView()
]);
}
core_data.html.twig
{{ form_start(propertyCostFrequencyForm) }}
<div class="row" id="cost_frequency_box">
{% for single_frequency in propertyCostFrequencyForm.cost_frequency_collection %}
<div class="row js-single-cost-frequency-box">
<div class="col-sm-1 js-delete-cost-frequency">
<a href="/admin/property/delete/5">
<i class="fa fa-trash"></i>
</a>
</div>
<div class="col-sm-4">
{{ form_widget(single_frequency.label) }}
</div>
</div>
{% endfor %}
</div>
<div class="row">
<div class="col-sm-3">
<button class="col-sm-12 btn btn-success" type="submit">Speichern</button>
</div>
</div>
{{ form_end(propertyCostFrequencyForm) }}
a dump() of $propertyAdditionCostFrequency_form right before '$em->persist($propertyAdditionCostFrequency_form)'
PropertyAdditionCostFrequency {#394 ▼
-id: null
-label: ""
-costFrequencyCollection: ArrayCollection {#395 ▼
-elements: array:4 [▼
0 => PropertyAdditionCostFrequency {#422 ▼
-id: 1
-label: "1"
-costFrequencyCollection: null
}
1 => PropertyAdditionCostFrequency {#424 ▼
-id: 2
-label: "2"
-costFrequencyCollection: null
}
2 => PropertyAdditionCostFrequency {#425 ▼
-id: 47
-label: "3"
-costFrequencyCollection: null
}
3 => PropertyAdditionCostFrequency {#426 ▼
-id: 38
-label: "4"
-costFrequencyCollection: null
}
]
}
}
The reason why this is happening is evident from the dump you have posted: the top level PropertyAdditionCostFrequency has the id set to null, so of course it is going to create a new row for that, so if you were simply wondering why it creates a new row thats your reason :).
On the other hand I can't help but think, you have misunderstood how symfony forms / doctrine works, UNLESS having multiple child PropertyAdditionCostFrequency added to one parent PropertyAdditionCostFrequency is the desired effect.
IF it is not the desired effect and you simply want to create a form which contains a form for every entry of PropertyAdditionCostFrequency in the database(which I would not recommend, because once the DB gets big, it won't be very efficient), you can just use createFormBuilder method inside the controller, add an arbitrary CollectionType field (i.e. $builder->add('frequencies', CollectionType::class), then just create an array and assign the result of findAll() to a key(key name should correspond to the field name, in this case, 'frequencies') and finally set that as the data for the form you have just created (use $builder->getForm() method to get the form, since thats what you want in the controller and you should use the set data method on that :)) (P.S. these are just rough guidelines, I'm not sure, that I haven't missed anything)

Symfony: Use stylesheet with PdfBundle

In an implementation of PdfBundle, adding a stylesheet to the Pdf() annotation neither throws an error or is used. The page rendered is a default 8.5 x 11, not the expected 5 x 8. Replacing the stylesheet file name with random characters does not elicit an error. Is other configuration required to take advantage of a stylesheet?
Controller:
/**
* #Pdf(stylesheet="ManaClientBundle:Test:pdfstyle.xml.twig",
* #Route("/card")
*/
public function cardAction() {
$em = $this->getDoctrine()->getManager();
$household = $em->getRepository('ManaClientBundle:Household')->find(8607);
$facade = $this->get('ps_pdf.facade');
$response = new Response();
$this->render('ManaClientBundle:Test:card.pdf.twig', array(
'household' => $household,
'date' => date_create(),
), $response);
$xml = $response->getContent();
$content = $facade->render($xml);
return new Response($content, 200, array('content-type' => 'application/pdf'));
}
Template (in .../Resources/views/Test/)
<pdf>
<page id="card">
...
</page>
</pdf>
Stylesheet in .../Resources/views/Test/pdfstyle.xml.twig
<stylesheet>
<page id="card" page-size="8in:5in" margin=".5in" font-size="12">
</page>
</stylesheet>
From the author of the bundle:
If you use $facade object directly, Pdf annotation is unnecessary. You should use Pdf annotation when you want to use pdf rendering in implicit way. In you code you should pass stylesheet xml as second argument of $facade->render(...) method.
Controller now reads:
/**
* #Route("/card")
*/
public function cardAction() {
$em = $this->getDoctrine()->getManager();
$household = $em->getRepository('ManaClientBundle:Household')->find(8607);
$stylesheetXml = $this->renderView('ManaClientBundle:Test:pdfstyle.xml.twig', array());
$facade = $this->get('ps_pdf.facade');
$response = new Response();
$this->render('ManaClientBundle:Test:card.pdf.twig', array(
'household' => $household,
'date' => date_create(),
), $response);
$xml = $response->getContent();
$content = $facade->render($xml, $stylesheetXml);
return new Response($content, 200, array('content-type' => 'application/pdf'));
}

Categories