How can I output ManyToOne data with doctrine (Symfony 4)? - arrays

This is my entity "data":
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\DataRepository")
*/
class Data
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=10, unique=true)
*/
private $uuid;
/**
* #ORM\Column(type="string", length=255)
*/
private $content;
/**
* #ORM\ManyToOne(targetEntity="Fields")
* #ORM\JoinColumn(name="field", referencedColumnName="id")
*/
private $fields;
public function getId(): ?int
{
return $this->id;
}
public function getContent()
{
return $this->content;
}
public function setContent($content)
{
$this->content = $content;
}
public function getUuid(): ?string
{
return $this->uuid;
}
public function setUuid(string $uuid): self
{
$this->uuid = $uuid;
return $this;
}
public function getFields(): ?Fields
{
return $this->fields;
}
public function setFields(?Fields $fields): self
{
$this->fields = $fields;
return $this;
}
}
I am getting the data via doctrine:
$output = $this->em->getRepository(Data::class)->findAll();
The output:
array:2 [▼
0 => Data {#7060 ▼
-id: 1
-uuid: "12345"
-content: "blabla"
-fields: Fields {#7164 ▼
+__isInitialized__: false
-id: 6
-name: null
-uuid: null
-productgroup: null
-type: null
…2
}
}
1 => Data {#7165 ▶}
]
The problem is, that the data of the ManyToOne "fields" captures only the id. But not the name or productgroup. It is all "null". But in my database it is not null.

This usually happen when you are dumping objects with relations. For checking that fields relation has not null values. Do this:
dump($output[0]->getFields()->getName())

This is because relations from doctrine are not loaded in this case.
If you access the relation before, e.g.
$output->fields
The relation will actually be loaded and the fields won't be null if you dump it afterwards
See Relationships and Proxy Classes

This is cause by doctrine lazy loading, in order to avoid overloading memmory.
As #AythaNzt says, if you loop trougth entities you will be able to acces their properties (when you as for getField() doctrine trigger a query to fetch their data)
Check this to display all child fields: Avoid lazy loading Doctrine Symfony2

Related

CakePHP 4.2 Annotate: incorrect app namespace

I'm using CakePHP version 4.2 and am noticing some odd behavior from the annotate script that comes bundled with the API. For one component, the annotate script wants to default to the App\ domain that is CakePHP's default. I've changed the application name so most other classes default to the correct application name. But not this one script and so far, only for this one file.
I've included the body of the component, for review, below. You can see that the #method annotation uses the App\ domain. The trouble comes in when I use PHPStan to analyze my code. If I leave the annotation as is, PHPStan will tell me:
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
Line src/Controller/Component/CartManagerComponent.php
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
43 Property Visualize\Controller\Component\CartManagerComponent::$Controller (Visualize\Controller\AppController) does not accept App\Controller\AppController.
44 Call to method loadModel() on an unknown class App\Controller\AppController.
💡 Learn more at https://phpstan.org/user-guide/discovering-symbols
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
The file itself doesn't use the App\ domain anywhere. I'm not sure where to look for the script to figure out whats wrong. Here is the body of my component in case you see something I do not:
<?php
declare(strict_types=1);
namespace Visualize\Controller\Component;
use Authorization\Identity;
use Cake\Controller\Component;
use Cake\Log\Log;
/**
* CartManager component
*
* #method \App\Controller\AppController getController()
* #property \Visualize\Controller\AppController $Controller
* #property \Visualize\Model\Table\CartsTable $Carts
*/
class CartManagerComponent extends Component
{
/**
* Default configuration.
*
* #var array
*/
protected $_defaultConfig = [];
/**
* #var \Visualize\Controller\AppController
*/
protected $Controller;
/**
* #var \Visualize\Model\Table\CartsTable
*/
protected $Carts;
/**
* #param array $config The current configuration array
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->Controller = $this->getController();
$this->Controller->loadModel('Carts');
}
/**
* Returns the most recent active cart.
*
* #param \Authorization\Identity $user The User entity.
* #return array|\Cake\Datasource\EntityInterface|null
* #noinspection PhpUnnecessaryFullyQualifiedNameInspection
*/
public function getUserCart(Identity $user)
{
$cart = $this->Controller->Carts->newEmptyEntity();
if (!empty($this->Controller->Carts) && is_a($this->Controller->Carts, '\Visualize\Model\Table\CartsTable')) {
$query = $this->Controller->Carts->find('userCart', ['user_id' => $user->getIdentifier()]);
if (!$query->isEmpty()) {
$cart = $query->first();
} else {
$cart->set('user_id', $user->getIdentifier());
$this->Controller->Carts->save($cart);
}
if (is_object($cart) && is_a($cart, '\Cake\Datasource\EntityInterface')) {
$session = $this->Controller->getRequest()->getSession();
$session->write('Cart.id', $cart->id);
}
}
return $cart;
}
/**
* Abandons carts
*
* #param int $user_id The associated user ID
* #param int $cart_id The current cart ID
* #return void
*/
public function pruneCarts(int $user_id, int $cart_id): void
{
if (!empty($this->Controller->Carts) && is_a($this->Controller->Carts, '\Visualize\Model\Table\CartsTable')) {
// Find all the carts we didn't just create:
$userCarts = $this->Controller->Carts->find('all', ['fields' => ['id', 'user_id', 'cart_status']])
->where([
'id !=' => $cart_id,
'user_id' => $user_id,
'cart_status' => 'active',
]);
if (!$userCarts->isEmpty()) {
$count = 0;
foreach ($userCarts as $cart) {
if ($count < 5) {
$record = $this->Controller->Carts->newEmptyEntity();
$record = $this->Controller->Carts->patchEntity($record, $cart->toArray());
$record->set('id', $cart->id);
$record->set('cart_status', ABANDONED_CART);
if (!$this->Controller->Carts->save($record)) {
Log::alert('Error abandoning cart');
}
} else {
$this->Controller->Carts->delete($cart);
}
$count++;
}
}
}
}
}

Cannot see validation errors when form is invalid (submitted as json from react)

How do I see validation errors when posting json data and self submitting to a symfony form? isValid() is false but I can't access error messages to return. But the Symfony Profiler DOES show the error messages in the Ajax request history. E.g. when duplicate username the profiler shows:
Validator calls in ValidationListener.php
data.username There is already an account with this username
Forms "registration_form" "App\Form\RegistrationFormType"
There is already an account with this username
Caused by: Symfony\Component\Validator\ConstraintViolation
When all the fields are valid the new User is created in the database successfully as expected.
Here is my controller:
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\LoginFormAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
class RegistrationController extends AbstractController
{
/**
* #Route("/api/register", name="app_register")
*/
public function register(
Request $request,
UserPasswordEncoderInterface $passwordEncoder,
GuardAuthenticatorHandler $guardHandler,
LoginFormAuthenticator $authenticator
): Response {
if ($request->isMethod('POST')) {
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$data = json_decode($request->getContent(), true);
$form->submit($data);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
// login the newly registered user
$login = $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'main' // firewall name in security.yaml
);
if ($login !== null) {
return $login;
}
return $this->json([
'username' => $user->getUsername(),
'roles' => $user->getRoles(),
]);
} else {
$formErrors = $form->getErrors(true); // returns {}
return $this->json($formErrors, Response::HTTP_BAD_REQUEST);
}
}
}
}
Here is my RegistrationFormType:
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class, [
'label' => 'First Name',
'required' => false
])
->add('lastName', TextType::class, [
'label' => 'Last Name',
'required' => false
])
->add('username')
->add('emailAddress', EmailType::class, [
'label' => 'Email Address'
])
->add('plainPassword', PasswordType::class, [
'mapped' => false
])
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You must comply.',
]),
],
])
->add('Register', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => false
]);
}
public function getName()
{
return 'registration_form';
}
}
Here is my entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"username"}, message="There is already an account with this username")
* #UniqueEntity(fields={"emailAddress"}, message="There is already an account with this email address")
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $emailAddress;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $firstName;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $lastName;
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getEmailAddress(): ?string
{
return $this->emailAddress;
}
public function setEmailAddress(string $emailAddress): self
{
$this->emailAddress = $emailAddress;
return $this;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
}
Got it working.
$formErrors = [];
foreach ($form->all() as $childForm) {
if ($childErrors = $childForm->getErrors()) {
foreach ($childErrors as $error) {
$formErrors[$error->getOrigin()->getName()] = $error->getMessage();
}
}
}
return $this->json(
['errors' => $formErrors],
Response::HTTP_BAD_REQUEST
);
Returns:
{
"errors": {
"username": "There is already an account with this username"
}
}
Basically you get form errors at the top level like if there are extra fields etc but you get the form field errors from the actual child elements. You are only returning the top level form errors.
I have a helper function I use to return all errors from a form if I am returning a JsonResponse.
The following is my extended abstract controller. All my controllers extend this and then I can keep a few helper methods here.
I use the createNamedForm() method and keep the name blank as it's easier when sending from Ajax as I don't need to nest the data in the array with the form name as the key.
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as SymfonyAbstractController;
use Symfony\Component\Form\FormInterface;
class AbstractController extends SymfonyAbstractController
{
protected function createNamedForm(string $name, string $type, $data = null, array $options = []): FormInterface
{
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
}
protected function getFormErrors($form)
{
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $childForm) {
if ($childForm instanceof FormInterface) {
if ($childErrors = $this->getFormErrors($childForm)) {
$errors[$childForm->getName()] = $childErrors;
}
}
}
return $errors;
}
}
I would create the form in the following way:
$form = $this->createNamedForm('', RegistrationFormType::class);
and return my form errors as follows:
return $this->json($this->getFormErrors($form), 400)
If you want to use the normal createForm() function then you will need to send the data in the following format:
['registration_form' => ['firstName' => 'Tom', 'lastName' => 'Thumb']]
This works in both Symfony4 and Symfony5

