APIv4 - Refactor writeObjects to choose BAO function based on annotations
authorColeman Watts <coleman@civicrm.org>
Thu, 6 Jan 2022 22:46:10 +0000 (17:46 -0500)
committerColeman Watts <coleman@civicrm.org>
Mon, 10 Jan 2022 21:18:03 +0000 (16:18 -0500)
Instead of using a hardcoded list of BAO functions to call, with extra
hardcoded params to e.g. the Contact & Address BAOs, this switches
to picking the BAO's create/add only if it's not `@deprecated`,
and falling back to WriteRecords.

Also refactors the large `writeObjects` function into two smaller
functions so individual action classes can more easily override them.

13 files changed:
Civi/Api4/Action/Address/AddressSaveTrait.php
Civi/Api4/Action/CiviCase/CiviCaseSaveTrait.php
Civi/Api4/Action/Contact/Save.php [new file with mode: 0644]
Civi/Api4/Action/Contact/Update.php [new file with mode: 0644]
Civi/Api4/Action/EntityTag/Create.php [new file with mode: 0644]
Civi/Api4/Action/EntityTag/EntityTagSaveTrait.php [new file with mode: 0644]
Civi/Api4/Action/EntityTag/Save.php [new file with mode: 0644]
Civi/Api4/Action/EntityTag/Update.php [new file with mode: 0644]
Civi/Api4/Action/GroupContact/GroupContactSaveTrait.php
Civi/Api4/Contact.php
Civi/Api4/EntityTag.php
Civi/Api4/Generic/Traits/CustomValueActionTrait.php
Civi/Api4/Generic/Traits/DAOActionTrait.php

