You are here

class ReservationConflicts in MERCI (Manage Equipment Reservations, Checkout and Inventory) 8.2

A null implementation of EntityReference_SelectionHandler.

Hierarchy

Expanded class hierarchy of ReservationConflicts

1 file declares its use of ReservationConflicts
ReservationConflictConstraintValidator.php in src/Plugin/Validation/Constraint/ReservationConflictConstraintValidator.php
Contains \Drupal\merci\Plugin\Validation\Constraint\ReservationConflictConstraintValidator.
1 string reference to 'ReservationConflicts'
merci.services.yml in ./merci.services.yml
merci.services.yml
1 service uses ReservationConflicts
merci.reservation_conflicts in ./merci.services.yml
Drupal\merci\ReservationConflicts

File

src/ReservationConflicts.php, line 20
Contains \Drupal\merci\ReservationConflicts. Abstraction of the selection logic of an entity reference field.

Namespace

Drupal\merci
View source
class ReservationConflicts implements ReservationConflictsInterface {
  protected $entity;
  protected $date_field;
  protected $item_field;
  protected $quantity_field;
  protected $validated;
  protected $parent_quantity_field;
  protected $conflicting_entities;
  protected $total_buckets_filled;
  protected $buckets;
  protected $errors;
  protected $date_column, $date_column2;
  public function setEntity(\Drupal\Core\Entity\FieldableEntityInterface $entity) {
    $this->entity = $entity;
  }
  public function getEntity() {
    return $entity;
  }
  public function setDateField($date_field) {
    $this->date_field = $date_field;
    $date_storage = $this->entity
      ->get($this->date_field)
      ->getFieldDefinition()
      ->getFieldStorageDefinition();
    $date_columns = $date_storage
      ->getColumns();
    $this->date_column = $this->date_field . '_' . key($date_columns);
    next($date_columns);
    $this->date_column2 = $this->date_field . '_' . key($date_columns);
  }
  public function getDateField() {
    return $date_field;
  }
  public function setItemField($item_field) {
    $this->item_field = $item_field;
  }
  public function getItemField() {
    return $item_field;
  }
  public function setQuantityField($quantity_field) {
    $this->quantity_field = $quantity_field;
  }
  public function getQuantityField() {
    return $quantity_field;
  }
  public function setParentQuantityField($parent_quantity_field) {
    $this->parent_quantity_field = $parent_quantity_field;
  }
  public function getParentQuantityField() {
    return $parent_quantity_field;
  }
  public function validate() {
    if (!$this->validated) {
      $this->buckets = $this
        ->fillBuckets();
      $this->validated = TRUE;
      $conflicts = array();
      foreach ($this->buckets as $delta => $dates) {
        foreach ($dates as $date_value => $buckets) {
          if (!isset($this->total_buckets_filled[$delta])) {
            $this->total_buckets_filled[$delta] = array();
          }
          $this->total_buckets_filled[$delta][$date_value] = count($buckets);
          if (!isset($conflicts[$delta])) {
            $conflicts[$delta] = array();
          }
          $conflicts[$delta][$date_value] = array();
          foreach ($buckets as $bucket) {
            $conflicts[$delta][$date_value] = array_merge($conflicts[$delta][$date_value], $bucket);
          }
        }
      }
      $this->conflicting_entities = $conflicts;
    }
  }
  public function getErrors($delta = NULL) {
    if ($this->errors === NULL) {
      $this
        ->validate();
      $entity = $this->entity;
      $entity_type = $this->entity
        ->getEntityTypeId();
      $errors = array();

      // Determine if reserving too many of the same item.
      // How many of each item are we trying to reserve?
      if ($entity
        ->hasField($this->parent_quantity_field)) {
        $quantity_reserved = $entity
          ->get($this->parent_quantity_field)->value;
      }
      else {
        $quantity_reserved = 1;
      }

      // How many times was the item selected?
      foreach ($entity
        ->get($this->item_field) as $delta => $resource) {
        $item_id = $resource->target_id;
        if (empty($item_id)) {
          continue;
        }
        if (empty($item_count[$item_id])) {
          $item_count[$item_id] = 0;
        }
        $item_count[$item_id] += $quantity_reserved;
        if ($resource->entity
          ->hasField($this->quantity_field)) {
          $quantity_reservable = $resource->entity
            ->get($this->quantity_field)->value;
        }
        else {
          $quantity_reservable = 1;
        }

        // Did we select too many?
        if ($item_count[$item_id] > $quantity_reservable) {

          // Selected to many.
          if (!array_key_exists($delta, $errors)) {
            $errors[$delta] = array();
          }
          $parents_path = implode('][', array(
            $this->item_field,
            'und',
            $delta,
            'target_id',
          ));
          $errors[$delta][MERCI_ERROR_TOO_MANY] = t('@name: You have selected too many of the same item.  We only have @quantity available but you reserved @reserved.', array(
            '@name' => $resource->entity
              ->label(),
            '@quantity' => $quantity_reservable,
            '@reserved' => $item_count[$item_id],
          ));
        }
      }
      $total_buckets_filled = $this
        ->getTotalBucketsFilled();
      $total_buckets_filled = $total_buckets_filled ? $total_buckets_filled : array();
      $reservation_counter = array();
      foreach ($total_buckets_filled as $delta => $start_dates) {
        $conflict_errors = array();
        $resource = $entity
          ->get($this->item_field)[$delta];
        if ($resource->entity
          ->hasField($this->quantity_field)) {
          $quantity_reservable = $resource->entity
            ->get($this->quantity_field)->value;
        }
        else {
          $quantity_reservable = 1;
        }
        $item_id = $resource->target_id;
        if (empty($reservation_counter[$item_id])) {
          $reservation_counter[$item_id] = 0;
        }
        $reservation_counter[$item_id] += $quantity_reserved;
        foreach ($this->entity
          ->get($this->date_field) as $dates) {
          $used_buckets = $this
            ->getTotalBucketsFilled($delta, $dates);

          // Determine if there are conflicts for this date and item.
          if ($quantity_reservable >= $used_buckets + $reservation_counter[$item_id]) {
            continue;
          }

          // Load each conflicting entity so we can show information about it to
          // the user.
          $ids = array();
          foreach ($this
            ->getConflicts($delta, $dates) as $conflict) {
            $ids[] = $conflict->parent_id;
          }

          // Load the entities which hold the conflicting item.
          $entities = \Drupal::entityManager()
            ->getStorage($entity_type)
            ->loadMultiple($ids);
          $line_items = array();
          foreach ($entities as $id => $line_item) {
            $entity_uri = $line_item
              ->toUrl();

            //entity_uri($entity_type, $line_item);
            $entity_label = $line_item
              ->label();

            //entity_label($entity_type, $line_item);
            $line_items[] = Link::fromTextAndUrl($entity_label, $entity_uri)
              ->toString();
          }
          $date_start = $dates
            ->get('value')
            ->getValue();

          // Don't show the date repeat rule in the error message.
          // @FIXME

          //$render_dates = field_view_value($entity_type, $entity->value(), $this->date_field, $dates);
          $conflict_errors[$date_start] = t('@name is already reserved by: :items for selected dates @dates', array(
            '@name' => $resource->entity
              ->label(),
            ':items' => implode(', ', $line_items),
            '@dates' => render($render_dates),
          ));
        }
        if ($conflict_errors) {
          if (!array_key_exists($delta, $errors)) {
            $errors[$delta] = array();
          }
          $errors[$delta][MERCI_ERROR_CONFLICT] = $conflict_errors;
        }
      }
      $this->errors = $errors;
    }
    return $this->errors;
  }
  public function getConflicts($delta = NULL, $dates = NULL) {
    $this
      ->validate();
    $conflicts = $this->conflicting_entities;
    if ($delta === NULL) {
      return $conflicts;
    }
    if (empty($dates)) {
      return array_key_exists($delta, $conflicts) ? $conflicts[$delta] : FALSE;
    }
    $date_value = $dates
      ->get('value')
      ->getValue();
    return (array_key_exists($delta, $conflicts) and array_key_exists($date_value, $conflicts[$delta])) ? $conflicts[$delta][$date_value] : FALSE;
  }
  public function getTotalBucketsFilled($delta = NULL, $dates = NULL) {
    $this
      ->validate();
    $total_buckets_filled = $this->total_buckets_filled;
    if ($delta === NULL) {
      return $total_buckets_filled;
    }
    if (empty($dates)) {
      return array_key_exists($delta, $total_buckets_filled) ? $total_buckets_filled[$delta] : 0;
    }
    $date_value = $dates
      ->get('value')
      ->getValue();
    return (array_key_exists($delta, $total_buckets_filled) and array_key_exists($date_value, $total_buckets_filled[$delta])) ? $total_buckets_filled[$delta][$date_value] : 0;
  }

