contacts_events-8.x-1.x-dev/modules/village_allocation/src/Plugin/rest/resource/VillageAllocationBookingsResource.php
modules/village_allocation/src/Plugin/rest/resource/VillageAllocationBookingsResource.php
<?php
namespace Drupal\village_allocation\Plugin\rest\resource;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\search_api\Utility\QueryHelper as SearchApiQueryHelper;
use Drupal\views\ResultRow;
use Drupal\views\Views;
use Drupal\village_allocation\VillageAllocationGroup;
use Drupal\village_allocation\VillageAllocationQueries;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides bookings list.
*
* @RestResource(
* id = "village_allocation_bookings",
* label = @Translation("Village Allocation Bookings"),
* uri_paths = {
* "canonical" = "/admin/village_allocation/bookings/{event_id}"
* }
* )
*/
class VillageAllocationBookingsResource extends ResourceBase {
const VIEW_ID = 'camping_groups_indexed_';
const VIEW_DISPLAY_ID = 'block_1';
/**
* Queries.
*
* @var \Drupal\village_allocation\VillageAllocationQueries
*/
protected $queries;
/**
* Facets.
*
* @var \Drupal\facets\FacetManager\DefaultFacetManager
*/
protected $facetManager;
/**
* Search API query tool.
*
* @var \Drupal\search_api\Utility\QueryHelper
*/
protected $searchApiQuery;
/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $db;
/**
* Drupal rendering service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, VillageAllocationQueries $queries, DefaultFacetManager $facet_manager, SearchApiQueryHelper $search_api_query, Connection $db, RendererInterface $renderer, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->queries = $queries;
$this->facetManager = $facet_manager;
$this->searchApiQuery = $search_api_query;
$this->db = $db;
$this->renderer = $renderer;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('village_allocation.queries'),
$container->get('facets.manager'),
$container->get('search_api.query_helper'),
$container->get('database'),
$container->get('renderer'),
$container->get('entity_type.manager')
);
}
/**
* Request handler for GETs.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Current HTTP request.
* @param string $event_id
* Event ID.
*
* @return \Drupal\rest\ResourceResponse
* The resource for returning to the client. This will be a JSON object
* with the following keys:
* - count: total number of bookings
* - groups: camping groups
* - facets: information on the solr facets that can be used for filtering.
*/
public function get(Request $request, $event_id) {
$village_filter = $request->query->get('village', [])['value'];
$village_operator = $request->query->get('village_op');
$village_groups = $this->getGroupsFromSolrSearch($count);
$groups = $this->getVillageAllocationGroups($event_id, $village_filter, $village_operator, $village_groups);
$facets = $this->getFacets();
// Get the array representation of each group.
$groups = array_map(function ($group) {
return $group->toArray();
}, $groups);
// Only want the array values, so the serializer gives us a JS array
// rather than a JS object.
$groups = array_values($groups);
$response = [
'count' => $count ?? 0,
'page_size' => $this->getPageSize(),
'groups' => $groups,
'facets' => $facets,
];
// Don't want the response cached.
return (new ResourceResponse($response))
->addCacheableDependency([
'#cache' => ['max-age' => 0],
]);
}
/**
* Gets the village allocation groups.
*
* @param string $event_id
* Event ID.
* @param int $village
* ID of the village to limit to. This could be 0 to indicate unallocated.
* @param string $village_operator
* Operator to use for the village filter. May be "=" or ">" etc.
* @param \Drupal\contacts_events_villages\Entity\VillageGroup[] $groups
* Group IDs to filter the bookings. Used by the solr facets.
*
* @return \Drupal\village_allocation\VillageAllocationGroup[]
* Array of village allocation groups.
*/
protected function getVillageAllocationGroups($event_id, $village, $village_operator, array $groups) {
// Get the bookings within the groups we've found.
$bookings = $this->queries->getBookingsInManualAllocationGroups($event_id, $village, $village_operator, array_keys($groups));
$booking_ids_with_village_hosts = $this->queries->getBookingIdsWithVillageHosts($event_id);
// Preload all the booking entities based on the ids.
/** @var \Drupal\commerce_order\Entity\Order[] $orders */
$orders = $this->entityTypeManager->getStorage('commerce_order')->loadMultiple(array_keys($bookings));
// With group bookings enabled, we want to aggregate all the bookings by
// their Group Booking, so that only the Group Bookings appear in the
// Allocation Groups.
foreach ($bookings as $id => $booking) {
// If the ID of this order is the same as the group booking ID, then this
// is the Group Booking. Alternatively if there's no Group Booking ID
// treat this as a Group Booking too.
$is_group_booking = $booking->group_booking_id == $booking->order_id || $booking->group_booking_id == NULL;
if (!$is_group_booking && isset($bookings[$booking->group_booking_id])) {
// This is not a group booking. Add it to the parent group booking
// then remove it from the list.
$bookings[$booking->group_booking_id]->childBookings[$id] = $booking;
unset($bookings[$id]);
}
}
/** @var \Drupal\village_allocation\VillageAllocationGroup[] $allocation_groups */
$allocation_groups = [];
// Create the groups.
foreach ($groups as $group) {
$group_id = $group->id();
$group_name = $group->getName() ?? 'Unknown';
$allocation_groups[$group_id] = new VillageAllocationGroup($group_id, $group_name);
}
// Add bookings to their corresponding Village Groups.
foreach ($bookings as $booking) {
$order = $orders[$booking->order_id];
$group_id = $booking->group_id ?? 0;
$is_village_host_in_booking = in_array($order->id(), $booking_ids_with_village_hosts);
$pitches = floatval($booking->pitches) ?? 0;
$child_bookings = [];
// If this is a group booking, also add in the child pitches.
if (isset($booking->childBookings)) {
foreach ($booking->childBookings as $child_booking) {
$child_order = $orders[$child_booking->order_id];
$child_pitches = floatval($child_booking->pitches) ?? 0;
$pitches += $child_pitches;
// Take into account if the village host is in a child booking.
if (in_array($child_order->id(), $booking_ids_with_village_hosts)) {
$is_village_host_in_booking = TRUE;
}
$child_bookings[] = [
'order' => $child_order,
'pitches' => $child_pitches,
];
}
}
$allocation_groups[$group_id]->addBooking($order, $pitches, $is_village_host_in_booking, $child_bookings);
}
$this->calculateVillagesAllocated($allocation_groups);
return $allocation_groups;
}
/**
* Calculates villages allocated and applies the results to the groups.
*
* Note that this does not use the village IDs returned by the bookings query
* as the bookings query may have filtered out certain bookings (depending on
* the village filter). So we use a separate query to get the villages of all
* bookings in the groups, regardless of village filter.
*
* @param \Drupal\village_allocation\VillageAllocationGroup[] $groups
* Array of village allocation groups to process.
*/
protected function calculateVillagesAllocated(array &$groups) {
if (!count($groups)) {
return;
}
$group_ids = array_keys($groups);
$q = $this->db->select('commerce_order', 'o');
$q->leftJoin('commerce_order__village_group', 'vg', 'o.order_id = vg.entity_id');
$q->innerJoin('commerce_order__village', 'v', 'o.order_id = v.entity_id');
$q->addField('v', 'village_target_id', 'village_id');
$q->addField('vg', 'village_group_target_id', 'group_id');
$q->condition('vg.village_group_target_id', $group_ids, 'IN');
$q->condition('o.state', 'draft', '<>');
$q->distinct();
$results = $q->execute()->fetchAll();
foreach ($results as $result) {
$groups[$result->group_id ?? 0]->addVillage($result->village_id);
}
}
/**
* Gets facets that can be used on the village allocation page.
*
* @return array
* Array of facets containing elements id, name and items. Each item is
* an array with elements name, count (int), showCount (bool),
* filter (string) and id.
*/
protected function getFacets() {
// The ID of our facet source based on the view that indexes camping
// groups.
$facet_source_id = 'search_api:views_block__' . self::VIEW_ID . '__' . self::VIEW_DISPLAY_ID;
$facets = $this->facetManager->getFacetsByFacetSourceId($facet_source_id);
$facets_for_serialization = [];
foreach ($facets as $facet) {
// There isn't really a good api to get the facet info without having
// to re-implement a lot of what the facet manager does. Instead, let the
// facet manager build its render array, and extract the facet info from
// this to build our json. This causes the solr query to be run
// (and the results cached) so we don't need to explicitly run it again
// later.
$build = $this->facetManager->build($facet);
$facet_json = [
'id' => $facet->id(),
'name' => $facet->getName(),
'items' => [],
];
foreach ($build[0]['#items'] as $item) {
if ($item['#type'] == 'link') {
$facet_json['items'][] = [
'name' => $item['#title']['#value'],
'count' => $item['#title']['#count'],
'showCount' => $item['#title']['#show_count'],
// Extract the filter querystring from the url.
'filter' => $item['#url']->getOption('query')['f'][0],
'id' => $item['#attributes']['data-drupal-facet-item-id'],
];
}
}
$facets_for_serialization[] = $facet_json;
}
return $facets_for_serialization;
}
/**
* Gets the village group IDs returned by the solr search.
*
* @param int $count
* Output parameter - total results.
*
* @return \Drupal\contacts_events_villages\Entity\VillageGroup[]
* Array of groups.
*/
private function getGroupsFromSolrSearch(&$count) {
// Need to execute the view inside in a rendering context
// otherwise Drupal contains about leaked cache metadata.
// This is only necessary because of the exposed filters on the view.
$view = $this->renderer->executeInRenderContext(new RenderContext(), function () {
$view = Views::getView(self::VIEW_ID);
$view->setDisplay(self::VIEW_DISPLAY_ID);
$view->execute();
return $view;
});
// The solr results don't give us back entities (or even entity ids)
// but instead we get an array of searchApi items. We can infer the IDs of
// the VA groups from the this.
// The ids of the search api results will be in the format:
// entity:c_events_village_group/3:en
// So we can infer the id (3) by splitting at the / character and then
// stripping out the colon and everything after.
$entity_ids = array_map(function (ResultRow $row) {
/** @var \Drupal\search_api\Item\Item $item */
$item = $row->_item;
$id = explode('/', $item->getId());
// Take everything to the right of the slash (eg 3:en)
$id = $id[1];
$id = explode(':', $id);
// Everything left of the colon (eg 3).
return $id[0];
}, $view->result);
// Now we can load the groups with these IDs.
$groups = $this->entityTypeManager->getStorage('c_events_village_group')
->loadMultiple($entity_ids);
// We also need to know the total number of groups that match the search
// before paging was applied, so we can build the pager.
// This is not available on the view but is available on the cached solr
// results. Calling getResults with the same query ID will return a cached
// copy, and will not execute the query again.
$solr_search_id = 'views_block:' . self::VIEW_ID . '__' . self::VIEW_DISPLAY_ID;
$solr_results = $this->searchApiQuery->getResults($solr_search_id);
$count = $solr_results->getResultCount();
return $groups;
}
/**
* Gets the page size.
*
* @return int
* Page size.
*/
private function getPageSize() {
/** @var \Drupal\views\Entity\View $view */
$view = $this->entityTypeManager->getStorage('view')
->load(self::VIEW_ID);
// Pager is stored on the default display, not the block display.
$display = $view->getDisplay('default');
return $display['display_options']['pager']['options']['items_per_page'];
}
}