Create a bidirectional many-to-many association with an extra field on doctrine

I work with symfony 3 and doctrine and this is my problem:
I want to create dishes composed of ingredients of different quantity.
The database will look like this:
[ Dish ] <===> [ IngredientDish ] <===> [ Ingredient ]
[------] [----------------] [------------]
[- name] [- Dish ] [-name ]
[ ] [- Ingredient ] [ ]
[ ] [- quantity ] [ ]
[------] [----------------] [------------]
This is my code :
Dish.php
/**
* #ORM\Table(name="dish")
* #ORM\Entity(repositoryClass="AppBundle\Repository\DishRepository")
*/
class Dish
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
* mappedBy="dish")
*/
private $ingredientsDish;
[...]
public function addIngredientDish(IngredientDish $ingredient)
{
$this->ingredientsDish[] = $ingredient;
return $this;
}
public function getIngredientsDish()
{
return $this->ingredientsDish;
}
}
Ingredient.php
/**
* #ORM\Table(name="ingredient")
* #ORM\Entity(repositoryClass="AppBundle\Repository\IngredientRepository")
*/
class Ingredient
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
* mappedBy="ingredient")
* #Assert\Type(type="AppBundle\Entity\IngredientDish")
*/
private $ingredientsDish;
[...]
public function addIngredientDish(IngredientDish $ingredientDish)
{
$this->ingredientDish[] = $ingredientDish;
return $this;
}
public function getingredientsDish()
{
return $this->ingredients;
}
}
IngredientDish.php
/**
* #ORM\Table(name="ingredient_dish")
* #ORM\Entity(repositoryClass="AppBundle\Repository\IngredientDishRepository")
*/
class IngredientDish
{
/**
* #ORM\Column(name="quantity", type="smallint")
* #Assert\NotBlank()
* #Assert\Length(min=1)
*/
private $quantity = 1;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Ingredient",
* inversedBy="ingredientsDish")
* #Assert\Type(type="AppBundle\Entity\Ingredient")
*/
private $ingredient;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Dish",
* inversedBy="ingredientsDish")
* #ORM\JoinColumn()
* #Assert\Type(type="AppBundle\Entity\Dish")
*/
private $dish;
public function __construct(Ingredient $ingredient, Dish $dish, $quantity = 1)
{
$this->setIngredient($ingredient);
$this->setDish($dish);
$this->setQuantity($quantity);
}
public function setQuantity($quantity)
{
$this->quantity = $quantity;
return $this;
}
public function getQuantity()
{
return $this->quantity;
}
public function setIngredient(Ingredient $ingredient)
{
$this->ingredient = $ingredient;
return $this;
}
public function getIngredient()
{
return $this->ingredient;
}
public function setDish(Dish $dish)
{
$this->dish = $dish;
return $this;
}
public function getDish()
{
return $this->dish;
}
}
My test code
$em = $this->getDoctrine()->getManager();
//Get an apple pie
$dish = $em->getRepository('AppBundle:Dish')->find(6);
//Get an apple
$ingredient = $em->getRepository('AppBundle:Ingredient')->find(11);
$quantityApple = 5;
$ingredientDish = new IngredientDish($ingredient, $dish, $quantityApple);
$ingredient->addIngredientDish($ingredientDish);
$dish->addIngredientDish($ingredientDish);
$em->persist($ingredientDish);
$em->persist($dish);
$em->flush();
After execution, i have an interesting entry:
mysql> select * from ingredient_dish;
+----+---------------+----------+---------+
| id | ingredient_id | quantity | dish_id |
+----+---------------+----------+---------+
| 1 | 11 | 5 | 6 |
+----+---------------+----------+---------+
But after, if I try to get my dish:
$dish = $em->getRepository('AppBundle:Dish')->find(6);
dump($dish->getIngredientsDish());
It has no ingredients :
PersistentCollection {#1180 ▼
-snapshot: []
-owner: Dish {#1146 ▶}
-association: array:15 [ …15]
-em: EntityManager {#1075 …11}
-backRefFieldName: "dish"
-typeClass: ClassMetadata {#1157 …}
-isDirty: false
#collection: ArrayCollection {#1181 ▼
-elements: [] <<<<<<<<<<<<<<<<<<<<< EMPTY
}
#initialized: false
}
The database is not empty after the execution of my test code, so I think there is an error of getter.
Can you help me, do you see something false ?
Thanks you for your help ! :)
I think that everything is fine, but you got mislead by lazy loading which is apparently smarter than you think it is. ;-)
When you do
$dish->getIngredientsDish();
you receive PersistentCollection which extends AbstractLazyCollection.
But the collection is still not fetched from DB(!)
Take a look closer into your var_dump result
PersistentCollection {#1180 ▼
-snapshot: []
-owner: Dish {#1146 ▶}
-association: array:15 [ …15]
-em: EntityManager {#1075 …11}
-backRefFieldName: "dish"
-typeClass: ClassMetadata {#1157 …}
-isDirty: false
#collection: ArrayCollection {#1181 ▼
-elements: [] <<<<<<<<<<<<<<<<<<<<< EMPTY //<= yeah empty, but...
}
#initialized: false // <= it's still not initialized!
}
As you can see there's initialized property which says that the collection is still not initialized (not fetched form DB).
Just try to use it. It will fetch the collection on first usage.
First, there are some problems with you annotations mappings.
Make the follow changes:
/**
* #ORM\Table(name="dish")
* #ORM\Entity(repositoryClass="AppBundle\Repository\DishRepository")
*/
class Dish
{
/**
* #ORM\Column(name="dish_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $dish_id;
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
* mappedBy="dish")
*/
private $ingredientsDish = null;
public function __construct() {
$this->ingredientsDish = new ArrayCollection();
}
...
}
/**
* #ORM\Table(name="ingredient_dish")
* #ORM\Entity(repositoryClass="AppBundle\Repository\IngredientDishRepository")
*/
class IngredientDish
{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Dish",
* inversedBy="ingredientsDish")
* #ORM\JoinColumn(name="dish_id", referencedColumnName="country_id")
* #Assert\Type(type="AppBundle\Entity\Dish")
*/
private $dish;
...
}
In the Dish entity you need to have a constructor for the ArrayCollection, and set it to null.
You can also take a look at this other post of mine (on stackoverflow for a reference:
Doctrine Entities Relations confusing

symfony render json_array entity type and save using form

Imagine I have an Article entity.
And in this entity have a report attribute which is an json_array type.
Json_array's data may like
{"key1":"value1","key2":{"k1":"v1","k2","v2"...},"key3":["v1","v2","v3"...]...}.
I mean the json_array may contains simple key:value or the value may also contains key:vaule or the value may an array.
Now I don't know how to use symfony form to render and save these json_array like other normal attribute(e.g.,title).At the same time,I want to manage the key label name with an meaning name just like change the title field's label.
How to achieve this,I feel very difficult.
class Article
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* #var array
*
* #ORM\Column(name="report", type="json_array")
*/
private $report;
}
Maybe you can use json_decode to pass from json to array and later in the form you can use:
->add('someField', null, array('mapped' => false))
And in the success do something with this values
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// some awesome code here
}
Hope this can help you.
Roger
you can create a data type to manage your report field :
namespace Acme\TestBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ReportType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('key1',TextType::class,array('label' => 'K1'))
->add('key2',TextType::class,array('label' => 'K2'))
;
}
public function getName()
{
return 'report';
}
}
Then declare the new data type :
# src/Acme/TestBundle/Resources/config/services.yml
services:
acme_test.form.type.report:
class: Acme\TestBundle\Form\Type\ReportType
tags:
- { name: form.type, alias: report }
And finally use this new dataType in your form :
->add('reports',
'collection',
array(
'type'=>'report',
'prototype'=>true,
'allow_add'=>true,
'allow_delete'=>true,
'options'=>array(
)
)
)

