I have a module which is setup with the following entities like this:
- Product
- ProductTranslation
- ContentBlock
- ProductTranslation
I can add a new product just fine but when I go to edit it, the form can't be loaded. It gives me the following error:
The form's view data is expected to be an instance of class ContentBlockDataTransferObject, but is an instance of class ContentBlock. 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 ContentBlock to an instance of ContentBlockDataTransferObject.
Setting the data_class
to null didn't seem to change anything.
ProductType.php
<?php
namespace Backend\Modules\Products\Domain\Product;
use Backend\Core\Language\Language;
use Backend\Modules\MediaLibrary\Domain\MediaGroup\MediaGroupType;
use Backend\Modules\Products\Domain\Categories\Category;
use Backend\Modules\Products\Domain\Divisions\Division;
use Common\Form\CollectionType;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Common\Form\ImageType;
use Symfony\Component\Validator\Constraints\Valid;
final class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'categories',
EntityType::class,
[
'class' => Category::class,
'multiple' => true,
'expanded' => true,
'label' => 'lbl.Categories',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('i')
->join('i.translations', 't')
->innerJoin(Division::class, 'd', Join::WITH, 'i.division = d')
->addOrderBy('d.id', 'ASC')
->addOrderBy('t.title', 'ASC');
},
]
);
$helpTextMessageMainImage = sprintf(
Language::msg('HelpImageFieldWithMaxFileSize'),
$this->getUploadMaxFileSize()
);
$helpTextMessageMainImage .= '<br><strong>' . Language::msg('MainImageHelp') . '</strong>';
$builder->add(
'mainImage',
ImageType::class,
[
'image_class' => MainImage::class,
'help_text_message' => $helpTextMessageMainImage,
]
);
$builder->add(
'translations',
CollectionType::class,
[
'entry_type' => ProductTranslationType::class,
'error_bubbling' => false,
'constraints' => [new Valid()],
]
);
$builder->add(
'imageGallery',
MediaGroupType::class,
[
'required' => false,
]
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => ProductDataTransferObject::class]);
}
private function getUploadMaxFileSize(): ?string
{
$uploadMaxFileSize = ini_get('upload_max_filesize');
if ($uploadMaxFileSize === false) {
return null;
}
// reformat if defined as an integer
if (is_numeric($uploadMaxFileSize)) {
return $uploadMaxFileSize / 1024 . 'MB';
}
// reformat if specified in kB
if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'K') {
return mb_substr($uploadMaxFileSize, 0, -1) . 'kB';
}
// reformat if specified in MB
if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'M') {
return $uploadMaxFileSize . 'B';
}
// reformat if specified in GB
if (mb_strtoupper(mb_substr($uploadMaxFileSize, -1)) === 'G') {
return $uploadMaxFileSize . 'B';
}
return $uploadMaxFileSize;
}
}
ProductTranslationType.php
<?php
namespace Backend\Modules\Products\Domain\Product;
use Backend\Core\Engine\Model;
use Backend\Core\Language\Language;
use Backend\Form\Type\EditorType;
use Backend\Form\Type\MetaType;
use Backend\Modules\Products\Domain\ContentBlock\ContentBlockType;
use Backend\Modules\Products\Domain\Product\Command\CreateProduct;
use Backend\Modules\Products\Domain\Product\Command\UpdateProduct;
use Backend\Modules\Products\Domain\Product\ProductTranslation\ProductTranslationRepository;
use Common\Form\CollectionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
final class ProductTranslationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
'title',
TextType::class,
[
'label' => 'lbl.Title',
]
)
->add(
'contentBlocks',
CollectionType::class,
[
'entry_type' => ContentBlockType::class,
'allow_delete' => true,
'allow_add' => true,
'allow_sequence' => false,
'attr' => [
'class' => 'tab-pane tab-pane-collection',
'data-role' => 'products-content-blocks',
],
]
)
->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
/** @var CreateProduct|UpdateProduct $productFormData */
$productFormData = $event->getForm()->getParent()->getParent()->getData();
// Add meta
$event->getForm()
->add(
'meta',
MetaType::class,
[
'detail_url' => Model::getUrlForBlock('Products', 'ProductDetail'),
'base_field_name' => 'title',
'generate_url_callback_class' => ProductTranslationRepository::class,
'generate_url_callback_method' => 'self::getUrl',
'generate_url_callback_parameters' => [
$event->getData()->getLocale(),
$productFormData->hasExistingProduct()
? $productFormData->getProductEntity()->getId() : null,
],
]
);
}
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => ProductTranslationDataTransferObject::class]);
}
}
ContentBlockType.php
<?php
namespace Backend\Modules\Products\Domain\ContentBlock;
use Backend\Core\Engine\TemplateModifiers;
use Backend\Form\Type\EditorType;
use Common\Form\CollectionType;
use Common\Form\ImageType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Valid;
final class ContentBlockType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'sequence',
HiddenType::class,
[
'required' => false,
'attr' => [
'data-role' => 'sequence'
]
]
);
$builder->add(
'type',
ChoiceType::class,
[
'label' => 'lbl.Type',
'choices' => array_map(
function ($contentType) {
return new ContentType($contentType);
},
ContentType::POSSIBLE_VALUES
),
'choice_label' => function (ContentType $contentType) {
return TemplateModifiers::toLabel($contentType);
},
'choice_translation_domain' => false,
'choice_value' => function (ContentType $contentType = null) {
return (string) $contentType;
},
'attr' => [
'data-role' => 'products-content-blocks-type',
],
]
);
$builder->add(
'text',
EditorType::class,
[
'label' => 'lbl.Text',
'required' => false,
'attr' => [
'class' => 'inputEditor',
'data-show' => 'type-text',
],
]
);
$builder->add(
'image',
ImageType::class,
[
'label' => 'lbl.Image',
'image_class' => Image::class,
'required' => false,
'show_remove_image' => false,
]
);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => ContentBlockDataTransferObject::class]);
}
}
ProductAdd.php Action
<?php
namespace Backend\Modules\Products\Actions;
use Backend\Core\Engine\Base\ActionAdd;
use Backend\Core\Language\Language;
use Backend\Core\Language\Locale;
use Backend\Modules\Products\Domain\Product\Command\CreateProduct;
use Backend\Modules\Products\Domain\Product\Command\CreateProductHandler;
use Backend\Modules\Products\Domain\Product\ProductType;
use Backend\Core\Engine\Model as BackendModel;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
final class ProductAdd extends ActionAdd
{
public function execute(): void
{
parent::execute();
$form = $this->getForm();
if (!$form->isSubmitted() || !$form->isValid()) {
$this->template->assign('activeTranslationTab', 'tab' . ucfirst(Locale::workingLocale()));
$this->template->assign('locale', Locale::workingLocale());
$this->template->assign('form', $form->createView());
$this->template->assign('backLink', $this->getBackLink());
$this->template->assign('imageGallery', $form->getData()->imageGallery);
$this->parse();
$this->display();
return;
}
$this->handleForm($form);
}
private function handleForm(Form $form): void
{
/** @var CreateProduct $product */
$product = $form->getData();
// Handle the Product create
$this->get('command_bus')->handle($product);
$this->redirect(
$this->getBackLink(
[
'report' => 'added',
'var' => $product->translations[(string) Locale::workingLocale()]->title,
]
)
);
}
private function getForm(): Form
{
$form = $this->createForm(ProductType::class, new CreateProduct());
$form->handleRequest($this->getRequest());
return $form;
}
private function getBackLink(array $parameters = []): string
{
return BackendModel::createUrlForAction(
'ProductsIndex',
null,
null,
$parameters
);
}
}
ProductEdit.php Action
<?php
namespace Backend\Modules\Products\Actions;
use Backend\Core\Engine\Base\ActionEdit;
use Backend\Core\Language\Language;
use Backend\Core\Language\Locale;
use Backend\Form\Type\DeleteType;
use Backend\Modules\Products\Domain\Product\Command\UpdateProductHandler;
use Backend\Modules\Products\Domain\Product\Product;
use Backend\Modules\Products\Domain\Product\Command\UpdateProduct;
use Backend\Core\Engine\Model as BackendModel;
use Backend\Modules\Products\Domain\Product\ProductRepository;
use Backend\Modules\Products\Domain\Product\ProductType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormError;
final class ProductEdit extends ActionEdit
{
public function execute(): void
{
parent::execute();
/** @var Product $product */
$product = $this->getProduct();
/** @var Form $form */
$form = $this->getForm($product);
$deleteForm = $this->createForm(
DeleteType::class,
['id' => $product->getId()],
[
'module' => $this->getModule(),
'action' => 'ProductDelete',
]
);
$this->template->assign('deleteForm', $deleteForm->createView());
if (!$form->isSubmitted() || !$form->isValid()) {
$this->template->assign('activeTranslationTab', 'tab' . ucfirst(Locale::workingLocale()));
$this->template->assign('locale', Locale::workingLocale());
$this->template->assign('form', $form->createView());
$this->template->assign('product', $product);
$this->template->assign('imageGallery', $form->getData()->imageGallery);
$this->parse();
$this->display();
return;
}
/** @var UpdateProduct $updateProduct */
$updateProduct = $form->getData();
// Handle the Product update
$this->get('command_bus')->handle($updateProduct);
$this->redirect(
$this->getBackLink(
[
'report' => 'edited',
'var' => $updateProduct->translations[(string)Locale::workingLocale()]->title,
'highlight' => 'row-' . $updateProduct->getProductEntity()->getId(),
]
)
);
}
private function getProduct(): Product
{
try {
/** @return Product */
return $this->get(ProductRepository::class)->findOneById($this->getRequest()->query->get('id'));
} catch (\Exception $e) {
$this->redirect(
$this->getBackLink(
[
'error' => 'non-existing'
]
)
);
}
}
private function getBackLink(array $parameters = []): string
{
return BackendModel::createUrlForAction(
'ProductsIndex',
null,
null,
$parameters
);
}
private function getForm(Product $product): Form
{
$form = $this->createForm(ProductType::class, new UpdateProduct($product));
$form->handleRequest($this->getRequest());
return $form;
}
}