Symfony Form Collections: How to prevent duplicate INSERT on EntityManager->flush()? - php

I have the following data model:
MainEntity.php
<?php declare(strict_type=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class MainEntity
{
// $id, etc...
/**
* #ORM\OneToMany(
* targetEntity="App\Entity\RelationEntity",
* mappedBy="mainEntity",
* cascade={"persist", "merge", "remove"},
* orphanRemoval=true
* )
*/
protected Collection $sideRelations;
// getSideRelations(), setSideRelations($relations), addSideRelation($sideRelation), removeSideRelation($sideRelation)...
}
RelationEntity.php
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(
* name="main_side_relation",
* uniqueConstraints={
* #ORM\UniqueConstraint(name="id_pair", columns={"main_entity_id", "side_entity_id"})
* }
* )
*/
class RelationEntity
{
// $id, etc...
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected ?int $relationValue;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\MainEntity", inversedBy="sideRelations")
* #ORM\JoinColumn(name="main_entity_id", referencedColumnName="id")
*/
protected MainEntity $mainEntity;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\SideEntity", inversedBy="mainRelations")
* #ORM\JoinColumn(name="side_entity_id", referencedColumnName="id")
*/
protected SideEntity $sideEntity;
// getRelationValue(), setRelationValue($relationValue), getMainEntity(), setMainEntity($mainEntity), getSideEntity(), setSideEntity($sideEntity)...
}
SideEntity.php
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class SideEntity
{
// $id, etc...
/**
* #ORM\OneToMany(
* targetEntity="App\Entity\RelationEntity",
* mappedBy="sideEntity",
* cascade={"persist", "merge", "remove"},
* orphanRemoval=true
* )
*/
protected Collection $mainRelations;
// getMainRelations(), setMainRelations($mainRelations), addMainRelation($mainRelation), removeMainRelation($mainRelation)...
I have existing Symfony form classes which I extended to fit the data model.
RelationEntityType.php
<?php declare(strict_types=1);
namespace App\Form;
class RelationEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('relationValue', HiddenType::class)
->add('mainEntity', HiddenType::class)
->add('sideEntity', HiddenType::class)
;
// Add Transformers to switch between Entity and database ID for input value
}
public function configureOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(['data_class' => RelationEntity::class]);
}
}
MainEntityType.php
<?php declare(strict_types=1);
namespace App\Form;
class MainEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ...
$builder->add('sideRelations', CollectionType::class, [
'required' => false,
'by_reference' => false,
'entry_type' => RelationEntity::class,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true
]);
}
public function configureOptions(OptionsResolverInterface $resolver): void
{
$resolver->setDefaults(['data_class' => MainEntity::class]);
}
}
Submission works not as intended, as by using by_reference I explicitely remove and re-add existing unchanged collection entries, but when omitting the option it happens implicitly.
In both cases, UnitOfWork is filled with insertion queries for the already existing collection entries (and throws errors due to the unique constraint). I don't want that.
Symfony's form collection configuration docs doesn't yield valuable information on how to prevent collections from becoming "dirty" although their entries have technically not been changed.
How exactly do I avoid overwriting unchanged existing collection entries?
(If necessary, I can provide the remaining code for an MWE)

Related

Symfony 4 + Doctrine: organize entities in subfolders