  /*
   * Determine if merci_line_item $entity conflicts with any other existing line_items.
   *
   * Returns array of conflicting line items.
   */
  public function conflicts($date) {
    $conflicts = array();
    $date_value = $date
      ->get('value')
      ->getValue();
    $query = $this
      ->buildConflictQuery($date);
    $result = $query
      ->execute();
    $line_item_entities = \Drupal::entityTypeManager()
      ->getStorage($this->entity
      ->getEntityTypeId())
      ->loadMultiple($result);
    foreach ($line_item_entities as $entity) {
      $dates = $entity->{$this->date_field}
        ->getValue();
      $dates = reset($dates);
      foreach ($entity->{$this->item_field} as $item) {
        $target_id = $item->{'target_id'};
        $record = new \stdClass();
        $record->item_id = $target_id;
        $record->parent_id = $entity
          ->id();
        if ($entity
          ->hasField($this->parent_quantity_field)) {
          $record->quantity = (int) $entity
            ->get($this->parent_quantity_field)->value;
        }
        else {
          $record->quantity = 1;
        }
        $record->{$this->date_column} = $dates['value'];
        $record->{$this->date_column2} = $dates['end_value'];
        if (!isset($conflicts[$target_id])) {
          $conflicts[$target_id] = array();
        }
        if (!isset($conflicts[$target_id][$date_value])) {
          $conflicts[$target_id][$date_value] = array();
        }
        $conflicts[$target_id][$date_value][] = $record;
      }
    }
    $return = array();
    $items = $this->entity
      ->get($this->item_field);
    foreach ($items as $delta => $item) {
      if (isset($conflicts[$item->target_id])) {
        $return[$delta] = $conflicts[$item->target_id];
      }
    }
    return $return;
  }
  public function conflictingEntities($date, $item = NULL) {
    $date_value = $date
      ->get('value')
      ->getValue();
    $query = $this
      ->buildConflictQuery($date, $item);
    $query
      ->addTag('debug');
    $result = $query
      ->execute();
    $line_item_entities = \Drupal::entityTypeManager()
      ->getStorage($this->entity
      ->getEntityTypeId())
      ->loadMultiple($result);
    return $line_item_entities;
  }
  public function buildConflictQuery($date, $item = NULL) {
    $exclude_id = $this->entity
      ->id();
    $entity_type = $this->entity
      ->getEntityTypeId();
    $items = array();
    if ($item) {
      $items[] = $item->target_id;
    }
    else {
      foreach ($this->entity
        ->get($this->item_field) as $delta => $item) {
        $items[] = $item->target_id;
      }
    }

    // Build the query.
    // Entity type is the entity holding the date and item fields.
    $query = \Drupal::entityQuery($entity_type);
    if (count($items) == 1) {
      $query
        ->condition($this->item_field, reset($items));
    }
    else {
      $query
        ->condition($this->item_field, $items, 'IN');
    }

    // Ignore myself.
    if ($exclude_id) {
      $entity_type_id_key = $this->entity
        ->getEntityType()
        ->getKey('id');
      $query
        ->condition($entity_type_id_key, $exclude_id, '!=');
    }
    $dates = array(
      'value' => $date
        ->get('value')
        ->getValue(),
      'end_value' => $date
        ->get('end_value')
        ->getValue(),
    );

    //  start falls within another reservation.
    //                     |-------------this-------------|
    //            |-------------conflict-------------------------|
    //            OR
    //                     |-------------this-------------------------------|
    //            |-------------conflict-------------------------|
    $and1 = $query
      ->andConditionGroup()
      ->condition($this->date_field . '.value', $dates['value'], '<=')
      ->condition($this->date_field . '.end_value', $dates['value'], '>=');

    //  end falls within another reservation.
    //                     |-------------this-------------------------------|
    //                                   |-------------conflict-------------------------|
    $and2 = $query
      ->andConditionGroup()
      ->condition($this->date_field . '.value', $dates['end_value'], '<=')
      ->condition($this->date_field . '.end_value', $dates['end_value'], '>=');

    //  start before another reservation.
    //  end after another reservation.
    //                     |-------------------------this-------------------------------|
    //                            |----------------conflict------------------|
    $and3 = $query
      ->andConditionGroup()
      ->condition($this->date_field . '.value', $dates['value'], '>')
      ->condition($this->date_field . '.end_value', $dates['end_value'], '<');
    $or = $query
      ->orConditionGroup()
      ->condition($and1)
      ->condition($and2)
      ->condition($and3);
    $query
      ->condition($or);
    $query
      ->sort($this->date_field . '.value');

    // Add a generic entity access tag to the query.
    $query
      ->addTag('merci_resource');
    $query
      ->addMetaData('merci_reservable_handler', $this);
    return $query;
  }
  public function reservations($dates, $exclude_id) {
    $bestfit = $this
      ->bestFit($dates);
    $reservations = array();
    foreach ($bestfit as $enity_id => $reservation) {
      $reservations[] = $entity_id;
    }
    return $reservations;
  }
  public function fillBuckets() {
    $conflicts = array();
    $dates = $this->entity
      ->get($this->date_field);
    foreach ($dates as $date) {
      $date_value = $date
        ->get('value')
        ->getValue();
      $result = $this
        ->bestFit($date);

      // Result is array indexed by $delta of filled buckets.
      foreach ($result as $delta => $buckets) {
        if (!isset($conflicts[$delta])) {
          $conflicts[$delta] = array();
        }
        $conflicts[$delta][$date_value] = $buckets;
      }
    }
    return $conflicts;
  }

