Move Email, Address, etc. is_primary handling on delete to a hook
authorColeman Watts <coleman@civicrm.org>
Sun, 22 Aug 2021 00:00:32 +0000 (20:00 -0400)
committerColeman Watts <coleman@civicrm.org>
Fri, 10 Sep 2021 12:01:28 +0000 (08:01 -0400)
Previously this was all done in a delegated function.
This moves that logic to a hook listener.

CRM/Contact/BAO/Contact.php
CRM/Core/BAO/Address.php
CRM/Core/BAO/Email.php
CRM/Core/BAO/IM.php
CRM/Core/BAO/OpenID.php
CRM/Core/BAO/Phone.php
CRM/Core/DAO.php
tests/phpunit/api/v4/Entity/AddressTest.php [new file with mode: 0644]

index cb855c0b46db49c3d32442f2ecc45e61d06f656b..ab2850c07b34a2545485538de71e6f19e79b0934 100644 (file)
@@ -14,7 +14,7 @@
  * @package CRM
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
-class CRM_Contact_BAO_Contact extends CRM_Contact_DAO_Contact {
+class CRM_Contact_BAO_Contact extends CRM_Contact_DAO_Contact implements Civi\Test\HookInterface {
 
   /**
    * SQL function used to format the phone_numeric field via trigger.
@@ -3511,53 +3511,31 @@ LEFT JOIN civicrm_address ON ( civicrm_address.contact_id = civicrm_contact.id )
   }
 
   /**
-   * Delete a contact-related object that has an 'is_primary' field.
-   *
-   * Ensures that is_primary gets assigned to another object if available
-   * Also calls pre/post hooks
-   *
-   * @param string $type
-   * @param int $id
-   *
-   * @return bool
+   * Event fired after modifying any entity.
+   * @param \Civi\Core\Event\PostEvent $event
    */
-  public static function deleteObjectWithPrimary($type, $id) {
-    if (!$id || !is_numeric($id)) {
-      return FALSE;
-    }
-    $daoName = "CRM_Core_DAO_$type";
-    $obj = new $daoName();
-    $obj->id = $id;
-    $obj->find();
-
-    if ($obj->fetch()) {
-      CRM_Utils_Hook::pre('delete', $type, $id);
-      $contactId = $obj->contact_id;
-      $obj->delete();
-    }
-    else {
-      return FALSE;
-    }
-    // is_primary is only relavent if this field belongs to a contact
-    if ($contactId) {
-      $dao = new $daoName();
-      $dao->contact_id = $contactId;
+  public static function on_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
+    // Handle deleting a related entity with is_primary
+    $hasPrimary = ['Address', 'Email', 'IM', 'OpenID', 'Phone'];
+    if (
+      $event->action === 'delete' && $event->id &&
+      in_array($event->entity, $hasPrimary) &&
+      !empty($event->object->is_primary) &&
+      !empty($event->object->contact_id)
+    ) {
+      $daoClass = CRM_Core_DAO_AllCoreTables::getFullName($event->entity);
+      $dao = new $daoClass();
+      $dao->contact_id = $event->object->contact_id;
       $dao->is_primary = 1;
       // Pick another record to be primary (if one isn't already)
       if (!$dao->find(TRUE)) {
         $dao->is_primary = 0;
-        $dao->find();
-        if ($dao->fetch()) {
-          $dao->is_primary = 1;
-          $dao->save();
-          if ($type === 'Email') {
-            CRM_Core_BAO_UFMatch::updateUFName($dao->contact_id);
-          }
+        if ($dao->find(TRUE)) {
+          $baoClass = CRM_Core_DAO_AllCoreTables::getBAOClassName($daoClass);
+          $baoClass::writeRecord(['id' => $dao->id, 'is_primary' => 1]);
         }
       }
     }
-    CRM_Utils_Hook::post('delete', $type, $id, $obj);
-    return TRUE;
   }
 
   /**
index 480501d81589ad5e147aabd1714f0d25863d6d6f..f7c8ca8ecc3ca461c11eae347b489dbfafb50f95 100644 (file)
@@ -1210,12 +1210,14 @@ SELECT is_primary,
   /**
    * Call common delete function.
    *
-   * @param int $id
+   * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
    *
+   * @param int $id
+   * @deprecated
    * @return bool
    */
   public static function del($id) {
-    return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Address', $id);
+    return (bool) self::deleteRecord(['id' => $id]);
   }
 
   /**
index 5341997ecb8d50fe7d37d80e30090029e0540adc..1261608d23648bac0f20945ab9d06ad56a8206d6 100644 (file)
@@ -20,7 +20,7 @@ use Civi\Api4\Email;
 /**
  * This class contains functions for email handling.
  */
-class CRM_Core_BAO_Email extends CRM_Core_DAO_Email {
+class CRM_Core_BAO_Email extends CRM_Core_DAO_Email implements Civi\Test\HookInterface {
   use CRM_Contact_AccessTrait;
 
   /**
@@ -80,15 +80,21 @@ WHERE  contact_id = {$params['contact_id']}
       self::updateContactName($contactId, $address);
     }
 
-    if ($email->is_primary) {
-      // update the UF user email if that has changed
-      CRM_Core_BAO_UFMatch::updateUFName($email->contact_id);
-    }
-
     CRM_Utils_Hook::post($hook, 'Email', $email->id, $email);
     return $email;
   }
 
+  /**
+   * Event fired after modifying an Email.
+   * @param \Civi\Core\Event\PostEvent $event
+   */
+  public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
+    if ($event->action !== 'delete' && !empty($event->object->is_primary) && !empty($event->object->contact_id)) {
+      // update the UF user email if that has changed
+      CRM_Core_BAO_UFMatch::updateUFName($event->object->contact_id);
+    }
+  }
+
   /**
    * Takes an associative array and adds email.
    *
@@ -342,12 +348,14 @@ AND    reset_date IS NULL
   /**
    * Call common delete function.
    *
-   * @param int $id
+   * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
    *
+   * @param int $id
+   * @deprecated
    * @return bool
    */
   public static function del($id) {
-    return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Email', $id);
+    return (bool) self::deleteRecord(['id' => $id]);
   }
 
   /**
index 12fc6238d433536474772b52ab9d575842136431..a26e6c4ea34e5fd0e454d6f552662ef5e2fdbc63 100644 (file)
@@ -158,12 +158,14 @@ ORDER BY cim.is_primary DESC, im_id ASC ";
   /**
    * Call common delete function.
    *
-   * @param int $id
+   * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
    *
+   * @param int $id
+   * @deprecated
    * @return bool
    */
   public static function del($id) {
-    return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('IM', $id);
+    return (bool) self::deleteRecord(['id' => $id]);
   }
 
 }
index 33a7c269fc3c4292a2a426fd9413b7cb28e48bf7..2b16d2c702f68013df3c767e767177441fc37f14 100644 (file)
@@ -121,12 +121,14 @@ ORDER BY
   /**
    * Call common delete function.
    *
-   * @param int $id
+   * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
    *
+   * @param int $id
+   * @deprecated
    * @return bool
    */
   public static function del($id) {
-    return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('OpenID', $id);
+    return (bool) self::deleteRecord(['id' => $id]);
   }
 
 }
index 260b2e69736ab34e2cd7d7ad88621c17f09efb51..6f9da9eccb516a131fee3082cd770c5d820bd44b 100644 (file)
@@ -234,12 +234,14 @@ ORDER BY ph.is_primary DESC, phone_id ASC ";
   /**
    * Call common delete function.
    *
-   * @param int $id
+   * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
    *
+   * @param int $id
+   * @deprecated
    * @return bool
    */
   public static function del($id) {
-    return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Phone', $id);
+    return (bool) self::deleteRecord(['id' => $id]);
   }
 
 }
index 04c63d5a631c298d211be9efde0a8c44e3223e3e..774b3548b60e7997fe85c759ebf4a46b728c03bd 100644 (file)
@@ -962,9 +962,14 @@ class CRM_Core_DAO extends DB_DataObject {
     CRM_Utils_Hook::pre('delete', $entityName, $record['id'], $record);
     $instance = new $className();
     $instance->id = $record['id'];
-    if (!$instance->delete()) {
+    // Load complete object for the sake of hook_civicrm_post, below
+    $instance->find(TRUE);
+    if (!$instance || !$instance->delete()) {
       throw new CRM_Core_Exception("Could not delete {$entityName} id {$record['id']}");
     }
+    // For other operations this hook is passed an incomplete object and hook listeners can load if needed.
+    // But that's not possible with delete because it's gone from the database by the time this hook is called.
+    // So in this case the object has been pre-loaded so hook listeners have access to the complete record.
     CRM_Utils_Hook::post('delete', $entityName, $record['id'], $instance);
 
     return $instance;
diff --git a/tests/phpunit/api/v4/Entity/AddressTest.php b/tests/phpunit/api/v4/Entity/AddressTest.php
new file mode 100644 (file)
index 0000000..c3d8f83
--- /dev/null
@@ -0,0 +1,62 @@
+<?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       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+namespace api\v4\Entity;
+
+use api\v4\UnitTestCase;
+use Civi\Api4\Address;
+use Civi\Api4\Contact;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * Test Address functionality
+ *
+ * @group headless
+ */
+class AddressTest extends UnitTestCase implements TransactionalInterface {
+
+  /**
+   * Check that 2 addresses for the same contact can't both be primary
+   */
+  public function testPrimary() {
+    $cid = Contact::create(FALSE)->addValue('first_name', uniqid())->execute()->single()['id'];
+
+    $a1 = Address::create(FALSE)
+      ->addValue('is_primary', TRUE)
+      ->addValue('contact_id', $cid)
+      ->addValue('location_type_id', 1)
+      ->addValue('city', 'Somewhere')
+      ->execute();
+
+    $a2 = Address::create(FALSE)
+      ->addValue('is_primary', TRUE)
+      ->addValue('contact_id', $cid)
+      ->addValue('location_type_id', 2)
+      ->addValue('city', 'Elsewhere')
+      ->execute();
+
+    $addresses = Address::get(FALSE)
+      ->addWhere('contact_id', '=', $cid)
+      ->addOrderBy('id')
+      ->execute();
+
+    $this->assertFalse($addresses[0]['is_primary']);
+    $this->assertTrue($addresses[1]['is_primary']);
+  }
+
+}