From 15beac00a473b51ce8b698053a474e7c3483306b Mon Sep 17 00:00:00 2001
From: John Voskuilen <john.voskuilen@sapito.nl>
Date: Thu, 23 Jan 2025 16:54:30 +0100
Subject: [PATCH] Issue #3501772: Add subfield 'status' and use it in View
 filter

---
 office_hours.module                           |  7 ++
 .../Field/FieldType/OfficeHoursItem.php       |  2 +-
 .../Field/FieldType/OfficeHoursItemBase.php   |  9 ++
 .../Field/FieldType/OfficeHoursItemList.php   | 19 ++++
 .../OfficeHoursItemListInterface.php          | 11 +++
 .../Field/FieldType/OfficeHoursStatus.php     | 39 ++++++++
 src/Plugin/views/field/FieldBase.php          | 27 +++++-
 src/Plugin/views/field/Status.php             | 38 ++++++++
 .../views/filter/OfficeHoursStatusFilter.php  | 96 +++++++++----------
 9 files changed, 195 insertions(+), 53 deletions(-)
 create mode 100644 src/Plugin/Field/FieldType/OfficeHoursStatus.php
 create mode 100644 src/Plugin/views/field/Status.php

diff --git a/office_hours.module b/office_hours.module
index fde2afc9..ba1fab84 100644
--- a/office_hours.module
+++ b/office_hours.module
@@ -97,6 +97,13 @@ function office_hours_views_query_substitutions(ViewExecutable $view) {
   return OfficeHoursStatusFilter::viewsQuerySubstitutions($view);
 }
 
+/**
+ * Implements hook_views_pre_execute().
+ */
+function office_hours_views_pre_execute(ViewExecutable $view) {
+  return OfficeHoursStatusFilter::viewsPreExecute($view);
+}
+
 /**
  * Implements hook_views_post_execute().
  */