I'm trying to make a website with Symfony 4 and Doctrine. I'm a complete beginner (both with Symfony and PHP in general), so I apologise if my question is trivial.
I want to create a database with doctrine, which means that I have to create classes in src/Entity. But I also want to add forms to the site, and they also require classes in src/Entity. I'd like to separate these classes in two subfolders: src/Entity/database and src/Entity/forms. I tried to edit config/packages/doctrine.yaml as follows:
doctrine:
#...
orm:
#...
mappings:
App:
#...
dir: '%kernel.project_dir%/src/Entity/database'
prefix: 'App\Entity\database'
But I when I use bin/console make:entity Entity it creates the file in src/Entity and gives the following error:
[ERROR] Only annotation mapping is supported by make:entity, but the
<info>App\Entity\Entity</info> class uses a different format. If you
would like this command to generate the properties & getter/setter
methods, add your mapping configuration, and then re-run this command
with the <info>--regenerate</info> flag.
When I run bin/console make:entity Entity --regenerate it says:
[ERROR] No entities were found in the "Entity" namespace.
I also tried bin/console make:entity database/Entity, but it fails with:
[ERROR] "App\Entity\Database/Entity" is not valid as a PHP class name (it must start with a letter or underscore,
followed by any number of letters, numbers, or underscores)
If I do the same with a backslash (database\Entity) it creates a DatabaseEntity.php file in the wrong directory and gives the same error as the first one.
Be very careful, because with such approach you might mess your architecture up. This question is a bit opinionated, but I'm gonna tell you how we make it with entities and forms.
First, my strong belief, Entities and Forms should be separated. Therefore, we contain Entites in src/Entity and Forms in src/Form. The connection between them is a FormType, we contain those in src/FormType.
Here's an example User entity contained in src/Entity/User.php:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #UniqueEntity("username")
*
* #ORM\Entity()
* #ORM\Table(name="users")
*/
class User implements UserInterface, \Serializable
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*
* #var int
*/
private $id;
/**
* #Assert\NotBlank()
* #Assert\Email
* #Assert\Length(max="255")
*
* #ORM\Column(type="string", length=255, unique=true)
*
* #var string
*/
private $username;
/**
* #ORM\Column(type="string", length=64)
*
* #var string
*/
private $password;
/**
* #return int
*/
public function getId(): int
{
return $this->id;
}
/**
* #return string The username
*/
public function getUsername()
{
return $this->username;
}
/**
* #param null|string $username
*
* #return User
*/
public function setUsername(?string $username): User
{
$this->username = (string) $username;
return $this;
}
/**
* #return string
*/
public function getPassword(): string
{
return $this->password;
}
/**
* #param null|string $password
*
* #return User
*/
public function setPassword(?string $password): User
{
$this->password = (string) $password;
return $this;
}
}
Now, we need a user to be able to register. For this we create a FormType and a Form. Take a look at src/FormType/User.php:
namespace App\FormType;
use App\Entity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as NativeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
class User extends AbstractType
{
public function getParent(): string
{
return BaseType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// This maps `Entity\User::username` to the respective field
$builder->add(
'username',
NativeType\EmailType::class,
['label' => 'username']
);
// This maps `Entity\User::password` to the respective field
$builder->add(
'password',
NativeType\RepeatedType::class,
[
'constraints' => [new NotBlank()],
'invalid_message' => 'nonMatchingPasswords',
'first_options' => ['label' => 'password'],
'second_options' => ['label' => 'password again'],
'type' => NativeType\PasswordType::class,
]
);
}
// This tells Symfony to resolve the form to the `Entity\User` class
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Entity\User::class]);
}
}
And now the Form itself, it's src/Form/UserRegistration.php:
namespace App\Form;
use App\FormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as NativeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints;
class UserRegistration extends AbstractType
{
public function getParent()
{
// Note this!
return FormType\User::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'fields' => ['username', 'password'],
'translation_domain' => 'forms',
]
);
}
}
And a final stroke on this. In src/Controller/Registration.php we do this:
$form = $this->createForm(
Form\UserRegistration::class,
$user = new Entity\User()
);
The rest (how to handle forms etc.) you know. If you don't, read Symfony docs, they cover it perfectly.
I have cut out / edited some sensitive or non-essential things from this example. For instance, we do not bind password to password, we ask for plain password and then encrypt it. I have not tested the above, so it might not be stable. But for a demonstration on how your architecture should be done it's a good example, IMO.

Expected value of type Entity for association field got Entity1 instead when using ResolveTargetEntityListener

