Symfony 2 Form Validation Groups - forms

I am trying to use a form class for add & edit. In add mode, iconFile is required. In edit mode, iconFile is optional (to replace the current icon). How can I acheive this?
I tried setting a mode in the constructor
class ItemForm extends AbstractType {
public function __construct($mode) {
$this->mode = $mode;
}
public function getDefaultOptions(array $opts) {
if ($mode == 'add') {
return array('validation_groups' => array('Default', 'add'));
} else {
return array('validation_groups' => array('Default'));
}
}
}
// doctrine entity, data_class of form
class Item {
/**
* #Assert\NotBlank(groups={"add"})
* #Assert\Image
*/
protected $iconFile;
}
// creating the form in controller
$form = $this->createForm(new ItemForm($mode));
Problem is even in edit mode, I still need to select an image. HTML5 validation triggers

In your ItemForm constructor, you are setting $mode to $this->mode, but you are then trying to access $mode from getDefaultOptions(). $mode obviously doesn't exist within this scope - try changing to the following:
public function getDefaultOptions(array $opts) {
if ($this->mode == 'add') {
return array('validation_groups' => array('Default', 'add'));
} else {
return array('validation_groups' => array('Default'));
}
}

You can do :
public function setDefaultOptions(OptionsResolver\OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'LIG\Bundle\UserBundle\Entity\User',
'validation_groups' => function(Form\FormInterface $form) {
$data= $form->getData();
if($data->getId())
{
return array('Default', 'Edit');
}
else
{
return array('Default', 'Add');
}
},
));
}

Related

Symfony 5 dynamic form conditional default logic