diff --git a/src/Plugin/Field/FieldType/OfficeHoursItem.php b/src/Plugin/Field/FieldType/OfficeHoursItem.php
index b8fa0d09..f28d6871 100644
--- a/src/Plugin/Field/FieldType/OfficeHoursItem.php
+++ b/src/Plugin/Field/FieldType/OfficeHoursItem.php
@@ -183,7 +183,7 @@ class OfficeHoursItem extends OfficeHoursItemBase {
    * @return int
    *   a predefined constant with the status.
    */
-  public function getStatus($time) {
+  public function getStatus($time = 0): int {
     $status = static::UNDEFINED;
 
     $now_weekday = OfficeHoursDateHelper::getWeekday($time);
diff --git a/src/Plugin/Field/FieldType/OfficeHoursItemBase.php b/src/Plugin/Field/FieldType/OfficeHoursItemBase.php
index ff06e1f4..554ce71d 100644
--- a/src/Plugin/Field/FieldType/OfficeHoursItemBase.php
+++ b/src/Plugin/Field/FieldType/OfficeHoursItemBase.php
@@ -71,6 +71,15 @@ class OfficeHoursItemBase extends FieldItemBase {
       ->addConstraint('Length', ['max' => 255])
       ->setDescription("Stores the comment.");
 
+    // @todo #3501772 Convert to complex datatype, for usingkey/value formatter
+    //$properties['status'] = DataDefinition::create('field_item:list_string')
+    //$properties['status'] = DataDefinition::create('map')
+    $properties['status'] = DataDefinition::create('integer')
+      ->setLabel(t('Status'))
+      ->setDescription(t('Is the entity currently open, currently closed or never open.'))
+      ->setComputed(TRUE)
+      ->setClass('\Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursStatus');
+
     return $properties;
   }
 
diff --git a/src/Plugin/Field/FieldType/OfficeHoursItemList.php b/src/Plugin/Field/FieldType/OfficeHoursItemList.php
index e44cd095..7b231b2b 100644
--- a/src/Plugin/Field/FieldType/OfficeHoursItemList.php
+++ b/src/Plugin/Field/FieldType/OfficeHoursItemList.php
@@ -269,6 +269,25 @@ class OfficeHoursItemList extends FieldItemList implements OfficeHoursItemListIn
     return count($exception_days);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getStatus($time = 0): int {
+    $items = $this;
+
+    switch (TRUE) {
+      case is_null($items):
+      case $items->isEmpty():
+        $status = OfficeHoursStatus::NEVER;
+        break;
+
+      default:
+        $status = $items->isOpen($time);
+        break;
+    }
+    return $status;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/Plugin/Field/FieldType/OfficeHoursItemListInterface.php b/src/Plugin/Field/FieldType/OfficeHoursItemListInterface.php
index 137ca17e..e2cccdd6 100644
--- a/src/Plugin/Field/FieldType/OfficeHoursItemListInterface.php
+++ b/src/Plugin/Field/FieldType/OfficeHoursItemListInterface.php
@@ -122,6 +122,17 @@ interface OfficeHoursItemListInterface extends FieldItemListInterface {
    */
   public function countExceptionDays();
 
+   /**
+   * Returns if an entity currently open, currently closed or never open.
+   *
+   * @param int $time
+   *   A timestamp. Might be adapted for User Timezone.
+   *
+   * @return int
+   *   a predefined constant with the status.
+   */
+  public function getStatus($time = 0): int;
+
   /**
    * Determines if the Entity is Open or Closed.
    *
diff --git a/src/Plugin/Field/FieldType/OfficeHoursStatus.php b/src/Plugin/Field/FieldType/OfficeHoursStatus.php
new file mode 100644
index 00000000..a28f4910
--- /dev/null
+++ b/src/Plugin/Field/FieldType/OfficeHoursStatus.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\office_hours\Plugin\Field\FieldType;
+
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * A computed property for displaying the open/closed status of a field.
+ *
+ * Plugin implementation of the 'list_string' field type.
+ * @see OfficeHoursItemBase~propertyDefinitions()
+ */
+class OfficeHoursStatus extends TypedData {
+  // @todo #3501772 Convert to complex datatype, for usingkey/value formatter
+  // Mapitem, ListStringItem, Map, TypedData {
+
+  public const string ANY = 'all';
+  public const bool CLOSED = FALSE;
+  public const bool OPEN = TRUE;
+  public const int NEVER = 2;
+
+  /**
+   * Cached open/closed status.
+   *
+   * @var int
+   */
+  protected $value = FALSE;
+
+  /**
+   * Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
+   */
+  public function getValue() {
+    $items = $this->getParent()->getParent();
+    $status = $items->getStatus();
+    $this->setValue((int) $status);
+    return $status;
+  }
+
+}
diff --git a/src/Plugin/views/field/FieldBase.php b/src/Plugin/views/field/FieldBase.php
index bbf77059..ab837a7a 100644
--- a/src/Plugin/views/field/FieldBase.php
+++ b/src/Plugin/views/field/FieldBase.php
@@ -26,6 +26,7 @@ class FieldBase extends FieldPluginBase {
     $columns = [
       'season' => 'office_hours_season',
       'timeslot' => 'office_hours_timeslot',
+      'status' => 'office_hours_status',
     ];
 
     foreach ($data as $table_name => $table_data) {
@@ -91,6 +92,30 @@ class FieldBase extends FieldPluginBase {
         unset($field_data['filter']);
         unset($field_data['sort']);
 
+        // Extend 'Status'.
+        $column = 'status';
+        $label = $field_label . ' - Status';
+        $real_field = 'delta';
+        $title = t('@label (@name:@column)',
+          ['@label' => $label, '@name' => $field_name, '@column' => $column]
+        );
+        $title_short = t('@label:@column',
+          ['@label' => $label, '@column' => $column]);
+
+        // @todo Do not take over all field attributes.
+        $field_data = &$data[$table_name][$field_name . "_$column"];
+        // Use ?? to avoid TypeError: Unsupported operand types: array + null.
+        // This may have side effects, since data should exist. @see #3421574.
+        $field_data += $data[$table_name][$field_name] ?? [];
+        $field_data['field'] += $data[$table_name][$real_field]['field'] ?? [];
+        $field_data['field']['real field'] = $real_field;
+        $field_data['field']['property'] = $real_field;
+        $field_data['title'] = $title;
+        $field_data['title short'] = $title_short;
+        unset($field_data['argument']);
+        // Filter is set in \views\filter\OfficeHoursStatusFilter.
+        // unset($field_data['filter']);
+        unset($field_data['sort']);
       }
     }
 
@@ -157,7 +182,7 @@ class FieldBase extends FieldPluginBase {
 
     $entity = $this->getEntity($values);
     // Entities with no / empty office_hours will have delta = NULL.
-    $delta = $values->{$table . '_delta'} ?? NULL;
+    $delta = $values->{"{$table}_delta"} ?? NULL;
     // So, no need to check for $entity->hasField($field_name).
     if (!is_null($delta)) {
       $items = $entity->get($field_name);
diff --git a/src/Plugin/views/field/Status.php b/src/Plugin/views/field/Status.php
new file mode 100644
index 00000000..658c1dfe
--- /dev/null
+++ b/src/Plugin/views/field/Status.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\office_hours\Plugin\views\field;
+
+use Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursStatus;
+use Drupal\views\ResultRow;
+
+/**
+ * Computed field to display the open/closed status.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("office_hours_status")
+ *
+ * @see https://www.drupal.org/docs/drupal-apis/entity-api/dynamicvirtual-field-values-using-computed-field-property-classes
+ */
+class Status extends FieldBase {
+
+  /**
+   * Called to add the field to a query.
+   */
+  public function query() {
+    // Do not add the computed subfield to the query.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(ResultRow $values) {
+    $entity = $values->_entity;
+    $field_name = $this->configuration['field_name'];
+    $property_name = 'status';
+
+    $result = $entity->{$field_name}->{$property_name} ?? OfficeHoursStatus::NEVER;
+    return $result; // $this->sanitizeValue($result);
+  }
+
+}
diff --git a/src/Plugin/views/filter/OfficeHoursStatusFilter.php b/src/Plugin/views/filter/OfficeHoursStatusFilter.php
index de117154..a9fcf69d 100644
--- a/src/Plugin/views/filter/OfficeHoursStatusFilter.php
+++ b/src/Plugin/views/filter/OfficeHoursStatusFilter.php
@@ -3,12 +3,13 @@
 namespace Drupal\office_hours\Plugin\views\filter;
 
 use Drupal\field\FieldStorageConfigInterface;
+use Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursStatus;
 use Drupal\views\Plugin\views\cache\CachePluginBase;
 use Drupal\views\Plugin\views\filter\ManyToOne;
 use Drupal\views\ViewExecutable;
 
 /**
- * Filter by open/closed status.
+ * Views Filter by open/closed status.
  *
  * @ingroup views_filter_handlers
  *
@@ -20,17 +21,14 @@ use Drupal\views\ViewExecutable;
  * @see https://www.webomelette.com/creating-custom-views-filter-drupal-8
  * @see https://www.drupal.org/docs/drupal-apis/entity-api/dynamicvirtual-field-values-using-computed-field-property-classes
  * @see https://drupal.stackexchange.com/questions/249963/how-to-add-a-custom-views-filter-handler-for-a-specific-field
+ * @see https://drupal.stackexchange.com/questions/291236/creating-a-custom-field-with-dynamic-virtual-computed-property-value
  */
 class OfficeHoursStatusFilter extends ManyToOne {
 
   /*
    * Duplicate of the @ViewsFilter annotation.
    */
-  const VIEWS_FILTER_ID = "office_hours_is_open";
-  const ANY = 'all';
-  const CLOSED = FALSE;
-  const OPEN = TRUE;
-  const NEVER = 2;
+  public const string VIEWS_FILTER_ID = "office_hours_is_open";
 
   /**
    * Implements hook_field_views_data().
@@ -65,10 +63,10 @@ class OfficeHoursStatusFilter extends ManyToOne {
    */
   public function getValueOptions() {
     $this->valueOptions = [
-      static::ANY => $this->t('Select all'),
-      static::OPEN => $this->t('Open now'),
-      static::CLOSED => $this->t('Temporarily closed'),
-      static::NEVER => $this->t('Permanently closed'),
+      OfficeHoursStatus::ANY => $this->t('Select all'),
+      OfficeHoursStatus::OPEN => $this->t('Open now'),
+      OfficeHoursStatus::CLOSED => $this->t('Temporarily closed'),
+      OfficeHoursStatus::NEVER => $this->t('Permanently closed'),
     ];
 
     return $this->valueOptions;
@@ -106,60 +104,43 @@ class OfficeHoursStatusFilter extends ManyToOne {
       return;
     }
 
-    $filterValue = $filter->value;
-    $filterValue = array_filter($filterValue, function ($statusValue) {
-      return $statusValue !== 0;
-    });
-
-    if (in_array(static::ANY, $filterValue)) {
-      return;
-    }
-
-    $fieldName = $filter->realField;
+    // Remove duplicate rows from the view.
     $previous_id = -1;
-    /** @var \Drupal\views\ResultRow $value */
     foreach ($view->result as $key => $value) {
-      // Remove duplicate rows from the view.
       $id = $value->_entity->id();
       if ($previous_id === $id) {
         unset($view->result[$key]);
-        continue;
       }
       $previous_id = $id;
+    }
 
-      // Remove filtered rows from the view.
-      // Since this is a calculated field, it cannot be done via query().
-      /** @var \Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursItemList $items */
-      $items = $value->_entity->$fieldName;
-      if (is_null($items) || $items->isEmpty()) {
-        if (!in_array(static::NEVER, $filterValue)) {
-          unset($view->result[$key]);
-        }
-        continue;
-      }
+    $filterValue = $filter->value;
+    if (empty($filterValue)) {
+      return;
+    }
+    if (in_array(OfficeHoursStatus::ANY, $filterValue)) {
+      return;
+    }
 
-      $is_open = $items->isOpen();
-      if ($is_open && in_array((int) static::OPEN, $filterValue)) {
-        continue;
-      }
-      if ((!$is_open) && in_array((int) static::CLOSED, $filterValue)) {
-        continue;
+    // Remove filtered rows from the view.
+    // Since this is a calculated field, it cannot be done via query().
+    $fieldName = $filter->realField;
+    foreach ($view->result as $key => $value) {
+      $id = $value->_entity->id();
+      $status = $value->_entity->$fieldName->getStatus($time = 0);
+      if (!in_array($status, $filterValue)) {
+        unset($view->result[$key]);
       }
-      unset($view->result[$key]);
     }
-
   }
 
   /**
    * {@inheritdoc}
    */
   public function query() {
+    // Do not add query details for this computed field. No SQL is possible.
     // The views.inc file is not always loaded. Lazy load here.
     \Drupal::moduleHandler()->loadInclude('office_hours', 'inc', 'office_hours.views');
-
-    // Do not add query details, since this is a computed field,
-    // and no SQL is possible.
-    // parent::query();
   }
 
   /**
@@ -172,29 +153,42 @@ class OfficeHoursStatusFilter extends ManyToOne {
     return ['***OFFICE_HOURS_REQUEST_TIME***' => \Drupal::time()->getRequestTime()];
   }
 
+  /**
+   * Implements hook_views_pre_execute().
+   */
+  public static function viewsPreExecute(ViewExecutable $view) {
+    // Nothing to do here.
+    // if (static::getFilter($view)) {
+    // self::filter($view);
+    // }
+  }
+
   /**
    * Implements hook_views_post_execute().
    */
   public static function viewsPostExecute(ViewExecutable $view) {
     // Nothing to do here.
+    if (static::getFilter($view)) {
+      self::filter($view);
+    }
   }
 
   /**
    * Implements hook_field_views_pre_render().
    */
   public static function viewsPreRender(ViewExecutable $view) {
-    if (static::getFilter($view)) {
-      self::filter($view);
-    }
+    // if (static::getFilter($view)) {
+    // self::filter($view);
+    // }
   }
 
   /**
    * Implements hook_views_post_render().
    */
   public static function viewsPostRender(ViewExecutable $view, array &$output, CachePluginBase $cache) {
-    if (!static::getFilter($view)) {
-      return;
-    }
+    // if (static::getFilter($view)) {
+    // return;
+    // }
 
     // @todo Improve time-based caching (is_open/closed status),
     // setting $output['#cache']['max-age'] from $items->getCacheMaxAge().
-- 
GitLab