Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operator filter #83

Closed
wants to merge 14 commits into from
Closed

Conversation

CheapHasz
Copy link

@CheapHasz CheapHasz commented Dec 6, 2018

Here is a first implementation to address #64.

Is there a place I should add type shortcuts ? Something like 'alterphp_gte_text'.

I used a TextType, but maybe a NumberType would be more useful.

I was also thinking of experimenting with DateTypes , which would be a obvious candidate for operator filters.

@alterphp
Copy link
Owner

alterphp commented Dec 7, 2018

Hi @CheapHasz ! This looks nice !

My suggestions to improve implementation and extensibility :

  • Use a manager in order to list filters form operators,
  • Use a compiler pass to list all operator form types (and inject the list to their manager)
  • Make ListFormFiltersConfigPass::configureFilter method to check and replace the form alias in the case $filterConfig['type'] is defined (use the manager to replace aliases),
  • Using a transformer is a good idea but I think they should transform the data returned from submitted form so that you don't have to deal with transformers in PostQueryBuilderSubscriber => Operator form type could return a specific object (OperatorModel ?) that holds operator (the constant) and the value. PostQueryBuilderSubscriber test something like case $value instanceof OperatorModel.
  • Do we need a concrete Transformer class for each operator ? What if the constant OPERATOR_PREFIX is directly held by the form type class ?

Hoping i'm clear enough. Please let me know if you need help and/or more details !

Anyway thanks for this first implementation, this seems to be a good base !

@coveralls
Copy link

coveralls commented Dec 8, 2018

Coverage Status

Coverage decreased (-4.0%) to 86.95% when pulling bff5129 on CheapHasz:operator-filter into 765dfb3 on alterphp:refactor/list-filters.

@CheapHasz
Copy link
Author

Hi @alterphp ,
I won't be able to work on this issue until wednesday, but I'll try to clarify the points before I can get on it.

  • Use a manager in order to list filters form operators,
  • Use a compiler pass to list all operator form types (and inject the list to their manager)

I didn't get what you mean by that

  • Make ListFormFiltersConfigPass::configureFilter method to check and replace the form alias in the case $filterConfig['type'] is defined (use the manager to replace aliases),

That part is only about replacing aliases ? wouldn't adding them to the AlterPHP\EasyAdminExtensionBundle\Configuration\ShortFormTypeConfigPass make more sense ?

  • Using a transformer is a good idea but I think they should transform the data returned from submitted form so that you don't have to deal with transformers in PostQueryBuilderSubscriber => Operator form type could return a specific object (OperatorModel ?) that holds operator (the constant) and the value. PostQueryBuilderSubscriber test something like case $value instanceof OperatorModel.

It might be a good idea indeed, I'll think about it.

  • Do we need a concrete Transformer class for each operator ? What if the constant OPERATOR_PREFIX is directly held by the form type class ?

This idea stems from the fact that multiple FormType might be used for GreaterThan (off the top of my head, Number, Date and DateTime). But we still could factor the logic in one place, and the transformer seemed a good place. It also allows an external to create a new GreaterThan*Type and keep all the logic by simply adding the transformer.
Setting the constant in each of them would have messed with the tests in the PostQueryBuilderSubscriber.

Now if the logic is indeed refactored using an OperatorModel, that logic might indeed be obsolete .

(if some points are easier to explain in French, fell free to pm me directly using it).

@alterphp
Copy link
Owner

Hi @CheapHasz !

No worries for not having time to work on it every day, we all have some work and fortunately time doing anything but coding !

While thinking about the feature, I'm not sure handling the operator into the form type is the best implementation. Maybe the good move is to apply operator as a modifier of the form type => Transformer seems matching our needs.

What about a single ListFilterType that adds a form type (text, number, date) based on a input_type form option and its own options with input_type_option with TRUE inherit_data option, handles the operator as an optional operator form option (default is equals unless input is a multiple choice -> in operator), and returns a ListFilter OperatorModel that holds value and operator.

Operator list :

  • equals
  • in
  • gte
  • gt
  • lte
  • lt

Every list form filters (even equals) would be handled through the same process (with ListFilter operator/value object).

Forget about the manager and compiler pass (the idea was to allow custom filter), this is over-engineered for now.

What do you think ? I'm working on upgrade for EasyAdmin 2.0 at the time, I'll be able to help on operators after that. And I think those operators will be available for 2.x brnach only if there is an important BC break...

@CheapHasz
Copy link
Author

CheapHasz commented Dec 12, 2018

Hi @alterphp .

New implementation, not quite sure if it was what you had in mind, but it probably is better than what I did previously.