  /*
   * Perform first-fit algorhtym on reservations into buckets.
   *
   * Return array indexed by item delta of array of filled buckets.
   */
  public function bestFit($dates) {
    $entity = $this->entity;
    $best_fit = array();
    $parent_conflicts = $this
      ->conflicts($dates);
    $date_value = $dates
      ->get('value')
      ->getValue();
    foreach ($entity
      ->get($this->item_field) as $delta => $item) {

      // No need to sort into buckets if there is nothing to sort into buckets.
      if (!array_key_exists($delta, $parent_conflicts) or !array_key_exists($date_value, $parent_conflicts[$delta])) {
        continue;
      }
      if ($item->entity
        ->hasField($this->quantity_field)) {
        $quantity = $item->entity
          ->get($this->quantity_field)->value;
      }
      else {
        $quantity = 1;
      }

      // Split reservations based on quantity.
      $reservations = array();
      foreach ($parent_conflicts[$delta][$date_value] as $reservation) {
        for ($i = 0; $i < $reservation->quantity; $i++) {
          $reservations[] = $reservation;
        }
      }

      // Determine how many bucket items are needed for this time period.
      // Need to sort like this:
      //            .... time ....
      // item1  x x a a a x x x x x f x e e e x x x x x
      // item2  x x x d d d d d d x x x x c c c x x x x
      // item3  x x b b b b b b b b b b b b b x x x x x
      // etc ......
      //
      //      // Order by lenght of reservation descending.
      //      // Do first-fit algorythm.
      // Sort by length of reservation.
      uasort($reservations, array(
        $this,
        "merci_bucket_cmp_length",
      ));
      $buckets = array();

      // First-fit algorythm.
      foreach ($reservations as $test_reservation) {

        // Go through each bucket item to look for a available slot for this reservation.
        //
        // Find a bucket to use for this reservation.
        for ($i = 0; $i < $quantity; $i++) {
          $fits = TRUE;

          // Bucket already has other reservations we need to check against for a fit.
          if (array_key_exists($i, $buckets)) {
            foreach ($buckets[$i] as $reservation) {
              if ($this
                ->merci_bucket_intersects($reservation, $test_reservation)) {

                //Conflict so skip saving the reservation to this slot and try to use the next bucket item.
                $fits = FALSE;
                break;
              }
            }
          }

          // We've found a slot so test the next reservation.
          if ($fits) {
            if (array_key_exists($i, $buckets)) {
              $buckets[$i] = array();
            }
            $buckets[$i][] = $test_reservation;
            break;
          }
        }
      }
      if (count($buckets)) {
        $best_fit[$delta] = $buckets;
      }
    }
    return $best_fit;
  }

