www.lowcodeapp.de - Beschleunigung der digitalen Transformation mit Open Source Low-Code Development.

Oro: MassActions für Datagrids erstellen

Man sieht einen Laptop auf dem gerade getippt wird und im Hintergrund 4 Monitore.

Mit seinen Datagrids bietet die OroPlatform Entwicklern eine ausgereifte und flexible Möglichkeit, Daten tabellarisch darzustellen. Features wie Filterung, Sortierung, Pagination sowie Zeilen- und Massenoperationen bekommt man fast geschenkt und statt vieler Zeilen PHP-Code wird fast alles einfach per yml konfiguriert. In diesem Beitrag wollen wir uns näher mit den MassActions beschäftigen. Also der Möglichkeit, eine Operation auf mehrere oder auch alle Elemente eines Datagrids auszuführen. Die MassActions findet man in der Schaltfläche rechts oberhalb eines Datagrids. Meist findet man dort nur die Möglichkeit, Datensätze zu löschen, aber man kann auch eigene Operationen dort ablegen.

Vorbereitung

Für eine Mass-Action müssen recht viele Komponenten angelegt werden. Der Prozess ist nicht ganz trivial. Das liegt aber in der Natur der Sache, denn eine Action muss so flexibel sein, dass sie theoretisch in jedes beliebige DataGrid eingebunden werden kann. Ob das dann immer sinnvoll ist, steht natürlich auf einem anderen Blatt.

Sobald in einem Datagrid eine Mass-Action konfiguriert wurde, werden automatisch die CheckBoxen in der ersten Spalte des Grids eingeblendet. Die Integration im oro/datagrids.yml ist simpel:

datagrids:
  dmkclub-memberfees-grid-billing:
    mass_actions:
      sepadebitxml:
        type: dmksepadebitxml
        entity_name: "%dmkclub_member.memberfee.entity.class%"
        data_identifier: f.id
        label: dmkclub.payment.datagrid.action.sepa_direct_debit
        icon: eur

Bei einer Mass-Action muss man keine eigene Route anlegen. Man kann hier einen Dispatcher von Oro nutzen. Der type verweist auf einen Eintrag in der mass_action.yml. Diese Datei muss man anlegen und in der DependencyInjection/YourBundleExtension.php eintragen:

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
$loader->load('mass_action.yml');

Der Inhalt der Datei sieht dann so aus:

parameters:
  dmkclub_payment.mass_action.sepadebitxml.class:  DMKClub\Bundle\PaymentBundle\DataGrid\Extension\MassAction\SepaDebitXmlAction
  dmkclub_payment.mass_action.sepadebitxml_handler.class:  DMKClub\Bundle\PaymentBundle\DataGrid\Extension\MassAction\SepaDebitXmlHandler

services:
  dmkclub_payment.datagrid.mass_action.sepadebitxml:
    class: "%dmkclub_payment.mass_action.sepadebitxml.class%"
    scope: prototype
    tags:
      - { name: oro_datagrid.extension.mass_action.type, type: dmksepadebitxml }
dmkclub_payment.datagrid.mass_action.sepadebitxml_handler:
  class: "%dmkclub_payment.mass_action.sepadebitxml_handler.class%"
  arguments:
    - "@doctrine.orm.entity_manager"
    - "@translator"
    - "@logger"

Das Schlüsselwort dmksepadebitxml findet man als Tag in der Konfiguration der Action-Klasse wieder. Neben der Action-Klasse gibt es noch eine Handlerklasse, die später die eigentliche Verarbeitung der Daten übernehmen wird. Dafür können wie üblich alle notwendigen Abhängigkeiten injected werden.

PHP-Klassen für Handler und Action

Legen wir nun also diese beiden Klassen an. So sieht der Inhalt der SepaDebitXmlAction aus:

<?php
 
namespace DMKClub\Bundle\PaymentBundle\DataGrid\Extension\MassAction;
 
use Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\AbstractMassAction;
use Oro\Bundle\DataGridBundle\Extension\Action\ActionConfiguration;
 
 class SepaDebitXmlAction extends AbstractMassAction {
 	/** @var array */
 	protected $requiredOptions = ['entity_name', 'data_identifier'];
 
 	/**
 	 * {@inheritDoc}
 	 */
 	public function setOptions(ActionConfiguration $options)
 	{
 		if (empty($options['route'])) {
 			// Wir nutzen den zentralen Controller von Oro für den Dispatch
 			$options['route'] = 'oro_datagrid_mass_action';
 		}
 		if (empty($options['route_parameters'])) {
 			// Das Array muss initialisiert werden, damit die Parameter per JS gesetzt werden
 			$options['route_parameters'] = [];
 		}
 		if (empty($options['handler'])) {
 			$options['handler'] = 'dmkclub_payment.datagrid.mass_action.sepadebitxml_handler';
 		}
 		if (empty($options['frontend_type'])) {
 			// Der Wert bezieht sich auf den Key in der requirejs.yml, wobei das "-action" weggelassen werden muss
 			$options['frontend_type'] = 'dmksepadebitxml-mass';
 		}
 		if (empty($options['frontend_handle'])) {
 			$options['frontend_handle'] = 'ajax';
 		}
 		$options['confirmation'] = false;
 		return parent::setOptions($options);
 	}
 }

Die Options kommen direkt aus der Konfiguration im datagrid.yml. Theoretisch könnten wir da also viel mehr konfigurieren. Alle notwendigen Werte werden hier mit Default-Werten befüllt. Wie man sieht, erben wir von Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\AbstractMassAction.

Schauen wir uns die wichtigsten Teile der Klasse an. Die $requiredOptions sorgen dafür, dass in der datagrid.yml bestimmte Werte immer konfiguriert werden müssen. Da diese Mass-Action grundsätzlich für alle Entities funktionieren soll, müssen wir wissen, über welchen Wert wir die ID des Datensatzes aus dem ResultSet des Datagrids bekommen (data_identifier). Mit entity_name lassen wir uns die Klasse übergeben, damit wir Instanzen erzeugen können. Hinweis am Rande: das ResultSet des Datagrids liefert immer nur Arrays, keine Entities!

Wichtig sind dann noch die Route (der schon erwähnte Oro-Dispatcher), die Handler-Klasse und der frontend_type. Den Handler haben wir schon in der mass_action.yml konfiguriert. Der frontend_type verweist auf eine kleine Javascript-Klasse. Hier muss man höllisch aufpassen, dass man die Konventionen bei der Einbindung beachtet. Dazu aber später mehr. Jetzt schauen wir erstmal in die Handlerklasse. 

class SepaDebitXmlHandler implements MassActionHandlerInterface {
 protected $entityManager;
 protected $translator;
 private $logger;
 /**
 	 * @param EntityManager $entityManager
 	 * @param TranslatorInterface $translator
 	 * @param ServiceLink $securityFacadeLink
 	 */
 	public function __construct(
 		EntityManager $entityManager, TranslatorInterface $translator, LoggerInterface $logger) {
 		$this->entityManager = $entityManager;
 		$this->translator = $translator;
 		$this->logger = $logger;
 	}
 
 	/**
 	 * {@inheritDoc}
 	 */
 	public function handle(MassActionHandlerArgs $args) {
 		$data = $args->getData();
 		$massAction = $args->getMassAction();
 		$options = $massAction->getOptions()->toArray();
 		$query = $args->getResults()->getSource(); 
 		$this->entityManager->beginTransaction();
 		try {
 			set_time_limit(0);
 			$iteration = $this->handleExport($options, $data, $query);
 			$this->entityManager->commit();
 		} catch (\Exception $e) {
 			$this->entityManager->rollback();
 			throw $e;
 		}
 
 		return $this->getResponse($args, $iteration);
 	}
 
 	/**
 	 * @param array $options
 	 * @param array $data
 	 * @param Query $query Die Query des Datagrids
 	 * @return int
 	 */
 	protected function handleExport($options, $data, $query) {
 		$isAllSelected = $this->isAllSelected($data);
 		$iteration = 0;
 
 		$data_identifier = $options['data_identifier'];
 		$entity_name =$options['entity_name'];
 
 		if (array_key_exists('values', $data) && !empty($data['values'])) {
 			$entity_ids = explode(',', $data['values']);
 			$iteration = count($entity_ids);
 			foreach ($entity_ids As $entityId) {
 				$this->handleItem($entityId, $data_identifier);
 			}
 		} elseif($isAllSelected) {
 			$result = $query->iterate();
 			foreach ($result as $row) {
 				$row = reset($row);
 				$entityId = $row['id'];
 				$this->handleItem($entityId, $data_identifier);
 				$iteration++;
 			}
 		} 
 		return $iteration;
 	}
 
