/   /   /  OroCommerce For Developer: Create your custom related products in admin like (Related & Upsell products )

Note:

For more extensions and themes visit our store

OroCommerce For Developer: Create your custom related products in admin like (Related & Upsell products )


We will call our new Bundle CustomRelatedBundle , we’re going to add our custom related block section inside edit product page like Related products and Upsell products .

Download The Full Bundle from Github


For that we use EventListener inside

  1.  
  2.  services.yml:
  3.     ibnab_customrelated.event_listener.related_customrelated_edit_listener:
  4.         class: Ibnab\Bundle\CustomRelatedBundle\EventListener\RelatedCustomRelatedEditListener
  5.         arguments:
  6.             - '@translator'
  7.             - '@security.authorization_checker'
  8.             - '@ibnab_customrelated.service.related_items_handler'            
  9.         tags:
  10.             - { name: kernel.event_listener, event: oro_ui.scroll_data.before.product-edit, method: onProductEdit }
  11.             - { name: kernel.event_listener, event: oro_ui.scroll_data.before.product-related-items-update, method: onProductEdit }
  12.             - { name: kernel.event_listener, event: oro.form.update_handler.before_form_data_set.oro_product, method: onFormDataSet }
  13.             - { name: kernel.event_listener, event: oro.form.update_handler.before_entity_flush.oro_product, method: onPostSubmit }
  14.  

The event tags for  kernel.event_listener , we're adding our block section by  oro_ui.scroll_data.before.product-edit  & oro_ui.scroll_data.before.product-related-items-update managed by function onProductEdit , and two other  before_form_data_set.oro_product &  oro.form.update_handler.before_entity_flush.oro_product to save to manage data of our custom related products.

 