I've an use case where i need some default conditional logic on my dynamic form build in Symfony 5.
Let me try to explain what my use case is and my problem with a simple form.
For example i've a form Product with two fields:
Part (choiceType => left, right)
Length (numberType)
On change all fields (:input) are being submitted through an Ajax request.
I've two controller methods one for visiting the page (form is being build), the other
is being called for rendering the form through the ajax request (handle conditional logic).
For the conditional logic part the following needs te be done
When part is left, default length needs to be 50
When part is right, default length needs to be 100
user could change default data
Setting the default data on length based on left or right is not the problem.
When left is selected, default length becomes 50. When changing the value to 55 (form is being submitted through every change) it becomes 50 again.
This behaviour is logic, but how could the default data been overwritten?
Above situation could also been described as give user default data with option to change it
form type
<?php
// ... namespace, use statments
class ProductType extends AbstractType
{
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('part', ChoiceType::class, array(
'choices' => array(
'Left' => 'left',
'Right' => 'right',
)
));
$builder->add('length', NumberType::class);
$builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($options)
{
$form = $event->getForm();
if(null === $product = $event->getData()) {
return;
}
switch($product->getPart()) {
case 'left': $defaultLength = 50; break;
case 'right': $defaultLength = 100; break;
default: $defaultLength = 0;
}
$form->get('length')->setData($defaultLength);
});
}
/**
* {#inheritDoc}
*/
public function getName(): string
{
return 'product';
}
/**
* {#inheritDoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(array(
'data_class' => Product::class,
'translation_domain' => 'forms',
));
}
}
controller
// src/Controller/ProductController.php
// ... namespace, use statments
namespace App\Controller;
class ProductController extends AbstractController
{
public function productAction(Request $request): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$product = $form->getData();
dd($product);
}
return $this->render('product_view.html.twig', array(
'form' => $form->createView()
));
}
public function productConfigureAjaxAction(Request $request): Response
{
$product = new Product();
$part = $request->request->get('product')['part'] ?? null;
$product->setPart($part);
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
// product_form.html.twig is an separated file and included in product_view.html.twig
// by making the form separated is could been used for an ajax response
return $this->render('product_form.html.twig', array(
'form' => $form->createView()
));
}
}

yii2 losing user identity after login redirect

i have looked at the other similar questions, and while this one (Yii2 user identity loss after page redirection) asks pretty much the same question, there is no solution that applies to my situation.
i have created a new identity class, i have implemented all the necessary methods and implemented the IdentityInterface. note that it does NOT extend ActiveRecord, but rather extends my own base Model class that does not derive from AR.
everything seems to work fine, i can log in and be correctly authenticated. i have traced the code and i can see that my identity class is correctly set in Yii::$app->user after IdentityInterface::validatePassword() and User::login() have been called.
however, once the login succeeds and the user is redirected, Yii::$app->user contains an empty IdentityInterface, rather than the one that was there immediately prior to the redirect.
in SiteController::actionLogin(), i have modified the code from:
return $this->goHome();
to:
$tmp = $this->goHome();
// echo '<pre>';var_dump(Yii::$app->user);echo '</pre>';exit;
return($tmp);
and i can see that Yii::$app->user still has the correct identity up until the return() statement.
can anyone tell me why i am losing my identity? i have traced through everything i can think of: yii\web\Controller, yii\base\Controller, yii\web\Response, SiteController, yii\web\User, etc...
any help at all would be greatly appreciated. thanks!
Same problem, after 3 days find solution and I it working.
My User model is:
namespace app\models;
use Yii;
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
public $username;
/**
* #inheritdoc
*/
public static function tableName()
{
return 'tbl_users';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['name', 'email', 'password'], 'required'],
[['email'], 'unique'],
[['role', 'status'], 'integer'],
[['created', 'last_update'], 'safe'],
[['name', 'email', 'password'], 'string', 'max' => 250]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'name' => 'Name',
'email' => 'Email',
'password' => 'Password',
'role' => 'Role',
'status' => 'Status',
'created' => 'Created',
'last_update' => 'Last Update',
];
}
public function getAuthKey() {
}
public function getId() {
return $this->id;
}
public function validateAuthKey($authKey) {
}
public static function findIdentity($id) {
}
public static function findIdentityByAccessToken($token, $type = null) {
}
public static function findByEmail($email)
{
return static::findOne(array('email'=>$email));
}
public function validatePassword($password)
{
return $this->password === $password;
}
}
I change:
public static function findIdentity($id) {
}
to:
public static function findIdentity($id) {
return self::findOne(array('id'=>$id));
}
It working for me.
I had the same problem. I had my custom class implementing yii\web\IdentityInterface. I made sure that session was enabled and enableAutologin was also true, but I had no luck at all.
Finally, I realized that in config/web.php there's a setting 'user' => [ 'identityClass' => 'app\models\User', 'enableAutoLogin' => true]
Of course, the identityClass is the default in a basic yii application. After setting this value to my custom class, identity was finally persisted.
try like this
implement implements \yii\web\IdentityInterface for your Active
record and Implement all method from \yii\web\IdentityInterface.
declare $users as static variable as private static $users = []; in your class.
Alter findByUsername() function for Get user details form Username.
In Config file add
'user' => ['identityClass' => 'app\models\active record class',
'enableAutoLogin' => true,
],
class User extends \yii\db\ActiveRecord
implements\yii\web\IdentityInterface {
private $_user = false;
public $rememberMe = true;
/*
* static variable
*/
private static $users = [];
/**
* #inheritdoc
*/
public static function tableName() {
return 'user';
}
/**
* #inheritdoc
*/
public function rules() {
return [
[['username', 'email', 'password', 'loginType'], 'required'],
];
}
/**
* #inheritdoc
*/
public function attributeLabels() {
return [
'id' => Yii::t('app', 'ID'),
'username' => Yii::t('app', 'Username'),
'email' => Yii::t('app', 'Email'),
'password' => Yii::t('app', 'Password'),
];
}
/**
* Logs in a user using the provided username and password.
* #return boolean whether the user is logged in successfully
*/
public function login() {
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
} else {
return false;
}
}
/**
* Finds user by [[username]]
*
* #return User|null
*/
public function getUser() {
if ($this->_user === false) {
$this->_user = self::findByUsername($this->username);
}
return $this->_user;
}
public function validatePassword($password) {
if ($this->password == md5($password)) {
return true;
}
return false;
}
public static function findIdentity($id) {
return static::findOne($id);
}
public static function findIdentityByAccessToken($token, $type = null) {
return static::findOne(['access_token' => $token]);
}
public function getId() {
return $this->id;
}
public function getAuthKey() {
return $this->authkey;
}
public function validateAuthKey($authKey) {
return $this->authkey === $authKey;
}
public static function findByUsername($username) {
/*
* Get Record from Usertable
*/
$user = User::findOne(["username" => $username]);
if (!empty($user)) {
/* Create array with your active record like below */
$identity_use = ['id' => $user['id'],
'username' => $user['username'],
'password' => $user['password'],
'authKey' => '',
'accessToken' =>'',];
return new static($identity_use);
}
return null;
}
}
Another simple mistake that could result in session loss would be calling
$this->redirect(...);
instead of
return $this->redirect(...);
As Yii does not call die() or exit() in this method and the code on the lines following your redirect will be executed. Being used to CodeIgniter, I had assumed this and it took me days to figure it out.
I hope this can help you
<?php
namespace app\models;
use Yii;
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface {
private $_notification;
public static function tableName()
{
return 'users';
}
public function setNotification($n) {
Yii::$app->session['user_' .$this->id . '_notification'] = $n;
}
public function getNotification() {
$n = Yii::$app->session['user_' .$this->id . '_notification'];
Yii::$app->session['user_' .$this->id . '_notification'] = NULL;
return $n;
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['email', 'password', 'first_name'], 'required'],
[['role'], 'integer'],
[['email', 'first_name'], 'string', 'max' => 128],
[['password'], 'string', 'max' => 32]
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'email' => 'E-mail',
'password' => 'Пароль',
'first_name' => 'Имя',
'role' => 'Роль',
];
}
public static function findByEmail($email) {
return self::find()->where(['email' => $email])->one();
}
public function validatePassword($password) {
if ($this->password == md5($password)) {
return true;
}
return false;
}
public static function findIdentity($id)
{
return static::findOne($id);
}
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
public function getId()
{
return $this->id;
}
public function getAuthKey()
{
return $this->authkey;
}
public function validateAuthKey($authKey)
{
return $this->authkey === $authKey;
}
}