I am using the ResolveTargetEntityListener from Doctrine to access SQL tables that are in an external database.
when I use the createQueryBuilder to query the data on the external/base Vehicle class I get the following error:
Expected value of type "AppBundle\Entity\VehicleFox" for association field "AppBundle\Entity\FM_Positions#$truck", got "FoxBundle\Entity\Vehicle" instead.
When I do it on the internal VehicleFox class the Query returns NULL.
Is there anything that I have missed?
Below is the Controller which I am doing the query from withe the relevant functions. I have also included the classes and interfaces that have been used as well as the config.yml file.
FmController
<?php
namespace BWT\FMBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\HttpFoundation\Request;
use Vendors\FM;
use AppBundle\Entity\FuelData;
use MaxBundle\Entity\UdoDriver;
use FoxBundle\Entity\Vehicle;
use AppBundle\Entity\FM_Positions;
class FMController extends Controller
{
public function latestPositionsAction(Request $request)
{
$_render_args = array();
$_results = array();
$tripProcesses = new FM();
$success = $this->login($tripProcesses);
If ($success === true) {
$_results = $tripProcesses->getTrackingData();
}
if ($_results) {
$_render_args['results'] = $_results;
}
$this->CreateFmPositions($_results);
return $this->render('Telematics/latestPositions.html.twig', $_render_args);
}
private function CreateFmPositions(array $results)
{
foreach ($results as $result){
$resultArray = (array) $result;
$vehicle = $this->getVehicleFM($resultArray['VehicleID']);
$position = new FM_Positions();
if ($vehicle) {
$position->setTruck($vehicle[0]);
}
$em = $this->getDoctrine()->getManager();
$em->persist($position);
$em->flush();
}
}
private function getVehicleFM(int $vehFmId)
{
$repository = $this->getDoctrine()->getRepository('FoxBundle:Vehicle', 'foxem'); // error for incorrect Entity
//$repository = $this->getDoctrine()->getRepository('AppBundle:VehicleFox'); // returns NULL
$query = $repository->createQueryBuilder('v')
->where('v.fmId = :fmid')
->setParameter('fmid', $vehFmId)
->getQuery()
->getResult();
return $query;
}
}
Fm_Positions.php
<?php
// src/AppBundle/Entity/FM_Possitions.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Model\VehicleInterface;
use AppBundle\Model\UDODriverInterface;
/**
* #ORM\Entity
* #ORM\Table(name="fm_positions")
* #author sarah
*
*/
class FM_Positions
{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Model\VehicleInterface", inversedBy="fm_positions")
* #ORM\JoinColumn(name="truck_id", referencedColumnName="id")
* #var VehicleInterface
*/
private $truck;
// ...
}
Base Vehicle class
<?php
namespace FoxBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Vehicle
* #ORM\Table(name="vehicle", uniqueConstraints={#ORM\UniqueConstraint(name="fleet_number", columns={"fleet_number"})}, indexes={#ORM\Index(name="log_book_date", columns={"log_book_date"})})
* #ORM\Entity
*/
class Vehicle
{
/**
* #var string|null
* #ORM\Column(name="fleet_number", type="string", length=20, nullable=true)
*/
private $fleetNumber;
/**
* #var string|null
* #ORM\Column(name="FM_ID", type="string", length=20, nullable=true)
*/
private $fmId;
// ...
}
Extended Vehicle Class
<?php
// src/AppBundle/Entity/VehicleFox.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FoxBundle\Entity\Vehicle as BaseVehicle;
use AppBundle\Model\VehicleInterface;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="vehicle_fox")
*/
class VehicleFox extends BaseVehicle implements VehicleInterface
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string|null
* #ORM\Column(name="fleet_number", type="string", length=20, nullable=true)
*/
private $fleetNumber;
/**
* #var string|null
* #ORM\Column(name="FM_ID", type="string", length=20, nullable=true)
*/
private $fmId;
/**
* #ORM\OneToMany(targetEntity="FM_Positions", mappedBy="truck")
*/
private $fm_positions;
}
Vehicle Interface
<?php
// src/AppBundle/Model/VehicleInterface.php
namespace AppBundle\Model;
interface VehicleInterface
{
public function __construct();
/**
* #return string
*/
public function getId();
public function addTruckUnit(\AppBundle\Entity\Truck_Units $truckUnit);
public function removeTruckUnit(\AppBundle\Entity\Truck_Units $truckUnit);
public function getTruckUnits();
public function addFmPosition(\AppBundle\Entity\FM_Positions $fmPosition);
public function removeFmPosition(\AppBundle\Entity\FM_Positions $fmPosition);
public function getFmPositions();
public function addFmTrip(\AppBundle\Entity\FM_Trips $fmTrip);
public function removeFmTrip(\AppBundle\Entity\FM_Trips $fmTrip);
public function getFmTrips();
public function addSkytrackPosition(\AppBundle\Entity\Skytrack_Positions $skytrackPosition);
public function removeSkytrackPosition(\AppBundle\Entity\Skytrack_Positions $skytrackPosition);
public function getSkytrackPositions();
}
Config.yml
doctrine:
# ...
orm:
resolve_target_entities:
AppBundle\Model\VehicleInterface: AppBundle\Entity\VehicleFox
auto_generate_proxy_classes: "%kernel.debug%"
default_entity_manager: maxem
entity_managers:
maxem:
connection: maxdb
mappings:
AppBundle:
foxem:
connection: foxdb
mappings:
FoxBundle: ~
FMPositionType form
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
class FM_PositionsType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('position_id')
->add('truck_id', EntityType::class, Array(
'class' => 'FoxBundle:Vehicle',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('v')
->orderBy('v.fleetNumber', 'ASC');
},
'placeholder' => 'Select A Truck',
'choice_label' => 'fleetNumber',
'em' => 'foxem',
))
->add('driver_id', EntityType::class, Array(
'class' => 'MaxBundle:UdoDriver',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('d')
->orderBy('d.nickname', 'ASC');
},
'placeholder' => 'Select A Driver',
'choice_label' => 'nickname',
'em' => 'max2em',
))
->add('date')
->add('latitude')
->add('longitude')
->add('heading')
->add('speed')
->add('altitude');
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\FM_Positions'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_fm_positions';
}
}

