Sonata Admin Bundle - Custom field with URL - php

Is there way to show url with filter to another entity list instead of showing all related entities?
My entity has OneToMany reference to it's events:
/**
*
* #ORM\OneToMany(targetEntity="Event", mappedBy="organizer", cascade={"ALL"})
*/
private $events;
$formMapper->add('events') shows me select2 input with all events.
I just want to show a link to events list with filter to current owner.
I'm using Symfony 2.5.

Yes, this is possible.
You have to get the current owner and create a custom query builder to get only the events related to the owner identifier.
protected function configureFormFields(FormMapper $formMapper)
{
// get current owner
$ownerId = $this->subject->getId();
// using query_builder to create a custom query based on current owner
$formMapper->add('events', null, array(
'query_builder' => function(EntityRepository $er) use ($ownerId) {
$events = $er->createQueryBuilder('e');
if ($ownerId != null) {
$events = $er->where('e.owner = :ownerId')
->setParameter('ownerId', $ownerId);
}
return $events;
}
));
}
Also don't forget to add the use for EntityRepository :
use Doctrine\ORM\EntityRepository;

Related

Sorting of paginator object through a model relation column

I have three tables: products, product_inventories and product_inventory_details. The ORM of each model is shown below,
Product Model
class Product extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
...,
'title',
'selected_inventory_id',
...
];
/**
* Get the inventories those belongs to this model.
*/
public function inventory(){
return $this->hasMany('App\Models\ProductInventory');
}
/**
* Get the selected product_inventory_detail that owns this model.
*/
public function selected(){
return $this->hasOne('App\Models\ProductInventoryDetail', 'id', 'selected_inventory_id');
}
...
}
ProductInventory Model
class ProductInventory extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_id',
...
];
/**
* Get the inventory_details those belongs to this model.
*/
public function items(){
return $this->hasMany('App\Models\ProductInventoryDetail');
}
...
}
ProductInventoryDetail Model
class ProductInventoryDetail extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_inventory_id',
'price',
...
];
}
I'm sorting and limiting the results through user input of Sort by dropdown and Show per page dropdown. When sorting by Alphabetical: High to Low option I'm running the query builder method to order the results:
$products = $products->orderBy($sort['column'], $sort['order'])->paginate($limit);
Now with sorting by Price, I can't run the query builder method orderBy() since I'm not using joins and getting the data through relationship properties. Instead I'm using the Laravel's collection method to sort it out:
$products = $products->paginate($limit);
$products = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
The above block is working fine if I don't use pagination methods. But, I need to use the pagination as well since the user can also limit the results per page. I'm also using a Paginator object's method to append some parameters to each page URL:
$products->appends($paramsArr);
Since running the sortBy() method returns a collection instead of Paginator object, it's giving me undefined method exception.
My Question
How can I sort the result set by price in my current scenario without having to implement the joins? Is there a way to achieve that??
I would use QueryBuilder package of Spatie. It will make your life easier for creating sortable and filterable grid table. You use that package this way:
$query = Product::with(['inventory', 'selected']);
$products = \Spatie\QueryBuilder\QueryBuilder::for($query)
->allowedFilters([
'name' => 'name', // name column in the products DB table.
'selected.price' => 'product_inventory_details.column_price', // price column in the product_inventory_details DB table.
])
->defaultSort('name')
->allowedSorts([
'name',
\Spatie\QueryBuilder\AllowedSort::custom('selected.price', new SortSelectedPrice())
])
->paginate(20)
->appends(request()->query());
External custom sort class looks like this:
class SortSelectedPrice implements \Spatie\QueryBuilder\Sorts\Sort
{
public function __invoke(Builder $query, bool $descending, string $property)
{
$direction = $descending ? 'DESC' : 'ASC';
$query->leftJoin('product_inventory_details', 'products.id', '=', 'product_inventory_details.product_id');
$query->orderBy('product_inventory_details.column_price', direction);
}
}
Make sure your URL containing the query string like this for sorting name from A to Z and sorting price from 1xxxxxxxx to 0:
domain.com/products?sort=name,-selected.price
I installed the Spatie package using composer. Don't forget to do that.
I found a way to handle it without having to implement the joins. I added a new variable and stored the Paginator object's items() method result set into it.
$products = $products->paginate($limit);
$productItems = $products->items(); // returns the items array from paginator
And sort that particular variable instead of sorting the whole paginator object. That way my links and URLs are untouched and unmodified in the $products variable and the data of the products are in a separate variable.
if($sort['column'] == 'price'){
if($sort['order'] == 'DESC'){
$productItems = $products->sortByDesc(function($prod, $key){
return $prod->selected->price;
});
} else{
$productItems = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
}
}
I also had to change my rendering variable from $products to $productItems and accessed the pagination links from the old $products variable.
#forelse ($productItems as $product)
#include('site.components.product-grid')
#empty
<div class="col text-center">...</div>
#endforelse
...
{{ $products->links() }}
I'm posting it here for the community to benefit/discuss/criticize if there is a better way.

EasyAdmin 3: limit data to the logged-in user still shows other data in form dropdowns

I'm using Symfony 5.
I want every logged in user to have it's own space in EasyAdmin 3, so no user will see records of other users. I store the user with every table in the database.
For simple list views, I managed to get this to work using a extension of the AbstractCrudController:
<?php
namespace App\Controller\Admin;
use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
abstract class CustomCrudController extends AbstractCrudController
{
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
$qb = $this->get(EntityRepository::class)->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
$qb->andWhere('entity.user = :user');
$qb->setParameter('user', $this->getUser());
return $qb;
}
}
I also store/check the user through a EventSubscriber.
Problem is, some forms have a relation to another Entity(like AssociationField::new('food')) and when filling the dropdowns it ignores my new function. So you will see records belonging to another user.
How do I override these dropdowns to also only show data belonging to the current user?
I found the solution: pass a custom query to the underlying EntityType field of Symfony.
AssociationField::new('food')
->setRequired(true)
->setFormTypeOptions(['query_builder' => function (EntityRepository $em) {
return $em->createQueryBuilder('f')
->where('f.user = :user')
->orderBy('f.title', 'ASC')
->setParameter('user', $this->getUser())
;
}]),