How to add data transformer inside a event listener

So, here's my problem: I have to add a field to a form based on it's underlying data but i've to add a data transformer to the field.
I thought the solution will be simple, just add a PRE_SET_DATA event listener to the form (just to access to the underlying data) and add the field and the transformer inside the listener. But i can't add the transformer inside the listener because the form is already locked.
I´ve tried many workarounds but i couldn't solved it. Her's my code:
$builder->...
->add(
$builder->create('date', 'choice', array(
'label' => 'form.game.date',
'empty_value' => 'form.game.date',
'required' => false,
'choices' => array(
'2014-04-10' => '10/Apr', // just to test
'2014-04-11' => '11/Apr',
)
))
->addModelTransformer(new DateToStringTransformer())
);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
$game = $event->getData();
$form = $event->getForm();
$period = new \DatePeriod(
$game->getTournament()->getBeginDate(),
new \DateInterval('P1D'),
$game->getTournament()->getEndDate()->add(new \DateInterval('P1D'))
);
$dates = array();
foreach($period as $date){
$dates[$date->format("Y-m-d")] = $date->format("j/M");
}
$form->add('date', 'choice', array(
'label' => 'form.game.date',
'choices' => $dates,
));
});
When i add the date field to the form inside event listener, the previously added data field is replaced so it's data transformer...
Is there a way to do it?
I wrote some test and updated your code a bit. Check if i understand your question correctly.
SomeTypeTest.php:
<?php
class SomeTypeTest extends TypeTestCase
{
/**
* #test
*/
public function testSubmitValidData()
{
$begin = new \DateTime();
$formData = array(
'date' => '2014-01-15'
);
$type = new SomeType();
$form = $this->factory->create($type);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals(['date' => \DateTime::createFromFormat('Y-m-d', '2014-01-15')], $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}
SomeType.php:
<?php
class SomeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
//$game = $event->getData();
$form = $event->getForm();
$period = new \DatePeriod(
\DateTime::createFromFormat('Y-m-d', '2014-01-01'), // for test
new \DateInterval('P1D'),
\DateTime::createFromFormat('Y-m-d', '2014-01-30') // for test
);
$dates = array();
foreach ($period as $date) {
$dates[$date->format("Y-m-d")] = $date->format("j/M");
}
$form->add($builder->create('date', 'choice', array(
'label' => 'form.game.date',
'empty_value' => 'form.game.date',
'auto_initialize' => false,
'required' => false,
'choices' => $dates
))
->addModelTransformer(new DateToStringTransformer())->getForm()
);
});
}
public function getName()
{
return 'st';
}
}
DateToStringTransformer.php:
<?php
class DateToStringTransformer implements DataTransformerInterface
{
/**
* #param mixed $value
* #return mixed|void
*/
public function transform($value)
{
if (!$value) {
return null;
}
return $value->format('Y-m-d');
}
/**
* #param mixed $value
* #return mixed|void
*/
public function reverseTransform($value)
{
return \DateTime::createFromFormat('Y-m-d', $value);
}
}
https://gist.github.com/ChubV/11348928
I've managed to get it work by creating a custom type that always add the data transformer. Then i can call "form->add('date', 'my_type',..)" from any event listener without loosing the data transformer.
MyType.php
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field1')
->add('field2')
...;
$builder->addEventSubscriber(new AddDateSubscriber());
}
}
CustomType.php
class DateChoiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new DateToStringTransformer());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected date does not exist',
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'date_choice';
}
}
Every time i add a date_choice type to a form the data transformer will be added too.
class AddDateSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event)
{
$game = $event->getData();
$form = $event->getForm();
$endDate = \DateTime::createFromFormat('Y-m-d', $game->getTournament()->getEndDate()->format('Y-m-d'));
$period = new \DatePeriod(
$game->getTournament()->getBeginDate(),
new \DateInterval('P1D'),
$endDate
);
$dates = array();
foreach($period as $date){
$dates[$date->format("Y-m-d")] = $date->format("j/M");
}
$form->add('date', 'date_choice', array(
'label' => 'form.game.date.label',
'empty_value' => 'form.game.date.none',
'required' => false,
'choices' => $dates,
));
}
}
DateToStringTransformer.php
class DateToStringTransformer implements DataTransformerInterface
{
public function transform($date)
{
if (null === $date) {
return "";
}
return $date->format("Y-m-d");
}
public function reverseTransform($stringDate)
{
if (!$stringDate) {
return null;
}
$date = \DateTime::createFromFormat('Y-m-d', $stringDate);
if (false === $date) {
throw new TransformationFailedException('Sting to date transformation failed!');
}
return $date;
}
}
Hope that this will help someone.