Symfony, how to render and save use form

Imagine I have an Article entity, and in this entity have a report attribute which is a json_array type.
Json_array's data like {"key1":"value1","ke2":"value2",...}.
Now I don't know how to use symfony form to render and save these json_array like other normal attribute(e.g.,title).
I searched many articles but I haven't find a clear way to realize it.
class Article
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* #var array
*
* #ORM\Column(name="report", type="json_array")
*/
private $report;
}
Here is my take on it. It sounds like you need a custom form type in a collection and a transformer. The idea is that you create a custom form type to house your key/value pairs, let's call it KeyValueType. Next you want to add this type to your ArticleType using a CollectionType as a wrapper. In order to turn your json to a useable data structure for the form and vice versa a transformer is used.
The KeyValueType class:
class KeyValueType extends AbstractType
{
public function buildForm($builder, $options)
{
$builder
->add('key')
->add('value');
}
}
The ArticleType class:
class ArticleType extends AbstractType
{
public function buildForm($builder, $options)
{
$builder
->add('report', CollectionType::class, [
'entry_type' => KeyValueType::class,
'allow_add' => true,
'allow_delete' => true,
]);
$builder->get('report')
->addModelTransformer(new CallbackTransformer(
function ($reportJson) {
// $reportJson has the structure {"key1":"value1","ke2":"value2",...}
if ($reportJson == null) {
return null;
}
$data = [];
foreach (json_decode($reportJson, true) as $key => $value) {
$data[] = ['key' => $key, 'value' => $value];
}
return $data;
},
function ($reportArray) {
// $reportArray has the structure [ [ 'key' => 'key1', 'value' => 'value1'], [ 'key' => 'key2', 'value' => 'value2'] ]
if ($reportArray == null) {
return null;
}
$data = [];
foreach ($reportArray as $report) {
$data[$report['key']] = $report['value'];
}
return json_encode($data);
}
));
}
}

Resources