forked from civicrm/civicrm-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request civicrm#31508 from ufundo/api4-custom-group-cache
Api4 CustomGroup.get - use in-memory cache to answer simple calls
- Loading branch information
Showing
5 changed files
with
296 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?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\CustomGroup; | ||
|
||
use Civi\Api4\Generic\Result; | ||
use Civi\Api4\Generic\Traits\ArrayQueryActionTrait; | ||
use Civi\Api4\Generic\Traits\PseudoconstantOutputTrait; | ||
|
||
/** | ||
* @inheritDoc | ||
* | ||
*/ | ||
class Get extends \Civi\Api4\Generic\DAOGetAction { | ||
use ArrayQueryActionTrait; | ||
use PseudoconstantOutputTrait; | ||
|
||
/** | ||
* @var bool | ||
* | ||
* Should we use the in-memory cache to answer | ||
* this request? | ||
* | ||
* If unset, will be determined automatically based | ||
* on the complexity of the request | ||
*/ | ||
protected ?bool $useCache = NULL; | ||
|
||
/** | ||
* @param \Civi\Api4\Generic\Result $result | ||
* | ||
* Use self::getFromCache or DAOGetAction::getObjects | ||
*/ | ||
protected function getObjects(Result $result): void { | ||
if (is_null($this->useCache)) { | ||
$this->useCache = !$this->needDatabase(); | ||
} | ||
if ($this->useCache) { | ||
$this->getFromCache($result); | ||
return; | ||
} | ||
parent::getObjects($result); | ||
} | ||
|
||
/** | ||
* Determine whether this query needs to use the | ||
* database (or can be answered using the cache) | ||
* | ||
* @return bool | ||
*/ | ||
protected function needDatabase(): bool { | ||
if ($this->groupBy || $this->having || $this->join) { | ||
return TRUE; | ||
} | ||
|
||
$standardFields = \Civi::entity($this->getEntityName())->getFields(); | ||
foreach ($this->select as $field) { | ||
[$field] = explode(':', $field); | ||
if (!isset($standardFields[$field])) { | ||
return TRUE; | ||
} | ||
} | ||
foreach ($this->where as $clause) { | ||
[$field] = explode(':', $clause[0] ?? ''); | ||
if (!$field || !isset($standardFields[$field])) { | ||
return TRUE; | ||
} | ||
// ArrayQueryTrait doesn't yet support field-to-field comparisons | ||
if (!empty($clause[3])) { | ||
return TRUE; | ||
} | ||
} | ||
foreach ($this->orderBy as $field => $dir) { | ||
[$field] = explode(':', $field); | ||
if (!isset($standardFields[$field])) { | ||
return TRUE; | ||
} | ||
} | ||
return FALSE; | ||
} | ||
|
||
/** | ||
* This works like BasicGetAction: | ||
* - provide all the records upfront from the cache | ||
* - format suffixes using PseudoconstantOutputTrait | ||
* - filter using ArrayQueryActionTrait | ||
*/ | ||
protected function getFromCache($result): void { | ||
$values = $this->getCachedRecords(); | ||
$this->formatRawValues($values); | ||
$this->queryArray($values, $result); | ||
} | ||
|
||
protected function getCachedRecords() { | ||
return \CRM_Core_BAO_CustomGroup::getAll(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?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\Generic\Traits; | ||
|
||
use Civi\Api4\Utils\FormattingUtil; | ||
|
||
/** | ||
* Helper function for formatting optionvalue/pseudoconstant fields | ||
* | ||
* @package Civi\Api4\Generic | ||
*/ | ||
trait PseudoconstantOutputTrait { | ||
|
||
/** | ||
* Evaluate :pseudoconstant suffix expressions and replace raw values with option values | ||
* | ||
* @param $records | ||
* @throws \CRM_Core_Exception | ||
*/ | ||
protected function formatRawValues(&$records) { | ||
// Pad $records and $fields with pseudofields | ||
$fields = $this->entityFields(); | ||
foreach ($records as &$values) { | ||
foreach ($this->entityFields() as $field) { | ||
$values += [$field['name'] => $field['default_value'] ?? NULL]; | ||
if (!empty($field['options'])) { | ||
foreach ($field['suffixes'] ?? array_keys(\CRM_Core_SelectValues::optionAttributes()) as $suffix) { | ||
$pseudofield = $field['name'] . ':' . $suffix; | ||
if (!isset($values[$pseudofield]) && isset($values[$field['name']]) && $this->_isFieldSelected($pseudofield)) { | ||
$values[$pseudofield] = $values[$field['name']]; | ||
} | ||
} | ||
} | ||
} | ||
// Swap raw values with pseudoconstants | ||
FormattingUtil::formatOutputValues($values, $fields, $this->getActionName()); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<?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\Action; | ||
|
||
use api\v4\Api4TestBase; | ||
use Civi\Api4\CustomGroup; | ||
|
||
/** | ||
* Test Api4-level caching (currently = CustomGroup) | ||
* | ||
* | ||
* @group headless | ||
*/ | ||
class CachedGetTest extends Api4TestBase { | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public function setUp(): void { | ||
parent::setUp(); | ||
|
||
CustomGroup::create(FALSE) | ||
->addValue('name', 'LemonPreferences') | ||
->addValue('title', 'Lemon') | ||
->addValue('extends', 'Contact') | ||
->addValue('pre_help', 'Some people think lemons are all the same, but some have preferences.') | ||
->execute(); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public function tearDown(): void { | ||
CustomGroup::delete(FALSE) | ||
->addWhere('name', '=', 'LemonPreferences') | ||
->execute(); | ||
parent::tearDown(); | ||
} | ||
|
||
/** | ||
* @return \Civi\Api4\Action\AbstractAction[] | ||
*/ | ||
protected function cacheableCalls(): array { | ||
$calls = []; | ||
|
||
$calls[] = CustomGroup::get(FALSE) | ||
->addWhere('name', 'CONTAINS', 'lemon'); | ||
|
||
// note: comparison should be case-insensitive | ||
$calls[] = CustomGroup::get(FALSE) | ||
->addWhere('extends', '=', 'contact'); | ||
|
||
return $calls; | ||
} | ||
|
||
/** | ||
* @return \Civi\Api4\Action\AbstractAction[] | ||
*/ | ||
protected function uncacheableCalls(): array { | ||
$calls = []; | ||
|
||
// cache cant GROUP BY | ||
$calls[] = CustomGroup::get(FALSE) | ||
->addSelect('extends', 'COUNT(id) AS count') | ||
->addGroupBy('extends'); | ||
|
||
// cache cant do SQL functions | ||
$calls[] = CustomGroup::get(FALSE) | ||
->addSelect('UPPER(name)'); | ||
|
||
// TODO: cache cant do implicit joins (but none available on CustomGroup) | ||
|
||
return $calls; | ||
} | ||
|
||
/** | ||
* For easy calls the cached result should | ||
* match the database result | ||
*/ | ||
public function testCachedGetMatchesDatabase(): void { | ||
foreach ($this->cacheableCalls() as $call) { | ||
// we need two copies of the API action object | ||
$dbCall = clone $call; | ||
|
||
$cacheResult = (array) $call->setUseCache(TRUE)->execute(); | ||
|
||
$dbResult = (array) $dbCall->setUseCache(FALSE)->execute(); | ||
|
||
$this->assertEquals($cacheResult, $dbResult); | ||
} | ||
} | ||
|
||
/** | ||
* For hard calls the default result should | ||
* match the database result | ||
* (the API should determine it needs to escalate | ||
* to a DB call if `useCache` is left unspecified) | ||
*/ | ||
public function testHardQueryUsesDatabaseByDefault(): void { | ||
foreach ($this->uncacheableCalls() as $call) { | ||
// we need two copies of the API action object | ||
$dbCall = clone $call; | ||
|
||
$defaultResult = (array) $call->execute(); | ||
|
||
$dbResult = (array) $dbCall->setUseCache(FALSE)->execute(); | ||
|
||
$this->assertEquals($defaultResult, $dbResult); | ||
} | ||
} | ||
|
||
} |