INFRA-132 - @param type fixes
[civicrm-core.git] / CRM / Core / ManagedEntities.php
index 6d110fb95010d6f92d1d8d5d85f96ab5aecb5a79..a0935dcf28414dad3803ec998ab5c050588767ab 100644 (file)
@@ -6,43 +6,73 @@
  * deactivated, and deleted in tandem with their modules.
  */
 class CRM_Core_ManagedEntities {
+
+  public static function getCleanupOptions() {
+    return array(
+      'always' => ts('Always'),
+      'never' => ts('Never'),
+      'unused' => ts('If Unused'),
+    );
+  }
+
   /**
    * @var array($status => array($name => CRM_Core_Module))
    */
-  public $moduleIndex;
+  protected $moduleIndex;
 
   /**
    * @var array per hook_civicrm_managed
    */
-  public $declarations;
+  protected $declarations;
 
   /**
    * Get an instance
+   * @param bool $fresh
+   * @return \CRM_Core_ManagedEntities
    */
   public static function singleton($fresh = FALSE) {
     static $singleton;
     if ($fresh || !$singleton) {
-      $declarations = array();
-      foreach (CRM_Core_Component::getEnabledComponents() as $component) {
-        $declarations = array_merge($declarations, $component->getManagedEntities());
-      }
-      CRM_Utils_Hook::managed($declarations);
-      $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module::getAll(), $declarations);
+      $singleton = new CRM_Core_ManagedEntities(CRM_Core_Module::getAll(), NULL);
     }
     return $singleton;
   }
 
   /**
-   * @param $modules array CRM_Core_Module
-   * @param $declarations array per hook_civicrm_managed
+   * Perform an asynchronous reconciliation when the transaction ends.
+   */
+  public static function scheduleReconciliation() {
+    CRM_Core_Transaction::addCallback(
+      CRM_Core_Transaction::PHASE_POST_COMMIT,
+      function () {
+        CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();
+      },
+      array(),
+      'ManagedEntities::reconcile'
+    );
+  }
+
+  /**
+   * @param array $modules
+   *   CRM_Core_Module.
+   * @param array $declarations
+   *   Per hook_civicrm_managed.
    */
   public function __construct($modules, $declarations) {
     $this->moduleIndex = self::createModuleIndex($modules);
-    $this->declarations = self::cleanDeclarations($declarations);
+
+    if ($declarations !== NULL) {
+      $this->declarations = self::cleanDeclarations($declarations);
+    }
+    else {
+      $this->declarations = NULL;
+    }
   }
 
   /**
    * Read the managed entity
+   *
+   * @return array|NULL API representation, or NULL if the entity does not exist
    */
   public function get($moduleName, $name) {
     $dao = new CRM_Core_DAO_Managed();
@@ -52,20 +82,22 @@ class CRM_Core_ManagedEntities {
       $params = array(
         'id' => $dao->entity_id,
       );
+      $result = NULL;
       try {
         $result = civicrm_api3($dao->entity_type, 'getsingle', $params);
       }
       catch (Exception $e) {
-        $this->onApiError($params, $result);
+        $this->onApiError($dao->entity_type, 'getsingle', $params, $result);
       }
       return $result;
-    } else {
+    }
+    else {
       return NULL;
     }
   }
 
   public function reconcile() {
-    if ($error = $this->validate($this->declarations)) {
+    if ($error = $this->validate($this->getDeclarations())) {
       throw new Exception($error);
     }
     $this->reconcileEnabledModules();
@@ -79,13 +111,15 @@ class CRM_Core_ManagedEntities {
     // an active module -- because we got it from a hook!
 
     // index by moduleName,name
-    $decls = self::createDeclarationIndex($this->moduleIndex, $this->declarations);
+    $decls = self::createDeclarationIndex($this->moduleIndex, $this->getDeclarations());
     foreach ($decls as $moduleName => $todos) {
       if (isset($this->moduleIndex[TRUE][$moduleName])) {
         $this->reconcileEnabledModule($this->moduleIndex[TRUE][$moduleName], $todos);
-      } elseif (isset($this->moduleIndex[FALSE][$moduleName])) {
+      }
+      elseif (isset($this->moduleIndex[FALSE][$moduleName])) {
         // do nothing -- module should get swept up later
-      } else {
+      }
+      else {
         throw new Exception("Entity declaration references invalid or inactive module name [$moduleName]");
       }
     }
@@ -95,7 +129,8 @@ class CRM_Core_ManagedEntities {
    * Create, update, and delete entities declared by an active module
    *
    * @param \CRM_Core_Module|string $module string
-   * @param $todos array $name => array()
+   * @param array $todos
+   *   $name => array().
    */
   public function reconcileEnabledModule(CRM_Core_Module $module, $todos) {
     $dao = new CRM_Core_DAO_Managed();
@@ -104,44 +139,18 @@ class CRM_Core_ManagedEntities {
     while ($dao->fetch()) {
       if (isset($todos[$dao->name]) && $todos[$dao->name]) {
         // update existing entity; remove from $todos
-        $defaults = array('id' => $dao->entity_id, 'is_active' => 1); // FIXME: test whether is_active is valid
-        $params = array_merge($defaults, $todos[$dao->name]['params']);
-        $result = civicrm_api($dao->entity_type, 'create', $params);
-        if ($result['is_error']) {
-          $this->onApiError($params, $result);
-        }
-
+        $this->updateExistingEntity($dao, $todos[$dao->name]);
         unset($todos[$dao->name]);
-      } else {
+      }
+      else {
         // remove stale entity; not in $todos
-        $params = array(
-          'version' => 3,
-          'id' => $dao->entity_id,
-        );
-        $result = civicrm_api($dao->entity_type, 'delete', $params);
-        if ($result['is_error']) {
-          $this->onApiError($params, $result);
-        }
-
-        CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
-          1 => array($dao->id, 'Integer')
-        ));
+        $this->removeStaleEntity($dao);
       }
     }
 
     // create new entities from leftover $todos
     foreach ($todos as $name => $todo) {
-      $result = civicrm_api($todo['entity'], 'create', $todo['params']);
-      if ($result['is_error']) {
-        $this->onApiError($todo['params'], $result);
-      }
-
-      $dao = new CRM_Core_DAO_Managed();
-      $dao->module = $todo['module'];
-      $dao->name = $todo['name'];
-      $dao->entity_type = $todo['entity'];
-      $dao->entity_id = $result['id'];
-      $dao->save();
+      $this->insertNewEntity($todo);
     }
   }
 
@@ -155,19 +164,8 @@ class CRM_Core_ManagedEntities {
     $dao->whereAdd("module in ($in)");
     $dao->find();
     while ($dao->fetch()) {
-      // FIXME: if ($dao->entity_type supports is_active) {
-      if (TRUE) {
-        // FIXME cascading for payproc types?
-        $params = array(
-          'version' => 3,
-          'id' => $dao->entity_id,
-          'is_active' => 0,
-        );
-        $result = civicrm_api($dao->entity_type, 'create', $params);
-        if ($result['is_error']) {
-          $this->onApiError($params, $result);
-        }
-      }
+      $this->disableEntity($dao);
+
     }
   }
 
@@ -188,21 +186,142 @@ class CRM_Core_ManagedEntities {
     }
     $dao->find();
     while ($dao->fetch()) {
+      $this->removeStaleEntity($dao);
+    }
+  }
+
+  /**
+   * Create a new entity
+   *
+   * @param array $todo
+   *   Entity specification (per hook_civicrm_managedEntities).
+   */
+  public function insertNewEntity($todo) {
+    $result = civicrm_api($todo['entity'], 'create', $todo['params']);
+    if ($result['is_error']) {
+      $this->onApiError($todo['entity'], 'create', $todo['params'], $result);
+    }
+
+    $dao = new CRM_Core_DAO_Managed();
+    $dao->module = $todo['module'];
+    $dao->name = $todo['name'];
+    $dao->entity_type = $todo['entity'];
+    $dao->entity_id = $result['id'];
+    $dao->cleanup = CRM_Utils_Array::value('cleanup', $todo);
+    $dao->save();
+  }
+
+  /**
+   * Update an entity which (a) is believed to exist and which (b) ought to be active.
+   *
+   * @param CRM_Core_DAO_Managed $dao
+   * @param array $todo
+   *   Entity specification (per hook_civicrm_managedEntities).
+   */
+  public function updateExistingEntity($dao, $todo) {
+    $policy = CRM_Utils_Array::value('update', $todo, 'always');
+    $doUpdate = ($policy == 'always');
+
+    if ($doUpdate) {
+      $defaults = array(
+        'id' => $dao->entity_id,
+        'is_active' => 1, // FIXME: test whether is_active is valid
+      );
+      $params = array_merge($defaults, $todo['params']);
+      $result = civicrm_api($dao->entity_type, 'create', $params);
+      if ($result['is_error']) {
+        $this->onApiError($dao->entity_type, 'create',$params, $result);
+      }
+    }
+
+    if (isset($todo['cleanup'])) {
+      $dao->cleanup = $todo['cleanup'];
+      $dao->update();
+    }
+  }
+
+  /**
+   * Update an entity which (a) is believed to exist and which (b) ought to be
+   * inactive.
+   *
+   * @param CRM_Core_DAO_Managed $dao
+   */
+  public function disableEntity($dao) {
+    // FIXME: if ($dao->entity_type supports is_active) {
+    if (TRUE) {
+      // FIXME cascading for payproc types?
+      $params = array(
+        'version' => 3,
+        'id' => $dao->entity_id,
+        'is_active' => 0,
+      );
+      $result = civicrm_api($dao->entity_type, 'create', $params);
+      if ($result['is_error']) {
+        $this->onApiError($dao->entity_type, 'create',$params, $result);
+      }
+    }
+  }
+
+  /**
+   * Remove a stale entity (if policy allows)
+   *
+   * @param CRM_Core_DAO_Managed $dao
+   */
+  public function removeStaleEntity($dao) {
+    $policy = empty($dao->cleanup) ? 'always' : $dao->cleanup;
+    switch ($policy) {
+      case 'always':
+        $doDelete = TRUE;
+        break;
+      case 'never':
+        $doDelete = FALSE;
+        break;
+      case 'unused':
+        $getRefCount = civicrm_api3($dao->entity_type, 'getrefcount', array(
+          'debug' => 1,
+          'id' => $dao->entity_id,
+        ));
+
+        $total = 0;
+        foreach ($getRefCount['values'] as $refCount) {
+          $total += $refCount['count'];
+        }
+
+        $doDelete = ($total == 0);
+        break;
+      default:
+        throw new \Exception('Unrecognized cleanup policy: ' . $policy);
+    }
+
+    if ($doDelete) {
       $params = array(
         'version' => 3,
         'id' => $dao->entity_id,
       );
       $result = civicrm_api($dao->entity_type, 'delete', $params);
       if ($result['is_error']) {
-        $this->onApiError($params, $result);
+        $this->onApiError($dao->entity_type, 'delete', $params, $result);
       }
 
       CRM_Core_DAO::executeQuery('DELETE FROM civicrm_managed WHERE id = %1', array(
-        1 => array($dao->id, 'Integer')
+        1 => array($dao->id, 'Integer'),
       ));
     }
   }
 
+  public function getDeclarations() {
+    if ($this->declarations === NULL) {
+      $this->declarations = array();
+      foreach (CRM_Core_Component::getEnabledComponents() as $component) {
+        /** @var CRM_Core_Component_Info $component */
+        $this->declarations = array_merge($this->declarations, $component->getManagedEntities());
+      }
+      CRM_Utils_Hook::managed($this->declarations);
+      $this->declarations = self::cleanDeclarations($this->declarations);
+    }
+    return $this->declarations;
+  }
+
   /**
    * @param $modules
    *
@@ -272,17 +391,20 @@ class CRM_Core_ManagedEntities {
   }
 
   /**
-   * @param $params
-   * @param $result
+   * @param string $entity
+   * @param string $action
+   * @param array $params
+   * @param array $result
    *
    * @throws Exception
    */
-  protected function onApiError($params, $result) {
+  protected function onApiError($entity, $action, $params, $result) {
     CRM_Core_Error::debug_var('ManagedEntities_failed', array(
+      'entity' => $entity,
+      'action' => $action,
       'params' => $params,
       'result' => $result,
     ));
     throw new Exception('API error: ' . $result['error_message']);
   }
 }
-