Too few arguments to function Sonata\Form\Type\BasePickerType::__construct() - datepicker

I'm using Sonata for Admin and I have an error with all my Pickers. In the vendor directory, CRUDController, which is in Sonata-project/admin-bundle calls:
$form = $this->admin->getForm();
FormRegistry is called after that, which is in Symfony\Component\Form:
/**
* {#inheritdoc}
*/
public function getType($name)
{
if (!isset($this->types[$name])) {
$type = null;
foreach ($this->extensions as $extension) {
if ($extension->hasType($name)) {
$type = $extension->getType($name);
break;
}
}
if (!$type) {
// Support fully-qualified class names
if (!class_exists($name)) {
throw new InvalidArgumentException(sprintf('Could not load type "%s": class does not exist.', $name));
}
if (!is_subclass_of($name, 'Symfony\Component\Form\FormTypeInterface')) {
throw new InvalidArgumentException(sprintf('Could not load type "%s": class does not implement "Symfony\Component\Form\FormTypeInterface".', $name));
}
**$type = new $name();**
}
$this->types[$name] = $this->resolveType($type);
}
return $this->types[$name];
}
And the error is in bold. It calls the BasePickerType in Sonata\Form\Type:
<?php
declare(strict_types=1);
/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix#sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sonata\Form\Type;
use Sonata\Form\Date\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Class BasePickerType (to factorize DatePickerType and DateTimePickerType code.
*
* #author Hugo Briand <briand#ekino.com>
*/
abstract class BasePickerType extends AbstractType
{
/**
* #var TranslatorInterface|null
*/
protected $translator;
/**
* #var string
*/
protected $locale;
/**
* #var MomentFormatConverter
*/
private $formatConverter;
public function __construct(MomentFormatConverter $formatConverter, TranslatorInterface $translator, RequestStack $requestStack)
{
$this->formatConverter = $formatConverter;
$this->translator = $translator;
$this->locale = $this->getLocale($requestStack);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setNormalizer('format', function (Options $options, $format) {
if (isset($options['date_format']) && \is_string($options['date_format'])) {
return $options['date_format'];
}
if (\is_int($format)) {
$timeFormat = \IntlDateFormatter::NONE;
if ($options['dp_pick_time']) {
$timeFormat = $options['dp_use_seconds'] ?
DateTimeType::DEFAULT_TIME_FORMAT :
\IntlDateFormatter::SHORT;
}
$intlDateFormatter = new \IntlDateFormatter(
$this->locale,
$format,
$timeFormat,
null,
\IntlDateFormatter::GREGORIAN
);
return $intlDateFormatter->getPattern();
}
return $format;
});
}
public function finishView(FormView $view, FormInterface $form, array $options): void
{
$format = $options['format'];
// use seconds if it's allowed in format
$options['dp_use_seconds'] = false !== strpos($format, 's');
if ($options['dp_min_date'] instanceof \DateTime) {
$options['dp_min_date'] = $this->formatObject($options['dp_min_date'], $format);
}
if ($options['dp_max_date'] instanceof \DateTime) {
$options['dp_max_date'] = $this->formatObject($options['dp_max_date'], $format);
}
$view->vars['moment_format'] = $this->formatConverter->convert($format);
$view->vars['type'] = 'text';
$dpOptions = [];
foreach ($options as $key => $value) {
if (false !== strpos($key, 'dp_')) {
// We remove 'dp_' and camelize the options names
$dpKey = substr($key, 3);
$dpKey = preg_replace_callback('/_([a-z])/', static function ($c) {
return strtoupper($c[1]);
}, $dpKey);
$dpOptions[$dpKey] = $value;
}
}
$view->vars['datepicker_use_button'] = empty($options['datepicker_use_button']) ? false : true;
$view->vars['dp_options'] = $dpOptions;
}
/**
* Gets base default options for the date pickers.
*/
protected function getCommonDefaults(): array
{
return [
'widget' => 'single_text',
'datepicker_use_button' => true,
'dp_pick_time' => true,
'dp_pick_date' => true,
'dp_use_current' => true,
'dp_min_date' => '1/1/1900',
'dp_max_date' => null,
'dp_show_today' => true,
'dp_language' => $this->locale,
'dp_default_date' => '',
'dp_disabled_dates' => [],
'dp_enabled_dates' => [],
'dp_icons' => [
'time' => 'fa fa-clock-o',
'date' => 'fa fa-calendar',
'up' => 'fa fa-chevron-up',
'down' => 'fa fa-chevron-down',
],
'dp_use_strict' => false,
'dp_side_by_side' => false,
'dp_days_of_week_disabled' => [],
'dp_collapse' => true,
'dp_calendar_weeks' => false,
'dp_view_mode' => 'days',
'dp_min_view_mode' => 'days',
];
}
private function getLocale(RequestStack $requestStack): string
{
if (!$request = $requestStack->getCurrentRequest()) {
throw new \LogicException('A Request must be available.');
}
return $request->getLocale();
}
private function formatObject(\DateTime $dateTime, $format): string
{
$formatter = new \IntlDateFormatter($this->locale, \IntlDateFormatter::NONE, \IntlDateFormatter::NONE);
$formatter->setPattern($format);
return $formatter->format($dateTime);
}
}
I have no idea how to resolve that. Any idea? Thanks

Could you please try to enable SonataFormBundle?
// config/bundles.php
return [
// ...
Sonata\Form\Bridge\Symfony\SonataFormBundle::class => ['all' => true],
];

Related

Taxonomy list dependent on choice from another taxonomy list, drupal 8

I have a taxonomy option list make where I choose say Toyota.
I want the second taxonomy option list with the models of Toyota only (Eg. Corolla, hilux etc...).
When I choose Benz the second list will then contains C-Class, ML, etc...
I have created the entity vehicle from google examples on xampp localhost, windows 10.
In my vehicle form I'm able to populate the first list. But the second appears empty.
Here is my code. Please help:
public function buildForm(array $form, FormStateInterface $form_state, $params = NULL) {
$options = array();
$tax = "make";
$terms = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($tax, $parent = 0, $max_depth = NULL, $load_entities = FALSE);
foreach ($terms as $term) {
$options[] = $term->name;
}
$form['make'] = array(
'#type' => 'select',
'#title' => t('Make'),
'weight' => 0,
'#options' => $options,
'#ajax' => array(
'callback' => [$this, 'changeOptionsAjax'],
'wrapper' => 'model_wrapper',
),
);
$form['model'] = array(
'#type' => 'select',
'#title' => t('Model'),
'weight' => 1,
'#options' => $this->getOptions($form_state),
'#prefix' => '<div id="model_wrapper">',
'#suffix' => '</div>',
);
return $form;
}
public function getOptions(FormStateInterface $form_state) {
$options = array();
if ($form_state->getValue('make') == "Benz") {
$tax="benz";
}
elseif ($form_state->getValue('make') == "BMW") {
$tax="bmw";
}
elseif ($form_state->getValue('make') == "Toyota") {
$tax="toyota";
}
else {
$tax="title";
// title is just another taxonomy list I'm using as default if make is not found
}
$terms = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($tax, $parent = 0, $max_depth = NULL, $load_entities = FALSE);
foreach ($terms as $term) {
$options[] = $term->name;
}
return $options;
}
public function changeOptionsAjax(array &$form, FormStateInterface $form_state) {
return $form['model'];
}
Here I give you a working sample based on your example VehiculesForm.php:
I took the liberty to rename some variable for better readability.
<?php
namespace Drupal\example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* VehiculesForm.
*/
class VehiculesForm extends FormBase {
/**
* The term Storage.
*
* #var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* {#inheritdoc}
*/
public function __construct(EntityTypeManagerInterface $entity) {
$this->termStorage = $entity->getStorage('taxonomy_term');
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container) {
// Instantiates this form class.
return new static(
// Load the service required to construct this class.
$container->get('entity_type.manager')
);
}
/**
* {#inheritdoc}
*/
public function getFormId() {
return 'vehicules_form';
}
/**
* {#inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $params = NULL) {
$brands = $this->termStorage->loadTree('make', 0, NULL, TRUE);
$options = [];
if ($brands) {
foreach ($brands as $brand) {
$options[$brand->getName()] = $brand->getName();
}
}
$form['brand'] = array(
'#type' => 'select',
'#title' => $this->t('brand'),
'#options' => $options,
'#ajax' => array(
'callback' => [$this, 'selectModelsAjax'],
'wrapper' => 'model_wrapper',
),
);
$form['model'] = array(
'#type' => 'select',
'#title' => $this->t('Model'),
'#options' => ['_none' => $this->t('- Select a brand before -')],
'#prefix' => '<div id="model_wrapper">',
'#suffix' => '</div>',
'#validated' => TRUE,
);
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Send'),
];
return $form;
}
/**
* {#inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {#inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
/**
* Called via Ajax to populate the Model field according brand.
*
* #param array $form
* An associative array containing the structure of the form.
* #param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* #return array
* The form model field structure.
*/
public function selectModelsAjax(array &$form, FormStateInterface $form_state) {
$options = [];
$vocabulary = 'title';
switch ($form_state->getValue('brand')) {
case 'Benz':
$vocabulary = 'benz';
break;
case 'BMW':
$vocabulary = 'bmw';
break;
case 'Toyota':
$vocabulary = 'toyota';
break;
}
$models = $this->termStorage->loadTree($vocabulary, 0, NULL, TRUE);
if ($models) {
foreach ($models as $model) {
$options[$model->id()] = $model->getName();
}
}
$form['model']['#options'] = $options;
return $form['model'];
}
}
Also, I suggest you to make some improvments on you code such:
Don't use a switch but link your taxonomies with a reference fields.
Add validation to ensure security (check we don't spoof your field for example) !!
Don't use the brandname but the ID. Avoid $options[$brand->getName()] = $brand->getName(); and use something like $options[$brand->id()] = $brand->getName();.
Hope it will help you !

Yii2 create multiple instance from multiple model and with datepicker (kartik) on form

I have a table on my database (feuille_de_jour_responsable) that is linked on 2 other tables (personnel and poste_fdj).
In a form, I use a calendar with "Multiple Dates Selection" from the Kartik's widget datepicker (http://demos.krajee.com/widget-details/datepicker#comments) and I want, for all dates selected on the widget, that a new instance "feuille_de_jour_responsable" will be created with the Code_personnel and ID_poste_fdj.
For now, the form appears but the button "create" do nothing.
My form code:
<?php
$reqNomPoste = 'SELECT Nom_Poste_FDJ,ID_Poste_FDJ FROM poste_fdj';
$nomPoste = PosteFdj::findBySql($reqNomPoste)
->asArray()
->all();
//var_dump($nomPoste);
$reqNomPersonnel = 'SELECT Nom_Personnel,Code_Personnel FROM personnel';
$nomPersonnel = Personnel::findBySql($reqNomPersonnel)
->asArray()
->all();
//var_dump($nomPersonnel);
$form = ActiveForm::begin();
echo FormGrid::widget([
'model'=>$feuille_de_jour_responsable,
'form'=>$form,
'autoGenerateColumns'=>true,
'rows'=>[
[
'attributes'=>[
'ID_Poste_FDJ'=>['type'=>Form::INPUT_DROPDOWN_LIST, 'items'=>$nomPoste, 'hint'=>'Choisir poste'],
'Code_Personnel'=>['type'=>Form::INPUT_DROPDOWN_LIST, 'items'=>$nomPersonnel, 'hint'=>'Choisir Responsable'],
]
],
[
'attributes'=>[
'Date_Calendaire'=>['type'=>Form::INPUT_WIDGET, 'widgetClass'=>'\kartik\widgets\DatePicker',
'options' => [
'pluginOptions' => [
'todayHighlight' => true,
'format' => 'yyyy-mm-dd',
'multidate' => true,
'multidateSeparator' => ' ; ',
],
],
'hint'=>'Select Date',
],
]
],
[
'attributes'=>[
'actions'=>[ // embed raw HTML content
'type'=>Form::INPUT_RAW,
'value'=> '<div>' .
Html::submitButton('Create', ['class' => 'btn btn-primary']) .
'</div>'
],
],
],
]
]);
<?phpActiveForm::end();?>
And my Controller code :
<?php
namespace app\controllers;
use Yii;
use app\models\FeuilleDeJourResponsable;
use app\models\MeteoPrevision;
use app\models\PosteFdj;
use app\models\CategorieFdj;
use app\models\Personnel;
use app\models\FeuilleDeJourResponsableSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
/**
* FeuilleDeJourResponsableController implements the CRUD actions for FeuilleDeJourResponsable model.
*/
class FeuilleDeJourResponsableController extends Controller
{
/**
* #inheritdoc
*/
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['POST'],
],
],
];
}
/**
* Lists all FeuilleDeJourResponsable models.
* #return mixed
*/
public function actionIndex()
{
$searchModel = new FeuilleDeJourResponsableSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single FeuilleDeJourResponsable model.
* #param string $Date_Calendaire
* #param integer $ID_Poste_FDJ
* #return mixed
*/
public function actionView($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie, $Code_Personnel)
{
return $this->render('view', [
'model' => $this->findModel($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie, $Code_Personnel),
]);
}
/**
* Creates a new FeuilleDeJourResponsable model.
* If creation is successful, the browser will be redirected to the 'view' page.
* #return mixed
*/
public function actionCreate()
{
$feuille_de_jour_responsable = new FeuilleDeJourResponsable();
$meteo_prevision = new MeteoPrevision();
$poste_fdj = new PosteFdj();
$categorie_fdj = new CategorieFdj();
$personnel = new Personnel();
if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $meteo_prevision->load(Yii::$app->request->post()) && $poste_fdj->load(Yii::$app->request->post()) && $categorie_fdj->load(Yii::$app->request->post()) && $personnel->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable, $meteo_prevision, $poste_fdj, $categorie_fdj, $personnel]))
//if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $meteo_prevision->load(Yii::$app->request->post()) && $poste_fdj->load(Yii::$app->request->post()) && $categorie_fdj->load(Yii::$app->request->post()) && $personnel->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable]) && PosteFdj::validateMultiple([$poste_fdj]) && CategorieFdj::validateMultiple([$categorie_fdj]) )
{
$poste_fdj->save(false);
$categorie_fdj->save(false);
$personnel->save(false);
$feuille_de_jour_responsable->ID_Poste_FDJ = $poste_fdj->ID_Poste_FDJ; // no need for validation rule on user_id as you set it yourself
$feuille_de_jour_responsable->ID_Categorie = $categorie_fdj->ID_Categorie; // no need for validation rule on user_id as you set it yourself
$feuille_de_jour_responsable->Code_Personnel = $personnel->Code_Personnel; // no need for validation rule on user_id as you set it yourself
$feuille_de_jour_responsable->save(false); // skip validation as model is already validated
$meteo_prevision->Date_Calendaire = $feuille_de_jour_responsable->Date_Calendaire; // no need for validation rule on user_id as you set it yourself
$meteo_prevision->save(false);
return $this->redirect(['feuille_de_jour_responsable/view', 'Date_Calendaire' => $feuille_de_jour_responsable->Date_Calendaire]);
} else {
return $this->render('create', [
'feuille_de_jour_responsable' => $feuille_de_jour_responsable,
'meteo_prevision' => $meteo_prevision,
'poste_fdj' => $poste_fdj,
'categorie_fdj' => $categorie_fdj,
'personnel' => $personnel,
]);
}
}
/**
* Updates an existing FeuilleDeJourResponsable model.
* If update is successful, the browser will be redirected to the 'view' page.
* #param string $Date_Calendaire
* #param integer $ID_Poste_FDJ
* #return mixed
*/
public function actionUpdate($Date_Calendaire/*, $ID_Poste_FDJ*/)
//public function actionUpdate($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie)
{
$feuille_de_jour_responsable = FeuilleDeJourResponsable::findOne($Date_Calendaire);
if (!$feuille_de_jour_responsable) {
throw new NotFoundHttpException("The feuille_de_jour_responsable was not found.");
}
$meteo_prevision = MeteoPrevision::findOne($feuille_de_jour_responsable->Date_Calendaire);
if (!$meteo_prevision) {
throw new NotFoundHttpException("The feuille_de_jour_responsable has no meteo_prevision.");
}
$feuille_de_jour_responsable->scenario = 'update';
$meteo_prevision->scenario = 'update';
if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $meteo_prevision->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable, $meteo_prevision/*, $poste_fdj, $categorie_fdj, $personnel*/])) {
$isValid = $feuille_de_jour_responsable->validate();
$isValid = $meteo_prevision->validate() && $isValid;
if ($isValid) {
$feuille_de_jour_responsable->save(false);
$meteo_prevision->save(false);
return $this->redirect(['feuille_de_jour_responsable/view', 'Date_Calendaire' => $Date_Calendaire]);
}
}
$poste_fdj = PosteFdj::findOne($feuille_de_jour_responsable->ID_Poste_FDJ);
if (!$poste_fdj) {
throw new NotFoundHttpException("The feuille_de_jour_responsable has no poste_fdj.");
}
$feuille_de_jour_responsable->scenario = 'update';
$poste_fdj->scenario = 'update';
if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $poste_fdj->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable, $meteo_prevision, $poste_fdj, $categorie_fdj, $personnel])) {
$isValid = $feuille_de_jour_responsable->validate();
$isValid = $poste_fdj->validate() && $isValid;
if ($isValid) {
$feuille_de_jour_responsable->save(false);
$poste_fdj->save(false);
return $this->redirect(['feuille_de_jour_responsable/view', 'ID_Poste_FDJ' => $ID_Poste_FDJ]);
}
}
$categorie_fdj = CategorieFdj::findOne($feuille_de_jour_responsable->ID_Categorie);
if (!$categorie_fdj) {
throw new NotFoundHttpException("The feuille_de_jour_responsable has no categorie_fdj.");
}
$feuille_de_jour_responsable->scenario = 'update';
$categorie_fdj->scenario = 'update';
if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $categorie_fdj->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable, $meteo_prevision, $poste_fdj, $categorie_fdj, $personnel])) {
$isValid = $feuille_de_jour_responsable->validate();
$isValid = $categorie_fdj->validate() && $isValid;
if ($isValid) {
$feuille_de_jour_responsable->save(false);
$categorie_fdj->save(false);
return $this->redirect(['feuille_de_jour_responsable/view', 'ID_Categorie' => $ID_Categorie]);
}
}
$personnel = Personnel::findOne($feuille_de_jour_responsable-> Code_Personnel);
if (!$personnel) {
throw new NotFoundHttpException("The feuille_de_jour_responsable has no personnel.");
}
$feuille_de_jour_responsable->scenario = 'update';
$personnel->scenario = 'update';
if ($feuille_de_jour_responsable->load(Yii::$app->request->post()) && $personnel->load(Yii::$app->request->post()) && FeuilleDeJourResponsable::validateMultiple([$feuille_de_jour_responsable, $meteo_prevision, $poste_fdj, $categorie_fdj, $personnel])) {
$isValid = $feuille_de_jour_responsable->validate();
$isValid = $personnel->validate() && $isValid;
if ($isValid) {
$feuille_de_jour_responsable->save(false);
$personnel->save(false);
return $this->redirect(['feuille_de_jour_responsable/view', 'Code_Personnel' => $Code_Personnel]);
}
}
}
/**
* Deletes an existing FeuilleDeJourResponsable model.
* If deletion is successful, the browser will be redirected to the 'index' page.
* #param string $Date_Calendaire
* #param integer $ID_Poste_FDJ
* #return mixed
*/
public function actionDelete($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie, $Code_Personnel)
{
$this->findModel($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie, $Code_Personnel)->delete();
return $this->redirect(['index']);
}
/**
* Finds the FeuilleDeJourResponsable model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* #param string $Date_Calendaire
* #param integer $ID_Poste_FDJ
* #param integer $ID_Categorie
* #param integer $Code_Personnel
* #return FeuilleDeJourResponsable the loaded model
* #throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($Date_Calendaire, $ID_Poste_FDJ, $ID_Categorie, $Code_Personnel)
{
if (($feuille_de_jour_responsable = FeuilleDeJourResponsable::findOne(['Date_Calendaire' => $Date_Calendaire, 'ID_Poste_FDJ' => $ID_Poste_FDJ, 'ID_Categorie' => $ID_Categorie, 'Code_Personnel' => $Code_Personnel])) !== null) {
return $feuille_de_jour_responsable;
} else {
throw new NotFoundHttpException('The requested page does not exist.');
}
}
}
If anyone can help me, I'm searching since 2 weeks and am completely lost ...

How to get NULL as an option in a datagrid relation in sonata admin bundle?

I added the following to a Sonata admin in order to filter by category. However, the list does not show NULL as an option for category. I want also want to be able to filter by category for when category is NULL instead of an entity.
How can one achieve this? My current configuration:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add("category");
}
Try this:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add("category", 'doctrine_orm_callback', array(
'callback' => function ($queryBuilder, $alias, $field, $value) {
/**
* #var QueryBuilder $queryBuilder
*/
if ($value['value']) {
if ($value['value'] == 0) {
$queryBuilder->andWhere($queryBuilder->expr()->isNull($alias.'.category'));
return true;
} else {
$category = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager')->getReference('AcmeBundle:Category', $value['value']);
$queryBuilder->andWhere($queryBuilder->expr()->eq($alias.'.category', $category));
return true;
}
}
},
'field_type' => 'choice',
'field_options' => array(
'choices' => $this->getCategoryChoices()
),
'label' => 'Category'
));
}
private function getCategoryChoices()
{
$categories = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager')->getRepository('AcmeBundle:Category')->findAll();
$choices["0"] = "NULL";
foreach($categories as $category) {
$choices["{$category->getId()}"] = $category->getName();
}
return $choices;
}

