-
Notifications
You must be signed in to change notification settings - Fork 9.3k
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
#7698 - modularize admin global search #9192
Conversation
I know, I know. I have some tests to fix. |
app/code/Magento/Backend/etc/di.xml
Outdated
</item> | ||
</argument> | ||
<argument name="searchModules" xsi:type="array" /> | ||
<argument name="prevideModules" xsi:type="array" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo for previewModules?
Need to hook this up to elasticsearch so it really can be a global search! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the preview link 'keywords in Products' because the products admin grid does not have a full text search, so clicking on that link does not give out the expected result.
Currently it's a bug, that Product Grid doesn't provide Search functionality. And that would be fixed soon in the scope of #10089
which is awaiting to be processed.
So, no need to remove 'keywords in Products' drop-down item
/** | ||
* @return array | ||
*/ | ||
protected function getPreviewItems() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's give just "private" visibility for all the new functions
} | ||
$result[] = [ | ||
'url' => $this->getUrl($previewConfig['url']).'?search='.$query, | ||
'name' => __($previewConfig['text'], $query) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't have translate="true"
attribute in DI.xml , as we have for UI components configuration, thus translation would not work correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maghamed Any pointers on how to handle this so the translation will work and still be able to keep the texts in DI? O maybe move them somewhere else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tzyganu I found a possibility to specify "string" type to be translatable in DI.xml
using the "translate" attribute
Here is the definition of this attribute for any xsi:type="string"
attribute type in types.XSD schema which is used by ObjectManager
magento2/lib/internal/Magento/Framework/Data/etc/argument/types.xsd
Lines 31 to 33 in 99e85cb
<xs:extension base="argumentType"> | |
<xs:attribute name="translate" use="optional" type="xs:boolean"/> | |
</xs:extension> |
Here is how this attribute interpreted
magento2/lib/internal/Magento/Framework/Data/Argument/Interpreter/StringUtils.php
Lines 41 to 44 in 99e85cb
$needTranslation = isset($data['translate']) ? $this->booleanUtils->toBoolean($data['translate']) : false; | |
if ($needTranslation) { | |
$result = (string)new \Magento\Framework\Phrase($result); | |
} |
But there is no any usage of this attribute in Magento.
And looks like all the tries to pass strings using DI.xml just don't translate at all
Like this one:
<type name="Magento\Catalog\Model\Entity\Product\Attribute\Design\Options\Container">
<arguments>
<argument name="options" xsi:type="array">
<item name="option1" xsi:type="array">
<item name="value" xsi:type="string">container1</item>
<item name="label" xsi:type="string">Product Info Column</item>
</item>
<item name="option2" xsi:type="array">
<item name="value" xsi:type="string">container2</item>
<item name="label" xsi:type="string">Block after Info Column</item>
</item>
</argument>
</arguments>
</type>
or this one:
<type name="Magento\Customer\Ui\Component\MassAction\Group\Options">
<arguments>
<argument name="data" xsi:type="array">
<item name="urlPath" xsi:type="string">customer/index/massAssignGroup</item>
<item name="paramName" xsi:type="string">group</item>
<item name="confirm" xsi:type="array">
<item name="title" xsi:type="string">Assign a Customer Group</item>
<item name="message" xsi:type="string">Are you sure to assign selected customers to new group?</item>
</item>
</argument>
</arguments>
</type>
} | ||
} | ||
|
||
/** @var \Magento\Framework\Controller\Result\Json $resultJson */ | ||
$resultJson = $this->resultJsonFactory->create(); | ||
return $resultJson->setData($items); | ||
} | ||
|
||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please provide a clear description for this function
* | ||
* @var array | ||
*/ | ||
protected $previewModules; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't use protected for newly created properties
@@ -21,16 +21,26 @@ class GlobalSearch extends \Magento\Backend\Controller\Adminhtml\Index | |||
protected $_searchModules; | |||
|
|||
/** | |||
* modules that support preview |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw this controller should be marked with PHP DocBlock as @ api
because now it represents extension point for other search modules.
Btw it makes sense to use another extension point for these purposes
if (empty($className)) { | ||
continue; | ||
} | ||
$searchInstance = $this->_objectManager->create($className); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we use ObjectManager here?
the dedicated factory should be used instead. Btw, this factory should check the type of needed object . All the search classes should implement common interface/provide the same contract
Because for now there is no any guarantee that created class supports contract by which you work with it below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maghamed I didn't add this by myself. The code was there I just moved it around. But I will try to find a way around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marius, you right this looks like legacy code, even taking into account that it was written quite recently.
What we usually do for such cases is to introduce new Interface with the common contract, and Factory which is responsible for producing an instance of this interface and making sure that object which would be created is an instance of the interface mentioned above.
Factory also could serve as an extension point and other developers could introduce own instance of the contract and register it using DI configuration in this factory (constructor argument).
Doing so we get:
- well defined interface for some piece of functionality
- well defined Factory which creates some particular implementation of this interface
- extension point which helps 3rd party developers to extend basic implementations with custom one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maghamed already on it.
app/code/Magento/Backend/etc/di.xml
Outdated
</item> | ||
</argument> | ||
<argument name="searchModules" xsi:type="array" /> | ||
<argument name="previewModules" xsi:type="array" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not mandatory to provide these configurations
@maghamed Thanks for pointing me in the right direction. I will try to fix the requested changes and come back with a better code. |
*/ | ||
public function __construct( | ||
\Magento\Backend\App\Action\Context $context, | ||
ItemsFactory $itemsFactory, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New argument to constructor may only be added as the last on the list to preserve backwards compatibility.
Please refer to Backwards Compatible Development guide for more information
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By last, you mean last one that does not have a default value, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, @ishakhsuvarov means last in a list of all existing arguments (after all existing arguments).
And each new argument added should be optional (nullable)
use Magento\Backend\Api\Search\ItemsInterface; | ||
use Magento\Framework\ObjectManagerInterface; | ||
|
||
class ItemsFactory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider using ItemFactory
name for this class to be consistent with other Factories.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marius, please pay attention on my comments.
Also, could you please add 'keywords in Products' , because we already have this functionality worked in another PR
namespace Magento\Backend\Api\Search; | ||
|
||
/** | ||
* @api |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please provide description for this interface.
All interfaces marked as @ api should be accompanied with clear and precise description
$items = array_merge_recursive($items, $results); | ||
|
||
} catch (\LogicException $exception) { | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exception swallowing is a bad practice.
at least we need to log critical message here.
But in general, LogicalException should not be just swallowed and silently continue script execution
if you don't know in this specific case how to handle Exception, maybe make sense not to catch it at all
} | ||
|
||
/** | ||
* retrieve all entity items that should appear in global search |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please, start the sentences with the capital letter
use Magento\Backend\Model\Search\ItemsFactory; | ||
|
||
/** | ||
* @api |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
everything marked as @ api should be accompanied with description
* set offset | ||
* | ||
* @param int $start | ||
* @return ItemsInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't recommend to use "fluent interfaces" and "method chaining", we support CQS where the command should return void instead of $this.
Returning $this from the setter method is allowed just for Builder implementation
(I see that we have this chaining in code before your changes, just think that if we started to improve the code quality, let's fix this out)
or let's use Builder pattern here
Thus, there are two options @tzyganu :
- Build/Fill kind of SearchCriterial and introduce an independent interface for processing this SearchCriteria - in this case, we will segregate responsibilities.
- Or use Builder pattern - all setters change the state, getResults() - build objects and reset the state
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@maghamed I was trying the SearchCriteria approach, but hit a wall.
I was doing it like this.
have an ItemInterface
with a single method getResults(\Magento\Framework\Api\SearchCriteria $searchCriteria)
but I cannot add the filter in the search criteria for the query because the query filtering is dependent on the entity I'm filtering. So maybe I can do it via an additional parameter?. Have getResults
look like this getResults($searchCriteria, $query)
and the classes that implement this interface are responsible for handling the query.
I could also avoid changing the $searchCriteria
parameter by using an additional searchCriteriaBuilder inside the implementations of getResults
. (this is already like that for the customer search)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tzyganu I think the existing SearchCriteria we have in Framework \Magento\Framework\Api\SearchCriteria would not fit your needs. It's too heavyweight for your specific case.
When I said "SearchCriteria" I meant similar approach we use with SearchCriteria:
- To have a dedicated interface to describe your request (aka SearchCriteria). In your case that would be something like:
namespace Your\Custom\Api;
class SearchCriteria
{
public function setStart($start);
public function setQuery($query);
public function setLimit($limit);
public function getStart();
public function getQuery();
public function getLimit();
}
- To have Search interfaces which will process a search query based on the SearchCriteria given as Input parameter. Something like:
namespace Your\Custom\Api;
class QueryProcessor
{
public function getResults(SearchCriteria $searchCriteria); // or maybe Search(SearchCriteria $searchCriteria)
}
Doing so we make a responsibility segregation and don't have problems with managing and keeping the state in entity responsible for search request execution because all the parameters come as input from the outside each time.
* | ||
* @return array | ||
*/ | ||
public function getResults(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This interface is pretty bad one because it has a temporal coupling
on the level of contract. Because contract implies an order in which these methods would be called.
Firstly initialize all setters, and then call getResuls()
BTW This interface looks like a good application of Builder pattern for me.
So, if we would rename it to SearchResultBuilder that will make more sense.
use Magento\Backend\Api\Search\ItemsInterface; | ||
use Magento\Framework\DataObject; | ||
|
||
abstract class ItemsAbstract extends DataObject implements ItemsInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
introducing of abstract classes is a bad practice. In M2 we are trying to avoid using Inheritance Based API.
use composition instead, if your main reason for Inheritance usage to get rid of Copy-Paste
public function create($instanceName, array $data = []) | ||
{ | ||
$object = $this->objectManager->create($instanceName, $data); | ||
if ($object instanceof ItemsInterface) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's better to make a type check before the object instantiation.
Use string which represents class name and check whether it's an implementation of needed class
*/ | ||
public function create($instanceName, array $data = []) | ||
{ | ||
$object = $this->objectManager->create($instanceName, $data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like it's better to use 'get' instead of 'create' as this should be stateless object
return $object; | ||
} | ||
throw new \LogicException( | ||
"The class '{$instanceName}' does not implement ".ItemsInterface::class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use spaces between concatenation
"" . ItemsInterface::class
use Magento\Backend\Model\Search\ItemsFactory; | ||
|
||
/** | ||
* @api |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current Controller became big enough and now contains Business logic inside.
It's better to introduce dedicated handler/model for this.
In M2 we are trying to make Controllers as light/ thin as possible
This Handler would be marked as @ api intead of current controller
if ($previewConfig['acl'] && !$this->_authorization->isAllowed($previewConfig['acl'])) { | ||
continue; | ||
} | ||
if (!$previewConfig['url'] || !$previewConfig['text']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not correct comparison
!$previewConfig['url'] || !$previewConfig['text']
use isset instead
@maghamed Ok. Let's recap. It would be easier for me to follow your instructions since I'm not that familiarized with all your code quality standards.
For the string translation in di.xml files, I propose to add the Let me know if I missed something. One more question. Should I close this PR and open a new cleaner one following your guidelines? Or should I continue on this as it is easier to follow? |
get latest develop from upstream
Hey @tzyganu
We are against the usage of any abstract class in newly created code. And all the existing abstract classes is the point of refactoring now or in future. For example, we would like to eliminate all the "Layer Super Types" like AbstractBlock, AbstractModel, AbstractConstructor.
Correct. You should not design interface in a way to have an implicit relationship between two, or more, methods of a class requiring clients to invoke one method before the other.
It's not a general rule. just to be more secure, because in your specific case factory accepts class to instantiate as external argument.
We are not fans of having business logic in the controllers. In your case Controller became an extension point, and anybody who wants to extend/customize search. Or add new module for search should add additional parameter for this controller. But our Controllers are responsible just for specific entry point. For example, for Mobile application we could have another Controller. Based on the above this logic should belong to some model, not to Controller.
That's true for all the exception types
correct
would be nice
yes, let's do this
I think we can continue in the scope of this PR BTW , that would be helpful for you to get aware with main Magento principles and guidelines : |
@maghamed I'm done. I hope. Just codacy is complaining a bit. I did the last changes I can think of. Please have another look. |
ItemFactory $itemFactory, | ||
SearchCriteriaFactory $criteriaFactory, | ||
array $searchModules = [], | ||
array $previewModules = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such changes to the class constructor violate backwards compatibility policy:
- New dependencies must not be added to the middle, only as a last parameter.
- All newly added arguments must be nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like
array $searchModules = [],
ItemFactory $itemFactory = null,
SearchCriteriaFactory $criteriaFactory = null,
array $previewModules = []
And then loading the optional constructor if null via $newDependency ?: \Magento\Framework\App\ObjectManager::getInstance()->get(\New\Dependency\Interface::class)
would work well here I think.
/** | ||
* @api | ||
*/ | ||
interface ItemsInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest renaming this interface, as Items is too broad definition.
Also getResults
method in ItemsInterface
creates confusion, since there may not be results of items.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ishakhsuvarov do you have a suggestion for @tzyganu in this case? I could think that ResultsInterface
and getResults
would work well.
) { | ||
$this->itemFactory = $itemFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please refer to the Adding a constructor parameter section of Backwards Compatible Development Guide for a definition and code example for adding constructor parameters.
$this->_searchModules = $searchModules; | ||
$this->previewModules = $previewModules; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider extracting this logic to a dedicated model, since It does not look like it is in controllers responsibility.
Magento 2 is aiming to implement controllers as lightweight as possible, moving all possible parts outside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ishakhsuvarov I agree whit this, but I don't see a solution to move the search logic to a separate class and keep backwards compatibility (as suggested in other comments) at the same time. Maybe I can move this to a separate class, and you release it when you break BC for something else anyway.
* | ||
* @param string $instanceName | ||
* @param array $data | ||
* @return ItemsInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@throws annotation may be added here as well
use Magento\Backend\Api\Search\ItemsInterface; | ||
use Magento\Framework\ObjectManagerInterface; | ||
|
||
class ItemFactory |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brief description for this class may be added here
* @param \Magento\Framework\Stdlib\StringUtils $string | ||
* @param QueryFactory $queryFactory | ||
*/ | ||
public function __construct( | ||
\Magento\Backend\Helper\Data $adminhtmlData, | ||
\Magento\Backend\Helper\Data $adminHtmlData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is considered to be backwards incompatible, please revert to maximize backwards compatibility
*/ | ||
public function load() | ||
public function getResults(SearchCriteria $searchCriteria) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change effectively removes method load
, this violates backwards compatibility for the third-party code which may rely on it.
You may add a new method and proxy calls from the old one, given that result will be compatible
* @param \Magento\Framework\Api\FilterBuilder $filterBuilder | ||
* @param \Magento\Customer\Helper\View $customerViewHelper | ||
*/ | ||
public function __construct( | ||
\Magento\Backend\Helper\Data $adminhtmlData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
@@ -3,32 +3,19 @@ | |||
* Copyright © Magento, Inc. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately a class can not be moved, since already existing 3rd-party code may already rely on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ishakhsuvarov How about if I deprecate the class, leave it empty and make it extend my new class (the one in the right module)? Would that solve the issue for 3rd party code that may use plugins on it?
*/ | ||
public function load() | ||
public function getResults(SearchCriteria $searchCriteria) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
Hi @tzyganu |
@ishakhsuvarov Thanks for the tips. I'm on vacation for the next 2 weeks and I don't want to see any code in this period. 😄 If you're in a hurry, you can pic it up from where I left off. If not I will give it an other shot it august. |
@tzyganu We are not in a hurry for sure. Take your time. Just let us know if you are going to need any assistance. |
I love the fact you have moved the classes into the "right" modules. I think this a great improvement, though I cannot think 100% how to stop the BC breaks between the new and old classes other than leaving them as they are and just deprecating them. Maybe @ishakhsuvarov or @maghamed have a great example some place of this sort of change. |
@dmanners For now we may not remove the old implementations, only mark those with |
@tzyganu I am closing this PR for now due to inactivity. Please reopen and fix merge conflicts when you would like to continue. |
@ishakhsuvarov I don't think I will pick this up again. At least until you make up your mind. |
I don't think it will ever happen. The one always should deprecate some unneeded classes/methods and only in some next release they will be removed. If you feel some implementation approach with respect to http://devdocs.magento.com/guides/v2.1/ext-best-practices/extension-coding/backwards-compatible-development/index.html does not seem to be clean, let's discuss it first and try to figure out the cleanest possible BC-compliant approch (maybe, we will determine that it's still too dirty to be embodied into life, who knows...). |
This PR solves the issue #7698.
It modularizes the admin global search and translates the preview links "keyword in Pages"...
Description
The PR makes the admin global search modularized. The composition of the search was moved to the proper module. (sales search in Sales module
di.xml
. Also for customer and cms pages).I removed the preview link 'keywords in Products' because the products admin grid does not have a full text search, so clicking on that link does not give out the expected result.
In the
search.phtml
the text for previews ('keyword' in customers) was not translatable. Not it should be.The list of previews was not extensible. It was hardcoded in the search.phtml template.
I added one more class member of the
Magento\Backend\Controller\Adminhtml\Index\GlobalSearch
that holds an array of elements that can be displayed as preview for different modules.What I didn't do, is to increment the module version in module.xml and composer.json. I will need some guidance on that.
Fixed Issues (if relevant)
Manual testing scenarios
Steps to reproduce are listed in the issue
Contribution checklist