AssociationField->autocomplete() inside a CollectionField using ->useEntryCrudForm() doesn't work
Setting up a CRUD that has a CollectionField that uses ->useEntryCrudForm() and in that CrudForm there is an AssociationField using ->autocomplete() breaks query builder completely.
If I call the CRUD being used for the ->useEntryCrudForm() then everything works, no changed required.
Otherwise ->setQueryBuilder() on the association field has no effect at all. And even without it the default query isn't working. Calling directly both with and without query builder work as intended. Without autocomplete both versions work (but queryBuilder is still ignored in the embedded version).
this is what I see in autocomplete (with or without setQueryBuilder since it doesn't change thing)
<!-- An exception occurred while executing a query: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'f0_.food_id' in 'SELECT' (500 Internal Server Error) -->
SELECT DISTINCT id_0 FROM (SELECT DISTINCT id_0, f0_.food_id FROM (SELECT f0_.id AS id_0, f0_.active AS active_1, f0_.updated_at AS updated_at_2 FROM food_extension f0_ LEFT JOIN cnffood_name c1_ ON f0_.food_id = c1_.id) dctrn_result_inner ORDER BY f0_.food_id ASC) dctrn_result LIMIT 20
the inner select isn't selecting food_id which is the entity field, yet the outer DISTINCT is looking for it.
Again no code change and directly calling the exact same CRUD and not auto select and query builder work perfectly. In the Collection query build is 100% ignored, and autocomplete doesn't work at all as the query isn't valid. I can not seem to find a way around this issue and the select has 6000 entries so it must use autocomplete, specially since the collection has a min of 2 but could be 20+
https://github.com/EasyCorp/EasyAdminBundle/issues/6991 is likely related
I don't know if this can help you, autocomplete in collection does not use setQueryBuilder but the createIndexQueryBuilder of the crud controller of the associated entity (#5792 #6098)
In your associated crud controller :
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
if ($searchDto->getRequest()->get('crudAction') === Crud::PAGE_INDEX) {
// context index
} else {
// context autocomplete : the equivalent of a generic setQueryBuilder
// if you want you can use the propertyName to add criteria
$context = $searchDto->getRequest()->get('autocompleteContext');
if ($context['propertyName'] === 'food1') {
// add some criteria
}
if ($context['propertyName'] === 'food2') {
// add some criteria
}
}
}
Did you modify createIndexQueryBuilder of the crud controller of the associated entity ?
it works 100% with query builder on the main crud or directly. Just not at all when it's in a collection using a secondary crud sadly.
I'll try the createIndex, but seems odd since it's never used for index just new and edit. But it's worth a try either way, seems only named since it does more then index :D
Actually for the moment, createIndexQueryBuilder is used in two contexts: the index and the autocomplete.
If you modify it without specifying the context, in some case there may be errors in the autocomplete context.
well I'm going to give it a shot, odd that setQueryBuilder works for me in multiple CRUDs with autocomplete though, just not in the secondary CRUD
In my case, i never use setQueryBuilder and autocomplete in the main form, only with collections so I get around the problem like this. And like that, it saves me from putting queryBuilder everywhere when I use it several times.
I might do the same cause it is lots of repeat code which I'm not a fan of, thanks for the suggestion, I'll report back once I try it out.
I have it in the "embedded" crus which is where the association is, and it's not running createIndexQueryBuilder at all, I have var_dump('here') as the first line and it runs just like before, same error.
I have it in the "embedded" crud which is where the association is
In which createIndexQueryBuilder did you put var_dump('here') ?
I'm not talking about the crud of useEntryCrudForm, I'm talking about the crud of the association which is in useEntryCrudForm
In main form:
yield CollectionField::new('foods')
->useEntryCrudForm(FoodsCrudController::class);
In FoodsCrudController (the crud used by useEntryCrudForm):
yield AssociationField::new('food')
->autocomplete();
In FoodCrudController (the crud of the associated entity):
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
throw new Exception('here');
}
MealsCrudController.php (second last yield is the Collection)
public function configureFields(string $pageName): iterable
{
yield Field\IdField::new('id')
->onlyOnDetail()
;
yield Field\TextField::new('name')
;
yield Field\AssociationField::new('user')
->autocomplete()
->setSortProperty('email')
->setQueryBuilder(function (QueryBuilder $queryBuilder) {
$queryBuilder
->leftJoin('entity.profile', 'p')
->andWhere('p.active = :active')
->setParameter('active', true)
->orderBy('entity.email', 'ASC')
;
})
;
yield Field\NumberField::new('optionPortion', 'Portion')
->setNumDecimals(2)
->setStoredAsString()
->setFormTypeOptions([
'html5' => true,
])
->setHtmlAttribute('min', '0.00')
->setHtmlAttribute('step', '0.25')
;
yield Field\CollectionField::new('mealLines', 'Items')
->useEntryCrudForm(MealLinesCrudController::class)
->setFormTypeOptions([
'by_reference' => false,
])
->renderExpanded()
->setColumns(12)
;
yield Field\DateTimeField::new('createdAt', 'Created')
->setSortable(true)
->hideOnForm()
;
}
MealLinesCrudController.php (Second yield is the association)
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
throw new Exception('here');
$queryBuilder = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
if (in_array($searchDto->getRequest()->get('crudAction'), array(Crud::PAGE_NEW, Crud::PAGE_EDIT))) {
$context = $searchDto->getRequest()->get('autocompleteContext');
if ($context['propertyName'] === 'food') {
return $queryBuilder
->join('entity.food', 'f')
->andWhere('entity.active = :active')
->setParameter('active', true)
->orderBy('f.description', 'ASC')
;
}
}
}
public function configureFields(string $pageName): iterable
{
$entityId = $this
->requestStack
->getCurrentRequest()
->attributes
->get('entityId')
;
$unitId = null;
if (null !== $entityId) {
$entity = $this
->mealLinesRepository
->find($entityId)
;
$unit = $entity->getOptionUnit();
if ($unit) {
$unitId = $unit->getId();
}
}
yield Field\IdField::new('id')
->onlyOnDetail()
;
yield Field\AssociationField::new('food', 'Food')
->autocomplete()
/*->setQueryBuilder(function (QueryBuilder $queryBuilder) {
$queryBuilder
->join('entity.food', 'f')
->andWhere('entity.active = :active')
->setParameter('active', true)
->orderBy('f.description', 'ASC')*/
;
})
->addCssClass('food_id')
->setFormTypeOptions([
'attr' => [
'onchange' => "updateFoodFieldVisibility(this);",
],
])
;
yield Field\AssociationField::new('optionUnit', 'Units')
->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($unitId) {
if ($unitId) {
$queryBuilder
->andWhere('entity.id = :unitId')
->setParameter('unitId', $unitId)
;
} else {
$queryBuilder->andWhere('0 = 1');
}
})
->addCssClass('food_unit hideOnFormLoad')
->setFormTypeOptions([
'attr' => [
'onchange' => "updateFoodFieldVisibility(this);",
],
])
;
yield Field\NumberField::new('optionPortion', 'Portion')
->setNumDecimals(2)
->setStoredAsString()
->addCssClass('food_portion hideOnFormLoad')
->setFormTypeOptions([
'html5' => true,
])
->setHtmlAttribute('min', '0.00')
->setHtmlAttribute('step', '0.25')
;
}
public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
throw new Exception('here');
return $formBuilder;
}
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
throw new Exception('here');
return $formBuilder;
}
And no throw happens, don't worry about the setQueryBuilder() on the food item in MealLines it doesn't work anyhow I just don't want to remove things until I get it working. But since there is no throw at all createIndexQueryBuilder, createNewFormBuilder and createEditFormBuilder are not run at all when embedded in a Collection :. Since I need autocomplete and I need access to PRE_SUBMIT as per https://github.com/EasyCorp/EasyAdminBundle/issues/7099
Also why does the query without autocomplete() work, but with it it does not, shouldn't the query be the same for both if I'm not modifying it? if I remove autocomplete the field renders properly with all the options, just that it's 6000+ items so the men usage is insane. But I add autocomplete() and I get a query issue (even without setQueryBuilder() and without createEditFormBuilder. Even if I could get it working without filtering out inactive items I'd be happy. But I can't get it working no matter what. That being said. If I load MealLinesCrudController.php directly from Dashboard the autocomplete() with setQueryBuilder() work 100% perfectly and inactive is filtered. No other changes.
For Reference
I'm not talking about the crud of useEntryCrudForm (MealLinesCrudController), I'm talking about the crud of the association which is in useEntryCrudForm (FoodCrudController).
In FoodCrudController.php (the crud controller of the food entity) :
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
throw new Exception('here');
return parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
}
Can you give us the createIndexQueryBuilder of FoodCrudController (it is he who needs to be modified, not MealLinesCrudController) ?
there is no such thing
CNFFoodName -> FoodExtension -> MealLines -> Meals
CNFFoodName is a RO table that is populated from an other source, FoodExtension is so I could add columns to it without touching any of the data, things like FoodGrouping and Active.
So I assume you want FoodExtension since CNFFoodName has no CRUD
yield Field\AssociationField::new('food', 'Name')
->setSortProperty('description')
->autocomplete()
->hideOnForm()
;
Nothing in my tree uses createIndexQueryBuilder except the one you just asked me to try
Also on a separateNote as I test adding it to FoodExtension, in FoodExtension I can't filter out active, in meals it needs to. But first I'll see if it works, still doesn't explain why the query for food in (many many other CRUDS) works with autocomplete but fields on collection embedded, the query changes for no reason at all, I've commented on all setQueryBuilders in the entire path.
added to FoodExtension and no throw, if I go directly to the foodExtention Crud it does throw
Also created a CNFFoodNameCrud and put it in there, no throw
When you say no throw, is that in the ajax response ?
Can you show me createIndexQueryBuilder in FoodExtensionCrudController ?
Did you modify ->findBy() or ->findAll() in FoodExtensionRepository ?
You can do a final test : add a FoodExtension in Meal and try autocomplete only.
the Ajax returns what I have posted before, about the missing food_id in the initial post. That has not changed once in all my testing so far.
createIndexQueryBuilder is from what you posted I dont' use this function at all anyplace so I'm only using what you posted for testing.
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
throw new Exception('here');
return parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
}
no but I added
public function findActiveQueryBuilder(bool $active = true): QueryBuilder
{
return $this->createQueryBuilder('ext')
->join('ext.food', 'f')
->andWhere('ext.active = :active')
->setParameter('active', $active)
->orderBy('f.description', 'ASC')
;
}
public function findByActive(bool $active = true): array
{
return $this->findActiveQueryBuilder($active)
->getQuery()
->getResult()
;
}
but it's never used.
As for You can do a final test : add a FoodExtension in Meal and try autocomplete only.
I have many other CRUDs doing this and it works perfectly. It's only when in a Collection loading an other CRUD that NOTHING runs and some how the query changes and breaks (only with autocomplete on, with it off it loads the values properly but NONE of the functions run so I can't use PRE_SUBMIT or change the query for it.)
Sorry I edited my reply so wanted to make sure you saw the edit I missed parts of yours when I replied.
I have many other CRUDs doing this and it works perfectly
I know, but I want to know if the problem does not come from the structure of FoodExtension. I want to be sure that FoodExtension autocomplete works in the main form (others are ok, maybe not FoodExtension).
Because i have no problem with autocomplete in a collection. I don't understand why in your case it doesn't go into createIndexQueryBuilder
Well there is no way to use it in the FoodExtension CRUD, it's a OneToOne and you can't add or delete, it's just extends the RO food table from the external source (CNFFoodName). But about 3-4 other CRUDs use food from FoodExtension as an autocomplete association and it works perfectly with and without setQueryBuilder() all of them.
The other ticket I linked to this one at the start is claiming the same issue as me, it's never called in the Collection CRUD, which made me feel better that it wasn't just me.
Well there is no way to use it in the FoodExtension CRUD, it's a OneToOne and you can't add or delete
you say OneToOne between who ? FoodExtension and who ?
<!-- An exception occurred while executing a query: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'f0_.food_id' in 'SELECT' (500 Internal Server Error) -->
SELECT DISTINCT id_0, f0_.food_id FROM (SELECT f0_.id AS id_0, f0_.active AS active_1, f0_.updated_at AS updated_at_2 FROM food_extension f0_ LEFT JOIN cnffood_name c1_ ON f0_.food_id = c1_.id)
Why f0_.food_id isn't in the select ?
SELECT f0_.id AS id_0, f0_.active AS active_1, f0_.updated_at AS updated_at_2 FROM food_extension f0_
Does FoodExtension entity know food_id, or it's an inversed property (because we can link them in both directions in OneToOne case) ?
Can you show me the MealLines entity (just the food property) ?
Can't think of anything relevant here. My Repositories are very simple and nontrivial change find or findBy. I wish I knew why it wasn't selecting it, if I remove autocomplete it doesn't error, and if I keep autocomplete and add it to dashboard it works. It only fails when in the collection with no other changes at all. Why is the query changing ONLY for in a collection with autocomplete?
And why isn't createIndexQueryBuilder, createNewFormBuilder, createEditFormBuilder running at all when in the Collection but all work as intended outside of it?
CNFFoodName
#[ORM\Id]
#[ORM\Column]
#[Groups(['foodext:read'])]
private ?int $id = null;
FoodExtension
#[ORM\OneToOne(orphanRemoval: true, cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['foodext:read'])]
#[Assert\NotNull]
public ?CNFFoodName $food = null;
Meals
/**
* @var Collection<int, MealLines>
*/
#[ORM\OneToMany(targetEntity: MealLines::class, mappedBy: 'meal', orphanRemoval: true, cascade: ['persist', 'remove'])]
#[Assert\Count(
min: 2,
minMessage: 'Items must have be at least {{ limit }} entry.'
)]
#[Assert\Valid]
#[Groups(['foodext:read', 'foodext:write'])]
private Collection $mealLines;
MealLines
#[ORM\ManyToOne(inversedBy: 'mealLines')]
#[ORM\JoinColumn(nullable: true)]
#[Assert\NotNull]
#[Groups(['meals:read', 'meals:write'])]
private ?Meals $meal = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
#[Assert\NotNull]
#[Groups(['meals:read', 'meals:write'])]
private ?FoodExtension $food = null;
FoodExtensionCrudController
class FoodExtensionCrudController extends AbstractCrudController
{
use Trait\ExportCsvActionTrait;
private AdminUrlGenerator $adminUrlGenerator;
private EntityManagerInterface $entityManager;
private RequestStack $requestStack;
public function __construct(AdminUrlGenerator $adminUrlGenerator, EntityManagerInterface $entityManager, RequestStack $requestStack)
{
$this->adminUrlGenerator = $adminUrlGenerator;
$this->entityManager = $entityManager;
$this->requestStack = $requestStack;
}
public static function getEntityFqcn(): string
{
return FoodExtension::class;
}
public function configureActions(Actions $actions): Actions
{
return $actions
->add(Crud::PAGE_INDEX, Action::DETAIL)
->add(Crud::PAGE_INDEX, self::CSVAction())
->addBatchAction(Action::new('batchEdit', 'Edit')
->linkToCrudAction('batchEdit')
->addCssClass('modal-trigger btn btn-primary')
->setIcon('fa fa-pencil')
)
->remove(Crud::PAGE_DETAIL, Action::DELETE)
->remove(Crud::PAGE_INDEX, Action::DELETE)
->remove(Crud::PAGE_INDEX, Action::NEW)
;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setEntityLabelInSingular('Food')
->setEntityLabelInPlural('Foods')
->setPageTitle('index', '%entity_label_plural%')
->setPageTitle('edit', fn (FoodExtension $item) => sprintf('Editing <strong>%s</strong>', $item->food->getDescription()))
->setPageTitle('detail', fn (FoodExtension $item) => sprintf('<h1>%s <small>%s</small></h1>', '%entity_label_singular%', $item->food->getDescription()))
->setDefaultSort(['food' => 'ASC'])
->setTimezone($this->getUser()->getProfile()->getTimezone())
->setSearchFields(['food.description', 'food.groupId.name'])
->setPaginatorUseOutputWalkers(true)
;
}
public function configureFields(string $pageName): iterable
{
yield Field\IdField::new('id')
->onlyOnDetail()
;
yield Field\AssociationField::new('food.groupId', 'Group')
->setCrudController(CNFFoodGroupCrudController::class)
->setSortProperty('name')
//->setSortable(true)
->autocomplete()
->hideOnForm()
;
yield Field\AssociationField::new('food', 'Name')
->setSortProperty('description')
->autocomplete()
->hideOnForm()
;
yield Field\BooleanField::new('active')
->setSortable(true)
;
yield Field\AssociationField::new('groupingId', 'Grouping')
->setFormTypeOptions([
'query_builder' => function (FoodGroupingRepository $er) {
return $er->findActiveQueryBuilder();
},
])
->setCrudController(FoodGroupingController::class)
->setSortProperty('name')
->setSortable(true)
->setColumns(3)
;
yield Field\DateField::new('updatedAt', 'Last updated')
->hideOnForm()
;
}
public function configureFilters(Filters $filters): Filters
{
return $filters
->add(NestedFilter::wrap(
Filter\EntityFilter::new('food.groupId', 'Group')
->setFormTypeOption('value_type_options', [
'class' => CNFFoodGroup::class,
]),
'Group'
))
->add(NestedFilter::wrap(
Filter\TextFilter::new('food.description', 'Name'),
'Name'
))
->add(Filter\BooleanFilter::new('active', 'Active'))
->add(Filter\EntityFilter::new('groupingId', 'Grouping'))
->add(Filter\DateTimeFilter::new('updatedAt', 'Last Updated'))
;
}
public function batchEdit(AdminContext $context, Request $request, EntityManagerInterface $entityManager): Response
{
$ids = $request->request->all('batchActionEntityIds');
if (empty($ids)) {
$ids = $request->request->all('form')['batchActionEntityIds'];
}
if (!is_array($ids)) {
if (strpos($ids, ',') !== false) {
$ids = explode(',', $ids);
} else {
$ids = [$ids];
}
}
$targetUrl = $this->adminUrlGenerator
->setController(self::class)
->setAction(Crud::PAGE_INDEX)
->generateUrl()
;
if (!$ids) {
$this->addFlash('warning', 'No items selected.');
return $this->redirect($targetUrl);
}
$entities = $entityManager
->getRepository($this->getEntityFqcn())
->findBy(['id' => $ids])
;
$groupings = $entityManager
->getRepository(FoodGrouping::class)
->findByActive()
;
$groupingChoices = [];
foreach ($groupings as $grouping) {
$groupingChoices[$grouping->getName()] = $grouping->getId();
}
$form = $this->createFormBuilder()
->add('batchActionEntityIds', Type\HiddenType::class, [
'mapped' => false,
'data' => implode(',', $ids),
])
->add('active', Type\CheckboxType::class, [
'required' => false,
'label' => 'Active',
'row_attr' => ['class' => 'form-switch'],
'data' => $entities[0]->isActive(),
])
->add('grouping', EntityType::class, [
'required' => true,
'label' => 'Grouping',
'class' => FoodGrouping::class,
'choice_label' => 'name',
'query_builder' => function (FoodGroupingRepository $er) {
return $er->createQueryBuilder('c')
->where('c.active = :active')
->setParameter('active', true)
->orderBy('c.name', 'ASC')
;
},
'row_attr' => ['class' => 'col-md-6 col-xxl-5'],
])
->add('save', Type\SubmitType::class, ['label' => 'Update'])
->getForm()
;
$form->handleRequest($this->requestStack->getCurrentRequest());
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
foreach ($entities as $entity) {
$entity->setActive($data['active']);
if (!empty($data['grouping'])) {
$entity->setGroupingId($data['grouping']);
}
$entityManager->persist($entity);
}
$entityManager->flush();
$this->addFlash('success', 'Batch updated successfully.');
return $this->redirect($targetUrl);
}
return $this->render('admin/food_batch_edit.html.twig', [
'form' => $form->createView(),
'entities' => $entities,
]);
}
protected function getExportCsvFields(): iterable
{
return [
'id',
'food.description',
'food.groupId.name',
'active',
'groupingId.name',
];
}
#[Route('/admin/get-units-by-food-extension/{foodId}', name: 'app_admin_get_units_by_food_extension', methods: ['GET'])]
public function getUnitsByFoodExt(
int $foodId,
EntityManagerInterface $entityManager
): JsonResponse
{
$repository = $entityManager->getRepository(FoodExtension::class);
if (!$repository || empty($foodId)) {
return new JsonResponse(['status' => 'error', 'message' => 'Entity not found'], 404);
}
$food = $repository->findBy(['id' => $foodId]);
if (!$food) {
return new JsonResponse(['status' => 'error', 'message' => 'Entity not found'], 404);
}
$conversions = $food[0]->getFood()->getConverstionFactors();
$options = [];
foreach ($conversions as $conversion) {
$options[] = [
'id' => $conversion->getId(),
'name' => $conversion->getMeasureId()->getName(),
];
}
return new JsonResponse(['status' => 'ok', 'options' => $options]);
}
}
MealsCrudController (Lots of comments out code in here cause it's a WIP)
class MealsCrudController extends AbstractCrudController
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public static function getEntityFqcn(): string
{
return Meals::class;
}
public function configureActions(Actions $actions): Actions
{
return $actions
->add(Crud::PAGE_INDEX, Action::DETAIL)
;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setEntityLabelInSingular('Meal')
->setEntityLabelInPlural('Meals')
->setPageTitle('index', '%entity_label_plural%')
->setPageTitle('detail', fn (Meals $item) => sprintf('<h1>%s <small>(#%s)</small></h1>', '%entity_label_singular%', $item->getId()))
->setDefaultSort(['name' => 'ASC'])
->setTimezone($this->getUser()->getProfile()->getTimezone())
;
}
public function configureFields(string $pageName): iterable
{
yield Field\IdField::new('id')
->onlyOnDetail()
;
yield Field\TextField::new('name')
;
yield Field\AssociationField::new('user')
->autocomplete()
->setSortProperty('email')
->setQueryBuilder(function (QueryBuilder $queryBuilder) {
$queryBuilder
->leftJoin('entity.profile', 'p')
->andWhere('p.active = :active')
->setParameter('active', true)
->orderBy('entity.email', 'ASC')
;
})
;
yield Field\NumberField::new('optionPortion', 'Portion')
->setNumDecimals(2)
->setStoredAsString()
->setFormTypeOptions([
'html5' => true,
])
->setHtmlAttribute('min', '0.00')
->setHtmlAttribute('step', '0.25')
;
yield Field\CollectionField::new('mealLines', 'Items')
->useEntryCrudForm(MealLinesCrudController::class)
->setFormTypeOptions([
'by_reference' => false,
])
->renderExpanded()
/*->formatValue(function ($value, $entity) {
if (empty($value)) {
return 'N/A';
}
$lines = [];
foreach ($value as $line) {
$lines[] = $line;
}
return implode('<br>', $lines);
})*/
->setColumns(12)
;
yield Field\DateTimeField::new('createdAt', 'Created')
->setSortable(true)
->hideOnForm()
;
}
/*
public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
return $this->dynamicFormBuilder($formBuilder);
}
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
return $this->dynamicFormBuilder($formBuilder);
}
private function dynamicFormBuilder(FormBuilderInterface $formBuilder): FormBuilderInterface
{
$formBuilder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (empty($data['mealLines'])) {
return;
}
$mealLines = $data['mealLines'];
if (!is_array($mealLines)) {
$mealLines = [$mealLines];
}
foreach ($mealLines as $k => $line) {
$value = $line['optionUnit'];
$field = implode('_', array(
$form->getName(),
'mealLines',
$k,
'optionUnit',
));
var_dump($field);
var_dump($form->get('mealLines')->getConfig());
$form->add($field, FieldType\EntityType::class, [
'class' => \App\Entity\CNFConversionFactor::class,
'query_builder' => function ($repo) use ($value) {
return $repo->createQueryBuilder('entity')
->andWhere('entity.id = :value')
->setParameter('value', $value)
;
},
]);
}
die();
}
);
return $formBuilder;
}
#[Route('/admin/get-meals-by-user/{userId}', name: 'app_admin_get_meals_by_user', methods: ['GET'])]
public function getMealsByUser(
int $userId,
EntityManagerInterface $entityManager
): JsonResponse
{
$repository = $entityManager->getRepository(Meals::class);
if (!$repository || empty($userId)) {
return new JsonResponse(['status' => 'error', 'message' => 'Entity not found'], 404);
}
$meals = $repository->findBy(['user' => $userId]);
if (!$meals) {
return new JsonResponse(['status' => 'error', 'message' => 'Entity not found'], 404);
}
$options = [];
foreach ($meals as $meal) {
$options[] = [
'id' => $meal->getId(),
'name' => $meal->getName(),
];
}
return new JsonResponse(['status' => 'ok', 'options' => $options]);
}
*/
}
MealLinesCrudController
class MealLinesCrudController extends AbstractCrudController
{
public function __construct(
private readonly MealLinesRepository $mealLinesRepository,
private readonly RequestStack $requestStack,
) {
}
public static function getEntityFqcn(): string
{
return MealLines::class;
}
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
throw new Exception('here');
$queryBuilder = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
if (in_array($searchDto->getRequest()->get('crudAction'), array(Crud::PAGE_NEW, Crud::PAGE_EDIT))) {
$context = $searchDto->getRequest()->get('autocompleteContext');
if ($context['propertyName'] === 'food') {
return $queryBuilder
->join('entity.food', 'f')
->andWhere('entity.active = :active')
->setParameter('active', true)
->orderBy('f.description', 'ASC')
;
}
}
return $queryBuilder;
}
public function configureActions(Actions $actions): Actions
{
return $actions
;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setEntityLabelInSingular('Meal Line')
->setEntityLabelInPlural('Meal Lines')
->setPageTitle('index', '%entity_label_plural%')
->setPageTitle('detail', fn (MealLines $item) => sprintf('<h1>%s <small>(#%s)</small></h1>', '%entity_label_singular%', $item->getId()))
->setDefaultSort(['food' => 'ASC'])
->setTimezone($this->getUser()->getProfile()->getTimezone())
;
}
public function configureFields(string $pageName): iterable
{
$entityId = $this
->requestStack
->getCurrentRequest()
->attributes
->get('entityId')
;
$unitId = null;
if (null !== $entityId) {
$entity = $this
->mealLinesRepository
->find($entityId)
;
$unit = $entity->getOptionUnit();
if ($unit) {
$unitId = $unit->getId();
}
}
yield Field\IdField::new('id')
->onlyOnDetail()
;
yield Field\AssociationField::new('food', 'Food')
->autocomplete()
->setQueryBuilder(function (QueryBuilder $queryBuilder) {
$queryBuilder
->join('entity.food', 'f')
->andWhere('entity.active = :active')
->setParameter('active', true)
->orderBy('f.description', 'ASC')
;
})
->addCssClass('food_id')
->setFormTypeOptions([
'attr' => [
'onchange' => "updateFoodFieldVisibility(this);",
],
])
;
yield Field\AssociationField::new('optionUnit', 'Units')
->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($unitId) {
if ($unitId) {
$queryBuilder
->andWhere('entity.id = :unitId')
->setParameter('unitId', $unitId)
;
} else {
$queryBuilder->andWhere('0 = 1');
}
})
->addCssClass('food_unit hideOnFormLoad')
->setFormTypeOptions([
'attr' => [
'onchange' => "updateFoodFieldVisibility(this);",
],
])
;
yield Field\NumberField::new('optionPortion', 'Portion')
->setNumDecimals(2)
->setStoredAsString()
->addCssClass('food_portion hideOnFormLoad')
->setFormTypeOptions([
'html5' => true,
])
->setHtmlAttribute('min', '0.00')
->setHtmlAttribute('step', '0.25')
;
}
public function createNewFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context);
throw new Exception('here');
return $formBuilder;
}
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
throw new Exception('here');
return $formBuilder;
}
}
Sorry but i don't know.
In my case, autocomplete works in collection but setQueryBuilder is ignore, so i use createIndexQueryBuilder (of the associated entity) to filter.
Symfony : 6.4.24 EA : 4.24.9
Thanks for trying, I'm not sure what I'm going to do I have weeks into this project and I'm starting to regret going this route but it's too late to restart :\
Symfony: 7.3.4 EA: 4.25.1
I wish one of them would work :(
Last difference with me, I don't have OneToOne in this case but you can try this:
FoodExtension
#[ORM\OneToOne(orphanRemoval: true, cascade: ['persist', 'remove'], fetch: 'EAGER')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['foodext:read'])]
#[Assert\NotNull]
public ?CNFFoodName $food = null;
I'm not sure, maybe fetch: 'EAGER' will force food_id in the request.
sadly didn't work, but I was excited to try it.
MealLinesCrudController
yield Field\AssociationField::new('food', 'Name')
->setSortProperty('description')
->autocomplete()
->hideOnForm()
->setCrudController(FoodExtensionCrudController::class)
;
Maybe ->setCrudController force to go to FoodExtensionCrudController and use the createIndexQueryBuilder
Or maybe:
MealLines
#[ORM\ManyToOne(inversedBy: 'mealLines')]
#[ORM\JoinColumn(nullable: true)]
#[Assert\NotNull]
#[Groups(['meals:read', 'meals:write'])]
private ?Meals $meal = null;
#[ORM\ManyToOne(targetEntity: FoodExtension::class)]
#[ORM\JoinColumn(nullable: true)]
#[Assert\NotNull]
#[Groups(['meals:read', 'meals:write'])]
private ?FoodExtension $food = null;
#[ORM\ManyToOne(targetEntity: FoodExtension::class)]
Pretty sure I tried both of those, but I did again with no changes. It seems it doesn't matter what I do I can't change anything the association inside the collection no matter what I change. I wish I knew the inner workings more so I could trace it, but it seems like there are zero controls for it at all and I still don't know why the query changes inside the collection. Clling the CRUD directly works 100%, it listens to both createIndexQueryBuilder and setQueryBuilder for autocomplete. And the default autocomplete works without any SQL errors. It's need to find out why the query changes in the collection.
I decided to get the rest of the API working in the mean time, but in doing so I have found that api_platform isn't seeing this entity properly either, so I'm going to dig deeper, something else must be wrong here.
@dt-justin Tell me if you find the solution