This adds the first calculated field to APIv4. It works by injecting a SQL function and aliasing it
with the name of the fake field. This approach allows it to work as part of the WHERE clause
as well as the SELECT clause, even across joins.
Marking the old "is_current" api param deprecated because the field works better.
/**
* @inheritDoc
- *
- * Set current = true to get active, non past campaigns.
*/
class Get extends \Civi\Api4\Generic\DAOGetAction {
use \Civi\Api4\Generic\Traits\IsCurrentTrait;
/**
* @inheritDoc
- *
- * Set current = true to get active, non past events.
*/
class Get extends \Civi\Api4\Generic\DAOGetAction {
use \Civi\Api4\Generic\Traits\IsCurrentTrait;
/**
* @inheritDoc
- *
- * Set current = true to get active, non past relationships.
*/
class Get extends \Civi\Api4\Generic\DAOGetAction {
use \Civi\Api4\Generic\Traits\IsCurrentTrait;
use Civi\Api4\Utils\ReflectionUtils;
/**
- * Process $current api param for Get actions
- *
+ * @deprecated
* @see \Civi\Api4\Generic\Traits\IsCurrentTrait
*/
class IsCurrentSubscriber extends Generic\AbstractPrepareSubscriber {
'data_type' => 'Array',
'@internal' => TRUE,
];
+ $fields[] = [
+ 'name' => 'sql_renderer',
+ 'data_type' => 'Array',
+ '@internal' => TRUE,
+ ];
return $fields;
}
namespace Civi\Api4\Generic\Traits;
/**
- * This trait adds the $current param to a Get action.
- *
+ * @deprecated
* @see \Civi\Api4\Event\Subscriber\IsCurrentSubscriber
*/
trait IsCurrentTrait {
/**
- * Convenience filter for selecting items that are enabled and are currently within their start/end dates.
- *
- * Adding current = TRUE is a shortcut for
- * WHERE is_active = 1 AND (end_date IS NULL OR end_date >= now) AND (start_date IS NULL OR start_DATE <= now)
- *
- * Adding current = FALSE is a shortcut for
- * WHERE is_active = 0 OR start_date > now OR end_date < now
+ * Param deprecated in favor of is_current filter.
*
* @var bool
+ * @deprecated
*/
protected $current;
/**
+ * @deprecated
* @return bool
*/
public function getCurrent() {
}
/**
+ * @deprecated
* @param bool $current
* @return $this
*/
foreach ($expr->getFields() as $fieldName) {
$field = $this->getField($fieldName);
// Remove expressions with unknown fields without raising an error
- if (!$field || !in_array($field['type'], ['Field', 'Custom'], TRUE)) {
+ if (!$field || $field['type'] === 'Filter') {
$select = array_diff($select, [$item]);
$valid = FALSE;
}
if ($type === 'WHERE') {
$field = $this->getField($expr, TRUE);
FormattingUtil::formatInputValue($value, $expr, $field, $operator);
- $fieldAlias = $field['sql_name'];
+ $fieldAlias = $this->getExpression($expr)->render($this->apiFieldSpec);
}
// For HAVING, expr must be an item in the SELECT clause
elseif ($type === 'HAVING') {
return "($fieldAlias $isEmptyClause $fieldAlias $operator)";
}
}
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
return \CRM_Core_DAO::createSQLFilter($fieldAlias, [$operator => $value]);
}
else {
$joinEntityClass = CoreUtil::getApiClass($joinEntity);
foreach ($joinEntityClass::get($this->getCheckPermissions())->entityFields() as $name => $field) {
- $bridgeFields[$field['column_name']] = '`' . $joinAlias . '`.`' . $field['column_name'] . '`';
+ if ($field['type'] === 'Field') {
+ $bridgeFields[$field['column_name']] = '`' . $joinAlias . '`.`' . $field['column_name'] . '`';
+ }
}
$select = implode(',', $bridgeFields);
$joinConditions = array_merge($joinConditions, $bridgeConditions);
$bridgeFkFields = [$joinRef->getReferenceKey(), $joinRef->getTypeColumn(), $baseRef->getReferenceKey(), $baseRef->getTypeColumn()];
$bridgeEntityClass = CoreUtil::getApiClass($bridgeEntity);
foreach ($bridgeEntityClass::get($this->getCheckPermissions())->entityFields() as $name => $field) {
- if ($name === 'id' || ($side === 'INNER' && in_array($name, $bridgeFkFields, TRUE))) {
+ if ($field['type'] !== 'Field' || $name === 'id' || ($side === 'INNER' && in_array($name, $bridgeFkFields, TRUE))) {
continue;
}
// For INNER joins, these fields get a sql alias pointing to the bridge entity,
if ($fieldList[$this->expr] === FALSE) {
throw new UnauthorizedException("Unauthorized field '{$this->expr}'");
}
+ if (!empty($fieldList[$this->expr]['sql_renderer'])) {
+ $renderer = $fieldList[$this->expr]['sql_renderer'];
+ return $renderer($fieldList[$this->expr]);
+ }
return $fieldList[$this->expr]['sql_name'];
}
namespace Civi\Api4\Service\Schema\Joinable;
use Civi\Api4\Utils\CoreUtil;
-use CRM_Core_DAO_AllCoreTables as AllCoreTables;
class Joinable {
}
/**
- * @return \Civi\Api4\Service\Spec\FieldSpec[]
+ * @return \Civi\Api4\Service\Spec\RequestSpec
*/
public function getEntityFields() {
- $entityFields = [];
- $bao = AllCoreTables::getClassForTable($this->getTargetTable());
- if ($bao) {
- foreach ($bao::getSupportedFields() as $field) {
- $entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity());
- }
- }
- return $entityFields;
+ /** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
+ $gatherer = \Civi::container()->get('spec_gatherer');
+ $spec = $gatherer->getSpec($this->entity, 'get', FALSE);
+ return $spec;
}
/**
* @return \Civi\Api4\Service\Spec\FieldSpec|NULL
*/
public function getField($fieldName) {
- foreach ($this->getEntityFields() as $field) {
- if ($field->getName() === $fieldName) {
- return $field;
- }
- }
- return NULL;
+ return $this->getEntityFields()->getFieldByName($fieldName);
}
}
*/
public $outputFormatters;
+ /**
+ * @var callable
+ */
+ public $sqlRenderer;
/**
* @var callable[]
*/
public $sqlFilters;
+
/**
* Aliases for the valid data types
*
return $this;
}
+ /**
+ * @param callable $sqlRenderer
+ * @return $this
+ */
+ public function setSqlRenderer($sqlRenderer) {
+ $this->sqlRenderer = $sqlRenderer;
+
+ return $this;
+ }
+
/**
* @param callable[] $sqlFilters
* @return $this
--- /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\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class IsCurrentFieldSpecProvider implements Generic\SpecProviderInterface {
+
+ /**
+ * @param \Civi\Api4\Service\Spec\RequestSpec $spec
+ */
+ public function modifySpec(RequestSpec $spec) {
+ $field = new FieldSpec('is_current', $spec->getEntity(), 'Boolean');
+ $field->setLabel(ts('Is Current'))
+ ->setTitle(ts('Current'))
+ // This pseudo-field is like is_active with some extra criteria
+ ->setColumnName('is_current')
+ ->setDescription(ts('Is active with a non-past end-date'))
+ ->setType('Extra')
+ ->setSqlRenderer([__CLASS__, 'renderIsCurrentSql']);
+ $spec->addFieldSpec($field);
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function applies($entity, $action) {
+ if ($action !== 'get') {
+ return FALSE;
+ }
+ // TODO: If we wanted this to not be a hard-coded list, we could always return TRUE here
+ // and then in the `modifySpec` function check for the 3 fields `is_active`, `start_date`, and `end_date`
+ return in_array($entity, ['Relationship', 'RelationshipCache', 'Event', 'Campaign'], TRUE);
+ }
+
+ /**
+ * @param array $field
+ * return string
+ */
+ public static function renderIsCurrentSql(array $field): string {
+ $startDate = substr_replace($field['sql_name'], 'start_date', -11, -1);
+ $endDate = substr_replace($field['sql_name'], 'end_date', -11, -1);
+ $isActive = substr_replace($field['sql_name'], 'is_active', -11, -1);
+ $todayStart = date('Ymd', strtotime('now'));
+ $todayEnd = date('Ymd', strtotime('now'));
+ return "IF($isActive = 1 AND ($startDate <= '$todayStart' OR $startDate IS NULL) AND ($endDate >= '$todayEnd' OR $endDate IS NULL), '1', '0')";
+ }
+
+}
use Civi\Api4\Utils\CoreUtil;
-class RequestSpec {
+class RequestSpec implements \Iterator {
/**
* @var string
return $this->action;
}
+ public function rewind() {
+ return reset($this->fields);
+ }
+
+ public function current() {
+ return current($this->fields);
+ }
+
+ public function key() {
+ return key($this->fields);
+ }
+
+ public function next() {
+ return next($this->fields);
+ }
+
+ public function valid() {
+ return key($this->fields) !== NULL;
+ }
+
}
use Civi\Api4\Service\Spec\Provider\Generic\SpecProviderInterface;
use Civi\Api4\Utils\CoreUtil;
+/**
+ * Class SpecGatherer
+ * @package Civi\Api4\Service\Spec
+ */
class SpecGatherer {
/**
$onlyId = !empty($v3Params['format.only_id']);
$onlySuccess = !empty($v3Params['format.is_success']);
if (!empty($v3Params['filters']['is_current']) || !empty($v3Params['isCurrent'])) {
- $v4Params['current'] = TRUE;
+ $v3Params['is_current'] = 1;
+ unset($v3Params['filters']['is_current'], $v3Params['isCurrent']);
}
$language = $v3Params['options']['language'] ?? $v3Params['option.language'] ?? NULL;
if ($language) {
'is_active' => 0,
])->execute()->first();
- $getCurrent = (array) Relationship::get()->setCurrent(TRUE)->execute()->indexBy('id');
- $notCurrent = (array) Relationship::get()->setCurrent(FALSE)->execute()->indexBy('id');
- $getAll = (array) Relationship::get()->execute()->indexBy('id');
+ $getCurrent = Relationship::get()->addWhere('is_current', '=', TRUE)->execute()->indexBy('id');
+ $notCurrent = Relationship::get()->addWhere('is_current', '=', FALSE)->execute()->indexBy('id');
+ $getAll = Relationship::get()->addSelect('is_current')->execute()->indexBy('id');
- $this->assertArrayHasKey($current['id'], $getAll);
- $this->assertArrayHasKey($indefinite['id'], $getAll);
- $this->assertArrayHasKey($expiring['id'], $getAll);
- $this->assertArrayHasKey($past['id'], $getAll);
- $this->assertArrayHasKey($inactive['id'], $getAll);
+ $this->assertTrue($getAll[$current['id']]['is_current']);
+ $this->assertTrue($getAll[$indefinite['id']]['is_current']);
+ $this->assertTrue($getAll[$expiring['id']]['is_current']);
+ $this->assertFalse($getAll[$past['id']]['is_current']);
+ $this->assertFalse($getAll[$inactive['id']]['is_current']);
$this->assertArrayHasKey($current['id'], $getCurrent);
$this->assertArrayHasKey($indefinite['id'], $getCurrent);