Symfony 2: Avoid persisting photo entity collection when no photo is uploaded

I have an entity AdsList which represents ads and I have linked entity Photos.
When I create or update the entity AdsList a new row is created in the Photos table in the database even when I do not upload a photo and I don't want that to happen. I wish the table to be updated ONLY if there is a photo uploaded.
The AdsList entity:
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use obbex\AdsBundle\Entity\Photos;
/**
* AdsList
*
* #ORM\Table()
* #ORM\Entity
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\AdsListRepository")
* #ORM\HasLifecycleCallbacks()
*/
class AdsList
{
... several properties
/**
* #ORM\OneToMany(targetEntity="obbex\AdsBundle\Entity\Photos",mappedBy="adslist", cascade={"persist","remove"})
* #ORM\JoinColumn(nullable=true)
*/
protected $photos;
... more getters and setters
And this is the entity Photos
namespace obbex\AdsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Photos
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="obbex\AdsBundle\Entity\PhotosRepository")
* #ORM\HasLifecycleCallbacks
*/
class Photos
{
/**
* #ORM\ManyToOne(targetEntity="obbex\AdsBundle\Entity\AdsList", inversedBy="photos")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="adslist_id", referencedColumnName="id",onDelete="CASCADE")
* })
*/
protected $adslist;
And here is the form AdsListType
class AdsListType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email',TextType::class)
->add('telephone', IntegerType::class,array('required'=>false))
->add('displayPhone',CheckboxType::class,array('required'=>false,'attr'=>array('checked'=>false)))
->add('title',TextType::class)
->add('description', TextareaType::class)
->add('country', EntityType::class,array(
'class' => 'obbexAdsBundle:Countries',
'choice_label'=>'countryName',
'multiple'=>false
))
->add('region',TextType::class)
->add('department',TextType::class)
->add('address',TextType::class, array('required' => false))
->add('city',TextType::class)
->add('zipCode',TextType::class)
->add('statusPro',TextType::class)
->add('publication', CheckboxType::class)
->add('price', IntegerType::class)
->add('photos',CollectionType::class, array('entry_type'=> 'obbex\AdsBundle\Form\PhotosType',
'allow_add' => true,
'allow_delete'=>true,
'data'=>array(new Photos() ),
'required' => false
)
)
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'obbex\AdsBundle\Entity\AdsList',
));
}
}
I guess that there is a procedure in order to avoid persisting the Photos entity but I've tried in the controller:
public function editAdAction(Request $request,AdsList $ads){
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(AdsListEditType::class, $ads);
$form->handleRequest($request);
if ($form->isValid()) {
if($form->get('save')->isClicked()){
//this condition in case no photo is uploaded
if($form->get('photos')->getData()[0]->getFile() === null){
$photos = $ads->getPhotos();
//what do here? or maybe the problem is somewhere else
//I've tried $em->refresh($photo); but it throws the error I gave
}
$em->flush();
$id = $ads->getId();
return $this->redirect($this->generateUrl('ad_edition',array('id'=>$id)));
}
return $this->render('obbexAdsBundle:Default:editAd.html.twig',
array(
'ads'=>$ads,
'form'=>$form->createView()
));
}
But I have the following error
Entity obbex\AdsBundle\Entity\Photos#000000000b2d669200007fb2152337c5
is not managed. An entity is managed if its fetched from the database or
registered as new through EntityManager#persist
Has anyone done this? it seems pretty standard to me. In case no photo is uploaded don't do anything in the database to keep it clean
In the case there is no file set
We should not touch the image property
/**
* Add image
*
* #param \obbex\AdsBundle\Entity\Photo $image
*
* #return Painting
*/
public function addImage(\Art\GeneralBundle\Entity\Photo $photo)
{
if($photos->getFile() !== NULL){
$this->photos[] = $photo;
$image->setAdlist($this);
}
return $this;
}
and then no photo will be persisted if the field file is not filled with information.
Note that I linked the AdsList entity with the Photo Entity via the object AdsList itself not from the controller which was incorrect.

Symfony creating choice from entity in form type

I have a lot of Categories in database.
Here is Category Entity
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="categories")
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category")
*/
protected $rootCategory;
/**
* #ORM\Column(type="text")
*/
protected $name;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set rootCategory
*
* #param \AppBundle\Entity\Category $rootCategory
*
* #return Category
*/
public function setRootCategory(\AppBundle\Entity\Category $rootCategory = null)
{
$this->rootCategory = $rootCategory;
return $this;
}
/**
* Get rootCategory
*
* #return \AppBundle\Entity\Category
*/
public function getRootCategory()
{
return $this->rootCategory;
}
}
I want to get all categories in my edit form
EditFormType:
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use AppBundle\Controller\CategoryController;
class EditPhotoFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$categoryController = new CategoryController();
$builder->add('title', 'text');
$builder->add('description', 'textarea');
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'choices' => $categoryController->getCategories(),
));
}
public function getName()
{
return 'app_photo_edit';
}
}
getCategories()
public function getCategories() {
$em = $this->getDoctrine()->getManager();
return $em->getRepository('AppBundle:Category')->findAll();
}
I am getting next error:
Error: Call to a member function has() on null
Thats because there is not Doctrine in controller object. Where should i get Doctrine and Repository in this case?
How should i do it correct way?
First, you should NEVER instantiate any Controller class yourself. Controller classes are used by Symfony's Kernel to handle a request, and they are loaded automatically with dependencies to do so.
Right here, you don't even need to require the EntityManager in your FormType, because EntityType has a built-in option query_builder to do what you need:
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c');
},
);
This should do the trick. (check here for more details)
However, if one day you really need to import a dependancy inside your Form (whether it is EntityManager or another service), here's how you should do:
A. import the given dependency in your constructor:
private $dependency;
public function __construct(Dependency $dependency)
{
$this->$dependency = $dependency;
}
B. Declare your Form as a Service, with your dependency's id as argument:
<service id="app.form.type.edit_photo"
class="AppBundle\Form\Type\EditPhotoFormType">
<tag name="form.type" />
<argument type="service" id="app.dependencies.your_dependency" />
</service>
Then use $this->dependency in your Form wherever you need.
Hope this helps! :)