index 1a2746f1db9b16418b8d82131e545ad525ba9ae6..de055b08f54b759016158cf5b4723974af04432b 100644 (file)
@@ -47,14 +47,16 @@ trait AddressSaveTrait {
   /**
    * @inheritDoc
    */
-  protected function writeObjects(&$items) {
-    foreach ($items as &$item) {
+  protected function write(array $items) {
+    $saved = [];
+    foreach ($items as $item) {
       if ($this->streetParsing && !empty($item['street_address'])) {
         $item = array_merge($item, \CRM_Core_BAO_Address::parseStreetAddress($item['street_address']));
       }
       $item['skip_geocode'] = $this->skipGeocode;
+      $saved[] = \CRM_Core_BAO_Address::add($item, $this->fixAddress);
     }
-    return parent::writeObjects($items);
+    return $saved;
   }
 
 }
index b112a4a7c007723bc4c2b4d7935ce851079dad45..6b50e7593666a8d674e8ae7215fd232604efd7ab 100644 (file)
@@ -18,20 +18,19 @@ namespace Civi\Api4\Action\CiviCase;
 trait CiviCaseSaveTrait {
 
   /**
-   * @param array $cases
+   * @param array $items
    * @return array
    */
-  protected function writeObjects(&$cases) {
-    $cases = array_values($cases);
-    $result = parent::writeObjects($cases);
-
-    // If the case doesn't have an id, it's new & needs to be opened.
-    foreach ($cases as $idx => $case) {
+  protected function write(array $items) {
+    $saved = [];
+    foreach ($items as $case) {
+      $saved[] = $result = \CRM_Case_BAO_Case::create($case);
+      // If the case doesn't have an id, it's new & needs to be opened.
       if (empty($case['id'])) {
-        $this->openCase($case, $result[$idx]['id']);
+        $this->openCase($case, $result->id);
       }
     }
-    return $result;
+    return $saved;
   }
 
   /**
diff --git a/Civi/Api4/Action/Contact/Save.php b/Civi/Api4/Action/Contact/Save.php
new file mode 100644 (file)
index 0000000..5c08909
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\Contact;
+
+/**
+ * @inheritDoc
+ */
+class Save extends \Civi\Api4\Generic\DAOSaveAction {
+
+  /**
+   * @param array $items
+   * @return array
+   */
+  protected function write(array $items) {
+    $saved = [];
+    foreach ($items as $item) {
+      // For some reason the contact BAO requires this for updates
+      if (!empty($item['id']) && !\CRM_Utils_System::isNull($item['id'])) {
+        $item['contact_id'] = $item['id'];
+      }
+      $saved[] = \CRM_Contact_BAO_Contact::create($item);
+    }
+    return $saved;
+  }
+
+}
diff --git a/Civi/Api4/Action/Contact/Update.php b/Civi/Api4/Action/Contact/Update.php
new file mode 100644 (file)
index 0000000..0743d57
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\Contact;
+
+/**
+ * @inheritDoc
+ */
+class Update extends \Civi\Api4\Generic\DAOUpdateAction {
+
+  /**
+   * @param array $items
+   * @return array
+   */
+  protected function write(array $items) {
+    $saved = [];
+    foreach ($items as $item) {
+      // For some reason the contact BAO requires this for updates
+      $item['contact_id'] = $item['id'];
+      $saved[] = \CRM_Contact_BAO_Contact::create($item);
+    }
+    return $saved;
+  }
+
+}
diff --git a/Civi/Api4/Action/EntityTag/Create.php b/Civi/Api4/Action/EntityTag/Create.php
new file mode 100644 (file)
index 0000000..afa0dba
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\EntityTag;
+
+/**
+ * @inheritDoc
+ */
+class Create extends \Civi\Api4\Generic\DAOCreateAction {
+  use EntityTagSaveTrait;
+
+}
diff --git a/Civi/Api4/Action/EntityTag/EntityTagSaveTrait.php b/Civi/Api4/Action/EntityTag/EntityTagSaveTrait.php
new file mode 100644 (file)
index 0000000..be8ff4b
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\EntityTag;
+
+/**
+ * @inheritDoc
+ */
+trait EntityTagSaveTrait {
+
+  /**
+   * Override method which defaults to 'create' for oddball DAO which uses 'add'
+   *
+   * @param array $items
+   * @return array
+   */
+  protected function write(array $items) {
+    $saved = [];
+    foreach ($items as $item) {
+      $saved[] = \CRM_Core_BAO_EntityTag::add($item);
+    }
+    return $saved;
+  }
+
+}
diff --git a/Civi/Api4/Action/EntityTag/Save.php b/Civi/Api4/Action/EntityTag/Save.php
new file mode 100644 (file)
index 0000000..e0819f1
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\EntityTag;
+
+/**
+ * @inheritDoc
+ */
+class Save extends \Civi\Api4\Generic\DAOSaveAction {
+  use EntityTagSaveTrait;
+
+}
diff --git a/Civi/Api4/Action/EntityTag/Update.php b/Civi/Api4/Action/EntityTag/Update.php
new file mode 100644 (file)
index 0000000..7ae4573
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\EntityTag;
+
+/**
+ * @inheritDoc
+ */
+class Update extends \Civi\Api4\Generic\DAOUpdateAction {
+  use EntityTagSaveTrait;
+
+}
index 0060ba0487fcc0828809a754ebacb18d00ced3b8..4ceea29b40354671454152265502555473d6e964 100644 (file)
@@ -39,12 +39,12 @@ trait GroupContactSaveTrait {
   /**
    * @inheritDoc
    */
-  protected function writeObjects(&$items) {
+  protected function write(array $items) {
     foreach ($items as &$item) {
       $item['method'] = $this->method;
       $item['tracking'] = $this->tracking;
     }
-    return parent::writeObjects($items);
+    return \CRM_Contact_BAO_GroupContact::writeRecords($items);
   }
 
 }
index 34f6463f5530f8c5f28962f67c3330352255d88f..800d8f6aec9b37f3686f280d594bdbe73e2679d8 100644 (file)
@@ -26,6 +26,24 @@ namespace Civi\Api4;
  */
 class Contact extends Generic\DAOEntity {
 
+  /**
+   * @param bool $checkPermissions
+   * @return Action\Contact\Update
+   */
+  public static function update($checkPermissions = TRUE) {
+    return (new Action\Contact\Update(__CLASS__, __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
+  /**
+   * @param bool $checkPermissions
+   * @return Action\Contact\Save
+   */
+  public static function save($checkPermissions = TRUE) {
+    return (new Action\Contact\Save(__CLASS__, __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
   /**
    * @param bool $checkPermissions
    * @return Action\Contact\Delete
index 57070981873d2dfc62eaf43c82233b6525378c5d..d0d8f22a8f95919e96252a05f4980b899e86fee6 100644 (file)
@@ -21,4 +21,31 @@ namespace Civi\Api4;
 class EntityTag extends Generic\DAOEntity {
   use Generic\Traits\EntityBridge;
 
+  /**
+   * @param bool $checkPermissions
+   * @return Action\EntityTag\Create
+   */
+  public static function create($checkPermissions = TRUE) {
+    return (new Action\EntityTag\Create('EntityTag', __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
+  /**
+   * @param bool $checkPermissions
+   * @return Action\EntityTag\Save
+   */
+  public static function save($checkPermissions = TRUE) {
+    return (new Action\EntityTag\Save('EntityTag', __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
+  /**
+   * @param bool $checkPermissions
+   * @return Action\EntityTag\Update
+   */
+  public static function update($checkPermissions = TRUE) {
+    return (new Action\EntityTag\Update('EntityTag', __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
 }
index 280f5bd75b41a373d3cf02ed6f427131f5a556b1..e194ef6af4c91119e14c12bf96e4c2bf15a52291 100644 (file)
@@ -63,7 +63,7 @@ trait CustomValueActionTrait {
   /**
    * @inheritDoc
    */
-  protected function writeObjects(&$items) {
+  protected function writeObjects($items) {
     $fields = $this->entityFields();
     foreach ($items as $idx => $item) {
       FormattingUtil::formatWriteParams($item, $fields);
index ced33ad1ab5fc675ba6ff71abdc266da8325f23d..201f4c2ab98ac56c2ee580d6d2c946bacbe932cb 100644 (file)
@@ -16,6 +16,7 @@ use Civi\Api4\CustomField;
 use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable;
 use Civi\Api4\Utils\FormattingUtil;
 use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\ReflectionUtils;
 
 /**
  * @method string getLanguage()
@@ -105,24 +106,8 @@ trait DAOActionTrait {
    * @throws \API_Exception
    * @throws \CRM_Core_Exception
    */
-  protected function writeObjects(&$items) {
-    $baoName = $this->getBaoName();
+  protected function writeObjects($items) {
     $updateWeights = FALSE;
-
-    // TODO: Opt-in more entities to use the new writeRecords BAO method.
-    $functionNames = [
-      'Address' => 'add',
-      'CustomField' => 'writeRecords',
-      'EntityTag' => 'add',
-      'GroupContact' => 'add',
-      'Navigation' => 'writeRecords',
-      'WordReplacement' => 'writeRecords',
-    ];
-    $method = $functionNames[$this->getEntityName()] ?? NULL;
-    if (!isset($method)) {
-      $method = method_exists($baoName, 'create') ? 'create' : (method_exists($baoName, 'add') ? 'add' : 'writeRecords');
-    }
-
     // Adjust weights for sortable entities
     if (in_array('SortableEntity', CoreUtil::getInfoItem($this->getEntityName(), 'type'))) {
       $weightField = CoreUtil::getInfoItem($this->getEntityName(), 'order_by');
@@ -145,39 +130,18 @@ trait DAOActionTrait {
         $this->updateWeight($item);
       }
 
-      // Skip individual processing if using writeRecords
-      if ($method === 'writeRecords') {
-        continue;
-      }
       $item['check_permissions'] = $this->getCheckPermissions();
+    }
 
-      // For some reason the contact bao requires this
-      if ($entityId && $this->getEntityName() === 'Contact') {
-        $item['contact_id'] = $entityId;
-      }
-
-      if ($this->getEntityName() === 'Address') {
-        $createResult = $baoName::$method($item, $this->fixAddress);
-      }
-      else {
-        $createResult = $baoName::$method($item);
-      }
+    // Ensure array keys start at 0
+    $items = array_values($items);
 
-      if (!$createResult) {
+    foreach ($this->write($items) as $index => $dao) {
+      if (!$dao) {
         $errMessage = sprintf('%s write operation failed', $this->getEntityName());
         throw new \API_Exception($errMessage);
       }
-
-      $result[] = $this->baoToArray($createResult, $item);
-    }
-
-    // Use bulk `writeRecords` method if the BAO doesn't have a create or add method
-    // TODO: reverse this from opt-in to opt-out and default to using `writeRecords` for all BAOs
-    if ($method === 'writeRecords') {
-      $items = array_values($items);
-      foreach ($baoName::writeRecords($items) as $i => $createResult) {
-        $result[] = $this->baoToArray($createResult, $items[$i]);
-      }
+      $result[] = $this->baoToArray($dao, $items[$index]);
     }
 
     \CRM_Utils_API_HTMLInputCoder::singleton()->decodeRows($result);
@@ -185,6 +149,31 @@ trait DAOActionTrait {
     return $result;
   }
 
+  /**
+   * Overrideable function to save items using the appropriate BAO function
+   *
+   * @param array[] $items
+   *   Items already formatted by self::writeObjects
+   * @return \CRM_Core_DAO[]
+   *   Array of saved DAO records
+   */
+  protected function write(array $items) {
+    $saved = [];
+    $baoName = $this->getBaoName();
+
+    $method = method_exists($baoName, 'create') ? 'create' : (method_exists($baoName, 'add') ? 'add' : NULL);
+    // Use BAO create or add method if not deprecated
+    if ($method && !ReflectionUtils::isMethodDeprecated($baoName, $method)) {
+      foreach ($items as $item) {
+        $saved[] = $baoName::$method($item);
+      }
+    }
+    else {
+      $saved = $baoName::writeRecords($items);
+    }
+    return $saved;
+  }
+
   /**
    * @inheritDoc
    */