farm-2.x-dev/modules/core/location/src/Plugin/Validation/Constraint/CircularAssetLocationConstraintValidator.php
modules/core/location/src/Plugin/Validation/Constraint/CircularAssetLocationConstraintValidator.php
<?php
namespace Drupal\farm_location\Plugin\Validation\Constraint;
use Drupal\asset\Entity\AssetInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\farm_location\AssetLocationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the CircularAssetLocation constraint.
*/
class CircularAssetLocationConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* Asset location service.
*
* @var \Drupal\farm_location\AssetLocationInterface
*/
protected $assetLocation;
/**
* CircularAssetLocationConstraintValidator constructor.
*
* @param \Drupal\farm_location\AssetLocationInterface $asset_location
* Asset location service.
*/
public function __construct(AssetLocationInterface $asset_location) {
$this->assetLocation = $asset_location;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('asset.location'),
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\Core\Field\EntityReferenceFieldItemList $value */
/** @var \Drupal\farm_location\Plugin\Validation\Constraint\CircularAssetLocationConstraint $constraint */
// Get the log that this field is on.
$log = $value->getParent()->getValue();
// If the log is not a movement, we have nothing to validate.
if (empty($log->get('is_movement')->value)) {
return;
}
// Get the locations(s) that asset(s) are being moved to.
$locations = $log->get('location')->referencedEntities();
// If there are no locations, we have nothing to validate.
if (empty($locations)) {
return;
}
// Get the log's timestamp.
$timestamp = $log->get('timestamp')->value;
// Iterate through referenced entities.
foreach ($value->referencedEntities() as $delta => $asset) {
// Load assets that are located in the asset being referenced.
// Use our own method to recurse into sub-location assets as well.
$assets_in_location = $this->getAssetsByLocationRecursively($asset, $timestamp);
// Make sure that none of the assets are located in this asset.
// Iterate through the locations and check for violations.
$violation = FALSE;
foreach ($locations as $location) {
// Make sure that the asset and location are not the same.
if ($location->id() == $asset->id()) {
$violation = TRUE;
}
// Make sure that none of the assets are located in this asset.
foreach ($assets_in_location as $asset_in_location) {
if ($location->id() == $asset_in_location->id()) {
$violation = TRUE;
break;
}
}
}
// If a violation was found, flag it.
if ($violation) {
$this->context->buildViolation($constraint->message, ['%asset' => $asset->label()])
->atPath((string) $delta . '.target_id')
->setInvalidValue($asset->id())
->addViolation();
}
}
}
/**
* Recursively get all assets in a location based on movement logs.
*
* @param \Drupal\asset\Entity\AssetInterface $location
* The location asset.
* @param int $timestamp
* Include logs with a timestamp less than or equal to this.
*
* @return \Drupal\asset\Entity\AssetInterface[]
* An array of assets in the location.
*/
private function getAssetsByLocationRecursively(AssetInterface $location, int $timestamp) {
// This should never limit itself to assets with `is_location` property
// set to TRUE, because that property can change. And changes to it are on
// the asset itself, not on the log that we're validating here. This means
// that if we only checked `is_location` assets here, they could potentially
// be changed later, making it possible for a "valid" log to become
// "invalid" without editing it. In order to thoroughly prevent circular
// asset location we need to be able to be able to find assets that are
// "located" in both location and non-location assets.
// @see Drupal\farm_location\AssetLocation::getAssetsByLocation()
// Get assets in this location.
$assets = $this->assetLocation->getAssetsByLocation([$location], $timestamp);
// Recurse into each asset to get any other assets located in each.
foreach ($assets as $asset) {
$assets += $this->getAssetsByLocationRecursively($asset, $timestamp);
}
// Return assets.
return $assets;
}
}