Sylius: adding resource with translateable content

I am building an app based on Sylius standard edition. With the ResourceBundle i handled to integrate my own entities and the corresponding relations. This new resources should be related later to the products entity, but first i want to get it working "standalone". The backend works for both, the added resource and for relations. These are editable via form-collections. Very fine! Now i want to get translated database-content for my new resource. I tried the way i did it in Symfony earlier, but it didn't work. The last vew days i tried every possible solution found, but none of this works, or i made mistakes... Neither the translation-tables where constructed when typing:
app/console doctrine:schema:update --force
nor translatable content is visible in the forms. When calling the edit action, i get following error:
error:
Neither the property "translations" nor one of the methods "getTranslations()", "translations()", "isTranslations()", "hasTranslations()", "__get()" exist and have public access in class "KontaktBundle\Entity\Kontakte".
Is somebody out there with an example implementation of a extended Sylius-resource with translateable database-content? I'm still learning symfony and sylius too, can you tell me what i'm missing or doing wrong?
Many Thanks to #gvf. Now i figured out that the config entry must be set. This gave me a functional example which i want to provide here:
Configs
# app/config/sylius_config.yml (must be imported in config)
# Adding Resource
sylius_resource:
# Resource Settings
settings:
sortable: true
paginate: 50
allowed_paginate: [50, 100, 500]
filterable: true
resources:
dbk.authors:
driver: doctrine/orm
templates: AuthorBundle:Backend
object_manager: default
classes:
model: AuthorBundle\Entity\Kontakte
#interface: // if you have an interface configured
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: AuthorBundle\Entity\AuthorsTranslation
mapping:
fields: ['anrede','biografie']
Services
# app/config/sylius_services.yml (must be imported in config)
parameters:
# Parameter for our author entity
app.form.type.authors.class: AuthorBundle\Form\AuthorsType
app.form.type.authors_translation.class: AuthorBundle\Form\AuthorsTranslationType
services:
# Adding Authors Backend menu Item
dbk_backend_authors.menu_builder:
class: AuthorBundle\EventListener\MenuBuilderListener
tags:
- { name: kernel.event_listener, event: sylius.menu_builder.backend.main, method: addBackendMenuItems }
- { name: kernel.event_listener, event: sylius.menu_builder.backend.sidebar, method: addBackendMenuItems }
# Adding Authors FormType
app.form.type.authors:
class: "%app.form.type.authors.class%"
tags:
- {name: form.type, alias: dbk_authors }
app.form.type.authors_translation:
class: "%app.form.type.authors_translation.class%"
tags:
- {name: form.type, alias: dbk_authors_translation }
EventListener (Adds Menu Entry in Sylius-Backend)
<?php
// AuthorBundle/EventListener/MenuBuilderListener/MenuBuilderListener.php
namespace AuthorBundle\EventListener;
use Sylius\Bundle\WebBundle\Event\MenuBuilderEvent;
class MenuBuilderListener
{
public function addBackendMenuItems(MenuBuilderEvent $event)
{
$menu = $event->getMenu();
$menu['assortment']->addChild('vendor', array(
'route' => 'Authors',
'labelAttributes' => array('icon' => 'glyphicon glyphicon-user'),
))->setLabel('Authors');
}
}
Entities
Authors (The new resource hich we want to add and to translate)
<?php
// AuthorBundle/Entity/Authors.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslatable;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Authors
*
* #ORM\Entity
* #ORM\Table(name="authors")
*/
class Authors extends AbstractTranslatable
{
//
// IDENTIFIER FIELDS
//
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//
// FIELDS
//
/**
* #var string
*
* #ORM\Column(name="vorname", type="string", length=255)
*/
private $vorname;
/**
* #var string
*
* #ORM\Column(name="nachname", type="string", length=255)
*/
private $nachname;
public function __construct() {
parent::__construct();
}
//
// TranslationFields - Getters and Setters
//
/**
* Get Anrede
* #return string
*/
public function getAnrede()
{
return $this->translate()->getAnrede();
}
/**
* Set Anrede
*
* #param string $anrede
*
* #return Authors
*/
public function setAnrede($anrede)
{
$this->translate()->setAnrede($anrede);
return $this;
}
/**
* Get Biografie
* #return string
*/
public function getBiografie()
{
return $this->translate()->getBiografie();
}
/**
* Set Biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->translate()->setBiografie($biografie);
return $this;
}
//
// Getters and Setters
//
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set vorname
*
* #param string $vorname
*
* #return Authors
*/
public function setVorname($vorname)
{
$this->vorname = $vorname;
return $this;
}
/**
* Get vorname
*
* #return string
*/
public function getVorname()
{
return $this->vorname;
}
/**
* Set nachname
*
* #param string $nachname
*
* #return Authors
*/
public function setNachname($nachname)
{
$this->nachname = $nachname;
return $this;
}
/**
* Get nachname
*
* #return string
*/
public function getNachname()
{
return $this->nachname;
}
public function __toString(){
return $this->getFullName();
}
}
AuthorsTranslation (This is the Entity for tanslation of Authors)
<?php
// AuthorBundle/Entity/AuthorsTranslation.php
namespace AuthorBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Translation\Model\AbstractTranslation;
/**
* AuthorsTranslation
*
* #ORM\Entity
* #ORM\Table(name="authors_translation")
*/
class AuthorsTranslation extends AbstractTranslation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", length=20, nullable=false, options={"unsigned":"true"})
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
//
// TRANSLATABLE - FIELDS
//
/**
* #var string
* #ORM\Column(name="anrede", type="string", length=255)
*/
private $anrede;
/**
* #var string
* #ORM\Column(name="biografie", type="text")
*/
private $biografie;
/**
* {#inheritdoc}
*/
public function getId()
{
return $this->id;
}
//
// GETTERS AND SETTERS
//
/**
* Set anrede
*
* #param string $anrede
* #return Authors
*/
public function setAnrede($anrede)
{
$this->anrede = $anrede;
return $this;
}
/**
* Get anrede
*
* #return string
*/
public function getAnrede()
{
return $this->anrede;
}
/**
* Set biografie
*
* #param string $biografie
*
* #return Authors
*/
public function setBiografie($biografie)
{
$this->biografie = $biografie;
return $this;
}
/**
* Get biografie
*
* #return string
*/
public function getBiografie()
{
return $this->biografie;
}
}
FormTypes
AuthorsType
<?php
// AuthorBundle/Form/AuthorsType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\QueryBuilder;
class AuthorsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Add Translations to Form.
->add('translations', 'a2lix_translations', array(
'required' => false,
'fields' => array(
'anrede' => array(
'label' => 'Anrede',
),
'biografie' => array(
'label' => 'Biografie',
'attr' => array('data-edit' => 'wysiwyg', 'rows' => '15'),
'required' => false,
)
)
))
->add('vorname', null, array(
'label' => 'Vorname',
'required' => false,
))
->add('nachname', null, array(
'label' => 'Nachname',
'required' => false,
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
'data_class' => 'AuthorBundle\Entity\Authors'
));
}
/**
* #return string
*/
public function getName()
{
return 'dbk_authors';
}
}
AuthorsTranslationType
<?php
// AuthorBundle/Form/AuthorsTranslationType.php
namespace AuthorBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class AuthorsTranslationType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('anrede', 'text', array(
'label' => 'Anrede'
))
->add('biografie', 'textarea', array(
'label' => 'Biografie'
))
;
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'dbk_authors_translation';
}
}
Form Template
{# AuthorBundle/Resources/views/backend/edit||add.html.twig - as you like... the call of form.translation (line 5) is the point here #}
{% form_theme form 'SyliusWebBundle:Backend:forms.html.twig' %}
<div class="row">
<div class="col-md-12">
{{ form_row(form.translations, {'attr': {'class': 'input-lg'}}) }}
{{ form_row(form.vorname) }}
{{ form_row(form.nachname) }}
</div>
</div>
Have a look at the Product and ProductTranslation under the models folder in Sylius Components to see an example of how Sylius implements it.
Kontakte needs to extend AbstractTranslatable, you also need to create a class KontakteTranslation that extends AbstractTranslation. Under sylius_resource you also need to configure the translations:
sylius_resource:
resources:
dbk.contact:
driver: doctrine/orm
templates: KontaktBundle:Backend
object_manager: default
classes:
model: KontaktBundle\Entity\Kontakte
controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
repository: Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository
translation:
model: KontaktBundle\Entity\KontakteTranslation
mapping:
fields: {...fields that will be translated...}
Get rid of gedmo translatable extension because Sylius doesn't use it.

Categories