symfony2 form create new type combining collection and entity

With symfony 2, I am willing to create a new field type combining the behaviour of the entity field type and the one of the collection field type:
- if the user selects an existing entity, the collection of new entity is null
- if the user creates a new entity, the first field is not required
Do ou have any idea as how to proceed? Can I reuse existing symfony types? Where do I put the logic (if old, collection is not required, if new, entity is not required) ?
Thansk a lot
I finally got it ! Wow, this was not that easy.
So basically, adding a new entry to a select with javascript when the form type is an entity type triggers a Symfony\Component\Form\Exception\TransformationFailedException.
This exception comes from the getChoicesForValues method called on a ChoiceListInterface in the reverseTransform method of the ChoicesToValuesTransformer. This DataTransformer is used in the ChoiceType so to overcome this, I had to build a new type extending the ChoiceType and replacing just a tiny part of it.
The steps to make it work :
Create a new type :
<?php
namespace AppBundle\Form\Type;
use AppBundle\Form\DataTransformer\ChoicesToValuesTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TagType extends ChoiceType
{
/**
* #var ManagerRegistry
*/
protected $registry;
/**
* #var array
*/
private $choiceListCache = array();
/**
* #var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* #var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager, ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->entityManager = $entityManager;
$this->propertyAccessor = $propertyAccessor;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
throw new LogicException('Either the option "choices" or "choice_list" must be set.');
}
if ($options['expanded']) {
// Initialize all choices before doing the index check below.
// This helps in cases where index checks are optimized for non
// initialized choice lists. For example, when using an SQL driver,
// the index check would read in one SQL query and the initialization
// requires another SQL query. When the initialization is done first,
// one SQL query is sufficient.
$preferredViews = $options['choice_list']->getPreferredViews();
$remainingViews = $options['choice_list']->getRemainingViews();
// Check if the choices already contain the empty value
// Only add the empty value option if this is not the case
if (null !== $options['placeholder'] && 0 === count($options['choice_list']->getChoicesForValues(array('')))) {
$placeholderView = new ChoiceView(null, '', $options['placeholder']);
// "placeholder" is a reserved index
$this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
}
$this->addSubForms($builder, $preferredViews, $options);
$this->addSubForms($builder, $remainingViews, $options);
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
$builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
} else {
$builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
$builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
}
} else {
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
if ($options['multiple']) {
$builder
->addEventSubscriber(new MergeDoctrineCollectionListener())
->addViewTransformer(new CollectionToArrayTransformer(), true)
;
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$choiceListCache = & $this->choiceListCache;
$choiceList = function (Options $options) use (&$choiceListCache) {
// Harden against NULL values (like in EntityType and ModelType)
$choices = null !== $options['choices'] ? $options['choices'] : array();
// Reuse existing choice lists in order to increase performance
$hash = hash('sha256', serialize(array($choices, $options['preferred_choices'])));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
}
return $choiceListCache[$hash];
};
$emptyData = function (Options $options) {
if ($options['multiple'] || $options['expanded']) {
return array();
}
return '';
};
$emptyValue = function (Options $options) {
return $options['required'] ? null : '';
};
// for BC with the "empty_value" option
$placeholder = function (Options $options) {
return $options['empty_value'];
};
$placeholderNormalizer = function (Options $options, $placeholder) {
if ($options['multiple']) {
// never use an empty value for this case
return;
} elseif (false === $placeholder) {
// an empty value should be added but the user decided otherwise
return;
} elseif ($options['expanded'] && '' === $placeholder) {
// never use an empty label for radio buttons
return 'None';
}
// empty value has been set explicitly
return $placeholder;
};
$compound = function (Options $options) {
return $options['expanded'];
};
$resolver->setDefaults(array(
'multiple' => false,
'expanded' => false,
'choice_list' => $choiceList,
'choices' => array(),
'preferred_choices' => array(),
'empty_data' => $emptyData,
'empty_value' => $emptyValue, // deprecated
'placeholder' => $placeholder,
'error_bubbling' => false,
'compound' => $compound,
// The view data is always a string, even if the "data" option
// is manually set to an object.
// See https://github.com/symfony/symfony/pull/5582
'data_class' => null,
));
$resolver->setNormalizers(array(
'empty_value' => $placeholderNormalizer,
'placeholder' => $placeholderNormalizer,
));
$resolver->setAllowedTypes(array(
'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
));
$choiceListCache = & $this->choiceListCache;
$registry = $this->registry;
$propertyAccessor = $this->propertyAccessor;
$type = $this;
$loader = function (Options $options) use ($type) {
if (null !== $options['query_builder']) {
return $type->getLoader($options['em'], $options['query_builder'], $options['class']);
}
};
$choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
// Support for closures
$propertyHash = is_object($options['property'])
? spl_object_hash($options['property'])
: $options['property'];
$choiceHashes = $options['choices'];
// Support for recursive arrays
if (is_array($choiceHashes)) {
// A second parameter ($key) is passed, so we cannot use
// spl_object_hash() directly (which strictly requires
// one parameter)
array_walk_recursive($choiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
} elseif ($choiceHashes instanceof \Traversable) {
$hashes = array();
foreach ($choiceHashes as $value) {
$hashes[] = spl_object_hash($value);
}
$choiceHashes = $hashes;
}
$preferredChoiceHashes = $options['preferred_choices'];
if (is_array($preferredChoiceHashes)) {
array_walk_recursive($preferredChoiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
}
// Support for custom loaders (with query builders)
$loaderHash = is_object($options['loader'])
? spl_object_hash($options['loader'])
: $options['loader'];
// Support for closures
$groupByHash = is_object($options['group_by'])
? spl_object_hash($options['group_by'])
: $options['group_by'];
$hash = hash('sha256', json_encode(array(
spl_object_hash($options['em']),
$options['class'],
$propertyHash,
$loaderHash,
$choiceHashes,
$preferredChoiceHashes,
$groupByHash,
)));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new EntityChoiceList(
$options['em'],
$options['class'],
$options['property'],
$options['loader'],
$options['choices'],
$options['preferred_choices'],
$options['group_by'],
$propertyAccessor
);
}
return $choiceListCache[$hash];
};
$emNormalizer = function (Options $options, $em) use ($registry) {
/* #var ManagerRegistry $registry */
if (null !== $em) {
if ($em instanceof ObjectManager) {
return $em;
}
return $registry->getManager($em);
}
$em = $registry->getManagerForClass($options['class']);
if (null === $em) {
throw new RuntimeException(sprintf(
'Class "%s" seems not to be a managed Doctrine entity. '.
'Did you forget to map it?',
$options['class']
));
}
return $em;
};
$resolver->setDefaults(array(
'em' => null,
'property' => null,
'query_builder' => null,
'loader' => $loader,
'choices' => null,
'choice_list' => $choiceList,
'group_by' => null,
));
$resolver->setRequired(array('class'));
$resolver->setNormalizers(array(
'em' => $emNormalizer,
));
$resolver->setAllowedTypes(array(
'em' => array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'),
'loader' => array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'),
));
}
/**
* #return string
*/
public function getName()
{
return 'fmu_tag';
}
/**
* Return the default loader object.
*
* #param ObjectManager $manager
* #param mixed $queryBuilder
* #param string $class
*
* #return ORMQueryBuilderLoader
*/
public function getLoader(ObjectManager $manager, $queryBuilder, $class)
{
return new ORMQueryBuilderLoader(
$queryBuilder,
$manager,
$class
);
}
/**
* Adds the sub fields for an expanded choice field.
*
* #param FormBuilderInterface $builder The form builder.
* #param array $choiceViews The choice view objects.
* #param array $options The build options.
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
if (is_array($choiceView)) {
// Flatten groups
$this->addSubForms($builder, $choiceView, $options);
} else {
$choiceOpts = array(
'value' => $choiceView->value,
'label' => $choiceView->label,
'translation_domain' => $options['translation_domain'],
'block_name' => 'entry',
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add($i, $choiceType, $choiceOpts);
}
}
}
}
Register the type in your services :
tag.type:
class: %tag.type.class%
arguments: [#doctrine.orm.entity_manager, #doctrine ,#property_accessor]
tags:
- { name: form.type, alias: fmu_tag }
Create a new view for the type copying the choice one :
{#app/Resources/views/Form/fmu_tag.html.twig#}
{% block fmu_tag_widget %}
{% if expanded %}
{{- block('choice_widget_expanded') -}}
{% else %}
{{- block('choice_widget_collapsed') -}}
{% endif %}
{% endblock %}
Register the view in your twig config.yml :
# Twig Configuration
twig:
form:
resources:
- 'Form/fmu_tag.html.twig'
Create a new ChoiceToValueDataTransformer replace the default class used in the choiceType
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien#symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\Form\DataTransformer;
use AppBundle\Entity\Core\Tag;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
/**
* #author Bernhard Schussek <bschussek#gmail.com>
*/
class ChoicesToValuesTransformer implements DataTransformerInterface
{
private $choiceList;
/**
* Constructor.
*
* #param ChoiceListInterface $choiceList
*/
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
/**
* #param array $array
*
* #return array
*
* #throws TransformationFailedException If the given value is not an array.
*/
public function transform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
return $this->choiceList->getValuesForChoices($array);
}
/**
* #param array $array
*
* #return array
*
* #throws TransformationFailedException If the given value is not an array
* or if no matching choice could be
* found for some given value.
*/
public function reverseTransform($array)
{
if (null === $array) {
return array();
}
if (!is_array($array)) {
throw new TransformationFailedException('Expected an array.');
}
$choices = $this->choiceList->getChoicesForValues($array);
if (count($choices) !== count($array)) {
$missingChoices = array_diff($array, $this->choiceList->getValues());
$choices = array_merge($choices, $this->transformMissingChoicesToEntities($missingChoices));
}
return $choices;
}
public function transformMissingChoicesToEntities(Array $missingChoices)
{
$newChoices = array_map(function($choice){
return new Tag($choice);
}, $missingChoices);
return $newChoices;
}
}
Loot at the last method of this file : transformMissingChoicesToEntities
This is where, when missing, I have created a new entity. So if you want to use all this, you need to adapt the new Tag($choice) ie. replace it by a new entity of your own.
So the form to which you add a collection now uses your new type:
$builder
->add('tags', 'fmu_tag', array(
'by_reference' => false,
'required' => false,
'class' => 'AppBundle\Entity\Core\Tag',
'multiple' => true,
'label'=>'Tags',
));
In order to create new choices, I am using the select2 control.
Add the file in your javascripts : http://select2.github.io
Add the following code in your view :
<script>
$(function() {
$('#appbundle_marketplace_product_ingredient_tags').select2({
closeOnSelect: false,
multiple: true,
placeholder: 'Tapez quelques lettres',
tags: true,
tokenSeparators: [',', ' ']
});
});
</script>
That's all, you're good to select existing entities or create new ones from a new entry generated by the select2.
You shouldn't really need a brand new form type for this behavior (although you can certainly create one if you want).
Check out Symfony dynamic form modification which has an example of modifying form fields depending on if an entity is 'new' or not. You can start with that as a base and modify to your needs.
If you already know what you want as you're creating the form from your Controller, then you could instead pass options flagging what you would like to display. For example, from your Controller:
$form = $this->createForm(
new MyType(),
$entity,
array('show_my_entity_collection' => false)
);
Then in your form type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['show_my_entity_collection'])
{
$builder->add('entity', 'entity', array(
'class' => 'MyBundle:MyEntity',
'required' => false,
'query_builder' => function(MyEntityRepository $repository) {
return $repository->findAll();
},
));
}
// rest of form builder here
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\MyEntity',
'show_my_entity_collection' => true,
));
}
If you need to create a new field type, with new template you can check here how to do this:
http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html

