symfony admin filter with join - symfony1

I have a table, heading, that has an import_profile_id. import_profile has a bank_id.
On my headings list page in my admin, I'd like to add the ability to filter by bank_id. However, since heading doesn't have a bank_id - it needs to go through import_profile to get that - I can't just add a bank_id field and expect it to work.
Can anyone explain how to do this? The closest thing I've found is this post but I don't think it really addresses my issue.

This can be done by using virtual columns like the post you found. The virtual column is a way to add a new criteria to filter using the autogenerated filter provided by symfony. It works like this:
1 - Go to the generator.yml of the admin module and add the name of the virtual column that will create and add
<!-- apps/backend/modules/module_name/config/generator.yml -->
filter:
[virtual_column_name, and, other, filter, columns]
2 - In your lib/filter/{TableName}FormFilter.class.php (I think in your case must be HeadingFormFilter) you have to define that virtual column in the configure() method
public function configure()
{
//Type of widget (could be sfWidgetFormChoice with bank names)
$this->widgetSchema['virtual_column_name'] = new sfWidgetFormInputText(array(
'label' => 'Virtual Column Label'
));
//Type of validator for filter
$this->validatorSchema['virtual_column_name'] = new sfValidatorPass(array ('required' => false));
}
3 - Override the getFields() of that class to define it in the filter and set the filter function
public function getFields()
{
$fields = parent::getFields();
//the right 'virtual_column_name' is the method to filter
$fields['virtual_column_name'] = 'virtual_column_name';
return $fields;
}
4 - Finally you have to define the filter method. This method must be named after the add...ColumnQuery pattern, in our case must be addVirtualColumnNameColumnQuery(not a happy name choice :P), so
public function addVirtualColumnNameColumnQuery($query, $field, $value)
{
//add your filter query!
//for example in your case
$rootAlias = $query->getRootAlias();
$query->innerJoin($rootAlias . '.ImportProfile ip')
->andWhere('ip.BankId = ?', $value);
//remember to return the $query!
return $query;
}
Done! You can know filter by bank_id.

Related

Laravel Nova show computed field in BelongsToMany sub table

