/**
* Load the smart group cache for a saved search.
*
- * @param object $group
+ * @param CRM_Core_DAO $group
* The smart group that needs to be loaded.
* @param bool $force
* deprecated parameter = Should we force a search through.
*
+ * return bool
* @throws \CRM_Core_Exception
*/
public static function load($group, $force = FALSE) {
self::releaseGroupLocks([$groupID]);
$groupContactsTempTable->drop();
}
+ return in_array($groupID, $lockedGroups);
}
/**
}
/**
- * Invalidates the smart group cache for a particular group
- * @param int $groupID - Group to invalidate
+ * Invalidates the smart group cache for one or more groups
+ * @param int|int[] $groupID - Group to invalidate
*/
public static function invalidateGroupContactCache($groupID): void {
+ $groupIDs = implode(',', (array) $groupID);
CRM_Core_DAO::executeQuery('UPDATE civicrm_group
SET cache_date = NULL
- WHERE id = %1 AND (saved_search_id IS NOT NULL OR children IS NOT NULL)', [
- 1 => [$groupID, 'Positive'],
+ WHERE id IN (%1) AND (saved_search_id IS NOT NULL OR children IS NOT NULL)', [
+ 1 => [$groupIDs, 'CommaSeparatedIntegers'],
]);
}
--- /dev/null
+<?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\Group;
+
+use Civi\Api4\Generic\Result;
+
+/**
+ * @inheritDoc
+ */
+class Refresh extends \Civi\Api4\Generic\BasicBatchAction {
+
+ protected function processBatch(Result $result, array $items) {
+ if ($items) {
+ \CRM_Contact_BAO_GroupContactCache::invalidateGroupContactCache(array_column($items, 'id'));
+ }
+ foreach ($items as $item) {
+ $group = new \CRM_Contact_DAO_Group();
+ $group->id = $item['id'];
+ if (\CRM_Contact_BAO_GroupContactCache::load($group)) {
+ $result[] = $item;
+ }
+ }
+ }
+
+}
class Group extends Generic\DAOEntity {
use Generic\Traits\ManagedEntity;
+ /**
+ * @param bool $checkPermissions
+ * @return \Civi\Api4\Action\Group\Refresh
+ * @throws \CRM_Core_Exception
+ */
+ public static function refresh(bool $checkPermissions = TRUE): Action\Group\Refresh {
+ return (new Action\Group\Refresh(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
/**
* Provides more-open permissions that will be further restricted by checkAccess
*
return [
// Create permission depends on the group type (see CRM_Contact_BAO_Group::_checkAccess).
'create' => ['access CiviCRM', ['edit groups', 'access CiviMail', 'create mailings']],
+ 'refresh' => ['access CiviCRM'],
] + $permissions;
}
->setReadonly(TRUE)
->setSqlRenderer([__CLASS__, 'countContacts']);
$spec->addFieldSpec($field);
+
+ // Calculated field to check smart group cache status
+ $field = new FieldSpec('cache_expired', 'Group', 'Boolean');
+ $field->setLabel(ts('Cache Expired'))
+ ->setDescription(ts('Is the smart group cache expired'))
+ ->setColumnName('cache_date')
+ ->setReadonly(TRUE)
+ ->setSqlRenderer([__CLASS__, 'getCacheExpiredSQL']);
+ $spec->addFieldSpec($field);
}
/**
* @return bool
*/
public function applies($entity, $action): bool {
- return $entity === 'Group' && $action === 'get';
+ return $entity === 'Group' && in_array($action, ['get', 'refresh'], TRUE);
}
/**
)";
}
+ /**
+ * Generate SQL for checking cache expiration for smart groups and parent groups
+ *
+ * @return string
+ */
+ public static function getCacheExpiredSQL(array $field): string {
+ $smartGroupCacheTimeoutDateTime = \CRM_Contact_BAO_GroupContactCache::getCacheInvalidDateTime();
+ $cacheDate = $field['sql_name'];
+ $savedSearchId = substr_replace($field['sql_name'], 'saved_search_id', -11, -1);
+ $children = substr_replace($field['sql_name'], 'children', -11, -1);
+ return "IF(($savedSearchId IS NULL AND $children IS NULL) OR $cacheDate > $smartGroupCacheTimeoutDateTime, 0, 1)";
+ }
+
}
// For get actions, just run a get and ACLs will be applied to the query.
// It's a cheap trick and not as efficient as not running the query at all,
// but BAO::checkAccess doesn't consistently check permissions for the "get" action.
- if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
+ if (is_a($apiRequest, '\Civi\Api4\Generic\AbstractGetAction')) {
return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
}
*/
class GroupTest extends Api4TestBase {
+ public function testSmartGroupCache(): void {
+ \Civi::settings()->set('smartGroupCacheTimeout', 5);
+ $savedSearch = $this->createTestRecord('SavedSearch', [
+ 'api_entity' => 'Contact',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => ['id'],
+ 'where' => [],
+ ],
+ ]);
+ $smartGroup = $this->createTestRecord('Group', [
+ 'saved_search_id' => $savedSearch['id'],
+ ]);
+ $parentGroup = $this->createTestRecord('Group');
+ $childGroup = $this->createTestRecord('Group', [
+ 'parents' => [$parentGroup['id']],
+ ]);
+ $groupIds = [$smartGroup['id'], $parentGroup['id'], $childGroup['id']];
+
+ $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+ ->addSelect('id', 'cache_date', 'cache_expired')
+ ->execute()->indexBy('id');
+ // Static (non-parent) groups should always have a null cache_date and expired should always be false.
+ $this->assertNull($get[$childGroup['id']]['cache_date']);
+ $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+ // The others will start off with no cache date
+ $this->assertNull($get[$parentGroup['id']]['cache_date']);
+ $this->assertTrue($get[$parentGroup['id']]['cache_expired']);
+ $this->assertNull($get[$smartGroup['id']]['cache_date']);
+ $this->assertTrue($get[$smartGroup['id']]['cache_expired']);
+
+ $refresh = Group::refresh(FALSE)
+ ->addWhere('id', 'IN', $groupIds)
+ ->execute();
+ $this->assertCount(2, $refresh);
+
+ $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+ ->addSelect('id', 'cache_date', 'cache_expired')
+ ->execute()->indexBy('id');
+ $this->assertNull($get[$childGroup['id']]['cache_date']);
+ $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+ $this->assertNotNull($get[$smartGroup['id']]['cache_date']);
+ $this->assertFalse($get[$smartGroup['id']]['cache_expired']);
+
+ // Pretend the group was refreshed 6 minutes ago
+ $lastRefresh = date('YmdHis', strtotime("-6 minutes"));
+ \CRM_Core_DAO::executeQuery("UPDATE civicrm_group SET cache_date = $lastRefresh WHERE id = %1", [
+ 1 => [$smartGroup['id'], 'Integer'],
+ ]);
+
+ $get = Group::get(FALSE)->addWhere('id', 'IN', $groupIds)
+ ->addSelect('id', 'cache_date', 'cache_expired')
+ ->execute()->indexBy('id');
+ $this->assertNull($get[$childGroup['id']]['cache_date']);
+ $this->assertFalse($get[$childGroup['id']]['cache_expired']);
+ $this->assertNotNull($get[$smartGroup['id']]['cache_date']);
+ $this->assertTrue($get[$smartGroup['id']]['cache_expired']);
+ }
+
public function testCreate() {
$this->createLoggedInUser();
\CRM_Core_Config::singleton()->userPermissionClass->permissions = [