Symfony2: Delete entity through post request (improvements required)

I need some improvements about my actual way to delete entities:
public function deleteAction($path)
{
$form = $this->createFormBuilder(array('path' => $path))
->add('path')
->setReadOnly(true)
->getForm();
if ($this->getRequest()->getMethod() === 'POST') {
$form->bindRequest($this->getRequest());
if ($form->isValid()) {
$image = $this->getImageManager()->findImageByPath($path);
$this->getImageManager()->deleteImage($image);
return $this->redirect($this->generateUrl('AcmeImageBundle_Image_index'));
}
}
return $this->render('AcmeImageBundle:Image:delete.html.twig', array(
'form' => $form->createView(),
));
}
Two improvements I already found while writting:
CreateFormBuilder in extra method in controller
Hidden field and overgive extra image-entity to get rendered
Are there other thing I could make better?
Regards
(my answer is too long for the comment so i add it here)
First you have to create a Type file (generally in YourApp\YourBundle\Form\yourHandler.php), some basique code to put inside if you don't know:
<?php
namespace ***\****Bundle\Form;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManager;
use ***\****Bundle\Entity\your_entity;
class *****Handler
{
protected $form;
protected $request;
protected $em;
public function __construct(Form $form, Request $request, EntityManager $em)
{
$this->form = $form;
$this->request = $request;
$this->em = $em;
}
public function process()
{
if( $this->request->getMethod() == 'POST' )
{
$this->form->bindRequest($this->request);
if( $this->form->isValid() )
{
$this->onSuccess($this->form->getData());
return true;
}
}
return false;
}
public function onSuccess(your_entity $object)
{
// Make your stuff here (remove,....)
}
}
And in your controller i just call it this way:
if (!empty($_POST))
{
$formHandler = new *****Handler($my_form, $this->get('request'), $this->getDoctrine()->getEntityManager());
$formHandler->process();
}
Hope i'm clear enough

EmbedMany in Form