Using Laravel Nova, I want to show a computed field in a BelongsToMany subview. In the intermediate model I have setup a virtual attribute field which computes a value using connected tables. However this field is not getting shown probably because Nova checks the withPivot fields (to which it cannot be added as it's not a real field). Is there an alternative method to get this to work?
The database model behind this is:
[GameVariation] has BelongsToMany using [GameVariationMatch] with [GameMatch]
[GameMatch] HasOne [Match]
In the GameVariation resource I've setup a BelongsToMany field which should display the computed field:
BelongsToMany::make( 'Game Matches', 'game_matches', GameMatch::class )
->fields( function () {
return [
Text::make( 'Match Data' )->displayUsing(function() {
return $this->match_data;
})
];
} ),
In the GameVariation model the tables are connected with BelongsToMany:
final public function game_matches(): BelongsToMany {
return $this->belongsToMany(
GameMatch::class
)
->using( GameVariationMatch::class );
}
In the pivot tabel model GameVariationMatch the computed field is setup like this:
final public function getMatchDataAttribute(): string {
return $this
->game_match
->match
->match_data;
}
As it turns out, on the GameVariation resource the index view of the BelongsToMany to GameMatch is served from the GameMatch resource. When only setting up ->fields() on the GameVariation BelongsToMany field, it won't show up. The recommended way is to set up ->fields() using a field class like this:
<?php
namespace App\Nova\Fields;
use App\Models\GameVariationMatch;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
class GameVariationMatchFields {
/**
* Get the pivot fields for the relationship.
*
* #param Request $request
*
* #return array
*/
final public function __invoke( Request $request ): array {
return [
Text::make( 'Match Data', function ( GameVariationMatch $GameVariationMatch ) {
return $GameVariationMatch->match_data;
} ),
];
}
}
In the above, the computed Text field receives the intermediate pivot model and can therefore access all computed attributes.
The field class is used in the GameVariation resource as:
BelongsToMany::make( 'Game Matches', 'game_matches', GameMatch::class )
->fields( new RoundMatchVariationFields ),
And in the GameMatch resource as:
BelongsToMany::make( 'Game Variations', 'game_variations', GameVariation::class )
->fields( new GameVariationMatchFields ),
As described in official Nova docs on pivot fields https://nova.laravel.com/docs/2.0/resources/relationships.html#belongstomany

Zend\Db Model with Child Models

ZF2 project - no Doctrine, using native Zend\Db: Have the following structure:
Controller
ProductController
Model
Product
ProductTable
ProductType
ProductTypeTable
Product is the model, has variables corresponding to the “products" table fields.
ProductTable is table class which is connected to the database via tableGateway. ProductTable has getItem() method to retrieve requested product by “id”.
ProductType is the model, has variables like id, name, description corresponding to the “productTypes" table fields.
ProductTypeTable is table class just like ProductTable.
Each product belongs to a certain ProductType
products.productTypeId = productTypes.id
is the relation.
In ProductTable->getItem() method, I can simply get productTypeId.
I can use joins to get productTypes.name, productTypes.description, or any field from "productTypes" table.
But I don’t want to do this - instead dealing with new variables in Product entity like productTypeName, productTypeDesc,
I’d like to have Product->getProductType() and set it to be a ProductType object, so I can get Product->getProductType() ->getName() to get product type name.
Simply I’d like to assign a child model as a variable of the parent model.
I can do this in the controller like below:
$product = $this->getProductTable()->getItem(7); // id = 7
$product->setProductType($this->getProductTypeTable()
->getItem($product->getProductTypeId());
But I’d like to make it happen in product table class getItem() method. So I don’t have to think about it in every controller, and it is kind of encapsulated.
What is the right way to do this?
Thank you.
The issue that you have is the Table Gateway pattern is only really any good at abstracting database access to a a single database table. It does not in anyway allow for the hydration of entities or management of relationships. Object Relationship Mappers (ORM's), such as Doctrine, solve this problem.
If Doctrine, for whatever reason, is inappropriate for your use case an alternative could be implementing the Data Mapper Pattern
The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other
The data mapper will use the table gateway to fetch the required data for each table and construct the Product instance, including it's associated ProductType. You would then expose the mapper to the controller (rather than the table gateway).
A simple example of a ProductMapper.
class ProductMapper
{
// #var \Zend\Db\TableGateway\TableGateway
protected $productTable;
protected $productTypeMapper;
// an 'identity map' of loaded products
protected $loaded = [];
public function __construct(ProductTable $productTable, ProductTypeMapper $productTypeMapper)
{
$this->productTable = $productTable;
$this->productTypeMapper = $productTypeMapper;
}
protected function hydrate(Product $product, array $data)
{
$product->setId($data['id']);
$product->setName($data['name']);
$product->setFoo($data['foo']);
if (isset($data['type_id'])) {
// Load a fully constructed product type from the database
$type = $this->productTypeMapper->findById($data['type_id']);
$product->setType($type);
}
return $product;
}
public function findById($id)
{
if (isset($this->loaded[$id])) {
return $this->loaded[$id];
}
// Get the data
$row = $this->productTable->select(['id' => $id]);
if (empty($row)) {
throw new SomeCustomException("No product could be found with id $id");
}
// Create and hydrate the product
$product = $this->hydrate(new Product, $row->current())
$this->loaded[$id] = $product;
return $product;
}
public function save(array $data);
public function update($data);
public function delete($id);
}
You can achieve this, you just have to follow the following 3 steps:
Make your Product->exchangeArray() function smarter
Get all required ProductType fields, using a prefix helps for example: type_
Add #var ProductType so you will have proper autocompete (works for me in Eclipse)
<?php
namespace Product\Model\Product;
class Product {
public $id;
...
/**
* #var ProductType
*/
public $productType;
...
public function exchangeArray( $data ) {
$this->id = (isset($data['id'])) ? $data['id'] : null;
...
$productType = new ProductType();
$typeData = array(
'id' => $data['type_id'],
'value' => $data['type_value']
);
$productType->exchangeArray( $typeData );
$this->productType = $productType;
}
}

Selected attributes of genemu_jqueryselect2_entity not stored to database

I'm using genemu_jqueryselect2_entity for a multiple selection field within a form (located in an Sonata admin class) for a so called Uni (university) entity:
->add('courses', 'genemu_jqueryselect2_entity',array('multiple' => true, 'class' => 'PROJECT\UniBundle\Entity\Course'))
But the selected entries are not filled into my entity. With firebug I was able to detect, that the ids of the courses are passed correctly via POST.
Maybe the field is not correctly mapped to the Uni entity, but I have no idea why.
This is the adding method of my Uni entity, which doesn't even get called:
public function addCourse(\PROJECT\UniBundle\Entity\Course $courses)
{
$this->courses[] = $courses;
return $this;
}
How can I get the field to be mapped with the courses attribute of Uni? How could I debug this?
Any help will be appriciated!
Try writing that method like this:
public function addCourse(\PROJECT\UniBundle\Entity\Course $course)
{
$this->courses[] = $course;
$course->setUniversity($this); // Or similar.
return $this;
}
Otherwise foreign key is not set on a course row in the DB.
Try to create method setCourses
public function setCourses(\Doctrine\Common\Collections\Collection $courses)
{
$this->courses = $courses;
...
I don't know why, but the method addCourse isn't called.
Anyway, Tautrimas Pajarskas's answer was usefull to me so I gave an upvote.
The foreign key relationship was the necessary and missing part of my code.
I implemented it in the university sonata admin like this:
private function addUniToCourses ($university) {
foreach($university->getCourses() as $course) {
if(!$course->getUniversities()->contains($university)) {
$course->addUniversity($university);
}
}
}
public function prePersist($university) {
$this->addUniToCourses($university);
}
public function preUpdate($university) {
$this->addUniToCourses($university);
}
This was the solution to my problem.
I had the same problem a while ago: Symfony2, $form->bind() not calling adder methods of entity
Solution:
For the adder (addCourse()) to be called, you have to disable the by_reference option of the field:
->add('courses', 'genemu_jqueryselect2_entity',
array(
'by_reference' => false, // This line should do the trick
'multiple' => true,
'class' => 'PROJECT\UniBundle\Entity\Course'))

Nested bean : a collection inside an object

I got a simple POJO class that i wish to display / update in a form
Using the BeanItem class and the binding of component data, i was able to quickly display the first attributes of may data class. However i've hit a wall for tow related attributes :
my class posses a set of available status, as a list of object 'AppStatus'. it also possess a current status, that is one of the status in the 'available' list.
I would like to display the list in the form as a combobox, with the current status selected.
I'we managed to associate the 'available' attribute with a combobox, but i can't seem to be able to fill this combobox when setting the data source (method setItemDataSource). How do i get the avalaible status list and the current status from my Item ?
I could always use a workaround and add a parameter to the method to get the source objet in addition to the BeanItem, but i would prefer to avoid this if the Item properties can give me my attribute.
Regards
Edit : shortened exemple, with code from Eric R.
class Status {
String id;
Sting label
+ setter /getter
}
class App {
String AppId;
String AppLabel
ArrayList<Status> availablestatus;
Status currentStatus
+setter/getter
}
in the form extension, in the createField of the fieldfactory i added the following lines
if ("status".equals(propertyId)) {
// create the combobox
ComboBox status = new ComboBox(
texts.getString("application.label.status"));
status.setItemCaptionMode(AbstractSelect.ITEM_CAPTION_MODE_PROPERTY);
status.setItemCaptionPropertyId("label");
status.setImmediate(true);
status.setNullSelectionAllowed(false);
IndexedContainer container = new IndexedContainer(
(Collection<ApplicationStatus>) item.getItemProperty(
"availableStatus").getValue());
status.setContainerDataSource(container);
status.setPropertyDataSource(item.getItemProperty("currentStatus"));
return status;
} else...
this didn't work, i do get a combobox, with the correct number of lines, but all empties.
i tried to use a beanContainer instead of a IndexedContainer
BeanContainer<String, ApplicationStatus> container =
new BeanContainer<String, ApplicationStatus>(ApplicationStatus.class);
container.addAll((Collection<ApplicationStatus>) item
.getItemProperty("availableStatus").
container.setBeanIdProperty("id");
the result is slightly better, since i do have the available values in the combobox.
only the currentValue is not selected...
I also tried to use a nestedbean property to get the id of the currentstatus, but the result is still not valid... i get a combobox, with the correct value selected, but i can not see others values anymore, since the combobox is readonly ?(even with setReadOnly(false);)
I suggest my way to resolve this. I don't think this is the nicest way, but it's works.
The beanItem class contains all you need.
I did the following in a simple project and it's work verry well :
ComboBox status = new ComboBox("ComboBox");
status.setImmediate(true);
status.setNullSelectionAllowed(false);
for(Status st : (Collection<Status>)item.getItemProperty("availableStatus").getValue()) {
status.addItem(st);
status.setItemCaption(st, st.getLabel());
}
status.setPropertyDataSource(item.getItemProperty("currentStatus"));
Hope it's works.
Regards Éric
From the vaadin demo site you can get this sample that show how to fill a combobox with countries. You could do the same i would guess (not sure I understand your problem 100%):
myForm.setFormFieldFactory(new MyFormFieldFactory ());
private class MyFormFieldFactory extends DefaultFieldFactory {
final ComboBox countries = new ComboBox("Country");
public MyFormFieldFactory () {
countries.setWidth(COMMON_FIELD_WIDTH);
countries.setContainerDataSource(ExampleUtil.getISO3166Container());
countries
.setItemCaptionPropertyId(ExampleUtil.iso3166_PROPERTY_NAME);
countries.setItemIconPropertyId(ExampleUtil.iso3166_PROPERTY_FLAG);
countries.setFilteringMode(ComboBox.FILTERINGMODE_STARTSWITH);
}
#Override
public Field createField(Item item, Object propertyId,
Component uiContext) {
Field f = (Field)item;
if ("countryCode".equals(propertyId)) {
// filtering ComboBox w/ country names
return countries;
}
return f;
}
}

Symonfy 1.4 dynamic validation possible?

I'm trying to create a form that change the validation of a field based on the select option from the html form field.
Ex: if user select a option 1 from drop down field "options", I want the field "metric" to validate as sfValidatorInteger. If user select option 2 from field "options", I want the field "metric" to validate as sfValidatorEmail, etc.
So inside the public function configure() { I have the switch statement to capture the value of "options", and create the validator based on that value returned from the "options".
1.) How do I capture the value of "options" ? I've tried:
$this->getObject()->options
$this->getTaintedValues()
The only thing that's currently working for me is but it's not really MVC:
$params = sfcontext::getInstance()->getRequest()->getParameter('options');
2.) Once I've captured that information, how can I assign the value of "metric" to a different field? ("metric" is not a real column in db). So I need to assign the value of "metric" to different field such as "email", "age" ... Currently I'm handling this at the post validator like this, just wondering if I can assign value within the configure():
$this->validatorSchema->setPostValidator(new sfValidatorCallback(array('callback' => array($this, 'checkMetric'))));
public function checkMetric($validator, $values) {
}
Thanks!
You want to use a post validator. Try doing something like this in your form:
public function configure()
{
$choices = array('email', 'integer');
$this->setWidget('option', new sfWidgetFormChoice(array('choices' => $choices))); //option determines how field "dynamic_validation" is validated
$this->setValidator('option', new sfValidatorChoice(array('choices' => array_keys($choices)));
$this->setValidator('dynamic_validation', new sfValidatorPass()); //we're doing validation in the post validator
$this->mergePostValidator(new sfValidatorCallback(array(
'callback' => array($this, 'postValidatorCallback')
)));
}
public function postValidatorCallback($validator, $values, $arguments)
{
if ($values['option'] == 'email')
{
$validator = new sfValidatorEmail();
}
else //we know it's one of email or integer at this point because it was already validated
{
$validator = new sfValidatorInteger();
}
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']); //clean will throw exception if not valid
return $values;
}
1) In a post validator, values can be accessed by using the $values parameter. Just use $values['options'] and it should be fine... or did you want to access this values from another part of you code? $this->getObject()->widgetSchema['options'] should work too I think, once your form is bound to an object.
2) The configure() method is called at the end of the form constructor, so values are not bound nor accessible yet, unless you are initializing your form with an object from the db (which does not require any validation). But if you want to initialize your form from $_POST, a post validator is definitely the way to go IMHO.
I got the validation error to appear alongside the field by throwing a sfValidatorErrorSchema instead of a sfValidatorError.
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']);
…becomes…
try
{
$values['dynamic_validation'] = $validator->clean($values['dynamic_validation']);
}
catch(sfValidatorError $e)
{
$this->getErrorSchema()->addError($e, 'dynamic_validation');
throw $this->getErrorSchema();
}
Not sure if this is the best way to get this result, but it seems to be working for me at the moment.

Resources