contacts_events-8.x-1.x-dev/modules/village_allocation/src/AutomaticAllocation.php
modules/village_allocation/src/AutomaticAllocation.php
<?php namespace Drupal\village_allocation; use Drupal\contacts_events\Entity\Event; use Drupal\Core\Database\Connection; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Performs automatic allocation. * * @package Drupal\contacts_events_villages */ class AutomaticAllocation { use DebugTrait; // Need DependencySerializationTrait or Drupal complains about serializing // the database connection when invoked through the form batch api. use DependencySerializationTrait; /** * VA queries. * * @var \Drupal\village_allocation\VillageAllocationQueries */ protected $queries; /** * Logger. * * @var \Psr\Log\LoggerInterface */ protected $log; /** * Database. * * @var \Drupal\Core\Database\Connection */ protected $db; /** * AutomaticAllocation constructor. * * @param \Drupal\Core\Database\Connection $db * Database. * @param \Drupal\village_allocation\VillageAllocationQueries $queries * VA queries. */ public function __construct(Connection $db, VillageAllocationQueries $queries) { $this->db = $db; $this->queries = $queries; } /** * Batch API automatic village allocation process callback. * * Get an ordered list of churches with unassigned bookings and process them * one at a time with the following steps: * * - Check if it already assigned and if so, find the village. * - Otherwise, check if there is a church with a similar postcode (+/-) * and if so find the village. * * If there is a village found we then we attempt to allocate the entire * church moving out one village in each direction each time. * Otherwise we start at* the top of the village list and work our way down. * * When determining whether a village has space, we take the total pitch value * of the entire church and check whether it will fit in the village without * exceeding the fill percentage. * * @param \Drupal\contacts_events\Entity\Event $event * The event being allocated. * @param array $context * Drupal batch context. */ public function process(Event $event, array &$context) { // Set up the sandbox if it's not configured. if (empty($context['sandbox'])) { // Preload group id, pitches, num. orders. $groups = $this->queries->getGroupsForAutomaticAllocation($event->id()); $context['sandbox']['progress'] = 0; $context['sandbox']['max'] = count($groups); $context['sandbox']['groups'] = $groups; $context['results']['assigned'] = 0; $context['results']['skipped'] = 0; $context['results']['error'] = 0; $context['results']['messages'] = []; // Get hold of all the villages in order. $context['sandbox']['villages'] = $this->queries->getVillagesInOrder($event->id()); $this->debug(new TranslatableMarkup('Beginning allocation: @groups groups @villages villages', [ '@groups' => count($groups), '@villages' => count($context['sandbox']['villages']), ])); } // Get hold of the current item. // At this point item is an array of objects with properties // group_id, group_name, orders (count of orders), pitches, postal_code. $groups = array_slice($context['sandbox']['groups'], $context['sandbox']['progress'], 1, TRUE); $item = reset($groups); if ($item && $item->pitches > 0) { $this->debug("Processing group: {$item->group_id} Pitches: {$item->pitches} Bookings: {$item->orders} Postcode: {$item->postal_code}"); // See if any other bookings in this group are already assigned to a // village. If so, we want to put the remainder of this group with them. $village_ids = $this->queries->getVillageIdsAllocatedToGroup($item->group_id); if (count($village_ids)) { $this->debug("Group: {$item->group_id}. Some bookings already allocated to villages: " . implode(', ', $village_ids)); } $assigned = $this->findMatchWithSpace($item->pitches, $village_ids, $context['sandbox']['villages']); if ($assigned) { $this->debug("Group: {$item->group_id}. Assigning to village $assigned"); }; // If not, have a look for a similar postcode. if (!$assigned) { $village_ids = $this->queries->getVillagesContainingBookingsWithSimilarPostcode($event->id(), $item->postal_code); $assigned = $this->findMatchWithSpace($item->pitches, $village_ids, $context['sandbox']['villages']); if (count($village_ids)) { $this->debug("Group: {$item->group_id}. Villages containing bookings with similar postcode: " . print_r($village_ids, TRUE)); if ($assigned) { $this->debug("Group: {$item->group_id}. Assigning to village $assigned"); }; } } // Just run down the village list. if (!$assigned) { $village_ids = array_keys($context['sandbox']['villages']); $assigned = $this->findMatchWithSpace($item->pitches, $village_ids, $context['sandbox']['villages']); $this->debug("Group: {$item->group_id} assigning to first available village: $assigned"); } if ($assigned) { // Get hold of all the unassigned bookings in this group. $orders = $this->queries->getUnassignedBookingsInGroup($event->id(), $item->group_id); if (count($orders) == $item->orders) { $refs = []; /** @var \Drupal\commerce_order\Entity\Order $order */ foreach ($orders as $order_id => $order) { try { $order->set('village', $assigned); $order->save(); $context['results']['assigned']++; $context['sandbox']['villages'][$assigned]->pitches_allocated += $item->pitches; } catch (\Throwable $ex) { $this->debug($ex->getMessage()); $this->debug($ex->getTraceAsString()); $refs[] = $order->getOrderNumber(); } } if (!empty($refs)) { $context['message'] = new TranslatableMarkup('Failed to allocate booking: @booking_refs.', [ '@booking_refs' => implode(', ', $refs), ]); } else { $context['message'] = new TranslatableMarkup('@label assigned to @village.', [ '@label' => $item->group_name, '@village' => $context['sandbox']['villages'][$assigned]->label(), ]); } } else { $context['results']['error'] += $item->orders; $context['message'] = new TranslatableMarkup('Error with @label - there is inconsistent data.', ['@label' => $item->group_name]); } } else { $context['results']['skipped'] += $item->orders; $context['message'] = new TranslatableMarkup('@label skipped - insufficient space.', ['@label' => $item->group_name]); } } elseif ($item) { $context['results']['skipped'] += $item->orders; $context['message'] = new TranslatableMarkup('@label skipped - zero pitches.', ['@label' => $item->group_name]); } // Record progress through the batch. $context['sandbox']['progress']++; $context['finished'] = $context['sandbox']['max'] > 0 ? $context['sandbox']['progress'] / $context['sandbox']['max'] : 1; if (!empty($context['message'])) { $context['results']['messages'] = array_merge($context['results']['messages'], [$context['message']]); } } /** * Iterate over the villages to find a match with space. * * @param float $pitches * The space required for this item. * @param array $village_ids * An array of ids of possible village matches. * @param array $villages * The villages to look within. * * @return int|bool * Either a matched village with space, or FALSE if none found. */ protected function findMatchWithSpace($pitches, array $village_ids, array $villages) { // Find any valid matches. $village_ids = array_intersect(array_keys($villages), $village_ids); // Find a village with space. foreach ($village_ids as $id) { /** @var \Drupal\contacts_events_villages\Entity\Village $village */ $village = $villages[$id]; // pitches_allocated is a surrogate field that will have been added // by VillageAllocationQueries::getVillagesInOrder. if (($village->get('pitches')->value * $village->get('fill_value')->value / 100) >= ($village->pitches_allocated + $pitches)) { // Space found, so return. $this->debug('Checking village ' . $village->getName() . ': Has space'); return $id; } else { $this->debug('Checking village ' . $village->getName() . ': No space'); } } // Nothing found. return FALSE; } }