  /*
   * |----------------------|        range 1
   * |--->                           range 2 overlap
   *  |--->                          range 2 overlap
   *                        |--->    range 2 overlap
   *                         |--->   range 2 no overlap
   */
  private function merci_bucket_intersects($r1, $r2) {
    $value = $this->date_column;
    $end_value = $this->date_column2;

    /*
     * Make sure r1 start date is before r2 start date.
     */
    if (date_create($r1->{$value}) > date_create($r2->{$value})) {
      $temp = $r1;
      $r1 = $r2;
      $r2 = $temp;
    }
    if (date_create($r2->{$value}) <= date_create($r1->{$end_value})) {
      return true;
    }
    return false;
  }
  private function merci_bucket_cmp_length($a, $b) {
    $value = $this->date_column;
    $end_value = $this->date_column2;
    $len_a = date_format(date_create($a->{$end_value}), 'U') - date_format(date_create($a->{$value}), 'U');
    $len_b = date_format(date_create($b->{$end_value}), 'U') - date_format(date_create($b->{$value}), 'U');
    if ($len_a == $len_b) {
      return 0;
    }
    return $len_a < $len_b ? 1 : -1;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ReservationConflicts::$buckets protected property
ReservationConflicts::$conflicting_entities protected property
ReservationConflicts::$date_column protected property
ReservationConflicts::$date_field protected property
ReservationConflicts::$entity protected property
ReservationConflicts::$errors protected property
ReservationConflicts::$item_field protected property
ReservationConflicts::$parent_quantity_field protected property
ReservationConflicts::$quantity_field protected property
ReservationConflicts::$total_buckets_filled protected property
ReservationConflicts::$validated protected property
ReservationConflicts::bestFit public function
ReservationConflicts::buildConflictQuery public function Overrides ReservationConflictsInterface::buildConflictQuery
ReservationConflicts::conflictingEntities public function
ReservationConflicts::conflicts public function Overrides ReservationConflictsInterface::conflicts
ReservationConflicts::fillBuckets public function
ReservationConflicts::getConflicts public function Overrides ReservationConflictsInterface::getConflicts
ReservationConflicts::getDateField public function Overrides ReservationConflictsInterface::getDateField
ReservationConflicts::getEntity public function Overrides ReservationConflictsInterface::getEntity
ReservationConflicts::getErrors public function Overrides ReservationConflictsInterface::getErrors
ReservationConflicts::getItemField public function Overrides ReservationConflictsInterface::getItemField
ReservationConflicts::getParentQuantityField public function
ReservationConflicts::getQuantityField public function
ReservationConflicts::getTotalBucketsFilled public function
ReservationConflicts::merci_bucket_cmp_length private function
ReservationConflicts::merci_bucket_intersects private function
ReservationConflicts::reservations public function
ReservationConflicts::setDateField public function Overrides ReservationConflictsInterface::setDateField
ReservationConflicts::setEntity public function Overrides ReservationConflictsInterface::setEntity
ReservationConflicts::setItemField public function Overrides ReservationConflictsInterface::setItemField
ReservationConflicts::setParentQuantityField public function
ReservationConflicts::setQuantityField public function
ReservationConflicts::validate public function Overrides ReservationConflictsInterface::validate