Andrelec1
Andrelec1

Reputation: 382

Symfony, collection, multiple entity, got arrayCollection, need entity

On old project with symfony 2.8,

i have 3 type of value (computedValue, manualData, probeData) I have a entity name 'dataSource' that contains the 3 source, but only one can be set ( the 2 other is set to null )

I have other entity that contains 3 DataSource, and 3 ArrayCollection of DataSource.

I make my form like this:

        ...
        ->add('dsRef1', DataSourceType::class, [
            'site' => $site,
            'multiple' => false,
            'label_format' => 'form.%name%Ref1',
        ])


        ->add('dsList1', DataSourceType::class, [
            'site' => $site,
            'multiple' => true,
            'label_format' => 'form.%name%List1',
        ])
        ... ( 3 time , dsRef1 - 3 , dlList1 - 3 )

My DataSourceType is:

        $builder
        // This value is added for force symfony to think the form is submit when the form need to be empty
        ->add('hiddenCrapValue', HiddenType::class, [
            'required' => false,
            'mapped' => false,
            'attr' => [
                'front-attr' => [
                    'render' => 'hidden',
                ],
            ],
        ])
        ->add(DataSourceInterface::DATA_SOURCE, ChoiceType::class, [
            'choices' => $site->getDataSources(),
            'choices_as_values' => true,
            'multiple' => $multiple,
            'mapped' => false,
            'required' => false,
            'label_format' => $labelFormat,
        ])
        ->add(DataSourceInterface::PROBE_DATA, EntityType::class, [
            'class' => ProbeData::class,
            'required' => false,
            'multiple' => $multiple,
            'attr' => [
                'front-attr' => [
                    'render' => 'hidden',
                ],
            ],
        ])
        ->add(DataSourceInterface::MANUAL_DATA, EntityType::class, [
            'class' => ManualData::class,
            'required' => false,
            'multiple' => $multiple,
            'attr' => [
                'front-attr' => [
                    'render' => 'hidden',
                ],
            ],
        ])
        ->add(DataSourceInterface::COMPUTED_VALUE, EntityType::class, [
            'class' => ComputedValue::class,
            'required' => false,
            'multiple' => $multiple,
            'attr' => [
                'front-attr' => [
                    'render' => 'hidden',
                ],
            ],
        ])
    ;

That work for my ref ( dsRef1, dsRef2, dsRef3 ) ... But when i add the dsList1 i have a error that say :

"The form's view data is expected to be an instance of class EnergySolution\\ApiBundle\\Entity\\DataSource, but is an instance of class Doctrine\\Common\\Collections\\ArrayCollection. You can avoid this error by setting the \"data_class\" option to null or by adding a view transformer that transforms an instance of class Doctrine\\Common\\Collections\\ArrayCollection to an instance of EnergySolution\\ApiBundle\\Entity\\DataSource.""

Why my multiple option seem not working ?

Edit add mapping:

    $table = $builder->getClassMetadata()->getTableName();
    $builder
        ->setTable('chart_energy_goal')
        ->createField('name', Type::STRING)
            ->nullable()
        ->build()
        ->createField('title1', Type::STRING)
            ->nullable()
        ->build()
        ->createManyToOne('dsRef1', DataSource::class)
            ->cascadeAll()
        ->build()
        ->createManyToMany('dsList1', DataSource::class)
            ->setJoinTable("{$table}_dsList1")
            ->addJoinColumn("{$table}_id", 'id')
        ->build()
        ->createField('goal1', Type::FLOAT)
        ->build()
        ->createField('title2', Type::STRING)
            ->nullable()
        ->build()
        ->createManyToOne('dsRef2', DataSource::class)
            ->cascadeAll()
        ->build()
        ->createManyToMany('dsList2', DataSource::class)
            ->setJoinTable("{$table}_dsList2")
            ->addJoinColumn("{$table}_id", 'id')
        ->build()
        ->createField('goal2', Type::FLOAT)
        ->build()
        ->createField('title3', Type::STRING)
            ->nullable()
        ->build()
        ->createManyToOne('dsRef3', DataSource::class)
            ->cascadeAll()
        ->build()
        ->createManyToMany('dsList3', DataSource::class)
            ->setJoinTable("{$table}_dsList3")
            ->addJoinColumn("{$table}_id", 'id')
        ->build()
        ->createField('goal3', Type::FLOAT)
        ->build()
    ;

DataSource::class

    $builder = new ClassMetadataBuilder($metadata);
    $builder
        ->setTable('data_sources')
        ->setCustomRepositoryClass(EntityRepository::class)
        ->createField('id', Type::INTEGER)
        ->columnName('id')
        ->makePrimaryKey()
        ->generatedValue()
        ->build()
        ->addManyToOne('computedValue', ComputedValue::class)
        ->addManyToOne('probeData', ProbeData::class)
        ->addManyToOne('probeData', ManualData::class)
    ;

