farm-2.x-dev/modules/core/location/src/EventSubscriber/LogEventSubscriber.php
modules/core/location/src/EventSubscriber/LogEventSubscriber.php
<?php namespace Drupal\farm_location\EventSubscriber; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\farm_location\AssetLocationInterface; use Drupal\farm_location\LogLocationInterface; use Drupal\farm_geo\Traits\WktTrait; use Drupal\log\Entity\LogInterface; use Drupal\log\Event\LogEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Populate log geometry and invalidate asset cache on movement logs. */ class LogEventSubscriber implements EventSubscriberInterface { use WktTrait; /** * The name of the log asset field. * * @var string */ const LOG_FIELD_ASSET = 'asset'; /** * Log location service. * * @var \Drupal\farm_location\LogLocationInterface */ protected LogLocationInterface $logLocation; /** * Asset location service. * * @var \Drupal\farm_location\AssetLocationInterface */ protected AssetLocationInterface $assetLocation; /** * Cache tag invalidator service. * * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface */ protected CacheTagsInvalidatorInterface $cacheTagsInvalidator; /** * LogEventSubscriber Constructor. * * @param \Drupal\farm_location\LogLocationInterface $log_location * Log location service. * @param \Drupal\farm_location\AssetLocationInterface $asset_locaiton * Asset location service. * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator * Cache tag invalidator service. */ public function __construct(LogLocationInterface $log_location, AssetLocationInterface $asset_locaiton, CacheTagsInvalidatorInterface $cache_tags_invalidator) { $this->logLocation = $log_location; $this->assetLocation = $asset_locaiton; $this->cacheTagsInvalidator = $cache_tags_invalidator; } /** * {@inheritdoc} * * @return array * The event names to listen for, and the methods that should be executed. */ public static function getSubscribedEvents() { return [ LogEvent::DELETE => 'logDelete', LogEvent::PRESAVE => 'logPresave', ]; } /** * Perform actions on log delete. * * @param \Drupal\log\Event\LogEvent $event * The log event. */ public function logDelete(LogEvent $event) { $this->invalidateAssetCacheOnMovement($event->log); } /** * Perform actions on log presave. * * @param \Drupal\log\Event\LogEvent $event * The log event. */ public function logPresave(LogEvent $event) { // Get the log entity from the event. $log = $event->log; // Populate the log geometry from the location geometry. $this->populateGeometryFromLocation($log); // Invalidate asset caches when assets are moved. $this->invalidateAssetCacheOnMovement($log); } /** * Populate a log's geometry based on its location. * * @param \Drupal\log\Entity\LogInterface $log * The Log entity. */ protected function populateGeometryFromLocation(LogInterface $log): void { // Load location assets referenced by the log. $assets = $this->getLocationAssets($log); // If the log does not reference any location assets, we will have nothing // to copy from, so do nothing. if (empty($assets)) { return; } // If this is a new log and it has a geometry, do nothing. if (empty($log->original) && $this->logLocation->hasGeometry($log)) { return; } // If this is an update to an existing log, and the new geometry is not // empty, perform some checks to see if we should proceed or not. We always // want to proceed if the updated log's geometry is empty because this is // an indication that it was cleared manually by the user in order to // re-populate it. if (!empty($log->original) && $this->logLocation->hasGeometry($log)) { // If the original log has a custom geometry, do nothing. if ($this->hasCustomGeometry($log->original)) { return; } // If the geometry has changed, do nothing. $old_geometry = $this->logLocation->getGeometry($log->original); $new_geometry = $this->logLocation->getGeometry($log); if ($old_geometry != $new_geometry) { return; } } // Get the combined location asset geometry. $wkt = $this->getCombinedAssetGeometry($assets); // If the WKT is not empty, set the log geometry. if (!empty($wkt)) { $this->logLocation->setGeometry($log, $wkt); } } /** * Check if a log has a custom geometry. * * This is determined by checking to see if the log's geometry matches that * of the location assets it references. If it does not, and it is not empty, * them we assume it has a custom geometry. * * @param \Drupal\log\Entity\LogInterface $log * The Log entity. * * @return bool * Returns TRUE if it matches, FALSE otherwise. */ protected function hasCustomGeometry(LogInterface $log): bool { // If the log's geometry is empty, then it does not have a custom geometry. if (!$this->logLocation->hasGeometry($log)) { return FALSE; } // Load location assets referenced by the log. $assets = $this->getLocationAssets($log); // Get the combined location asset geometry. $location_geometry = $this->getCombinedAssetGeometry($assets); // Get the log geometry. $log_geometry = $this->logLocation->getGeometry($log); // Compare the log and location geometries. return $log_geometry != $location_geometry; } /** * Get location assets referenced by the log. * * This will first check for assets in the location reference field. If none * are found, it will also look for location assets in the asset reference * field. * * @param \Drupal\log\Entity\LogInterface $log * The Log entity. * * @return \Drupal\asset\Entity\AssetInterface[] * An array of location assets. */ protected function getLocationAssets(LogInterface $log) { // Load location assets referenced by the log. $assets = $this->logLocation->getLocation($log); // If there are no assets in the location reference field, look for location // assets in the asset reference field. Only do this if the log is not a // movement, otherwise it would be impossible to clear the geometry of a // non-fixed location asset via movement Logs. if (empty($assets) && !$log->get('is_movement')->value) { foreach ($log->{static::LOG_FIELD_ASSET}->referencedEntities() as $asset) { if ($this->assetLocation->isLocation($asset)) { $assets[] = $asset; } } } return $assets; } /** * Load a combined set of location asset geometries. * * @param \Drupal\asset\Entity\AssetInterface[] $assets * An array of location assets. * * @return string * Returns a WKT string of the combined asset geometries. */ protected function getCombinedAssetGeometry(array $assets): string { // Collect all the location geometries. $geoms = []; foreach ($assets as $asset) { if ($this->assetLocation->hasGeometry($asset)) { $geoms[] = $this->assetLocation->getGeometry($asset); } } // Combine the geometries into a single WKT string. $wkt = $this->combineWkt($geoms); return $wkt; } /** * Invalidate asset caches when assets are moved. * * @param \Drupal\log\Entity\LogInterface $log * The Log entity. */ protected function invalidateAssetCacheOnMovement(LogInterface $log): void { // Keep track if we need to invalidate the cache of referenced assets so // the computed 'location' and 'geometry' fields are updated. $update_asset_cache = FALSE; // If the log is a 'done' movement log, invalidate the cache. if ($this->isActiveMovementLog($log)) { $update_asset_cache = TRUE; } // If updating an existing 'done' movement log, invalidate the cache. // This catches any movement logs changing from done to pending. if (!empty($log->original) && $this->isActiveMovementLog($log->original)) { $update_asset_cache = TRUE; } // If an update is not necessary, bail. if (!$update_asset_cache) { return; } // Build a list of cache tags. // @todo Only invalidate cache if the movement log changes the asset's current location. This might be different for each asset. $tags = []; // Include assets that were previously referenced. if (!empty($log->original)) { foreach ($log->original->get('asset')->referencedEntities() as $asset) { array_push($tags, ...$asset->getCacheTags()); } } // Include assets currently referenced by the log. foreach ($log->get('asset')->referencedEntities() as $asset) { array_push($tags, ...$asset->getCacheTags()); } // Invalidate the cache tags. $this->cacheTagsInvalidator->invalidateTags($tags); } /** * Helper method to check if a log is an active movement log. * * @param \Drupal\log\Entity\LogInterface $log * The Log entity. * * @return bool * Boolean indicating if the log is an active movement log. */ public static function isActiveMovementLog(LogInterface $log): bool { return $log->get('status')->value == 'done' && $log->get('is_movement')->value && $log->get('timestamp')->value <= \Drupal::time()->getCurrentTime(); } }