class Update extends \Civi\Api4\Generic\DAOUpdateAction {
use \Civi\Api4\Generic\Traits\CustomValueActionTrait;
+ /**
+ * Ensure entity_id is returned by getBatchRecords()
+ * @return string[]
+ */
+ protected function getSelect() {
+ return ['id', 'entity_id'];
+ }
+
}
* @throws \API_Exception
*/
public static function replace($customGroup, $checkPermissions = TRUE) {
- return (new Generic\BasicReplaceAction("Custom_$customGroup", __FUNCTION__, ['id', 'entity_id']))
+ return (new Generic\BasicReplaceAction("Custom_$customGroup", __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
namespace Civi\Api4\Generic;
+use Civi\Api4\Utils\CoreUtil;
+
/**
* Base class for all batch actions (Update, Delete, Replace).
*
*/
protected $where = [];
- /**
- * @var array
- */
- private $select;
-
- /**
- * BatchAction constructor.
- * @param string $entityName
- * @param string $actionName
- * @param string|array $select
- * One or more fields to load for each item.
- */
- public function __construct($entityName, $actionName, $select = 'id') {
- $this->select = (array) $select;
- parent::__construct($entityName, $actionName);
- }
-
/**
* Get a list of records for this batch.
*
}
/**
- * Get a query which resolves the list of records for this batch.
+ * Get an API action object which resolves the list of records for this batch.
*
* This is similar to `getBatchRecords()`, but you may further refine the
* API call (e.g. selecting different fields or data-pages) before executing.
'offset' => $this->offset,
];
if (empty($this->reload)) {
- $params['select'] = $this->select;
+ $params['select'] = $this->getSelect();
}
return \Civi\API\Request::create($this->getEntityName(), 'get', ['version' => 4] + $params);
}
/**
- * @return array
+ * Determines what fields will be returned by getBatchRecords
+ *
+ * Defaults to an entity's primary key(s), typically ['id']
+ *
+ * @return string[]
*/
protected function getSelect() {
- return $this->select;
+ return CoreUtil::getInfoItem($this->getEntityName(), 'primary_key');
}
}
*/
protected $reload = FALSE;
- /**
- * @var string
- */
- private $idField;
-
- /**
- * BatchAction constructor.
- * @param string $entityName
- * @param string $actionName
- * @param string $idField
- */
- public function __construct($entityName, $actionName, $idField = 'id') {
- // $idField should be a string but some apis (e.g. CustomValue) give us an array
- $this->idField = array_values((array) $idField)[0];
- parent::__construct($entityName, $actionName);
- }
-
/**
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function validateValues() {
+ $idField = $this->getIdField();
// FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
$unmatched = [];
foreach ($this->records as $record) {
- if (empty($record[$this->idField])) {
+ if (empty($record[$idField])) {
$unmatched = array_unique(array_merge($unmatched, $this->checkRequiredFields($record)));
}
}
if ($this->checkPermissions) {
foreach ($this->records as $record) {
- $action = empty($record[$this->idField]) ? 'create' : 'update';
+ $action = empty($record[$idField]) ? 'create' : 'update';
if (!CoreUtil::checkAccessDelegated($this->getEntityName(), $action, $record, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
throw new UnauthorizedException("ACL check failed");
}
}
}
- $e = new ValidateValuesEvent($this, $this->records, new \CRM_Utils_LazyArray(function() {
- $existingIds = array_column($this->records, $this->idField);
+ $e = new ValidateValuesEvent($this, $this->records, new \CRM_Utils_LazyArray(function() use ($idField) {
+ $existingIds = array_column($this->records, $idField);
$existing = civicrm_api4($this->getEntityName(), 'get', [
'checkPermissions' => $this->checkPermissions,
- 'where' => [[$this->idField, 'IN', $existingIds]],
- ], $this->idField);
+ 'where' => [[$idField, 'IN', $existingIds]],
+ ], $idField);
$result = [];
foreach ($this->records as $k => $new) {
- $old = isset($new[$this->idField]) ? $existing[$new[$this->idField]] : NULL;
+ $old = isset($new[$idField]) ? $existing[$new[$idField]] : NULL;
$result[$k] = ['old' => $old, 'new' => $new];
}
return $result;
* @return string
*/
protected function getIdField() {
- return $this->idField;
+ return CoreUtil::getInfoItem($this->getEntityName(), 'primary_key')[0];
}
/**
namespace Civi\Api4\Generic;
+use Civi\API\Exception\UnauthorizedException;
use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\Api4\Utils\CoreUtil;
/**
* Base class for all `Update` api actions
*/
protected $reload = FALSE;
+ /**
+ * Criteria for selecting items to update.
+ *
+ * Required if no id is supplied in values.
+ *
+ * @var array
+ */
+ protected $where = [];
+
+ abstract protected function updateRecords(array $items): array;
+
+ /**
+ * @inheritDoc
+ */
+ public function _run(Result $result) {
+ $primaryKeys = CoreUtil::getInfoItem($this->getEntityName(), 'primary_key');
+ $this->formatWriteValues($this->values);
+
+ // Add primary keys from values to WHERE clause and check for mismatch
+ foreach ($primaryKeys as $id) {
+ if (!empty($this->values[$id])) {
+ $wheres = array_column($this->where, NULL, 0);
+ if (!isset($wheres[$id])) {
+ $this->addWhere($id, '=', $this->values[$id]);
+ }
+ elseif (!($wheres[$id][1] === '=' && $wheres[$id][2] == $this->values[$id])) {
+ throw new \Exception("Cannot update the $id of an existing " . $this->getEntityName() . '.');
+ }
+ }
+ }
+
+ // Require WHERE if we didn't get primary keys from values
+ if (!$this->where) {
+ throw new \API_Exception('Parameter "where" is required unless primary keys are supplied in values.');
+ }
+
+ // Update a single record by primary key (if this entity has a single primary key)
+ if (count($this->where) === 1 && count($primaryKeys) === 1 && $primaryKeys === $this->getSelect() && $this->where[0][0] === $id && $this->where[0][1] === '=' && !empty($this->where[0][2])) {
+ $this->values[$id] = $this->where[0][2];
+ if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $this->values, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
+ $items = [$this->values];
+ $this->validateValues();
+ $result->exchangeArray($this->updateRecords($items));
+ return;
+ }
+
+ // Batch update 1 or more records based on WHERE clause
+ $items = $this->getBatchRecords();
+ foreach ($items as &$item) {
+ $item = $this->values + $item;
+ if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
+ }
+
+ $this->validateValues();
+ $result->exchangeArray($this->updateRecords($items));
+ }
+
/**
* @param string $fieldName
*
*
* @param string $entityName
* @param string $actionName
- * @param string|array $select
- * One or more fields to select from each matching item.
* @param callable $doer
*/
- public function __construct($entityName, $actionName, $select = 'id', $doer = NULL) {
- parent::__construct($entityName, $actionName, $select);
+ public function __construct($entityName, $actionName, $doer = NULL) {
+ parent::__construct($entityName, $actionName);
$this->doer = $doer;
+ // Accept doer as 4th param for now, but emit deprecated warning
+ $this->doer = func_get_args()[3] ?? NULL;
+ if ($this->doer) {
+ \CRM_Core_Error::deprecatedWarning(__CLASS__ . ' constructor received $doer as 4th param; it should be the 3rd as the $select param has been removed');
+ }
+ else {
+ $this->doer = $doer;
+ }
}
/**
* @return BasicSaveAction
*/
public static function save($checkPermissions = TRUE) {
- return (new BasicSaveAction(static::getEntityName(), __FUNCTION__, static::$idField, static::$setter))
+ return (new BasicSaveAction(static::getEntityName(), __FUNCTION__, static::$setter))
->setCheckPermissions($checkPermissions);
}
* @return BasicUpdateAction
*/
public static function update($checkPermissions = TRUE) {
- return (new BasicUpdateAction(static::getEntityName(), __FUNCTION__, static::$idField, static::$setter))
+ return (new BasicUpdateAction(static::getEntityName(), __FUNCTION__, static::$setter))
->setCheckPermissions($checkPermissions);
}
* @return BasicBatchAction
*/
public static function delete($checkPermissions = TRUE) {
- return (new BasicBatchAction(static::getEntityName(), __FUNCTION__, static::$idField, static::$deleter))
+ return (new BasicBatchAction(static::getEntityName(), __FUNCTION__, static::$deleter))
->setCheckPermissions($checkPermissions);
}
* @return BasicReplaceAction
*/
public static function replace($checkPermissions = TRUE) {
- return (new BasicReplaceAction(static::getEntityName(), __FUNCTION__, static::$idField))
+ return (new BasicReplaceAction(static::getEntityName(), __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
*
* @param string $entityName
* @param string $actionName
- * @param string $idField
* @param callable $setter
*/
- public function __construct($entityName, $actionName, $idField = 'id', $setter = NULL) {
- parent::__construct($entityName, $actionName, $idField);
- $this->setter = $setter;
+ public function __construct($entityName, $actionName, $setter = NULL) {
+ parent::__construct($entityName, $actionName);
+ // Accept setter as 4th param for now, but emit deprecated warning
+ $this->setter = func_get_args()[3] ?? NULL;
+ if ($this->setter) {
+ \CRM_Core_Error::deprecatedWarning(__CLASS__ . ' constructor received $setter as 4th param; it should be the 3rd as the $select param has been removed');
+ }
+ else {
+ $this->setter = $setter;
+ }
}
/**
namespace Civi\Api4\Generic;
use Civi\API\Exception\NotImplementedException;
-use Civi\API\Exception\UnauthorizedException;
-use Civi\Api4\Utils\CoreUtil;
/**
* Update one or more $ENTITY with new values.
*
* @param string $entityName
* @param string $actionName
- * @param string|array $select
- * One or more fields to select from each matching item.
* @param callable $setter
*/
- public function __construct($entityName, $actionName, $select = 'id', $setter = NULL) {
- parent::__construct($entityName, $actionName, $select);
- $this->setter = $setter;
+ public function __construct($entityName, $actionName, $setter = NULL) {
+ parent::__construct($entityName, $actionName);
+ // Accept setter as 4th param for now, but emit deprecated warning
+ $this->setter = func_get_args()[3] ?? NULL;
+ if ($this->setter) {
+ \CRM_Core_Error::deprecatedWarning(__CLASS__ . ' constructor received $setter as 4th param; it should be the 3rd as the $select param has been removed');
+ }
+ else {
+ $this->setter = $setter;
+ }
}
/**
- * We pass the writeRecord function an array representing one item to update.
- * We expect to get the same format back.
- *
- * @param \Civi\Api4\Generic\Result $result
+ * @param array $items
+ * @return array
* @throws \API_Exception
- * @throws \Civi\API\Exception\NotImplementedException
*/
- public function _run(Result $result) {
- $this->formatWriteValues($this->values);
- $this->validateValues();
- foreach ($this->getBatchRecords() as $item) {
- $record = $this->values + $item;
- if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $record, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
- throw new UnauthorizedException("ACL check failed");
- }
- $result[] = $this->writeRecord($record);
- }
+ protected function updateRecords(array $items): array {
+ return array_map([$this, 'writeRecord'], $items);
}
/**
namespace Civi\Api4\Generic;
-use Civi\API\Exception\UnauthorizedException;
-use Civi\Api4\Utils\CoreUtil;
-
/**
* Update one or more $ENTITY with new values.
*
- * Use the `where` clause (required) to select them.
+ * Use the `where` clause to bulk update multiple records,
+ * or supply 'id' as a value to update a single record.
*/
class DAOUpdateAction extends AbstractUpdateAction {
use Traits\DAOActionTrait;
/**
- * Criteria for selecting items to update.
- *
- * Required if no id is supplied in values.
- *
- * @var array
- */
- protected $where = [];
-
- /**
- * @inheritDoc
+ * @param array $items
+ * @return array
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
*/
- public function _run(Result $result) {
- $this->formatWriteValues($this->values);
- // Add ID from values to WHERE clause and check for mismatch
- if (!empty($this->values['id'])) {
- $wheres = array_column($this->where, NULL, 0);
- if (!isset($wheres['id'])) {
- $this->addWhere('id', '=', $this->values['id']);
- }
- elseif (!($wheres['id'][1] === '=' && $wheres['id'][2] == $this->values['id'])) {
- throw new \Exception("Cannot update the id of an existing " . $this->getEntityName() . '.');
- }
- }
-
- // Require WHERE if we didn't get an ID from values
- if (!$this->where) {
- throw new \API_Exception('Parameter "where" is required unless an id is supplied in values.');
- }
-
- // Update a single record by ID unless select requires more than id
- if ($this->getSelect() === ['id'] && count($this->where) === 1 && $this->where[0][0] === 'id' && $this->where[0][1] === '=' && !empty($this->where[0][2])) {
- $this->values['id'] = $this->where[0][2];
- if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $this->values, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
- throw new UnauthorizedException("ACL check failed");
- }
- $items = [$this->values];
- $this->validateValues();
- $result->exchangeArray($this->writeObjects($items));
- return;
- }
-
- // Batch update 1 or more records based on WHERE clause
- $items = $this->getBatchRecords();
- foreach ($items as &$item) {
- $item = $this->values + $item;
- if ($this->checkPermissions && !CoreUtil::checkAccessRecord($this, $item, \CRM_Core_Session::getLoggedInContactID() ?: 0)) {
- throw new UnauthorizedException("ACL check failed");
- }
- }
-
- $this->validateValues();
- $result->exchangeArray($this->writeObjects($items));
+ protected function updateRecords(array $items): array {
+ return $this->writeObjects($items);
}
}
public function __construct($customGroup, $actionName) {
$this->customGroup = $customGroup;
- parent::__construct('CustomValue', $actionName, ['id', 'entity_id']);
+ parent::__construct('CustomValue', $actionName);
}
/**
* @return Action\Afform\Update
*/
public static function update($checkPermissions = TRUE) {
- return (new Action\Afform\Update('Afform', __FUNCTION__, 'name'))
+ return (new Action\Afform\Update('Afform', __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Action\Afform\Save
*/
public static function save($checkPermissions = TRUE) {
- return (new Action\Afform\Save('Afform', __FUNCTION__, 'name'))
+ return (new Action\Afform\Save('Afform', __FUNCTION__))
->setCheckPermissions($checkPermissions);
}
* @return Generic\BasicBatchAction
*/
public static function revert($checkPermissions = TRUE) {
- return (new BasicBatchAction('Afform', __FUNCTION__, ['name'], function($item, BasicBatchAction $action) {
+ return (new BasicBatchAction('Afform', __FUNCTION__, function($item, BasicBatchAction $action) {
$scanner = \Civi::service('afform_scanner');
$files = [
\CRM_Afform_AfformScanner::METADATA_FILE,
$result = MockBasicEntity::get()->execute();
$this->assertCount(1, $result);
- $id2 = MockBasicEntity::create()->addValue('foo', 'two')->execute()->first()['identifier'];
+ $id2 = MockBasicEntity::create()
+ ->addValue('foo', 'two')
+ ->addValue('group:label', 'First')
+ ->execute()->first()['identifier'];
$result = MockBasicEntity::get()->selectRowCount()->execute();
$this->assertEquals(2, $result->count());
+ // Updating a single record should support identifier either in the values or the where clause
+ // Test both styles of update
MockBasicEntity::update()->addWhere('identifier', '=', $id2)->addValue('foo', 'new')->execute();
+ MockBasicEntity::update()->addValue('identifier', $id2)->addValue('color', 'red')->execute();
$result = MockBasicEntity::get()->addOrderBy('identifier', 'DESC')->setLimit(1)->execute();
// The object's count() method will account for all results, ignoring limit, while the array results are limited
$this->assertCount(2, $result);
$this->assertCount(1, (array) $result);
$this->assertEquals('new', $result->first()['foo']);
+ $this->assertEquals('red', $result->first()['color']);
+ $this->assertEquals('one', $result->first()['group']);
$result = MockBasicEntity::save()
->addRecord(['identifier' => $id1, 'foo' => 'one updated', 'weight' => '5'])
}
$result = MockBasicEntity::get()
- ->addSelect('*e', 'weig*ht')
+ ->addSelect('s*e', 'weig*ht')
->execute()
->first();
$this->assertEquals(['shape', 'size', 'weight'], array_keys($result));
--- /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 |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace Civi\Api4\Action\MockBasicEntity;
+
+/**
+ * This class demonstrates how the getRecords method of Basic\Get can be overridden.
+ */
+class BatchFrobnicate extends \Civi\Api4\Generic\BasicBatchAction {
+
+ protected function doTask($item) {
+ return [
+ 'identifier' => $item['identifier'],
+ 'frobnication' => $item['number'] * $item['number'],
+ ];
+ }
+
+ protected function getSelect() {
+ return ['identifier', 'number'];
+ }
+
+}
* @return Generic\BasicBatchAction
*/
public static function batchFrobnicate($checkPermissions = TRUE) {
- return (new Generic\BasicBatchAction(__CLASS__, __FUNCTION__, ['identifier', 'number'], function($item) {
- return [
- 'identifier' => $item['identifier'],
- 'frobnication' => $item['number'] * $item['number'],
- ];
- }))->setCheckPermissions($checkPermissions);
+ return (new Action\MockBasicEntity\BatchFrobnicate(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
}
}