Warning, this is a long winded question about module architecture. I'm not mad if it gets closed as opinion based, but I hope someone has a better idea than me.
In Magento 2 we have builders like the SearchCriteriaBuilder
which we can inject into our classes similar to factories to let us create objects, in this case SearchCriteria
(filters, sort order, page size).
What I like about the builder pattern in general (not Magento specific), is that you can create named constructors for predefined configurations (i.e. parameters for the object to be created) but still can change these configurations before the object is created.
Fictional Example:
$shop = ShopBuilder::demoShop()->withSampleData()->build();
demoShop()
returns a preconfigured builder, withSampleData()
changes it and build()
creates the object.
Magento 2
In Magento 2, named constructors are not really a thing, because they cannot make use of the object manager. Virtual types might be an alternative but they are entirely defined in di.xml
, no PHP code. This is a bit limiting and obscure IMHO.
What I want:
I have some search criteria combinations that I need in different places with slight variations and don't want to configure them from scratch all the time, given the verbosity of their creation.
What I have:
I currently created separate classes that delegate to the actual SearchCriteriaBuilder.
This is for example a builder for a "all varchar attributes, sorted by label" search criteria with an optional blacklist:
namespace IntegerNet\Solr\Model\SearchCriteria;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
use Magento\Framework\Api\Filter;
use Magento\Framework\Api\Search\SearchCriteriaBuilder;
use Magento\Framework\Api\SimpleBuilderInterface;
class VarcharAttributes implements SimpleBuilderInterface
{
protected $searchCriteriaBuilder;
public function __construct(SearchCriteriaBuilder $searchCriteriaBuilder)
{
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$searchCriteriaBuilder->addSortOrder('frontend_label', AbstractCollection::SORT_ORDER_ASC);
$searchCriteriaBuilder->addFilter(new Filter([
Filter::KEY_FIELD => 'backend_type',
Filter::KEY_CONDITION_TYPE => 'in',
Filter::KEY_VALUE => ['static', 'varchar']
]));
$searchCriteriaBuilder->addFilter(new Filter([
Filter::KEY_FIELD => 'frontend_input',
Filter::KEY_VALUE => 'text'
]));
}
/**
* Exclude attributes by code
*
* @param array $attributeCodes
* @return $this
*/
public function except(array $attributeCodes)
{
$searchCriteriaBuilder = $this->searchCriteriaBuilder;
$searchCriteriaBuilder->addFilter(new Filter([
Filter::KEY_FIELD => 'attribute_code',
Filter::KEY_CONDITION_TYPE => 'nin',
Filter::KEY_VALUE => $attributeCodes
]));
return $this;
}
public function create()
{
return $this->searchCriteriaBuilder->create();
}
public function getData()
{
return $this->searchCriteriaBuilder->getData();
}
}
In the constructor, the actual builder gets preconfigured and in except()
this configuration can be extended. Now I can inject this class when I need this particular search criteria and use them like this:
$this->attributeRepository->getList(
$this->varcharAttributes->except(['name'])->create()
);
But I'm not happy with it
Why?
The builder is not immutable. As soon as I make changes to its configuration (the except()
call in the example), the default configuration is lost.
If I have these subsequent calls in one class:
$this->varcharAttributes->create()
$this->varcharAttributes->except(['name'])->create()
$this->varcharAttributes->create()
The second and third call create a SearchCriteria with the additional "name" filter.
Usually I would solve this by returning a new instance from the modifiers like except()
but I did not find a good way to do that yet. clone $this
does not help because the SearchCriteriaBuilder of Magento does not allow deep cloning and references a FilterGroupBuilder and a FilterBuilder.
Of course I could create a factory for each of my custom builders but that feels a bit too much, even if it's one factory for several related builders.
So my question is: Is there a clean way to use preconfigured and preferably immutable builders? Or do I really need to create a factory that creates custom builders that use the core builder?