This part of class Ibnab\Bundle\CustomRelatedBundle\EventListener\RelatedCustomRelatedEditListener which responsible of rendering of our custom block:

  1.  
  2. <?php
  3. namespace Ibnab\Bundle\CustomRelatedBundle\EventListener;
  4. use Oro\Bundle\FormBundle\Event\FormHandler\FormProcessEvent;
  5. use Oro\Bundle\FormBundle\Form\Type\EntityIdentifierType;
  6. use Oro\Bundle\ProductBundle\Entity\Product;
  7. use Oro\Bundle\UIBundle\Event\BeforeListRenderEvent;
  8. use Oro\Bundle\UIBundle\View\ScrollData;
  9. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  10. use Symfony\Contracts\Translation\TranslatorInterface;
  11. use Oro\Bundle\FormBundle\Event\FormHandler\AfterFormProcessEvent;
  12. use Twig\Environment;
  13. use Symfony\Component\Form\FormInterface;
  14. use Oro\Bundle\ProductBundle\Form\Handler\RelatedItemsHandler;
  15. /**
  16.  * Adds related product information (tabs, grids, forms) to the product edit page.
  17.  */
  18. class RelatedCustomRelatedEditListener
  19. {
  20.     const RELATED_ITEMS_ID = 'relatedItems';
  21.     /** @var int */
  22.     const BLOCK_PRIORITY = 1500;
  23.     /** @var TranslatorInterface */
  24.     private $translator;
  25.     /** @var AuthorizationCheckerInterface */
  26.     private $authorizationChecker;
  27.     /** @var RelatedItemsHandler */
  28.     private $relatedItemsHandler;
  29.     /**
  30.      * @param TranslatorInterface               $translator
  31.      * @param RelatedItemConfigHelper           $relatedItemConfigHelper
  32.      * @param AuthorizationCheckerInterface     $authorizationChecker
  33.      */
  34.     public function __construct(
  35.         TranslatorInterface $translator,
  36.         AuthorizationCheckerInterface $authorizationChecker,
  37.         RelatedItemsHandler $relatedItemsHandler
  38.     ) {
  39.         $this->translator = $translator;
  40.         $this->authorizationChecker = $authorizationChecker;
  41.         $this->relatedItemsHandler = $relatedItemsHandler;
  42.     }
  43.     /**
  44.      * @param BeforeListRenderEvent $event
  45.      */
  46.     public function onProductEdit(BeforeListRenderEvent $event)
  47.     {
  48.         $twigEnv = $event->getEnvironment();
  49.         $tabs = [];
  50.         $grids = [];
  51.             $tabs[] = [
  52.                 'id' => 'customrelated-products-block',
  53.                 'label' => $this->translator->trans('ibnab.customrelated.tabs.customrelatedProducts.label')
  54.             ];
  55.             $grids[] = $this->getCustomRelatedProductsEditBlock($event, $twigEnv);
  56.             $grids = array_merge([$this->renderTabs($twigEnv, $tabs)], $grids);
  57.         if (count($grids) > 0) {
  58.             $this->addEditPageBlock($event->getScrollData(), $grids);
  59.         }
  60.     }
  61.     /**
  62.      * @param ScrollData $scrollData
  63.      * @param string[] $htmlBlocks
  64.      */
  65.     private function addEditPageBlock(ScrollData $scrollData, array $htmlBlocks)
  66.     {
  67.         $subBlock = $scrollData->addSubBlock(self::RELATED_ITEMS_ID);
  68.         $scrollData->addSubBlockData(
  69.             self::RELATED_ITEMS_ID,
  70.             $subBlock,
  71.             implode('', $htmlBlocks),
  72.             'relatedItems'
  73.         );
  74.     }
  75.     /**
  76.      * @param BeforeListRenderEvent $event
  77.      * @param Environment $twigEnv
  78.      * @return string
  79.      */
  80.     private function getCustomRelatedProductsEditBlock(BeforeListRenderEvent $event, Environment $twigEnv)
  81.     {
  82.         return $twigEnv->render(
  83.             '@IbnabCustomRelated/CustomRelated/customrelatedProducts.html.twig',
  84.             [
  85.                 'form' => $event->getFormView(),
  86.                 'entity' => $event->getEntity(),
  87.                 'relatedProductsLimit' => 8
  88.             ]
  89.         );
  90.     }
  91.     /**
  92.      * @param Environment $twigEnv
  93.      * @param array $tabs
  94.      * @return string
  95.      */
  96.     private function renderTabs(Environment $twigEnv, array $tabs)
  97.     {
  98.         return $twigEnv->render(
  99.             '@OroProduct/Product/RelatedItems/tabs.html.twig',
  100.             [
  101.                 'relatedItemsTabsItems' => $tabs
  102.             ]
  103.         );
  104.     }
  105.  

The function  onProductEdit adding our custom block by usage of $twigEnv = $event->getEnvironment();  to pass our new tab to @OroProduct/Product/RelatedItems/tabs.html.twig and the content of our tab is @IbnabCustomRelated/CustomRelated/customrelatedProducts.html.twig (refer to functions renderTabs & getCustomRelatedProductsEditBlock in the code above ) and adding to related items section by function :

  1.  
  2.     private function addEditPageBlock(ScrollData $scrollData, array $htmlBlocks)
  3.     {
  4.         $subBlock = $scrollData->addSubBlock(self::RELATED_ITEMS_ID);
  5.         $scrollData->addSubBlockData(
  6.             self::RELATED_ITEMS_ID,
  7.             $subBlock,
  8.             implode('', $htmlBlocks),
  9.             'relatedItems'
  10.         );
  11.     }
  12.  

let’s explore the content of our tab inside @IbnabCustomRelated/CustomRelated/customrelatedProducts.html.twig , the content is:
  1.  
  2. {% import 'OroDataGridBundle::macros.html.twig' as dataGrid %}
  3. {% set gridName = 'products-customrelated-products-edit' %}
  4. {% set relatedGridParams = {
  5.     relatedItemsIds: get_customrelated_products_ids(entity),
  6.     _parameters: {
  7.         data_in: [],
  8.         data_not_in: []
  9.     }
  10. } %}
  11. {% if form.appendCustomRelated.vars.value is not empty %}
  12.     {% set relatedGridParams = relatedGridParams|merge({
  13.         _parameters: relatedGridParams._parameters|merge({
  14.             data_in: form.appendCustomRelated.vars.value|split(',')
  15.         })
  16.     }) %}
  17. {% endif %}
  18. {% if form.removeCustomRelated.vars.value is not empty %}
  19.     {% set relatedGridParams = relatedGridParams|merge({
  20.         _parameters: relatedGridParams._parameters|merge({
  21.             data_not_in: form.appendCustomRelated.vars.value|split(',')
  22.         })
  23.     }) %}
  24. {% endif %}
  25. <div id="customrelated-products-block" class="tab-content">
  26.     {{ form_widget(form.appendCustomRelated, {'id': 'productAppendCustomRelated'}) }}
  27.     {{ form_widget(form.removeCustomRelated, {'id': 'productRemoveCustomRelated'}) }}
  28.     {{ placeholder('ibnab_customrelated_related_ibnab_customrelateds_buttons', {
  29.         'entity':  entity,
  30.         'relatedProductsLimit': -1,
  31.         'gridName': gridName
  32.     }) }}
  33.     {{ dataGrid.renderGrid(gridName, relatedGridParams, {cssClass: 'inner-grid'}) }}
  34.     {{ placeholder('ibnab_customrelated_related_items_edit', {'entity': entity}) }}
  35. </div>
  36.  

Is clear that we well use datagrid macro to render our custom datagrid :

  1.  
  2. {% import 'OroDataGridBundle::macros.html.twig' as dataGrid %}
  3. {% set gridName = 'products-customrelated-products-edit' %}
  4. and the params passed to datagrid:
  5. {% set relatedGridParams = {
  6.     relatedItemsIds: get_customrelated_products_ids(entity),
  7.     _parameters: {
  8.         data_in: [],
  9.         data_not_in: []
  10.     }
  11. } %}
  12.  

we’re using get_customrelated_products_ids(entity) is our twig function responsible to get all customs related products of current product, the function get_customrelated_products_ids(entity)  is inside Ibnab/Bundle/CustomRelatedBundle/Twig/ProductExtension.php you can explore from bundle in github .
and our block content is:

  1.  
  2. <div id="customrelated-products-block" class="tab-content">
  3.     {{ form_widget(form.appendCustomRelated, {'id': 'productAppendCustomRelated'}) }}
  4.     {{ form_widget(form.removeCustomRelated, {'id': 'productRemoveCustomRelated'}) }}
  5.     {{ placeholder('ibnab_customrelated_related_ibnab_customrelateds_buttons', {
  6.         'entity':  entity,
  7.         'relatedProductsLimit': -1,
  8.         'gridName': gridName
  9.     }) }}
  10.     {{ dataGrid.renderGrid(gridName, relatedGridParams, {cssClass: 'inner-grid'}) }}
  11.     {{ placeholder('ibnab_customrelated_related_items_edit', {'entity': entity}) }}
  12. </div>
  13.  

We added two placeholder the first is for adding a button responsible to show the dialog content and datagrid of products to choose our custom related products.

Our placeholder Ibnab/Bundle/CustomRelatedBundle/Resources/config/oro/placeholders.yml:

  1.  
  2. placeholders:
  3.     placeholders:
  4.         ibnab_customrelated_related_items_edit: ~
  5.         ibnab_customrelated_related_ibnab_customrelateds_buttons:
  6.             items:
  7.                 ibnab_customrelated_select_related_customrelateds_button: ~
  8.     items:
  9.         ibnab_customrelated_select_related_customrelateds_button:
  10.             template: IbnabCustomRelatedBundle:CustomRelated:selectCustomRelatedButton.html.twig
  11.  

Note : 'id': 'productAppendCustomRelated' and 'id': 'productRemoveCustomRelated' the naming format is very important is used to define appended and removed product . See Datagrid on bottom

let’s explore our Ibnab/Bundle/CustomRelatedBundle/Resources/config/oro/datagrids.yml it has many many sub datagrid to explore but the one of important is:

  1.  
  2.     products-customrelated-products-edit:
  3.         extends: products-customrelated-items-edit
  4.         options:
  5.             rowSelection:
  6.                 dataField: id
  7.                 columnName: isRelated
  8.                 selectors:
  9.                     included: '#productAppendCustomRelated'
  10.                     excluded: '#productRemoveCustomRelated'
  11.  


And parent:
  1.  
  2.     products-customrelated-items-edit:
  3.         acl_resource: oro_product_update
  4.         extends: products-customrelated-items-base
  5.         options:
  6.             jsmodules:
  7.                 - orodatagrid/js/datagrid/listener/action-form-listener
  8.                 - oroproduct/js/app/datagrid/listener/related-product-listener
  9.         source:
  10.             query:
  11.                 where:
  12.                     or:
  13.                         - 'product.id IN (:relatedItemsIds) AND product.id NOT IN (:data_not_in)'
  14.                         - '(product.id IN (:data_in)) AND product.id NOT IN (:data_not_in)'
  15.             bind_parameters:
  16.                 -
  17.                   name: relatedItemsIds
  18.                   default: []
  19.         actions:
  20.             delete:
  21.                 type:          frontend
  22.                 label:         oro.grid.action.delete
  23.                 icon:          trash
  24.                 configuration:
  25.                     triggerAction: excludeRow
  26.  

Which got relatedItemsIds param of our current custom related products to using it in clause where.

What about our IbnabCustomRelatedBundle:CustomRelated:selectCustomRelatedButton.html.twig button which responsible to show new dialog to select our unselect products the content is:

  1.  
  2. {% import 'OroProductBundle:Product/RelatedItems:macros.html.twig' as relatedItemsUI %}
  3. <div class="related-items-widget">
  4.     {{ relatedItemsUI.renderRelatedItemButton(
  5.         path('ibnab_customrelated_possible_products_for_customrelated_products', {'id': entity.id}),
  6.         'ibnab.customrelated.widgets.select_products.label'|trans,
  7.         'ibnab.customrelated.widgets.select_products.title'|trans({'{{ sku }}': entity.sku, '{{ name }}': entity.name}),
  8.         relatedProductsLimit,
  9.         '#productAppendCustomRelated',
  10.         '#productRemoveCustomRelated',
  11.         gridName
  12.     ) }}
  13. </div>
  14.  

I hope that you’re seeing #productAppendCustomRelated & #productRemoveCustomRelated , but the more important is the on click action call a new controller with path  path('ibnab_customrelated_possible_products_for_customrelated_products', {'id': entity.id}) is our controller to show all available products which compatible with current products , our controller content:
  1.  
  2. <?php
  3. namespace Ibnab\Bundle\CustomRelatedBundle\Controller;
  4. use Oro\Bundle\ProductBundle\Entity\Product;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  6. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  7. use Symfony\Component\Routing\Annotation\Route;
  8. /**
  9.  * CRUD controller for the Product entity.
  10.  */
  11. class ProductController extends AbstractController
  12. {
  13.     /**
  14.      * @Route(
  15.      *     "/get-possible-products-for-customrelated-products/{id}",
  16.      *     name="ibnab_customrelated_possible_products_for_customrelated_products",
  17.      *     requirements={"id"="\d+"}
  18.      * )
  19.      * @Template(template="IbnabCustomRelatedBundle:Product:selectCustomRelatedProducts.html.twig")
  20.      *
  21.      * @param Product $product
  22.      * @return array
  23.      */
  24.     public function getPossibleProductsForCustomRelatedProductsAction(Product $product)
  25.     {
  26.         return ['product' => $product];
  27.     }
  28. }
  29.  

 Note: don’t forget to declare inside Ibnab/Bundle/CustomRelatedBundle/Resources/config/oro/routing.yml:
  1.  
  2. ibnab_customrelated:
  3.     resource:     "@IbnabCustomRelatedBundle/Controller/ProductController.php"
  4.     type:         annotation
  5.     prefix:       /customrelatedproduct
  6.  

This URL using action getPossibleProductsForCustomRelatedProductsAction and the action get rendered inside  IbnabCustomRelatedBundle:Product:selectCustomRelatedProducts.html.twig by passing the current product to this twig which render a new Grid to select products:
  1.  
  2. {% import 'OroDataGridBundle::macros.html.twig' as dataGrid %}
  3. <div class="widget-content">
  4.     <input
  5.         type="hidden"
  6.         data-role="related-items-appended-ids"
  7.         name="productAppendRelatedSelect"
  8.         id="productAppendCustomRelatedSelect"
  9.         value="{{ app.request.get('addedProductRelatedItems')|join(',') }}"
  10.     />
  11.     <input
  12.         type="hidden"
  13.         data-role="related-items-removed-ids"
  14.         name="productRemoveCustomRelatedSelect"
  15.         id="productRemoveCustomRelatedSelect"
  16.         value="{{ app.request.get('removedProductRelatedItems')|join(',') }}"
  17.     />
  18.     {{ dataGrid.renderGrid('products-customrelated-products-select', {'productId': product.id, 'relatedItemsIds': get_customrelated_products_ids(product)}) }}
  19.     <div class="widget-actions form-actions">
  20.         <button class="btn" type="reset">{{ 'Cancel'|trans }}</button>
  21.         <button class="btn btn-primary" data-role="related-items-submit-button" type="button">
  22.             {{ 'oro.product.widgets.select_products'|trans }}
  23.         </button>
  24.     </div>
  25. </div>
  26.  

And the datagrid content is :
  1.  
  2.     products-customrelated-products-select:
  3.         extends: products-customrelated-items-select
  4.         options:
  5.             rowSelection:
  6.                 dataField: id
  7.                 columnName: isRelated
  8.                 selectors:
  9.                     included: '#productAppendCustomRelatedSelect'
  10.                     excluded: '#productRemoveCustomRelatedSelect'
  11.  

I think the big points has get showed in this course but to explore how to manage save and find strategy you should explore the full bundle in github by your self .

Download The Full Bundle from Github

 

Comments

IBNAB is a company made of a group of professionals whose work is providing secure open source solutions. Our company strives for reaching magnificent results with each experience and provides professional open source solutions that cover every part of the business process.