Symfony how to validate EntityType field

I have this EntityType field on my UserType's form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('country', EntityType::class, array(
'class' => Country::class,
'choice_label' => 'nicename'
));
}
How can i use validation constraints to validate this type of field in the way that user can only select a value in the range of all rows of country's table? I think that I should use Choice constraint with callback, and call the getAllCountries function inside my CountryRepository class. So what's the best way to manage this scenario?
Somethins like this:
// UserEntity.php
class User {
/**
* #Assert\Choice(callback="App\Repository\CountryRepository", "getAllCountries")
* #ORM\ManyToOne(targetEntity="App\Entity\Country", inversedBy="users")
*/
protected $country;
}
But CountryRepository is not a static function!!
Entity field doesn't allow to select invalid value (if you have invalid value it would't find the entity -> wouldn't be able to send form). This is as well behaviour of choice type.
But for callback, there is special callback constraint - https://symfony.com/doc/current/reference/constraints/Callback.html which can be used to call constraint function.

HABTM form validation with CakePHP 2.x

I have a HABTM relation like : Post <-> Tag (a Post can have multiple Tag, and same the other way).
This work with the multiple checkbox selection generated by Cakephp. But I want to have at least one Tag for every Post and throw an error if someone try to insert an orphan.
I'm looking for the cleanest/most CakePHP alike way to do this.
This is more or less an update of this HABTM form validation in CakePHP question, as I get the same problem on my cakephp 2.7 (last cakephp 2.x for now with php 5.3 support at the date of 2016) and can't find a good way to do it.
Here are what I think is the best for now. It use the cakephp 3.x behaviour for HABTM validation.
I choose to only work in model, with the most generic code.
In your AppModel.php, set this beforeValidate() and afterValidate()
class AppModel extends Model {
/** #var array set the behaviour to `Containable` */
public $actsAs = array('Containable');
/**
* copy the HABTM post value in the data validation scope
* from data[distantModel][distantModel] to data[model][distantModel]
* #return bool true
*/
public function beforeValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$model][$model]))
$this->data[$this->name][$model] = $this->data[$model][$model];
}
return true;
}
/**
* delete the HABTM value of the data validation scope (undo beforeValidate())
* and add the error returned by main model in the distant HABTM model scope
* #return bool true
*/
public function afterValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
unset($this->data[$this->name][$model]);
if(isset($this->validationErrors[$model]))
$this->$model->validationErrors[$model] = $this->validationErrors[$model];
}
return true;
}
}
After this, you can use your validation in you model like this :
class Post extends AppModel {
public $validate = array(
// [...]
'Tag' => array(
// here we ask for min 1 tag
'rule' => array('multiple', array('min' => 1)),
'required' => true,
'message' => 'Please select at least one Tag for this Post.'
)
);
/** #var array many Post belong to many Tag */
public $hasAndBelongsToMany = array(
'Tag' => array(
// [...]
)
);
}
This answer use :
Painless HABTM Validation in CakePHP by #jesal
HABTM form validation in CakePHP
CakePHP 2.x Saving and validating a HABTM relation example

Symfony 2 Form with select list

How can i create a select list with values from a database table in Symfony 2?
I have 2 entities: Student and Classroom with a ManyToOne relationship and i need to create a form with the folowing fields: name, surname, age, classroom(select list from available classes).
In my Student Form i have
$builder
->add('name')
->add('surname')
->add('age')
->add('classroom', new ClassroomType())
;
In my Classroom Form i have this:
$classrooms =$this->getDoctrine()->getRepository('UdoCatalogBundle:Classroom')->findAll();
$builder
->add('clasa','choice',array('choices' => array($classrooms->getId() => $classrooms->getName())));
I get this following error:
Fatal error: Call to undefined method Udo\CatalogBundle\Form\ClassroomType::getDoctrine() in /var/www/html/pos/src/Udo/CatalogBundle/Form/ClassroomType.php on line 13
Kind Regards,
Cearnau Dan
Not sure if you found an answer yet but I just had to do some digging around to figure this out for my own project.
The form class isn't set up to use Doctrine like the controller is so you can't reference the Entity the same way. What you want to do is use the entity Field Type which is a special choice Field Type allowing you to load options from a Doctrine entity as you are trying to do.
Ok so long story short. Instead of doing what you are doing to create the choice field, do this:
->add('category', 'entity', array(
'class' => 'VendorWhateverBundle:Category',
'query_builder' => function($repository) { return $repository->createQueryBuilder('p')->orderBy('p.id', 'ASC'); },
'property' => 'name',
))
I'm not sure if you could place the query_builder function into a repository or what, I'm kind of swinging wildly as I go. Up to this point the documentation I linked to above is pretty clear on what to do. I guess the next step is to read up on Doctrine's QueryBuilder.
While you're in there I think you want to drop the bit where you are embedding the Classroom form,
->add('classroom', new ClassroomType())
You probably don't want people creating their own classrooms. Unless you do, then yeah.
If the entities are mapped, this is a clean solution for Symfony 2.8+ or 3+
<?php
namespace My\AppBundle\Form\Type;
use My\AppBundle\Entity\Student;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StudentType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('surname')
->add('age')
/*
* It will be resolved to EntityType, which acts depending on your doctrine configuration
*/
->add('classroom');
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Student::class]);
}
}

Categories