Skip to content

Commit

Permalink
Api4SelectQuery - Refactor field handling for looser coupling and mor…
Browse files Browse the repository at this point in the history
…e flexibility

Getting ready to support groupBy, having and sql functions
  • Loading branch information
colemanw committed Mar 27, 2020
1 parent ded1686 commit 48b12a9
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 196 deletions.
2 changes: 1 addition & 1 deletion Civi/Api4/Event/Subscriber/PostSelectQuerySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ private function formatSelects($alias, $selects, Api4SelectQuery $query) {
$selectFields = [];

foreach ($selects as $select) {
$selectAlias = $query->getFkSelectAliases()[$select];
$selectAlias = $query->getField($select)['sql_name'];
$fieldAlias = substr($select, strrpos($select, '.') + 1);
$selectFields[$fieldAlias] = $selectAlias;
}
Expand Down
239 changes: 84 additions & 155 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public function __construct($apiGet) {
$baoName = CoreUtil::getBAOFromApiName($this->entity);
$this->entityFieldNames = array_column($baoName::fields(), 'name');
$this->apiFieldSpec = $apiGet->entityFields();
foreach ($this->apiFieldSpec as $key => $field) {
$this->apiFieldSpec[$key]['sql_name'] = '`' . self::MAIN_TABLE_ALIAS . '`.`' . $field['column_name'] . '`';
}

$this->constructQueryObject($baoName);

Expand All @@ -93,26 +96,10 @@ public function __construct($apiGet) {
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function getSql() {
$this->addJoins();
$this->buildSelectFields();
$this->buildSelectClause();
$this->buildWhereClause();

// Select
if (in_array('row_count', $this->select)) {
$this->query->select("count(*) as c");
}
else {
foreach ($this->selectFields as $column => $alias) {
$this->query->select("$column as `$alias`");
}
// Order by
$this->buildOrderBy();
}

// Limit
if (!empty($this->limit) || !empty($this->offset)) {
$this->query->limit($this->limit, $this->offset);
}
$this->buildOrderBy();
$this->buildLimit();
return $this->query->toSQL();
}

Expand All @@ -135,7 +122,7 @@ public function run() {
break;
}
$results[$query->id] = [];
foreach ($this->selectFields as $column => $alias) {
foreach ($this->select as $alias) {
$returnName = $alias;
$alias = str_replace('.', '_', $alias);
$results[$query->id][$returnName] = property_exists($query, $alias) ? $query->$alias : NULL;
Expand All @@ -147,55 +134,41 @@ public function run() {
return $event->getResults();
}

/**
* Gets all FK fields and does the required joins
*/
protected function addJoins() {
$allFields = array_merge($this->select, array_keys($this->orderBy));
$recurse = function($clauses) use (&$allFields, &$recurse) {
foreach ($clauses as $clause) {
if ($clause[0] === 'NOT' && is_string($clause[1][0])) {
$recurse($clause[1][1]);
}
elseif (in_array($clause[0], ['AND', 'OR', 'NOT'])) {
$recurse($clause[1]);
}
elseif (is_array($clause[0])) {
array_walk($clause, $recurse);
}
else {
$allFields[] = $clause[0];
}
}
};
$recurse($this->where);
$dotFields = array_unique(array_filter($allFields, function ($field) {
return strpos($field, '.') !== FALSE;
}));

foreach ($dotFields as $dotField) {
$this->joinFK($dotField);
protected function buildSelectClause() {
if (empty($this->select)) {
$this->select = $this->entityFieldNames;
}
}

/**
* Populate $this->selectFields
*
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function buildSelectFields() {
$selectAll = (empty($this->select) || in_array('*', $this->select));
$select = $selectAll ? $this->entityFieldNames : $this->select;

// Always select the ID if the table has one.
if (array_key_exists('id', $this->apiFieldSpec) || strstr($this->entity, 'Custom_')) {
$this->selectFields[self::MAIN_TABLE_ALIAS . ".id"] = "id";
elseif (in_array('row_count', $this->select)) {
$this->query->select("COUNT(*) AS `c`");
return;
}

// core return fields
foreach ($select as $fieldName) {
if (strpos($fieldName, '.') === FALSE || !array_filter($this->getPathJoinTypes($fieldName))) {
$this->selectFields[$this->getFieldSqlName($fieldName)] = $fieldName;
else {
// Always select id field
$this->select = array_merge(['id'], $this->select);

// Expand wildcards in joins (the api wrapper already expanded non-joined wildcards)
$wildFields = array_filter($this->select, function($item) {
return strpos($item, '*') !== FALSE && strpos($item, '.') !== FALSE;
});
foreach ($wildFields as $item) {
$pos = array_search($item, array_values($this->select));
$this->joinFK($item);
$matches = SelectUtil::getMatchingFields($item, array_keys($this->apiFieldSpec));
array_splice($this->select, $pos, 1, $matches);
}
$this->select = array_unique($this->select);
}
foreach ($this->select as $fieldName) {
$field = $this->getField($fieldName);
if ($field) {
$this->query->select($field['sql_name'] . " AS `$fieldName`");
}
// Remove unknown fields without raising an error
else {
$this->select = array_diff($this->select, [$fieldName]);
if (is_array($this->debugOutput)) {
$this->debugOutput['undefined_fields'][] = $fieldName;
}
}
}
}
Expand All @@ -218,7 +191,16 @@ protected function buildOrderBy() {
if ($dir !== 'ASC' && $dir !== 'DESC') {
throw new \API_Exception("Invalid sort direction. Cannot order by $fieldName $dir");
}
$this->query->orderBy($this->getFieldSqlName($fieldName) . " $dir");
$this->query->orderBy($this->getField($fieldName, TRUE)['sql_name'] . " $dir");
}
}

/**
* @throws \CRM_Core_Exception
*/
protected function buildLimit() {
if (!empty($this->limit) || !empty($this->offset)) {
$this->query->limit($this->limit, $this->offset);
}
}

Expand Down Expand Up @@ -271,41 +253,17 @@ protected function treeWalkWhereClause($clause) {
protected function validateClauseAndComposeSql($clause) {
// Pad array for unary operators
list($fieldName, $operator, $value) = array_pad($clause, 3, NULL);
$fieldSpec = $this->getField($fieldName);
$sqlName = $this->getFieldSqlName($fieldName);
$field = $this->getField($fieldName, TRUE);

FormattingUtil::formatInputValue($value, $fieldSpec, $this->getEntity());
FormattingUtil::formatInputValue($value, $field, $this->getEntity());

$sql_clause = \CRM_Core_DAO::createSQLFilter($sqlName, [$operator => $value]);
$sql_clause = \CRM_Core_DAO::createSQLFilter($field['sql_name'], [$operator => $value]);
if ($sql_clause === NULL) {
throw new \API_Exception("Invalid value in where clause for field '$fieldName'");
}
return $sql_clause;
}

/**
* Translates an api fieldname to the table.column name used in the query.
*
* @param $fieldName
* @return string
* @throws \API_Exception
*/
protected function getFieldSqlName($fieldName) {
$tableName = $columnName = NULL;
if (in_array($fieldName, $this->entityFieldNames)) {
$field = $this->getField($fieldName);
$tableName = self::MAIN_TABLE_ALIAS;
$columnName = $field['column_name'] ?? $field['name'];
}
elseif (strpos($fieldName, '.') && isset($this->fkSelectAliases[$fieldName])) {
list($tableName, $columnName) = explode('.', $this->fkSelectAliases[$fieldName]);
}
if (!$tableName || !$columnName) {
throw new \API_Exception("Invalid field '$fieldName'.");
}
return "`$tableName`.`$columnName`";
}

/**
* @inheritDoc
*/
Expand All @@ -317,88 +275,66 @@ protected function getFields() {
* Fetch a field from the getFields list
*
* @param string $fieldName
* @param bool $strict
*
* @return string|null
* @throws \API_Exception
*/
protected function getField($fieldName) {
if ($fieldName) {
$fieldPath = explode('.', $fieldName);
if (count($fieldPath) > 1) {
$fieldName = implode('.', array_slice($fieldPath, -2));
}
return $this->apiFieldSpec[$fieldName] ?? NULL;
public function getField($fieldName, $strict = FALSE) {
// Perform join if field not yet available - this will add it to apiFieldSpec
if (!isset($this->apiFieldSpec[$fieldName]) && strpos($fieldName, '.')) {
$this->joinFK($fieldName);
}
$field = $this->apiFieldSpec[$fieldName] ?? NULL;
// Check if field exists and we have permission to view it
if ($field && (!$this->checkPermissions || empty($field['permission']) || \CRM_Core_Permission::check($field['permission']))) {
return $field;
}
elseif ($strict) {
throw new \API_Exception("Invalid field '$fieldName'");
}
return NULL;
}

/**
* @param $key
* @return Joinable|bool
* @throws \API_Exception
*/
protected function joinFK($key) {
$pathArray = explode('.', $key);

if (count($pathArray) < 2) {
return;
if (isset($this->apiFieldSpec[$key])) {
return TRUE;
}

$pathArray = explode('.', $key);

/** @var \Civi\Api4\Service\Schema\Joiner $joiner */
$joiner = \Civi::container()->get('joiner');
$field = array_pop($pathArray);
array_pop($pathArray);
$pathString = implode('.', $pathArray);

if (!$joiner->canJoin($this, $pathString)) {
return;
return FALSE;
}

$joinPath = $joiner->join($this, $pathString);
/** @var \Civi\Api4\Service\Schema\Joinable\Joinable $lastLink */
$lastLink = array_pop($joinPath);

$isWild = strpos($field, '*') !== FALSE;
if ($isWild) {
if (!in_array($key, $this->select)) {
throw new \API_Exception('Wildcards can only be used in the SELECT clause.');
}
$this->select = array_diff($this->select, [$key]);
// Custom fields are already prefixed
if ($lastLink instanceof CustomGroupJoinable) {
array_pop($pathArray);
}

$prefix = $pathArray ? implode('.', $pathArray) . '.' : '';
// Cache field info for retrieval by $this->getField()
$prefix = array_pop($pathArray) . '.';
if (!isset($this->apiFieldSpec[$prefix . $field])) {
$joinEntity = $lastLink->getEntity();
// Custom fields are already prefixed
if ($lastLink instanceof CustomGroupJoinable) {
$prefix = '';
}
foreach ($lastLink->getEntityFields() as $fieldObject) {
$this->apiFieldSpec[$prefix . $fieldObject->getName()] = $fieldObject->toArray() + ['entity' => $joinEntity];
}
}

if (!$isWild && !$lastLink->getField($field)) {
throw new \API_Exception('Invalid join');
}

$fields = $isWild ? [] : [$field];
// Expand wildcard and add matching fields to $this->select
if ($isWild) {
$fields = SelectUtil::getMatchingFields($field, $lastLink->getEntityFieldNames());
foreach ($fields as $field) {
$this->select[] = $pathString . '.' . $field;
}
$this->select = array_unique($this->select);
$joinEntity = $lastLink->getEntity();
foreach ($lastLink->getEntityFields() as $fieldObject) {
$fieldArray = ['entity' => $joinEntity] + $fieldObject->toArray();
$fieldArray['sql_name'] = '`' . $lastLink->getAlias() . '`.`' . $fieldArray['column_name'] . '`';
$this->apiFieldSpec[$prefix . $fieldArray['name']] = $fieldArray;
}

foreach ($fields as $field) {
// custom groups use aliases for field names
$col = ($lastLink instanceof CustomGroupJoinable) ? $lastLink->getSqlColumn($field) : $field;
// Check Permission on field.
if ($this->checkPermissions && !empty($this->apiFieldSpec[$prefix . $field]['permission']) && !\CRM_Core_Permission::check($this->apiFieldSpec[$prefix . $field]['permission'])) {
continue;
}
$this->fkSelectAliases[$pathString . '.' . $field] = sprintf('%s.%s', $lastLink->getAlias(), $col);
}
return TRUE;
}

/**
Expand Down Expand Up @@ -524,13 +460,6 @@ public function getApiVersion() {
return $this->apiVersion;
}

/**
* @return array
*/
public function getFkSelectAliases() {
return $this->fkSelectAliases;
}

/**
* @return \Civi\Api4\Service\Schema\Joinable\Joinable[]
*/
Expand Down
2 changes: 1 addition & 1 deletion Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function getEntityFields() {
if (!$this->entityFields) {
$fields = CustomField::get()
->setCheckPermissions(FALSE)
->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_required', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value', 'date_format', 'time_format', 'start_date_years', 'end_date_years'])
->setSelect(['custom_group.name', '*'])
->addWhere('custom_group.table_name', '=', $this->getTargetTable())
->execute();
foreach ($fields as $field) {
Expand Down
23 changes: 0 additions & 23 deletions Civi/Api4/Service/Spec/CustomFieldSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ class CustomFieldSpec extends FieldSpec {
*/
protected $tableName;

/**
* @var string
*/
protected $columnName;

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -116,22 +111,4 @@ public function setCustomTableName($customFieldColumnName) {
return $this;
}

/**
* @return string
*/
public function getCustomFieldColumnName() {
return $this->columnName;
}

/**
* @param string $customFieldColumnName
*
* @return $this
*/
public function setCustomFieldColumnName($customFieldColumnName) {
$this->columnName = $customFieldColumnName;

return $this;
}

}
Loading

0 comments on commit 48b12a9

Please sign in to comment.