WordReplacement - Use generic writeRecords/deleteRecords which call hooks
[civicrm-core.git] / Civi / Api4 / Generic / AbstractUpdateAction.php
index 91e8c3feac70072052882fafb44ae7184cd86871..ba8e3934a81d0ea369681d87b948bbc2b533acee 100644 (file)
  +--------------------------------------------------------------------+
  */
 
-/**
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- */
-
-
 namespace Civi\Api4\Generic;
 
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\Api4\Utils\CoreUtil;
+
 /**
  * Base class for all `Update` api actions
  *
@@ -49,6 +46,67 @@ abstract class AbstractUpdateAction extends AbstractBatchAction {
    */
   protected $reload = FALSE;
 
+  /**
+   * Criteria for selecting items to update.
+   *
+   * Required if no id is supplied in values.
+   *
+   * @var array
+   */
+  protected $where = [];
+
+  abstract protected function updateRecords(array $items): array;
+
+  /**
+   * @inheritDoc
+   */
+  public function _run(Result $result) {
+    $primaryKeys = CoreUtil::getInfoItem($this->getEntityName(), 'primary_key');
+    $this->formatWriteValues($this->values);
+
+    // Add primary keys from values to WHERE clause and check for mismatch
+    foreach ($primaryKeys as $id) {
+      if (!empty($this->values[$id])) {
+        $wheres = array_column($this->where, NULL, 0);
+        if (!isset($wheres[$id])) {
+          $this->addWhere($id, '=', $this->values[$id]);
+        }
+        elseif (!($wheres[$id][1] === '=' && $wheres[$id][2] == $this->values[$id])) {
+          throw new \Exception("Cannot update the $id of an existing " . $this->getEntityName() . '.');
+        }
+      }
+    }
+
+    // Require WHERE if we didn't get primary keys from values
+    if (!$this->where) {
+      throw new \API_Exception('Parameter "where" is required unless primary keys are supplied in values.');
+    }
+
+    // Update a single record by primary key (if this entity has a single primary key)
+    if (count($this->where) === 1 && count($primaryKeys) === 1 && $primaryKeys === $this->getSelect() && $this->where[0][0] === $id && $this->where[0][1] === '=' && !empty($this->where[0][2])) {
+      $this->values[$id] = $this->where[0][2];
+      if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $this->values, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
+        throw new UnauthorizedException("ACL check failed");
+      }
+      $items = [$this->values];
+      $this->validateValues();
+      $result->exchangeArray($this->updateRecords($items));
+      return;
+    }
+
+    // Batch update 1 or more records based on WHERE clause
+    $items = $this->getBatchRecords();
+    foreach ($items as &$item) {
+      $item = $this->values + $item;
+      if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
+        throw new UnauthorizedException("ACL check failed");
+      }
+    }
+
+    $this->validateValues();
+    $result->exchangeArray($this->updateRecords($items));
+  }
+
   /**
    * @param string $fieldName
    *
@@ -70,4 +128,23 @@ abstract class AbstractUpdateAction extends AbstractBatchAction {
     return $this;
   }
 
+  /**
+   * @throws \API_Exception
+   */
+  protected function validateValues() {
+    // FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
+    $e = new ValidateValuesEvent($this, [$this->values], new \CRM_Utils_LazyArray(function () {
+      $existing = $this->getBatchAction()->setSelect(['*'])->execute();
+      $result = [];
+      foreach ($existing as $record) {
+        $result[] = ['old' => $record, 'new' => $this->values];
+      }
+      return $result;
+    }));
+    \Civi::dispatcher()->dispatch('civi.api4.validate', $e);
+    if (!empty($e->errors)) {
+      throw $e->toException();
+    }
+  }
+
 }