wayfinding-2.1.x-dev/src/Query.php
src/Query.php
<?php
namespace Drupal\wayfinding;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\wayfinding\Entity\Wayfinding;
use Drupal\wayfinding\Event\Libraries;
use Drupal\wayfinding\Event\QueryEvent;
use Drupal\wayfinding\Plugin\views\style\Master;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Wayfinding query service.
*/
class Query {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected EntityTypeManager $entityTypeManager;
/**
* The entity type and bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected Connection $connection;
/**
* The configuration.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected ImmutableConfig $config;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The core renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected RequestStack $requestStack;
/**
* The list of source entities.
*
* @var \Drupal\Core\Entity\ContentEntityInterface[]|null
*/
protected ?array $sources = NULL;
/**
* Constructs an Entity update service.
*
* @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type and bundle info service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The core renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(EntityTypeManager $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityFieldManagerInterface $entity_field_manager, Connection $connection, ConfigFactoryInterface $config_factory, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RequestStack $request_stack) {
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityFieldManager = $entity_field_manager;
$this->connection = $connection;
$this->config = $config_factory->get('wayfinding.settings');
$this->eventDispatcher = $event_dispatcher;
$this->moduleHandler = $module_handler;
$this->renderer = $renderer;
$this->requestStack = $request_stack;
}
/**
* Get the media entity for the device-specific background.
*
* @param float $perspective
* The perspective.
* @param string $type
* The type.
* @param int $id
* The ID.
*
* @return \Drupal\Core\Entity\ContentEntityInterface|null
* The media entity if available, NULL otherwise.
*/
public function getMedia(float $perspective, string $type = 'user', int $id = 1): ?ContentEntityInterface {
try {
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
$entities = $this->entityTypeManager->getStorage('media')
->loadByProperties([
'field_perspective' => $perspective,
'field_destination' => [
'target_id' => $id,
],
]);
foreach ($entities as $entity) {
if ($entity->get('field_destination')->getValue()[0]['target_type'] === $type) {
return $entity;
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately ignored.
}
return NULL;
}
/**
* Build the rendered svg image.
*
* @param float $perspective
* The perspective.
* @param string $type
* The type.
* @param int $id
* The ID.
* @param bool $source
* TRUE, if the SVG source code is required, FALSE otherwise.
*
* @return array|string
* The SVG source code or the render array of an image.
*/
public function getRenderedMedia(float $perspective, string $type = 'user', int $id = 1, bool $source = FALSE): array|string {
if ($media = $this->getMedia($perspective, $type, $id)) {
if ($source && $file = File::load($media->get('field_svg_image')->getValue()[0]['target_id'])) {
$content = file_get_contents($file->getFileUri());
if ($pos = strpos($content, '<svg')) {
$content = substr($content, $pos);
}
return $content;
}
return $this->entityTypeManager->getViewBuilder($media->getEntityTypeId())
->view($media, 'wayfinding');
}
return [];
}
/**
* Build the rendered pin svg image.
*
* @param bool $removeSvgWrapper
* TRUE, if the SVG wrapper should be removed so that the code can be
* embedded into another SVG image.
*
* @return string
* The PIN code.
*/
public function getRenderedPin(bool $removeSvgWrapper = FALSE): string {
$content = file_get_contents($this->config->get('pin'));
if ($pos = strpos($content, '<svg')) {
$content = substr($content, $pos);
}
if ($removeSvgWrapper) {
$pos = strpos($content, '>');
$content = substr($content, $pos + 1);
$content = str_replace('</svg>', '', $content);
}
return $content;
}
/**
* Query the database for existing perspectives.
*
* @return array
* The list of existing perspectives.
*/
public function getPerspectives(): array {
$perspectives = ['0.00' => 0.00];
if ($this->moduleHandler->moduleExists('wayfinding_digital_signage')) {
/* @noinspection NullPointerExceptionInspection */
$perspectives += $this->connection
->query('# noinspection SqlResolve
select distinct(perspective) from {digital_signage_device_field_data} where perspective is not null and perspective > 0')
->fetchAllKeyed(0, 0);
}
return $perspectives;
}
/**
* Helper function to prepare source entities once.
*/
private function prepareSource(): void {
if ($this->sources === NULL) {
$this->sources = [];
foreach (Master::getDestinations() as $item) {
try {
/** @var \Drupal\Core\Entity\ContentEntityInterface $destination */
$destination = $this->entityTypeManager->getStorage($item['target_type'])->load($item['target_id']);
foreach ($this->getSources($destination) as $source) {
if (!in_array($item['target_id'], $this->sources[$source->getEntityTypeId()][$source->id()][$item['target_type']] ?? [], TRUE)) {
$this->sources[$source->getEntityTypeId()][$source->id()][$item['target_type']][] = $item['target_id'];
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately no handling.
}
}
}
}
/**
* Get the ID of the destination entity for a given source entity ID.
*
* @param array $srcid
* The source entity ID.
*
* @return array
* The list of destination entity IDs.
*/
public function getDestinationsForId(array $srcid): array {
$this->prepareSource();
$destinations = [];
foreach ($this->sources[$srcid['target_type']][$srcid['target_id']] ?? [] as $type => $ids) {
foreach ($ids as $id) {
$destinations[] = [
'target_type' => $type,
'target_id' => $id,
];
}
}
return $destinations;
}
/**
* Get the Drupal javascript settings.
*
* @param float $perspective
* The perspective.
* @param float $lat
* The latitude.
* @param float $lng
* The longitude.
*
* @return array
* The settings.
*/
public function getSettings(float $perspective, float $lat = 0, float $lng = 0): array {
if (($media = $this->getMedia($perspective)) && $positions = $media->get('field_top_left')->getValue()) {
$position = $positions[0];
}
else {
$position = [
'lat' => 0,
'lng' => 0,
];
}
return [
'origin' => trim(Url::fromUserInput('/', [
'absolute' => TRUE,
])->toString(TRUE)->getGeneratedUrl(), '/'),
'widgeturl' => trim(Url::fromRoute('wayfinding.widgets', [], [
'absolute' => TRUE,
])->toString(TRUE)->getGeneratedUrl(), '/'),
'pinDynamicPosition' => $this->config->get('pin dynamic position'),
'resetTimeout' => $this->config->get('reset timeout'),
'perspective' => $perspective,
'location' => $this->config->get('location'),
'topleft' => [
'lat' => $position['lat'],
'lng' => $position['lng'],
],
'position' => [
'lat' => $lat,
'lng' => $lng,
],
];
}
/**
* Tbd.
*
* @param bool $inline
* Tbd.
* @param float $perspective
* Tbd.
* @param float $lat
* Tbd.
* @param float $lng
* Tbd.
* @param int $did
* Tbd.
* @param string $eid
* Tbd.
*
* @return array
* Tbd.
*/
public function build(bool $inline, float $perspective, float $lat = 0, float $lng = 0, int $did = 0, string $eid = 'undefined'): array {
$event = new Libraries();
$event->addLibrary('wayfinding/general');
$this->eventDispatcher->dispatch($event, WayfindingEvents::LIBRARIES);
$attached = [
'drupalSettings' => [
'wayfinding' => $this->getSettings($perspective, $lat, $lng),
],
'library' => $event->getLibraries(),
];
if ($did === 0) {
$attached['library'][] = 'wayfinding/nodevice';
}
$output = [
'#theme' => $inline ? 'wayfinding_inline' : 'wayfinding',
'#view' => views_embed_view('wayfinding', 'wayfinding_wrapper_1', $perspective, $did),
'#blocks' => [],
'#did' => $did,
'#eid' => $eid,
'#attached' => $inline ? $attached : [],
];
return $inline ? $output : [
'#markup' => $this->renderer->renderInIsolation($output),
'#attached' => $attached + [
'html_response_attachment_placeholders' => [
'styles' => '<styles/>',
'scripts' => '<scripts/>',
'scripts_bottom' => '<scripts_bottom/>',
],
'placeholders' => [],
],
];
}
/**
* Get the source entities for a given destination.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $destination
* The destination.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* The list of source entities.
*/
protected function getSources(ContentEntityInterface $destination): array {
$sources = [];
$sourceTypes = $this->config->get('types.sources');
// Get all sources referenced by the destination.
foreach ($this->entityFieldManager->getFieldDefinitions($destination->getEntityTypeId(), $destination->bundle()) as $field) {
if ($field->getName() !== 'digital_signage' &&
!str_starts_with($field->getName(), 'revision_') &&
($field->getType() === 'entity_reference' || $field->getType() === 'dynamic_entity_reference')) {
$valid = FALSE;
if ($field->getType() === 'entity_reference') {
$valid = isset($sourceTypes[$field->getSetting('target_type')]);
}
elseif ($field->getType() === 'dynamic_entity_reference') {
foreach ($field->getSetting('entity_type_ids') as $type) {
if (isset($sourceTypes[$type])) {
$valid = TRUE;
break;
}
}
}
if ($valid) {
foreach ($destination->get($field->getName())->getValue() as $item) {
$target_type = $item['target_type'] ?? $field->getSetting('target_type');
$this->addSource($sources, $target_type, $item['target_id']);
}
}
}
}
// Get all sources referencing this destination.
foreach ($sourceTypes as $sourceType) {
foreach (array_keys($this->entityTypeBundleInfo->getBundleInfo($sourceType)) as $bundle) {
foreach ($this->entityFieldManager->getFieldDefinitions($sourceType, $bundle) as $field) {
if ($field->getName() !== 'digital_signage' &&
!str_starts_with($field->getName(), 'revision_') &&
($field->getType() === 'entity_reference' || $field->getType() === 'dynamic_entity_reference')) {
$valid = FALSE;
if ($field->getType() === 'entity_reference') {
$valid = $field->getSetting('target_type') === $destination->getEntityTypeId();
}
elseif ($field->getType() === 'dynamic_entity_reference') {
foreach ($field->getSetting('entity_type_ids') as $type) {
if ($type === $destination->getEntityTypeId()) {
$valid = TRUE;
break;
}
}
}
if ($valid) {
try {
$ids = $this->entityTypeManager->getStorage($sourceType)
->getQuery()
->accessCheck(FALSE)
->condition($field->getName(), $destination->id())
->execute();
foreach ($ids as $id) {
$this->addSource($sources, $sourceType, (int) $id);
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately ignored.
}
}
}
}
}
}
$event = new QueryEvent($destination, $sources);
$this->eventDispatcher->dispatch($event, WayfindingEvents::QUERY);
return array_merge($sources, $event->getSources());
}
/**
* Add a source entity to the list of sources.
*
* @param array $sources
* The list of existing sources.
* @param string $type
* The type of the new source.
* @param int $id
* The ID of the new source.
*/
protected function addSource(array &$sources, string $type, int $id): void {
try {
/** @var \Drupal\Core\Entity\ContentEntityInterface|null $source */
$source = $this->entityTypeManager->getStorage($type)->load($id);
if ($source) {
$value = $source->get('wayfinding')->getValue();
if ($value) {
/** @var \Drupal\wayfinding\Entity\Wayfinding|null $wayfinding */
$wayfinding = Wayfinding::load($value[0]['target_id']);
if ($wayfinding && $wayfinding->isEnabled()) {
$sources[] = $source;
}
}
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately ignored.
}
}
/**
* Get the popup destination.
*
* @return string
* The popup destination.
*/
public function getPopupDestination(): string {
return $this->config->get('popup destination');
}
/**
* Get the popup content.
*
* @return string
* The popup content.
*/
public function getPopupContent(): string {
if (!$this->config->get('enable popups') || $this->requestStack->getCurrentRequest()->get('no-popup-content', FALSE)) {
return '';
}
$this->prepareSource();
$output = '';
foreach ($this->sources as $entity_type => $sources) {
foreach ($sources as $entity_id => $destinations) {
$output .= $this->buildPopupContent($entity_type, (int) $entity_id);
}
}
return $output;
}
/**
* Helper function to build popup content.
*
* @param string $entityType
* The entity type.
* @param int $entityId
* The entity ID.
*
* @return string
* The rendered content for the given entity.
*/
private function buildPopupContent(string $entityType, int $entityId): string {
try {
$entity = $this->entityTypeManager->getStorage($entityType)->load($entityId);
if ($entity) {
$output = $this->entityTypeManager->getViewBuilder($entityType)->view($entity, 'wayfinding');
$srcid = 'wayfinding-src-id-' . $entity->getEntityTypeId() . '-' . $entity->id();
return '<div class="popup-content ' . $srcid . '">' . $this->renderer->renderInIsolation($output) . '</div>';
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
// Deliberately ignored.
}
return '';
}
}
