hyva-themes / magento2-hyva-admin

This module aims to make creating grids and forms in the Magento 2 adminhtml area joyful and fast.
https://hyva-themes.github.io/magento2-hyva-admin/
BSD 3-Clause "New" or "Revised" License
168 stars 39 forks source link

Need ability to declare bind params for grids and forms #24

Closed Vinai closed 3 years ago

Vinai commented 3 years ago

This issue is intended as a design discussion.

There is a problem I’m trying to find a good solution to. I would appreciate any ideas or suggestions.

The problem exists for grids and forms, but for forms it’s more obvious. Often a grid is embedded in a page with additional data, e.g. on a customer detail page all their pending reviews could be shown, or on a product page I want to show all orders.

In a nutshell, there needs to be some default limitation of the displayed data, that is, I want to show the orders related to the current product.

For forms problem is more obvious because it occurs every time an existing entity should be loaded to populate the form (that is, editing the entity).

Because hyva grids (and forms in future, too) are defined in XML only, there is no obvious way I can see where some kind of assignment can be made. For grids there are currently two workarounds:

What is needed is some simple way to declare a mapping from global state to query conditions in XML. How can this be accomplished?

Vinai commented 3 years ago

My current idea to only support mapping of query arguments to fields, either with an eq or an in type condition. This would probably be good enough for most cases. More complex scenarios would then require the use of an event observer.

Here is a first idea for the XML

<source>
    <repositoryListMethod>\Magento\Catalog\Api\ProductRepositoryInterface::getList</repositoryListMethod>
    <defaultSearchCriteriaBindings>
        <requestParam name="id" field="entity_id"/>
        <requestParam name="cust" field="customer_ids"/>
    </defaultSearchCriteriaBindings>
</source>

This would also allow adding additional bind params that would be bound to additional method parameters.

<paramBindings>
    <requestParam name="store" argument="storeId"/>
</paramBindings>

This would make the value of the request param store the argument value for the repository method argument $storeId.

These are both thoughts on how to pass request arguments to the source data providers. Ideally it would be possible to pass other state, too, but I don’t have ideas that are simple enough (according to the design principles).

Vinai commented 3 years ago

Comment via Slack from @schmengler:

What about something like this for arbitrary state:

<defaultSearchCriteriaBindings>
  <!-- 1. useful predefined accessors, like request params -->
  <field name="entity_id">
    <requestParam name="id">
  </field>
  <field name="store_id">
    <currentStoreId />
  </field>
  <!-- 2. generic accessor -->
  <field name="customer_id">
    <method class="Magento\Customer\Model\Session" method="getCustomerId">
      <!-- optionally pass arguments here with di.xml syntax or the same elements that are allowed in `<field>` -->
      <arguments></arguments>
    </method>
  </field>
</defaultSearchCriteriaBindings>
Vinai commented 3 years ago

I like that the most common case is quite simple and elegant. I also like a lot that both search criteria mappings and also method arguments could be covered, that is field and maybe something like an argument node, that then could be passed in addition to the search criteria. The wrapping node then should be something like queryBindings or such.

The method <field><arguments> node looks dangerously recursive (those arguments might need to be dynamic, too). I don’t want to turn this into another almost touring-complete dsl :slightly_smiling_face: I think you are on a good track though.

schmengler commented 3 years ago

If the recursive nature makes it too complex, 1 level is probably enough. In cases where it isn't you could still create a custom class. You wanted to prevent this for typical cases, not for every case I guess

Vinai commented 3 years ago

It shouldn't be possible to bind multiple values to the same field, so I think it would make sense to use field attributes instead. Here is what I thought could work instead:

<source>
  <defaultSearchCriteriaBindings>
    <field name="entity_id" requestParam="id">
    <field name="entity_id" class="Magento\Framework\App\RequestInterface" method="getParam" param="id"/>
    <field name="store_id" class="Magento\Store\Model\StoreManagerInterface" method="getStore" property="id"/>
    <field name="customer_ids" condition="finset" class="Magento\Customer\Model\Session" method="getCustomerId"/>
  </defaultSearchCriteriaBindings>
</source>

The first and second declarations are equivalent and would both map to

$objectManager->get('Magento\Framework\App\RequestInterface')->getParam('id');

The requestParam attribute is for convenience.

The third declaration would map to

$objectManager->get('Magento\Store\Model\StoreManagerInterface')->getStore()->getId();

This would be accomplished with the help of reflection. The field to access would be fetched with a matching getter (if it is an object and a getter exists), or getData($property) (if it's an object and that method exists. If it's an array it would default to array access (.e.g. ->getStore()['id']). Note: only a string type param value is supported.

The fourth (without param or property attribute) would map to

$objectManager->get('Magento\Customer\Model\Session')->getCustomerId()`

If more complex mappings with deeper nestings are required, they can always be wrapped in a simpler class method. All in all I prefer this approach over the existing event/observer idea because it will work in many cases without having to create additional classes. Even if a new class is required for a complex case, no additional configuration files are needed.

Vinai commented 3 years ago

To apply the bindings to the SearchCriteria instance an eq condition should be used by default. Other conditions, for example in or finset could be specified, too, using an condition attribute. I've updated the example above to include one such attribute.

Vinai commented 3 years ago

A fist implementation can be found in the bindparams branch: https://github.com/hyva-themes/magento2-hyva-admin/blob/bindparams/Model/GridSource/SearchCriteriaBindings.php

Tests: https://github.com/hyva-themes/magento2-hyva-admin/blob/bindparams/Test/Integration/Model/GridSource/SearchCriteriaBindingsTest.php

Vinai commented 3 years ago

Implemented and merged into main.

Vinai commented 3 years ago

Quick follow up in case someone stumbles over this thread. I refactored the syntax before releasing the feature so the class and method are specified in a single attribute instead of separate class and method. This avoids the issue where one is overridden through XML merging and the other is not.

<field name="customer_id" method="Magento\Customer\Model\Session::getCustomerId"/>