I'm working on a project on Symfony 3.4 without Doctrine (we use a homemade ORM similar to Eloquent) and have been stucked trying to make a CollectionType of an Entity I use : IntroductionSlide.
The form looks like this :
You can add or delete IntroductionSlides and each IntroductionSlide is defined only by its image. As the image depends on the locale, it's handled by a TranslatableImageType which creates a field per locale. Clicking on the little flag allows you to choose target locale's input field.
Here's what my code looks like :
The creation of the CollectionType :
case 'introduction_slides':
$options['label'] = "Pages de présentation";
$options['entry_type'] = IntroductionSlideType::class;
$options['allow_add'] = true;
$options['allow_delete'] = true;
$options['attr']['class'] = "introduction-slides";
$options['required'] = false;
$this->form->add($field, CollectionType::class, $options);
break;
The IntroductionSlideType :
<?php
namespace AppBundle\Form\Type;
use AppBundle\Model\IntroductionSlide;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
class IntroductionSlideType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$options['mapped'] = false;
$builder->add("images", TranslatableImageType::class, $options);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => IntroductionSlide::class,
));
}
}
And The TranslatableImageType :
<?php
namespace AppBundle\Form\Type;
use AppBundle\EventSubscriber\StaticConfig;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Image;
class TranslatableImageType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['locales'] = $options['locales'];
$view->vars['locale'] = $options['locale'];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$file_options = array_merge($options, $options["field_options"]);
unset($file_options['locales'], $file_options['locale'], $file_options["field_options"]); // not valid text options
$file_options['compound'] = false; // sub items are simple widgets
$file_options['constraints'] = [new Image([
'mimeTypes' => ["image/png", "image/jpg", "image/jpeg", "image/gif"],
'mimeTypesMessage' => "Seul un fichier avec l'extension png, jpg, jpeg ou gif peut être uploadé.",
"maxSize" => "7M",
'maxSizeMessage' => "Ce fichier est trop volumineux ({{ size }} {{ suffix }}). La limite de volume est de {{ limit }} {{ suffix }}.",
])];
foreach ($options['locales'] as $locale) {
$file_options['attr']['lang'] = $locale;
$builder->add($locale, FileType::class, $file_options);
}
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
));
$resolver->setDefaults(array(
'locales' => StaticConfig::getLocales(),
'locale' => StaticConfig::getLocale(),
'field_options' => [],
));
}
public function getBlockPrefix()
{
return 'translatableImage';
}
}
Here what I was thinking Symfony would do (with no data passed to the collection) :
1. define a CollectionType
2. define an IntroductionSlideType inside the CollectionType
3. instanciate an empty IntroductionSlide and use is as data for the IntroductionSlideType
4. create a field "images" to the builder and call $IntroductionSlide->getImages() to feed its value
5. define a TranslatableImageType inside the IntroductionSlideType
6. use $IntroductionSlide->getImages()'s returned value as data (contains an array of locale => image_name)
Instead, Symfony triggers an Exception :
Cannot read index "0" from object of type "AppBundle\Model\IntroductionSlide" because it doesn't implement \ArrayAccess.
I don't see why it would try to reach an index "0" in an IntroductionSlide.
Setting up a break-point showed me this happens when Symfony is trying to build the field "images" in IntroductionSlideType.
Adding $options['mapped'] = false; to $builder->add("images", TranslatableImageType::class, $options); makes it able to reach the view without triggering the Exception.
When submitting the form, after the data is processed by the Form component, here's what it looks like :

