I have a OneToMany relationship between Project and Application, and I want to be sure that 2 Applications cannot have the same name inside a Project.
I tried to configure my entity, form type and controller like it should be, but I am getting an Integrity contraint violation for duplicated entry, so I think the validation process is ignored.
Can someone tell me what am I missing ?
My Application entity like this :
namespace App\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use JsonSerializable;
/**
* #ORM\Entity
* #ORM\Table(name="application", uniqueConstraints={#ORM\UniqueConstraint(name="IDX_Unique", columns={"name", "project_id"})})
* #UniqueEntity(
* fields={"name", "project"},
* message="Name already used in this project.",
* groups="application"
* )
*/
class Application implements JsonSerializable {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string")
* #Assert\NotBlank(
* message = "Name cannot be empty."
* )
* #Assert\Length(
* min = "3",
* max = "50",
* minMessage = "Name is too short. It should have {{ limit }} characters or more.",
* maxMessage = "Name is too long. It should have {{ limit }} characters or less."
* )
*/
protected $name;
// other properties ...
/**
* #ORM\ManyToOne(targetEntity="Project", inversedBy="applications")
* #ORM\JoinColumn(name="project_id", referencedColumnName="id")
*/
protected $project;
// constructor, methods, getters, setters
}
My ApplicationType class looks like this :
namespace App\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ApplicationType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name', 'text', array(
'icon' => 'pencil'
));
$builder->add('description', 'textarea', array(
'required' => false,
'icon' => 'info'
));
$builder->add('url', 'url', array(
'required' => false,
'icon' => 'link'
));
}
public function getName() {
return 'application';
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'App\MainBundle\Entity\Application',
'validation_group' => array('application'),
'cascade_validation' => true
));
}
}
And in my Controller the action looks like this:
/**
* #Route("/project/{id}/application/add",
* name="app_add_application_ajax",
* requirements={"_method" = "post"},
* options={"expose" = true }
* )
* #Secure(roles="ROLE_SUPER_ADMIN")
* #ParamConverter("project", class="AppMainBundle:Project")
*/
public function addApplicationAction(Project $project, Request $request) {
$ajaxResponse = array();
$em = $this->getDoctrine()->getManager();
if ($request->getMethod() == 'POST' && $request->isXmlHttpRequest()) {
$formApp = new Application();
$formApp->setProject($project);
$form = $this->createForm(new ApplicationType(), $formApp);
$form->handleRequest($request);
if ($form->isValid()) {
$application = $form->getData();
$em->persist($application);
$em->flush();
// build ajax response ...
} else {
$ajaxResponse['error'] = $this->getErrorsAsString();
}
}
$response = new Response(json_encode($ajaxResponse));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
Your issue is that you configure a validation_group option in your form type, while the option used by Symfony is validation_groups. You don't get an error about an unknown option because you are setting this in the default options of your form type, and so you are marking the option as defined (but it is a separate one).
So the validator runs with the default group, which will validate different constraints (the constraints on the length of the name property are in the default group).
Note that you also have a second issue, which would appear once you run the constraint.
Your validation constraint does not match the DB constraints you have. You are asking the validator to have a unique name and a unique project, not a unique tuple (name, project). So you would reject too much things (the name will be validated as unique globally, not per project).
This is because you use 2 separate UniqueEntity constraints instead of a constraint asking for a tuple of multiple fields to be unique.
Related
I am trying to build a module under PS 1.7.6.1.
In design, I have a manyToOne relationship between a Product and a Preorder (many preorders can be associated to one product).
The Preorder object is an ORM entity:
//mymodule/src/Entity
class Preorder
{
/**
* #var int
*
* #ORM\Id
* #ORM\Column(name="id_preorder", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="id_product", type="integer")
*/
private $productId;
/**
* #var string
*
* #ORM\Column(name="email", type="string", length=64)
*/
private $email;
setter and getter
}
In controller:
//src/mymodule/src/Controller
use Doctrine\DBAL\Types\TextType;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Doctrine\ORM\EntityManagerInterface;
use MyModule\Entity\Preoder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\HttpFoundation\Request;
use Product;
class PreorderController extends FrameworkBundleAdminController
{
public function createAction(Request $request){
$preorder = new Preorder();
$preorderForm = $this->createFormBuilder($preorder)
->add('email', EmailType::class)
->add('productId', EntityType::class, [
'class' => Product::class,
])
->getForm();
$bookingForm->handleRequest($request);
// validate and persist
}}
The problem is that the form builder doesn't recognize the Product entity. It throws a runtime exception:
Class "Product" seems not to be a managed Doctrine entity. Did you
forget to map it?
I can't find in the core files an example where such a scenario is handled. Thank you very much in advance for guiding/helping me the resolve this issue.
The main issue is that product_id is not an entity so there is 0 chance that The formbuilder handle it with the EntityType::class. you need to properly define (as explained in the doc) your ManyToOne relation with objects
on the product side :
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
*/
class Product
{
// usual stuff
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="product", cascade={"persist"})
*/
private $preorders;
}
and on the preorder side:
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
*/
class Product
{
// usual stuff
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product", mappedBy="preorders")
*/
private $product;
}
In your formBuilder, product will be an entity and recognize by as such by the EntityType::class
EDIT
If your product is a legacy class unmapped by the ORM then you can use the dataTransformer to help your formBuilder recognize the legacy entity.
namespace App\DataTransformer;
class ProductToIdTransformer implements DataTransformerInterface
{
public function transform($product)
{
if (null === $product) {
return '';
}
return $product->getId();
}
public function reverseTransform($product_id)
{
if (!$product_id){
return;
}
//your db logic to retrieve the product
if (null === $field){
throw new TransformationFailedException(sprintf("the product '%s' does not exist!", $product_id));
}
return $product;
}
}
Then in your formbuilder you'll use a CollectionType instead:
$preorderForm = $this->createFormBuilder($preorder)
->add('email', EmailType::class)
->add('product', CollectionType::class, [
'class' => Product::class,
//some logic to adapt the different choices to your needs
])
;
$preorderForm
->get('product')
->addModelTransformer(ProductToIdTransformer::class)
;
$preorderForm = $preorderForm->getForm();
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.
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.
1. Quick overview
1.1 Goal
What I'm trying to achieve is a create/edit user tool. Editable fields are:
username (type: text)
plainPassword (type: password)
email (type: email)
groups (type: collection)
avoRoles (type: collection)
Note: the last property is not named $roles becouse my User class is extending FOSUserBundle's User class and overwriting roles brought more problems. To avoid them I simply decided to store my collection of roles under $avoRoles.
1.2 User Interface
My template consists of 2 sections:
User form
Table displaying $userRepository->findAllRolesExceptOwnedByUser($user);
Note: findAllRolesExceptOwnedByUser() is a custom repository function, returns a subset of all roles (those not yet assigned to $user).
1.3 Desired functionality
1.3.1 Add role:
WHEN user clicks "+" (add) button in Roles table
THEN jquery removes that row from Roles table
AND jquery adds new list item to User form (avoRoles list)
1.3.2 Remove roles:
WHEN user clicks "x" (remove) button in User form (avoRoles list)
THEN jquery removes that list item from User form (avoRoles list)
AND jquery adds new row to Roles table
1.3.3 Save changes:
WHEN user clicks "Zapisz" (save) button
THEN user form submits all fields (username, password, email, avoRoles, groups)
AND saves avoRoles as an ArrayCollection of Role entities (ManyToMany relation)
AND saves groups as an ArrayCollection of Role entities (ManyToMany relation)
Note: ONLY existing Roles and Groups can be assigned to User. If for any reason they are not found the form should not validate.
2. Code
In this section I present/or shortly describe code behind this action. If description is not enough and you need to see the code just tell me and I'll paste it. I'm not pasteing it all in the first place to avoid spamming you with unnecessary code.
2.1 User class
My User class extends FOSUserBundle user class.
namespace Avocode\UserBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Validator\ExecutionContext;
/**
* #ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\UserRepository")
* #ORM\Table(name="avo_user")
*/
class User extends BaseUser
{
const ROLE_DEFAULT = 'ROLE_USER';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\generatedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Group")
* #ORM\JoinTable(name="avo_user_avo_group",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
/**
* #ORM\ManyToMany(targetEntity="Role")
* #ORM\JoinTable(name="avo_user_avo_role",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $avoRoles;
/**
* #ORM\Column(type="datetime", name="created_at")
*/
protected $createdAt;
/**
* User class constructor
*/
public function __construct()
{
parent::__construct();
$this->groups = new ArrayCollection();
$this->avoRoles = new ArrayCollection();
$this->createdAt = new \DateTime();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set user roles
*
* #return User
*/
public function setAvoRoles($avoRoles)
{
$this->getAvoRoles()->clear();
foreach($avoRoles as $role) {
$this->addAvoRole($role);
}
return $this;
}
/**
* Add avoRole
*
* #param Role $avoRole
* #return User
*/
public function addAvoRole(Role $avoRole)
{
if(!$this->getAvoRoles()->contains($avoRole)) {
$this->getAvoRoles()->add($avoRole);
}
return $this;
}
/**
* Get avoRoles
*
* #return ArrayCollection
*/
public function getAvoRoles()
{
return $this->avoRoles;
}
/**
* Set user groups
*
* #return User
*/
public function setGroups($groups)
{
$this->getGroups()->clear();
foreach($groups as $group) {
$this->addGroup($group);
}
return $this;
}
/**
* Get groups granted to the user.
*
* #return Collection
*/
public function getGroups()
{
return $this->groups ?: $this->groups = new ArrayCollection();
}
/**
* Get user creation date
*
* #return DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
2.2 Role class
My Role class extends Symfony Security Component Core Role class.
namespace Avocode\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Security\Core\Role\Role as BaseRole;
/**
* #ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\RoleRepository")
* #ORM\Table(name="avo_role")
*/
class Role extends BaseRole
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\generatedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", unique="TRUE", length=255)
*/
protected $name;
/**
* #ORM\Column(type="string", length=255)
*/
protected $module;
/**
* #ORM\Column(type="text")
*/
protected $description;
/**
* Role class constructor
*/
public function __construct()
{
}
/**
* Returns role name.
*
* #return string
*/
public function __toString()
{
return (string) $this->getName();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Role
*/
public function setName($name)
{
$name = strtoupper($name);
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set module
*
* #param string $module
* #return Role
*/
public function setModule($module)
{
$this->module = $module;
return $this;
}
/**
* Get module
*
* #return string
*/
public function getModule()
{
return $this->module;
}
/**
* Set description
*
* #param text $description
* #return Role
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return text
*/
public function getDescription()
{
return $this->description;
}
}
2.3 Groups class
Since I've got the same problem with groups as with roles, I'm skipping them here. If I get roles working I know I can do the same with groups.
2.4 Controller
namespace Avocode\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContext;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Avocode\UserBundle\Entity\User;
use Avocode\UserBundle\Form\Type\UserType;
class UserManagementController extends Controller
{
/**
* User create
* #Secure(roles="ROLE_USER_ADMIN")
*/
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getEntityManager();
$user = new User();
$form = $this->createForm(new UserType(array('password' => true)), $user);
$roles = $em->getRepository('AvocodeUserBundle:User')
->findAllRolesExceptOwned($user);
$groups = $em->getRepository('AvocodeUserBundle:User')
->findAllGroupsExceptOwned($user);
if($request->getMethod() == 'POST' && $request->request->has('save')) {
$form->bindRequest($request);
if($form->isValid()) {
/* Persist, flush and redirect */
$em->persist($user);
$em->flush();
$this->setFlash('avocode_user_success', 'user.flash.user_created');
$url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));
return new RedirectResponse($url);
}
}
return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
'form' => $form->createView(),
'user' => $user,
'roles' => $roles,
'groups' => $groups,
));
}
}
2.5 Custom repositories
It is not neccesary to post this since they work just fine - they return a subset of all Roles/Groups (those not assigned to user).
2.6 UserType
UserType:
namespace Avocode\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserType extends AbstractType
{
private $options;
public function __construct(array $options = null)
{
$this->options = $options;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('username', 'text');
// password field should be rendered only for CREATE action
// the same form type will be used for EDIT action
// thats why its optional
if($this->options['password'])
{
$builder->add('plainpassword', 'repeated', array(
'type' => 'text',
'options' => array(
'attr' => array(
'autocomplete' => 'off'
),
),
'first_name' => 'input',
'second_name' => 'confirm',
'invalid_message' => 'repeated.invalid.password',
));
}
$builder->add('email', 'email', array(
'trim' => true,
))
// collection_list is a custom field type
// extending collection field type
//
// the only change is diffrent form name
// (and a custom collection_list_widget)
//
// in short: it's a collection field with custom form_theme
//
->add('groups', 'collection_list', array(
'type' => new GroupNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => true,
'error_bubbling' => false,
'prototype' => true,
))
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => true,
'error_bubbling' => false,
'prototype' => true,
));
}
public function getName()
{
return 'avo_user';
}
public function getDefaultOptions(array $options){
$options = array(
'data_class' => 'Avocode\UserBundle\Entity\User',
);
// adding password validation if password field was rendered
if($this->options['password'])
$options['validation_groups'][] = 'password';
return $options;
}
}
2.7 RoleNameType
This form is supposed to render:
hidden Role ID
Role name (READ ONLY)
hidden module (READ ONLY)
hidden description (READ ONLY)
remove (x) button
Module and description are rendered as hidden fields, becouse when Admin removes a role from a User, that role should be added by jQuery to Roles Table - and this table has Module and Description columns.
namespace Avocode\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class RoleNameType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('', 'button', array(
'required' => false,
)) // custom field type rendering the "x" button
->add('id', 'hidden')
->add('name', 'label', array(
'required' => false,
)) // custom field type rendering <span> item instead of <input> item
->add('module', 'hidden', array('read_only' => true))
->add('description', 'hidden', array('read_only' => true))
;
}
public function getName()
{
// no_label is a custom widget that renders field_row without the label
return 'no_label';
}
public function getDefaultOptions(array $options){
return array('data_class' => 'Avocode\UserBundle\Entity\Role');
}
}
3. Current/known Problems
3.1 Case 1: configuration as quoted above
The above configuration returns error:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
But setter for ID should not be required.
First becouse I don't want to create a NEW role. I want just to create a relation between existing Role and User entities.
Even if I did want to create a new Role, it's ID should be auto-generated:
/**
#ORM\Id
#ORM\Column(type="integer")
#ORM\generatedValue(strategy="AUTO")
*/
protected $id;
3.2 Case 2: added setter for ID property in Role entity
I think it's wrong, but I did it just to be sure. After adding this code to Role entity:
public function setId($id)
{
$this->id = $id;
return $this;
}
If I create new user and add a role, then SAVE... What happens is:
New user is created
New user has role with the desired ID assigned (yay!)
but that role's name is overwritten with empty string (bummer!)
Obviously, thats not what I want. I don't want to edit/overwrite roles. I just want to add a relation between them and the User.
3.3 Case 3: Workaround suggested by Jeppe
When I first encountered this problem I ended up with a workaround, the same that Jeppe suggested. Today (for other reasons) I had to remake my form/view and the workaround stopped working.
What changes in Case3 UserManagementController -> createAction:
// in createAction
// instead of $user = new User
$user = $this->updateUser($request, new User());
//and below updateUser function
/**
* Creates mew iser and sets its properties
* based on request
*
* #return User Returns configured user
*/
protected function updateUser($request, $user)
{
if($request->getMethod() == 'POST')
{
$avo_user = $request->request->get('avo_user');
/**
* Setting and adding/removeing groups for user
*/
$owned_groups = (array_key_exists('groups', $avo_user)) ? $avo_user['groups'] : array();
foreach($owned_groups as $key => $group) {
$owned_groups[$key] = $group['id'];
}
if(count($owned_groups) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$groups = $em->getRepository('AvocodeUserBundle:Group')->findById($owned_groups);
$user->setGroups($groups);
}
/**
* Setting and adding/removeing roles for user
*/
$owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();
foreach($owned_roles as $key => $role) {
$owned_roles[$key] = $role['id'];
}
if(count($owned_roles) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);
$user->setAvoRoles($roles);
}
/**
* Setting other properties
*/
$user->setUsername($avo_user['username']);
$user->setEmail($avo_user['email']);
if($request->request->has('generate_password'))
$user->setPlainPassword($user->generateRandomPassword());
}
return $user;
}
Unfortunately this does not change anything.. the results are either CASE1 (with no ID setter) or CASE2 (with ID setter).
3.4 Case 4: as suggested by userfriendly
Adding cascade={"persist", "remove"} to mapping.
/**
* #ORM\ManyToMany(targetEntity="Group", cascade={"persist", "remove"})
* #ORM\JoinTable(name="avo_user_avo_group",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
/**
* #ORM\ManyToMany(targetEntity="Role", cascade={"persist", "remove"})
* #ORM\JoinTable(name="avo_user_avo_role",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $avoRoles;
And changeing by_reference to false in FormType:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
And keeping workaround code suggested in 3.3 did change something:
Association between user and role was not created
.. but Role entity's name was overwritten by empty string (like in 3.2)
So.. it did change something but in the wrong direction.
4. Versions
4.1 Symfony2 v2.0.15
4.2 Doctrine2 v2.1.7
4.3 FOSUserBundle version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa
5. Summary
I've tried many diffrent approaches (above are only the most recent ones) and after hours spent on studying code, google'ing and looking for the answer I just couldn't get this working.
Any help will be greatly appreciated. If you need to know anything I'll post whatever part of code you need.
I've come to the same conclusion that there's something wrong with the Form component and can't see an easy way to fix it. However, I've come up with a slightly less cumbersome workaround solution that is completely generic; it doesn't have any hard-coded knowledge of entities/attributes so will fix any collection it comes across:
Simpler, generic workaround method
This doesn't require you to make any changes to your entity.
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\Form;
# In your controller. Or possibly defined within a service if used in many controllers
/**
* Ensure that any removed items collections actually get removed
*
* #param \Symfony\Component\Form\Form $form
*/
protected function cleanupCollections(Form $form)
{
$children = $form->getChildren();
foreach ($children as $childForm) {
$data = $childForm->getData();
if ($data instanceof Collection) {
// Get the child form objects and compare the data of each child against the object's current collection
$proxies = $childForm->getChildren();
foreach ($proxies as $proxy) {
$entity = $proxy->getData();
if (!$data->contains($entity)) {
// Entity has been removed from the collection
// DELETE THE ENTITY HERE
// e.g. doctrine:
// $em = $this->getDoctrine()->getEntityManager();
// $em->remove($entity);
}
}
}
}
}
Call the new cleanupCollections() method before persisting
# in your controller action...
if($request->getMethod() == 'POST') {
$form->bindRequest($request);
if($form->isValid()) {
// 'Clean' all collections within the form before persisting
$this->cleanupCollections($form);
$em->persist($user);
$em->flush();
// further actions. return response...
}
}
So a year has passed, and this question has become quite popular. Symfony has changed since, my skills and knowledge have also improved, and so has my current approach to this problem.
I've created a set of form extensions for symfony2 (see FormExtensionsBundle project on github) and they include a form type for handleing One/Many ToMany relationships.
While writing these, adding custom code to your controller to handle collections was unacceptable - the form extensions were supposed to be easy to use, work out-of-the-box and make life easier on us developers, not harder. Also.. remember.. DRY!
So I had to move the add/remove associations code somewhere else - and the right place to do it was naturally an EventListener :)
Have a look at the EventListener/CollectionUploadListener.php file to see how we handle this now.
PS. Copying the code here is unnecessary, the most important thing is that stuff like that should actually be handled in the EventListener.
1. The workaround solution
The workaround solution suggested by Jeppe Marianger-Lam is at the moment the only one working I know of.
1.1 Why did it stop working in my case?
I changed my RoleNameType (for other reasons) to:
ID (hidden)
name (custom type - label)
module & description (hidden, read-only)
The problem was my custom type label rendered NAME property as
<span> role name </span>
And since it was not "read only" the FORM component expected to get NAME in POST.
Instead only ID was POSTed, and thus FORM component assumed NAME is NULL.
This lead to CASE 2 (3.2) -> creating association, but overwriting ROLE NAME with an empty string.
2. So, what exacly is this workaround about?
2.1 Controller
This workaround is very simple.
In your controller, before you VALIDATE the form, you have to fetch the posted entity identyficators and get matching entities, then set them to your object.
// example action
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getEntityManager();
// the workaround code is in updateUser function
$user = $this->updateUser($request, new User());
$form = $this->createForm(new UserType(), $user);
if($request->getMethod() == 'POST') {
$form->bindRequest($request);
if($form->isValid()) {
/* Persist, flush and redirect */
$em->persist($user);
$em->flush();
$this->setFlash('avocode_user_success', 'user.flash.user_created');
$url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));
return new RedirectResponse($url);
}
}
return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
'form' => $form->createView(),
'user' => $user,
));
}
And below the workaround code in updateUser function:
protected function updateUser($request, $user)
{
if($request->getMethod() == 'POST')
{
// getting POSTed values
$avo_user = $request->request->get('avo_user');
// if no roles are posted, then $owned_roles should be an empty array (to avoid errors)
$owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();
// foreach posted ROLE, get it's ID
foreach($owned_roles as $key => $role) {
$owned_roles[$key] = $role['id'];
}
// FIND all roles with matching ID's
if(count($owned_roles) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);
// and create association
$user->setAvoRoles($roles);
}
return $user;
}
For this to work your SETTER (in this case in User.php entity) must be:
public function setAvoRoles($avoRoles)
{
// first - clearing all associations
// this way if entity was not found in POST
// then association will be removed
$this->getAvoRoles()->clear();
// adding association only for POSTed entities
foreach($avoRoles as $role) {
$this->addAvoRole($role);
}
return $this;
}
3. Final thoughts
Still, I think this workaround is doing the job that
$form->bindRequest($request);
should do! It's either me doing something wrong, or symfony's Collection form type is not complete.
There are some major changes in Form component comeing in symfony 2.1, hopefully this will be fixed.
PS. If it's me doing something wrong...
... please post the way it should be done! I'd be glad to see a quick, easy and "clean" solution.
PS2. Special thanks to:
Jeppe Marianger-Lam and userfriendly (from #symfony2 on IRC). You've been very helpful. Cheers!
This is what I have done before - I don't know if it's the 'right' way to do it, but it works.
When you get the results from the submitted form (i.e., just before or right after if($form->isValid())), simply ask the list of the roles, then remove them all from the entity (saving the list as a variable). With this list, simply loop through them all, ask the repository for the role entity that matches the ID's, and add these to your user entity before you persist and flush.
I just searched through the Symfony2 documentation because I remembered something about prototype for form collections, and this turned up: http://symfony.com/doc/current/cookbook/form/form_collections.html - It has examples of how to deal correctly with javascript add and remove of collection types in forms. Perhaps try this approach first, and then try what I mentioned above afterwards if you cannot get it to work :)
You need some more entities:
USER
id_user (type: integer)
username (type: text)
plainPassword (type: password)
email (type: email)
GROUPS
id_group (type: integer)
descripcion (type: text)
AVOROLES
id_avorole (type: integer)
descripcion (type: text)
*USER_GROUP*
id_user_group (type:integer)
id_user (type:integer) (this is the id on the user entity)
id_group (type:integer) (this is the id on the group entity)
*USER_AVOROLES*
id_user_avorole (type:integer)
id_user (type:integer) (this is the id on the user entity)
id_avorole (type:integer) (this is the id on the avorole entity)
You can have for example something like this:
user:
id: 3
username: john
plainPassword: johnpw
email: john#email.com
group:
id_group: 5
descripcion: group 5
user_group:
id_user_group: 1
id_user: 3
id_group: 5
*this user can have many groups so in another row *
As I've already found out, Doctrine2 "does not support to set the default values in columns through the “DEFAULT” keyword in SQL. ... you can just use your class properties as default values".
class Product
{
// ...
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name = "";
/**
* #var string $sale
*
* #ORM\Column(name="sale", type="boolean")
*/
private $sale = false;
But even when I do this, the generated CRUD forms still require me to fill out all forms. In case of boolean attributes this even means I can only set it to true (i.e. 1).
Am I doing something wrong?
(I know I can turn the validation off but I'd like a solution to the problem instead of just bypassing it)
Your boolean value need to have nullable set as true:
/**
* #var string $sale
*
* #ORM\Column(name="sale", type="boolean", nullable=true)
*/
private $sale = false;
In Object-oriented programming you should use constructor of the entity to set a default value for an attribute:
public function __construct() {
$this->sale = false;
}
I haven't used the CRUD auto-generation tool, but I know that by default, each and every field is required. YOu must explicitly pass 'required' => false as an option for your fields.
This can be done in the form classes
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class FooType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('field', 'text', array('required' => false));
}
public function getName()
{
return 'foo';
}
}
The same can be achived in a Form class generated inside your controller
namespace Acme\DemoBundle\Controller;
use Acme\DemoBundle\Entity\Foo;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// ...
$form = $this->createFormBuilder($foo)
->add('field', 'text', array('required' => false)
->getForm();
// ...
return $this->render('AcmeDemoBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
You can also use the 'data' parameter like in :
->add('date', 'date', array(
'widget' => 'single_text',
'format' => 'dd/MM/yyyy',
'attr' => array('class' => 'datepicker'),
'data' => new \DateTime()
))
Here I have set a class to make a jQuery UI datepicker of the field using JavaScript.
I also set the widget to a single_text so I won't get three select fields.
And then I set the default data to the current DateTime()
Or In annotations
Use:
options={"default":"foo"}
and not:
options={"default"="foo"}
For instance:
/**
* #ORM\Column(name="foo", type="smallint", options={"default":0})
*/
private $foo;