 	private function handleItem($entityId, $className) {
 		// TODO: implement
 	}
 
 	/**
 	 * @param array $data
 	 * @return bool
 	 */
 	protected function isAllSelected($data) {
 		return array_key_exists('inset', $data) && $data['inset'] === '0';
 	}
 
 	/**
 	 * @param MassActionHandlerArgs $args
 	 * @param int $entitiesCount
 	 *
 	 * @return MassActionResponse
 	 */
 	protected function getResponse(MassActionHandlerArgs $args, $entitiesCount = 0) {
 		$massAction = $args->getMassAction();
 		$responseMessage = 'dmkclub.payment.datagrid.action.sepa_direct_debit_success_message';
 		$responseMessage = $massAction->getOptions()->offsetGetByPath('[messages][success]', $responseMessage);
 
 		$successful = $entitiesCount > 0;
 		$options = ['count' => $entitiesCount];
 
 		return new MassActionResponse(
 				$successful,
 				$this->translator->transChoice(
 						$responseMessage,
 						$entitiesCount,
 						['%count%' => $entitiesCount]
 				),
 				$options
 		);
 	}
 }

Die Klasse tut jetzt natürlich noch nichts, außer die Anzahl der ausgewählten Elemente zu zählen. Hier müssen wir zwei Fälle unterscheiden. Wenn der Anwender einzelne Datensätze markiert hat, dann bekommen wir von Oro die IDs dieser Datensätze direkt in $data übergeben. Wenn er alle selektiert, dann müssen wir die aktuelle Query des Datagrids auswählen. Denn alle bedeutet alle in der aktuellen Filterung.

Einbindung im Frontend

Serverseitig ist jetzt alles vorbereitet. Nun müssen wir für die Action noch etwas Javascript bereitstellen. Dieses haben wir in den Actionklassen mit dem frontend_type dmksepadebitxml-mass bereits angemeldet. Der Name ist nun Teil einer Konvention. Oro wird in der requirejs.yml nach dem Pfad oro/datagrid/action/dmksepadebitxml-mass-action suchen:

config:
   paths:
   # Der Key wird in der URI verwendet und auf die Datei in Value gerouted
   'oro/datagrid/action/dmksepadebitxml-mass-action': 'bundles/dmkclubpayment/js/datagrid/action/dmksepadebitxml-mass-action.js'

Auch der Pfad auf die Javascript-Datei wird über eine Konvention aufgelöst. Der Prefix bundles/dmkclubpayment/ ergibt sich aus dem aktuellen Bundle. Und js/datagrid/action/dmksepadebitxml-mass-action.js wird dann unter Resources/public/ erwartet. Im Browser wird die Datei über die URL /bundles/oro/datagrid/action/dmksepadebitxml-mass-action.js aufgerufen werden. Der Pfad muss mit dem Key in der requirejs.yml übereinstimmen. Jede Menge Konventionen also!

Die JS-Datei selbst ist dann recht simpel aufgebaut:

define([
   'underscore',
   'oroui/js/messenger',
   'orotranslation/js/translator',
   'oro/datagrid/action/mass-action'
], function(_, messenger, __, MassAction) {
   'use strict';
 
   var CreateSepaDebitAction;
 
   /**
   * Create a SEPA direct debit xml file
   * der Wert in (at)export bezieht sich auf den Dateinamen in der requirejs.yml
   *
   * @export dmkclubpayment/js/datagrid/action/dmkcreatesepadebitxml-mass-action
   * @class oro.datagrid.action.CreateSepaDebitAction
   * @extends oro.datagrid.action.MassAction
   */
   CreateSepaDebitAction = MassAction.extend({
 
   });
 
   return CreateSepaDebitAction;
});

Das kann aber aufwendiger werden, wenn man bspw. noch zusätzliche Widgets einblenden will.

Abschlussarbeiten

Ist alles eingerichtet, dann muss der Oro-Cache geleert werden. Anschließend die Assets neu installieren.

php app/console oro:assets:install --symlink --relative --env=dev
php app/console assetic:dump --env=dev
php app/console fos:js-routing:dump --target=web/js/routes.js --env=dev
php app/console oro:requirejs:build --env=dev
ZURÜCK