I've created a small Symfony2-Website (with Symfony PR11) with MongoDB as DB.
I could create a form, that use a normal document, but how do I use a document with an embeddedDocument?
here are the documents:
/**
* #mongodb:Document(collection="location")
*/
class Location
{
/**
* #mongodb:Id
*/
protected $id;
/**
* #mongodb:String
*/
protected $locationName;
/**
* #mongodb:EmbedMany(targetDocument="LocationTerminal")
*/
protected $terminals = array();
// Setter
public function setTerminals(LocationTerminal $terminal)
{
array_push($this->terminals, $terminal);
}
public function setLocationName($locationName)
{
$this->locationName = $locationName;
}
// Getter
public function getId()
{
return $this->$id;
}
public function getLocationName()
{
return $this->locationName;
}
public function getTerminals()
{
return $this->terminals;
}
}
The EmbeddedDocument:
/**
* #mongodb:EmbeddedDocument
*/
class LocationTerminal
{
/**
* #mongodb:String
*/
protected $terminalName;
/**
* #mongodb:Int
*/
protected $since;
/**
* #mongodb:Int
*/
protected $to;
// Setter
public function setTerminalName($terminalName)
{
$this->terminalName = $terminalName;
}
public function setSince($since)
{
$this->since = $since;
}
public function setTo($to)
{
$this->to = $to;
}
// Getter
public function getTerminalName()
{
return $this->terminalName;
}
public function getSince()
{
return $this->since;
}
public function getTo()
{
return $this->to;
}
}
As you can see $terminals holds an EmbedMany-Document
Here's the form:
class LocationForm extends Form
{
public function configure()
{
$this->add(new TextField('locationName', array('max_length' => 255, 'required' => true)));
}
public function addTerminals($dm)
{
$this->add(new ChoiceField('terminals.terminalName', array('choices' => $dm)));
$this->add(new DateField('terminals.since', array('required' => true)));
$this->add(new DateField('terminals.to', array('required' => false)));
}
}
The used Controller looks like this:
class LocationController extends Controller
{
protected $location;
protected $locationTerminal;
protected function getDm()
{
return $this->get('doctrine.odm.mongodb.document_manager');
}
protected function getLocation($name = null)
{
if ($name != null)
{
$dm = $this->getDm();
$this->location = $dm->getRepository('RalfBundle:Location')->findOneBy(array('locationName' => $name));
if (! $this->location)
{
$this->location = new Location();
$this->locationTerminal = new LocationTerminal();
$this->location->setLocation($name);
$this->location->setTerminals($this->locationTerminal);
}
}
else
{
$this->location = new Location();
$this->locationTerminal = new LocationTerminal();
$this->location->setTerminals($this->locationTerminal);
$this->locationTerminal->setSince(0);
$this->locationTerminal->setTerminalName("");
$this->locationTerminal->setTo(0);
}
}
protected function getForm()
{
$form = LocationForm::create($this->get('form.context'), 'location');
$dm = $this->getDm();
$form->addTerminals($dm->getRepository('RalfBundle:Terminal')->findAll()->toArray());
return $form;
}
//... some Actions
public function createAction()
{
$this->getLocation();
$form = $this->getForm();
$form->bind($this->get('request'), $this->location);
if ($form->isValid())
{
$dm = $this->getDm();
$dm->persist($this->location);
$dm->flush();
return $this->redirect($this->generateUrl('Location'));
}
return $this->render('RalfBundle:Ralf:location_create.html.twig', array('form' => $form));
}
I could see, that locationName recieve the entered values in the form, but the EmbedMany-Array terminals is still empty.
What did I wrong?
Thanks for helping :D
UPDATED:
Ok, found a solution.
in public function addTerminals($dm) in LocationForm it should look like this:
public function addTerminals($dm)
{
$this->add(new ChoiceField('terminals.0.terminalName', array('choices' => $dm)));
$this->add(new DateField('terminals.0.since', array('required' => true, 'type'=> 'timestamp')));
$this->add(new DateField('terminals.0.to', array('required' => false, 'type' => 'timestamp')));
}
'type' => 'timestamp' is necessary, 'cause DateField will create an DateTime-Object, but the document expected an Int for timestamp.
a field from the terminals-array could be accessed by normal dot-notation.
Ok, found a solution.
in public function addTerminals($dm) in LocationForm it should look like this:
public function addTerminals($dm)
{
$this->add(new ChoiceField('terminals.0.terminalName', array('choices' => $dm)));
$this->add(new DateField('terminals.0.since', array('required' => true, 'type'=> 'timestamp')));
$this->add(new DateField('terminals.0.to', array('required' => false, 'type' => 'timestamp')));
}
'type' => 'timestamp' is necessary, 'cause DateField will create an DateTime-Object, but the document expected an Int for timestamp.
a field from the terminals-array could be accessed by normal dot-notation.
Symfony 2 actually gives you a tool to handle embedded documents in forms: It's called "collection field type" and it enables you to embed other formtypes (from other embedded documents) in a parent form.
It can be configured to allow/forbid addition/deletion of embedded documents and is actually quite powerful.