To prevent BC breaks, I didn't refactor everything using the ListFilter, and instead use it more as an OperatorModel.
I added a 'property' attribute, to allow multiple ListFilterType on the same field (ie a min and a max)
and 'input_type' / 'input_type_options' type_option, to allow configuration of the unerlying input.

Tell me what you think

src/Form/Type/ListFilterType.php Outdated Show resolved Hide resolved
src/Form/Type/ListFilterType.php Outdated Show resolved Hide resolved
src/Configuration/ShortFormTypeConfigPass.php Outdated Show resolved Hide resolved
@alterphp
Copy link
Owner

Thanks for this update, this seems simpler.

I did not expect the property/field (when you have many filters on the same property), I'm not willing to delegate the responsibility to ListFilter objects. I'm thinking about dealing with it straight in list.form_filters config...

@alterphp alterphp changed the base branch from master to refactor/list-filters December 12, 2018 14:34
@alterphp
Copy link
Owner

I have changed the target branch so that I could merge and continue working on it on my side.

Copy link
Owner

@alterphp alterphp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @CheapHasz, and thank you for restarting this PR ! I've mostly been working on new EasyAdmin last weeks but I'm willing to advance on your PR.

Do you need the support of EasyAdmin 1.x on the ListFilter feature ?

@@ -32,7 +32,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
->add('operator', HiddenType::class, [
'data' => self::$operators[$options['operator']]
]);
if ($options['property'] !== null) {
if (isset($options['property'])) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If property is not intended to be modified on client side, it's not required to pass it to the view form. WDYT about setting the value on ListFilter object data on POST_SET_DATA FormEvent ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the logic, and think it could be applied to the operator property

'required' => false
])
->add('operator', HiddenType::class, [
'data' => self::$operators[$options['operator']]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to use static::$operators for a inheritance.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, you use static at line 56.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed


class ListFilterType extends AbstractType
{
public static $operators = [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$operators do not need to hold ORM version here, only the keys. I think this is QueryBuilder job to translate equals into = and so on.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use constant to define the string for operators like :

    const OPERATOR_EQUALS = 'equals';
    const OPERATOR_GT = 'gt';
    const OPERATOR_GTE = 'gte';
    const OPERATOR_LT = 'lt';
    const OPERATOR_LTE = 'lte';

    private static $operators = [
        static::OPERATOR_EQUALS,
        static::OPERATOR_GT,
        static::OPERATOR_GTE,
        static::OPERATOR_LT,
        static::OPERATOR_LTE,
    ];

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all for using constants.

The problem I see with delegating the translation to symbol in the QueryBuilder is that it couples the PostQueryBuilderSubscriber and ListFilterType, with no simple way of implementing any new operator, modifying existing ones.

Not that we need that, but my implementation allowed to handle all that logic in the FilterType

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a reason behind this willing : I developed a port of EasyAdmin for MongoDB documents, and EasyAdminExtension has a branch that is compatible with both entities and documents.

Operators like = or >= have no sense for Mongo query builder... That's why I prefer delegating DB query building to the builder.

I know this is more work to add filters, but decoupling the form from the DB operators makes sense.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it makes sense

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'operator' => 'equals',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better not use a raw value here. Constant as suggested earlier ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

break;
}

$filterDqlPart = $field.' ' .$value->getOperator() .' :'.$parameter;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, QueryBuilder is responsible of selecting the ORM operator matching data returned by ListFilter::getOperator method

$property = $field;
if ($value instanceof ListFilter && $value->getProperty()) {
// if a property is specified in the ListFilter, it is on that property that we must filter
$property = $queryBuilder->getRootAlias().'.' .$value->getProperty();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefixing with root alias is not only for ListFilter. It has to be done just after this if statement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is what happens at lign 128, no ?

Maybe my mistake is not doing that test before the $field assignation though

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

On line 132, prefixing by root alias must only be done when there is no dot in the property returned by $value->getProperty(). This allows to filter on a relation property.

// Add root entity alias if none provided
$field = false === \strpos($field, '.') ? $queryBuilder->getRootAlias().'.'.$field : $field;
$property = $field;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer $ormField instead of $property.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine by me

@alterphp alterphp closed this Jan 24, 2019
@alterphp alterphp changed the base branch from refactor/list-filters to master January 24, 2019 14:53
@alterphp
Copy link
Owner

Sorry, my mistake

@alterphp alterphp reopened this Jan 24, 2019
@alterphp
Copy link
Owner

@CheapHasz I'm taking care of requested changes. I will open another PR to compare.

@alterphp alterphp mentioned this pull request Jan 24, 2019
@alterphp
Copy link
Owner

Closing in favor of #92

@alterphp alterphp closed this Jan 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants