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;
}
}