Edit crap solution :

After multiple try i found my formBuilder return something like

['computedValue' => [], 'probeData' => [], 'probeData' => []]

instead of

[dataSource ...] 

So i Transforme the data like this.

    $builder->get('dsList1')
        ->addModelTransformer(new CallbackTransformer(
            function ($dsListAsArray) {
                // never edit the form
                return $dsListAsArray;
            },
            function ($ArrayOfDataSources) {
                // transform list of 'dataSource' to liste of dataSource obj.
                $collection = new ArrayCollection();
                $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
                    ->enableExceptionOnInvalidIndex()
                    ->getPropertyAccessor()
                ;
                foreach ($ArrayOfDataSources as $dataSourceType => $dataSourcesArray) {
                    foreach ($dataSourcesArray as $dataSourceArray) {
                        $dataSource = new DataSource();
                        $propertyAccessor->setValue($dataSource, $dataSourceType, $dataSourceArray);
                        $collection->add($dataSource);
                    }
                }
                return $collection;
            }
        ))
    ;

Upvotes: 0

Views: 1350

Answers (1)

Jakumi
Jakumi

Reputation: 8374

okay, as far as i can tell DataSourceType can handle only one DataSource (not an array of them).

As I assume from your data transformer, that dsList1 is a many-to-many field, thus an array or ArrayCollection (or similar), probably coming from doctrine. It's probably an ArrayCollection of DataSources?

Now, the error message given implies exactly that that's the problem, you provide an ArrayCollection where a DataSource is expected.

In my opinion you have two options:

option 1. really just render the first data source

If your dsList1 is really only one entity all the time (although the question is, why it's a many-to-many, but let's say legacy reasons), you could access the first datasource in the collection by adapting your data transformer accordingly:

$builder->get('dsList1')
    ->addModelTransformer(new CallbackTransformer(
        function ($dsListAsArray) {
            // ### here you get a list, but want only the first entry? ###
            return reset($dsListAsArray); // returns first element
        },
        function ($ArrayOfDataSources) {
            $collection = new ArrayCollection();
            // stuff you already wrote, BUT, see text below
            return $collection;
        }
    ))
;

however, your second function in the callback transformer gets ONE DataSource and thus must turn a single DataSource into an ArrayCollection of DataSources, which is

$builder->get('dsList1')
    ->addModelTransformer(new CallbackTransformer(
        function ($dsListAsArray) {
            // ### here you get a list, but want only the first entry? ###
            return reset($dsListAsArray); // returns first element
        },
        function ($dataSource) {
            return new ArrayCollection([$dataSource]); 
        }
    ))
;

option 2. just render it as a collection

turn your ("parent") forms dsList1 field into a collection, since it only is supposed to have one entry:

    // in the file header part add (if not already there):  
    // use Symfony\Component\Form\Extension\Core\Type\CollectionType;

    ->add('dsList1', CollectionType::class, [
        'entry_type' => DataSourceType::class, 
        'entry_options' => [
            'site' => $site,
            'multiple' => true, // <-- can probably scrap this one?!
            'label_format' => 'form.%name%List1',
        ],
    ])

and you should be golden. On the logical form stuff. However, the templates will render this weirdly, probably... but you can update your form rendering, probably, by overriding the form_widget code with some custom block_prefix or something. (I guess you can figure this out from the symfony docs)

also, check out the CollectionType options, where you can (and possibly should) deny adding/removing (I believe the default is, that you can't anyway) and make a constraint, that there's always one (or none?).

as a comment: I would advise to not return an ArrayCollection from the entity on get{CollectionField}(), and in the set{CollectionField} expect an array and create the ArrayCollection on the spot there (or better: modify the existing one, so unnecessary updates are avoided ...)

and obligatory notice: since symfony 2.8 is not maintained anymore ... you should probably think about upgrading ... but I suppose this is not going to happen. ;o)

option 3. multiplexing like crazy - don't do it!

now, as far as I can tell, you wanted to turn your DataSourceType into being able to handle multiple DataSources, by providing the multiple option. To do this, you would have to do some evil logic to somehow manage this, multiplexing an ArrayCollection of DataSources into what the form expects to be a single DataSource (logic TBD) as a data transformer and do the same in reverse (logic TBD as well, and it's shaky af).

I would advise against it, you remove almost all advantages using the data_class of a DataSource by breaking into some incestuous Pseudo-DataSource or maybe even an array that resembles a DataSource. just to be able to use the same form. it's not worth it really. Keep the object. If you're certain there's always just one data source in your dsList[123], then just use one of the other options. if there might be another data source, option 2 is probably the preferred one. I would prefer it, TBH.

Upvotes: 1

Related Questions