Zend skeleton application Class 'Album\Model\AlbumTable' not found

I'm trying to figure out what's wrong with my first tutorial using Zend Skeleton App. I'm using Zend Studio 10 + ZendServer and Zf2.2; managed to get the skeleton app working and now got stuck on a missing class problem (see error below). I have tried various approaches but the result is the same: it's not working. Here are my files, any help would be appreciated.
My error:
Fatal error: Class 'Album\Model\AlbumTable' not found in C:\Program
Files\Zend\Apache2\htdocs\zf2album\module\Album\Module.php on line 55
Album/Module.php
namespace Album;
use Album\Model\Album;
use Album\Model\AlbumTable;
use Zend\Db\TableGateway\TableGateway;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
class Module implements ServiceProviderInterface {
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\ClassMapAutoloader' => array(
__DIR__ . '/autoload_classmap.php',
),
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
// if we're in a namespace deeper than one level we need to fix the \ in the path
__NAMESPACE__ => __DIR__ . '/src/' . str_replace('\\', '/' , __NAMESPACE__),
),
),
);
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
// Add this method:
public function getServiceConfig()
{
return array(
'factories' => array(
'Album\Model\AlbumTable' => function($sm) {
$tableGateway = $sm->get('AlbumTableGateway');
$table = new AlbumTable($tableGateway);
return $table;
},
'AlbumTableGateway' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Album());
return new TableGateway('album', $dbAdapter, null, $resultSetPrototype);
},
),
);
}
}
the AlbumController.php
namespace Album\Controller;
use Zend\Mvc\Controller\AbstractActionController; use
Zend\View\Model\ViewModel;
class AlbumController extends AbstractActionController { protected
$albumTable;
public function indexAction()
{
return new ViewModel(array(
'albums' => $this->getAlbumTable()->fetchAll(),
));
}
public function addAction()
{
}
public function editAction()
{
}
public function deleteAction()
{
}
public function fooAction()
{
// This shows the :controller and :action parameters in default route
// are working when you browse to /album/album/foo
return array();
}
public function getAlbumTable()
{
if (!$this->albumTable) {
$sm = $this->getServiceLocator();
$this->albumTable = $sm->get('Album\Model\AlbumTable');
}
return $this->albumTable;
} }
AlbumModel.php
namespace Album\Model;
use Zend\Db\TableGateway\TableGateway;
class AlbumTable {
protected $tableGateway;
public function __construct(TableGateway $tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
$resultSet = $this->tableGateway->select();
return $resultSet;
}
public function getAlbum($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(array('id' => $id));
$row = $rowset->current();
if (!$row) {
throw new \Exception("Could not find row $id");
}
return $row;
}
public function saveAlbum(Album $album)
{
$data = array(
'artist' => $album->artist,
'title' => $album->title,
);
$id = (int)$album->id;
if ($id == 0) {
$this->tableGateway->insert($data);
} else {
if ($this->getAlbum($id)) {
$this->tableGateway->update($data, array('id' => $id));
} else {
throw new \Exception('Form id does not exist');
}
}
}
public function deleteAlbum($id)
{
$this->tableGateway->delete(array('id' => $id));
} }
Assuming this isn't a typo in your question, the filename for the class AlbumTable should be AlbumTable.php, not AlbumModel.php.