diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service')
30 files changed, 2263 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php new file mode 100644 index 00000000..191f4389 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/ActivityToActivityContactAssigneesJoinable.php @@ -0,0 +1,40 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class ActivityToActivityContactAssigneesJoinable extends Joinable { + /** + * @var string + */ + protected $baseTable = 'civicrm_activity'; + + /** + * @var string + */ + protected $baseColumn = 'id'; + + /** + * @param $alias + */ + public function __construct($alias) { + $optionValueTable = 'civicrm_option_value'; + $optionGroupTable = 'civicrm_option_group'; + + $subSubSelect = sprintf( + 'SELECT id FROM %s WHERE name = "%s"', + $optionGroupTable, + 'activity_contacts' + ); + + $subSelect = sprintf( + 'SELECT value FROM %s WHERE name = "%s" AND option_group_id = (%s)', + $optionValueTable, + 'Activity Assignees', + $subSubSelect + ); + + $this->addCondition(sprintf('%s.record_type_id = (%s)', $alias, $subSelect)); + parent::__construct('civicrm_activity_contact', 'activity_id', $alias); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php new file mode 100644 index 00000000..370c5898 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/BridgeJoinable.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class BridgeJoinable extends Joinable { + /** + * @var Joinable + */ + protected $middleLink; + + public function __construct($targetTable, $targetColumn, $alias, Joinable $middleLink) { + parent::__construct($targetTable, $targetColumn, $alias); + $this->middleLink = $middleLink; + } + + /** + * @return Joinable + */ + public function getMiddleLink() { + return $this->middleLink; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php new file mode 100644 index 00000000..a1dd1a1d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/CustomGroupJoinable.php @@ -0,0 +1,71 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +use Civi\Api4\CustomField; + +class CustomGroupJoinable extends Joinable { + + /** + * @var string + */ + protected $joinSide = self::JOIN_SIDE_LEFT; + + /** + * @var string + * + * Name of the custom field column. + */ + protected $columns; + + /** + * @param $targetTable + * @param $alias + * @param bool $isMultiRecord + * @param null $entity + */ + public function __construct($targetTable, $alias, $isMultiRecord, $entity, $columns) { + $this->entity = $entity; + $this->columns = $columns; + parent::__construct($targetTable, 'entity_id', $alias); + $this->joinType = $isMultiRecord ? + self::JOIN_TYPE_ONE_TO_MANY : self::JOIN_TYPE_ONE_TO_ONE; + } + + /** + * @inheritDoc + */ + public function getEntityFields() { + if (!$this->entityFields) { + $fields = CustomField::get() + ->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']) + ->addWhere('custom_group.table_name', '=', $this->getTargetTable()) + ->execute(); + foreach ($fields as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + return $this->entityFields; + } + + /** + * @inheritDoc + */ + public function getField($fieldName) { + foreach ($this->getEntityFields() as $field) { + $name = $field->getName(); + if ($name === $fieldName || strrpos($name, '.' . $fieldName) === strlen($name) - strlen($fieldName) - 1) { + return $field; + } + } + return NULL; + } + + /** + * @return string + */ + public function getSqlColumn($fieldName) { + return $this->columns[$fieldName]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php new file mode 100644 index 00000000..0e92e3ab --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/Joinable.php @@ -0,0 +1,277 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +use Civi\Api4\Service\Spec\FieldSpec; +use CRM_Core_DAO_AllCoreTables as Tables; + +class Joinable { + + const JOIN_SIDE_LEFT = 'LEFT'; + const JOIN_SIDE_INNER = 'INNER'; + + const JOIN_TYPE_ONE_TO_ONE = '1_to_1'; + const JOIN_TYPE_MANY_TO_ONE = 'n_to_1'; + const JOIN_TYPE_ONE_TO_MANY = '1_to_n'; + + /** + * @var string + */ + protected $baseTable; + + /** + * @var string + */ + protected $baseColumn; + + /** + * @var string + */ + protected $targetTable; + + /** + * @var string + * + * Name (or alias) of the target column) + */ + protected $targetColumn; + + /** + * @var string + */ + protected $alias; + + /** + * @var array + */ + protected $conditions = []; + + /** + * @var string + */ + protected $joinSide = self::JOIN_SIDE_LEFT; + + /** + * @var int + */ + protected $joinType = self::JOIN_TYPE_ONE_TO_ONE; + + /** + * @var string + */ + protected $entity; + + /** + * @var array + */ + protected $entityFields; + + /** + * @param $targetTable + * @param $targetColumn + * @param string|null $alias + */ + public function __construct($targetTable, $targetColumn, $alias = NULL) { + $this->targetTable = $targetTable; + $this->targetColumn = $targetColumn; + if (!$this->entity) { + $this->entity = Tables::getBriefName(Tables::getClassForTable($targetTable)); + } + $this->alias = $alias ?: str_replace('civicrm_', '', $targetTable); + } + + /** + * Gets conditions required when joining to a base table + * + * @param string|null $baseTableAlias + * Name of the base table, if aliased. + * + * @return array + */ + public function getConditionsForJoin($baseTableAlias = NULL) { + $baseCondition = sprintf( + '%s.%s = %s.%s', + $baseTableAlias ?: $this->baseTable, + $this->baseColumn, + $this->getAlias(), + $this->targetColumn + ); + + return array_merge([$baseCondition], $this->conditions); + } + + /** + * @return string + */ + public function getBaseTable() { + return $this->baseTable; + } + + /** + * @param string $baseTable + * + * @return $this + */ + public function setBaseTable($baseTable) { + $this->baseTable = $baseTable; + + return $this; + } + + /** + * @return string + */ + public function getBaseColumn() { + return $this->baseColumn; + } + + /** + * @param string $baseColumn + * + * @return $this + */ + public function setBaseColumn($baseColumn) { + $this->baseColumn = $baseColumn; + + return $this; + } + + /** + * @return string + */ + public function getAlias() { + return $this->alias; + } + + /** + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + $this->alias = $alias; + + return $this; + } + + /** + * @return string + */ + public function getTargetTable() { + return $this->targetTable; + } + + /** + * @return string + */ + public function getTargetColumn() { + return $this->targetColumn; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @param $condition + * + * @return $this + */ + public function addCondition($condition) { + $this->conditions[] = $condition; + + return $this; + } + + /** + * @return array + */ + public function getExtraJoinConditions() { + return $this->conditions; + } + + /** + * @param array $conditions + * + * @return $this + */ + public function setConditions($conditions) { + $this->conditions = $conditions; + + return $this; + } + + /** + * @return string + */ + public function getJoinSide() { + return $this->joinSide; + } + + /** + * @param string $joinSide + * + * @return $this + */ + public function setJoinSide($joinSide) { + $this->joinSide = $joinSide; + + return $this; + } + + /** + * @return int + */ + public function getJoinType() { + return $this->joinType; + } + + /** + * @param int $joinType + * + * @return $this + */ + public function setJoinType($joinType) { + $this->joinType = $joinType; + + return $this; + } + + /** + * @return array + */ + public function toArray() { + return get_object_vars($this); + } + + /** + * @return \Civi\Api4\Service\Spec\FieldSpec[] + */ + public function getEntityFields() { + if (!$this->entityFields) { + $bao = Tables::getClassForTable($this->getTargetTable()); + if ($bao) { + foreach ($bao::fields() as $field) { + $this->entityFields[] = \Civi\Api4\Service\Spec\SpecFormatter::arrayToField($field, $this->getEntity()); + } + } + } + return $this->entityFields; + } + + /** + * @return \Civi\Api4\Service\Spec\FieldSpec|NULL + */ + public function getField($fieldName) { + foreach ($this->getEntityFields() as $field) { + if ($field->getName() === $fieldName) { + return $field; + } + } + return NULL; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php new file mode 100644 index 00000000..96f65488 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joinable/OptionValueJoinable.php @@ -0,0 +1,61 @@ +<?php + +namespace Civi\Api4\Service\Schema\Joinable; + +class OptionValueJoinable extends Joinable { + /** + * @var string + */ + protected $optionGroupName; + + /** + * @param string $optionGroup + * Can be either the option group name or ID + * @param string|null $alias + * The join alias + * @param string $keyColumn + * Which column to use to join, defaults to "value" + */ + public function __construct($optionGroup, $alias = NULL, $keyColumn = 'value') { + $this->optionGroupName = $optionGroup; + $optionValueTable = 'civicrm_option_value'; + + // default join alias to option group name, e.g. activity_type + if (!$alias && !is_numeric($optionGroup)) { + $alias = $optionGroup; + } + + parent::__construct($optionValueTable, $keyColumn, $alias); + + if (!is_numeric($optionGroup)) { + $subSelect = 'SELECT id FROM civicrm_option_group WHERE name = "%s"'; + $subQuery = sprintf($subSelect, $optionGroup); + $condition = sprintf('%s.option_group_id = (%s)', $alias, $subQuery); + } + else { + $condition = sprintf('%s.option_group_id = %d', $alias, $optionGroup); + } + + $this->addCondition($condition); + } + + /** + * The existing condition must also be re-aliased + * + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) { + foreach ($this->conditions as $index => $condition) { + $search = $this->alias . '.'; + $replace = $alias . '.'; + $this->conditions[$index] = str_replace($search, $replace, $condition); + } + + parent::setAlias($alias); + + return $this; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php new file mode 100644 index 00000000..cb30ab57 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Joiner.php @@ -0,0 +1,98 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Query\Api4SelectQuery; +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class Joiner { + /** + * @var SchemaMap + */ + protected $schemaMap; + + /** + * @var Joinable[][] + */ + protected $cache = []; + + /** + * @param SchemaMap $schemaMap + */ + public function __construct(SchemaMap $schemaMap) { + $this->schemaMap = $schemaMap; + } + + /** + * @param Api4SelectQuery $query + * The query object to do the joins on + * @param string $joinPath + * A path of aliases in dot notation, e.g. contact.phone + * @param string $side + * Can be LEFT or INNER + * + * @throws \Exception + * @return Joinable[] + * The path used to make the join + */ + public function join(Api4SelectQuery $query, $joinPath, $side = 'LEFT') { + $fullPath = $this->getPath($query->getFrom(), $joinPath); + $baseTable = $query::MAIN_TABLE_ALIAS; + + foreach ($fullPath as $link) { + $target = $link->getTargetTable(); + $alias = $link->getAlias(); + $conditions = $link->getConditionsForJoin($baseTable); + + $query->join($side, $target, $alias, $conditions); + $query->addJoinedTable($link); + + $baseTable = $link->getAlias(); + } + + return $fullPath; + } + + /** + * @param Api4SelectQuery $query + * @param $joinPath + * + * @return bool + */ + public function canJoin(Api4SelectQuery $query, $joinPath) { + return !empty($this->getPath($query->getFrom(), $joinPath)); + } + + /** + * @param string $baseTable + * @param string $joinPath + * + * @return array + * @throws \Exception + */ + protected function getPath($baseTable, $joinPath) { + $cacheKey = sprintf('%s.%s', $baseTable, $joinPath); + if (!isset($this->cache[$cacheKey])) { + $stack = explode('.', $joinPath); + $fullPath = []; + + foreach ($stack as $key => $targetAlias) { + $links = $this->schemaMap->getPath($baseTable, $targetAlias); + + if (empty($links)) { + throw new \Exception(sprintf('Cannot join %s to %s', $baseTable, $targetAlias)); + } + else { + $fullPath = array_merge($fullPath, $links); + $lastLink = end($links); + $baseTable = $lastLink->getTargetTable(); + } + } + + $this->cache[$cacheKey] = $fullPath; + } + + return $this->cache[$cacheKey]; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php new file mode 100644 index 00000000..3989afeb --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMap.php @@ -0,0 +1,140 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Service\Schema\Joinable\BridgeJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class SchemaMap { + + const MAX_JOIN_DEPTH = 3; + + /** + * @var Table[] + */ + protected $tables = []; + + /** + * @param $baseTableName + * @param $targetTableAlias + * + * @return Joinable[] + * Array of links to the target table, empty if no path found + */ + public function getPath($baseTableName, $targetTableAlias) { + $table = $this->getTableByName($baseTableName); + $path = []; + + if (!$table) { + return $path; + } + + $this->findPaths($table, $targetTableAlias, 1, $path); + + foreach ($path as $index => $pathLink) { + if ($pathLink instanceof BridgeJoinable) { + $start = array_slice($path, 0, $index); + $middle = [$pathLink->getMiddleLink()]; + $end = array_slice($path, $index, count($path) - $index); + $path = array_merge($start, $middle, $end); + } + } + + return $path; + } + + /** + * @return Table[] + */ + public function getTables() { + return $this->tables; + } + + /** + * @param $name + * + * @return Table|null + */ + public function getTableByName($name) { + foreach ($this->tables as $table) { + if ($table->getName() === $name) { + return $table; + } + } + + return NULL; + } + + /** + * Adds a table to the schema map if it has not already been added + * + * @param Table $table + * + * @return $this + */ + public function addTable(Table $table) { + if (!$this->getTableByName($table->getName())) { + $this->tables[] = $table; + } + + return $this; + } + + /** + * @param array $tables + */ + public function addTables(array $tables) { + foreach ($tables as $table) { + $this->addTable($table); + } + } + + /** + * Recursive function to traverse the schema looking for a path + * + * @param Table $table + * The current table to base fromm + * @param string $target + * The target joinable table alias + * @param int $depth + * The current level of recursion which reflects the number of joins needed + * @param Joinable[] $path + * (By-reference) The possible paths to the target table + * @param Joinable[] $currentPath + * For internal use only to track the path to reach the target table + */ + private function findPaths(Table $table, $target, $depth, &$path, $currentPath = [] + ) { + static $visited = []; + + // reset if new call + if ($depth === 1) { + $visited = []; + } + + $canBeShorter = empty($path) || count($currentPath) + 1 < count($path); + $tooFar = $depth > self::MAX_JOIN_DEPTH; + $beenHere = in_array($table->getName(), $visited); + + if ($tooFar || $beenHere || !$canBeShorter) { + return; + } + + // prevent circular reference + $visited[] = $table->getName(); + + foreach ($table->getExternalLinks() as $link) { + if ($link->getAlias() === $target) { + $path = array_merge($currentPath, [$link]); + } + else { + $linkTable = $this->getTableByName($link->getTargetTable()); + if ($linkTable) { + $nextStep = array_merge($currentPath, [$link]); + $this->findPaths($linkTable, $target, $depth + 1, $path, $nextStep); + } + } + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php new file mode 100644 index 00000000..b578b73a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/SchemaMapBuilder.php @@ -0,0 +1,217 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Entity; +use Civi\Api4\Event\Events; +use Civi\Api4\Event\SchemaMapBuildEvent; +use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable; +use Civi\Api4\Service\Schema\Joinable\Joinable; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Civi\Api4\Service\Schema\Joinable\OptionValueJoinable; +use CRM_Core_DAO_AllCoreTables as TableHelper; +use CRM_Utils_Array as UtilsArray; + +class SchemaMapBuilder { + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + /** + * @var array + */ + protected $apiEntities; + + /** + * @param EventDispatcherInterface $dispatcher + */ + public function __construct(EventDispatcherInterface $dispatcher) { + $this->dispatcher = $dispatcher; + $this->apiEntities = array_keys((array) Entity::get()->setCheckPermissions(FALSE)->addSelect('name')->execute()->indexBy('name')); + } + + /** + * @return SchemaMap + */ + public function build() { + $map = new SchemaMap(); + $this->loadTables($map); + + $event = new SchemaMapBuildEvent($map); + $this->dispatcher->dispatch(Events::SCHEMA_MAP_BUILD, $event); + + return $map; + } + + /** + * Add all tables and joins + * + * @param SchemaMap $map + */ + private function loadTables(SchemaMap $map) { + /** @var \CRM_Core_DAO $daoName */ + foreach (TableHelper::get() as $daoName => $data) { + $table = new Table($data['table']); + foreach ($daoName::fields() as $field => $fieldData) { + $this->addJoins($table, $field, $fieldData); + } + $map->addTable($table); + if (in_array($data['name'], $this->apiEntities)) { + $this->addCustomFields($map, $table, $data['name']); + } + } + + $this->addBackReferences($map); + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addJoins(Table $table, $field, array $data) { + $fkClass = UtilsArray::value('FKClassName', $data); + + // can there be multiple methods e.g. pseudoconstant and fkclass + if ($fkClass) { + $tableName = TableHelper::getTableForClass($fkClass); + $fkKey = UtilsArray::value('FKKeyColumn', $data, 'id'); + $alias = str_replace('_id', '', $field); + $joinable = new Joinable($tableName, $fkKey, $alias); + $joinable->setJoinType($joinable::JOIN_TYPE_MANY_TO_ONE); + $table->addTableLink($field, $joinable); + } + elseif (UtilsArray::value('pseudoconstant', $data)) { + $this->addPseudoConstantJoin($table, $field, $data); + } + } + + /** + * @param Table $table + * @param string $field + * @param array $data + */ + private function addPseudoConstantJoin(Table $table, $field, array $data) { + $pseudoConstant = UtilsArray::value('pseudoconstant', $data); + $tableName = UtilsArray::value('table', $pseudoConstant); + $optionGroupName = UtilsArray::value('optionGroupName', $pseudoConstant); + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'id'); + + if ($tableName) { + $alias = str_replace('civicrm_', '', $tableName); + $joinable = new Joinable($tableName, $keyColumn, $alias); + $condition = UtilsArray::value('condition', $pseudoConstant); + if ($condition) { + $joinable->addCondition($condition); + } + $table->addTableLink($field, $joinable); + } + elseif ($optionGroupName) { + $keyColumn = UtilsArray::value('keyColumn', $pseudoConstant, 'value'); + $joinable = new OptionValueJoinable($optionGroupName, NULL, $keyColumn); + + if (!empty($data['serialize'])) { + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + } + + $table->addTableLink($field, $joinable); + } + } + + /** + * Loop through existing links and provide link from the other side + * + * @param SchemaMap $map + */ + private function addBackReferences(SchemaMap $map) { + foreach ($map->getTables() as $table) { + foreach ($table->getTableLinks() as $link) { + // there are too many possible joins from option value so skip + if ($link instanceof OptionValueJoinable) { + continue; + } + + $target = $map->getTableByName($link->getTargetTable()); + $tableName = $link->getBaseTable(); + $plural = str_replace('civicrm_', '', $this->getPlural($tableName)); + $joinable = new Joinable($tableName, $link->getBaseColumn(), $plural); + $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY); + $target->addTableLink($link->getTargetColumn(), $joinable); + } + } + } + + /** + * Simple implementation of pluralization. + * Could be replaced with symfony/inflector + * + * @param string $singular + * + * @return string + */ + private function getPlural($singular) { + $last_letter = substr($singular, -1); + switch ($last_letter) { + case 'y': + return substr($singular, 0, -1) . 'ies'; + + case 's': + return $singular . 'es'; + + default: + return $singular . 's'; + } + } + + /** + * @param \Civi\Api4\Service\Schema\SchemaMap $map + * @param \Civi\Api4\Service\Schema\Table $baseTable + * @param string $entity + */ + private function addCustomFields(SchemaMap $map, Table $baseTable, $entity) { + // Don't be silly + if (!array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + return; + } + $queryEntity = (array) $entity; + if ($entity == 'Contact') { + $queryEntity = ['Contact', 'Individual', 'Organization', 'Household']; + } + $fieldData = \CRM_Utils_SQL_Select::from('civicrm_custom_field f') + ->join('custom_group', 'INNER JOIN civicrm_custom_group g ON g.id = f.custom_group_id') + ->select(['g.name as custom_group_name', 'g.table_name', 'g.is_multiple', 'f.name', 'label', 'column_name', 'option_group_id']) + ->where('g.extends IN (@entity)', ['@entity' => $queryEntity]) + ->where('g.is_active') + ->where('f.is_active') + ->execute(); + + $links = []; + + while ($fieldData->fetch()) { + $tableName = $fieldData->table_name; + + $customTable = $map->getTableByName($tableName); + if (!$customTable) { + $customTable = new Table($tableName); + } + + if (!empty($fieldData->option_group_id)) { + $optionValueJoinable = new OptionValueJoinable($fieldData->option_group_id, $fieldData->label); + $customTable->addTableLink($fieldData->column_name, $optionValueJoinable); + } + + $map->addTable($customTable); + + $alias = $fieldData->custom_group_name; + $links[$alias]['tableName'] = $tableName; + $links[$alias]['isMultiple'] = !empty($fieldData->is_multiple); + $links[$alias]['columns'][$fieldData->name] = $fieldData->column_name; + } + + foreach ($links as $alias => $link) { + $joinable = new CustomGroupJoinable($link['tableName'], $alias, $link['isMultiple'], $entity, $link['columns']); + $baseTable->addTableLink('id', $joinable); + } + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php new file mode 100644 index 00000000..1f464a45 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Schema/Table.php @@ -0,0 +1,128 @@ +<?php + +namespace Civi\Api4\Service\Schema; + +use Civi\Api4\Service\Schema\Joinable\Joinable; + +class Table { + + /** + * @var string + */ + protected $name; + + /** + * @var Joinable[] + * Array of links to other tables + */ + protected $tableLinks = []; + + /** + * @param $name + */ + public function __construct($name) { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return Joinable[] + */ + public function getTableLinks() { + return $this->tableLinks; + } + + /** + * @return Joinable[] + * Only those links that are not joining the table to itself + */ + public function getExternalLinks() { + return array_filter($this->tableLinks, function (Joinable $joinable) { + return $joinable->getTargetTable() !== $this->getName(); + }); + } + + /** + * @param Joinable $linkToRemove + */ + public function removeLink(Joinable $linkToRemove) { + foreach ($this->tableLinks as $index => $link) { + if ($link === $linkToRemove) { + unset($this->tableLinks[$index]); + } + } + } + + /** + * @param string $baseColumn + * @param Joinable $joinable + * + * @return $this + */ + public function addTableLink($baseColumn, Joinable $joinable) { + $target = $joinable->getTargetTable(); + $targetCol = $joinable->getTargetColumn(); + $alias = $joinable->getAlias(); + + if (!$this->hasLink($target, $targetCol, $alias)) { + if (!$joinable->getBaseTable()) { + $joinable->setBaseTable($this->getName()); + } + if (!$joinable->getBaseColumn()) { + $joinable->setBaseColumn($baseColumn); + } + $this->tableLinks[] = $joinable; + } + + return $this; + } + + /** + * @param mixed $tableLinks + * + * @return $this + */ + public function setTableLinks($tableLinks) { + $this->tableLinks = $tableLinks; + + return $this; + } + + /** + * @param $target + * @param $targetCol + * @param $alias + * + * @return bool + */ + private function hasLink($target, $targetCol, $alias) { + foreach ($this->tableLinks as $link) { + if ($link->getTargetTable() === $target + && $link->getTargetColumn() === $targetCol + && $link->getAlias() === $alias + ) { + return TRUE; + } + } + + return FALSE; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php new file mode 100644 index 00000000..2c689344 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/CustomFieldSpec.php @@ -0,0 +1,118 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +class CustomFieldSpec extends FieldSpec { + /** + * @var int + */ + protected $customFieldId; + + /** + * @var int + */ + protected $customGroup; + + /** + * @var string + */ + protected $tableName; + + /** + * @var string + */ + protected $columnName; + + /** + * @inheritDoc + */ + public function setDataType($dataType) { + switch ($dataType) { + case 'ContactReference': + $this->setFkEntity('Contact'); + $dataType = 'Integer'; + break; + + case 'File': + case 'StateProvince': + case 'Country': + $this->setFkEntity($dataType); + $dataType = 'Integer'; + break; + } + return parent::setDataType($dataType); + } + + /** + * @return int + */ + public function getCustomFieldId() { + return $this->customFieldId; + } + + /** + * @param int $customFieldId + * + * @return $this + */ + public function setCustomFieldId($customFieldId) { + $this->customFieldId = $customFieldId; + + return $this; + } + + /** + * @return int + */ + public function getCustomGroupName() { + return $this->customGroup; + } + + /** + * @param string $customGroupName + * + * @return $this + */ + public function setCustomGroupName($customGroupName) { + $this->customGroup = $customGroupName; + + return $this; + } + + /** + * @return string + */ + public function getCustomTableName() { + return $this->tableName; + } + + /** + * @param string $customFieldColumnName + * + * @return $this + */ + public function setCustomTableName($customFieldColumnName) { + $this->tableName = $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; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php new file mode 100644 index 00000000..1db2941e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/FieldSpec.php @@ -0,0 +1,320 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use Civi\Api4\Utils\CoreUtil; + +class FieldSpec { + /** + * @var mixed + */ + protected $defaultValue; + + /** + * @var string + */ + protected $name; + + /** + * @var string + */ + protected $title; + + /** + * @var string + */ + protected $entity; + + /** + * @var string + */ + protected $description; + + /** + * @var bool + */ + protected $required = FALSE; + + /** + * @var bool + */ + protected $requiredIf; + + /** + * @var array|boolean + */ + protected $options; + + /** + * @var string + */ + protected $dataType; + + /** + * @var string + */ + protected $fkEntity; + + /** + * @var int + */ + protected $serialize; + + /** + * Aliases for the valid data types + * + * @var array + */ + public static $typeAliases = [ + 'Int' => 'Integer', + 'Link' => 'Url', + 'Memo' => 'Text', + ]; + + /** + * @param string $name + * @param string $entity + * @param string $dataType + */ + public function __construct($name, $entity, $dataType = 'String') { + $this->entity = $entity; + $this->setName($name); + $this->setDataType($dataType); + } + + /** + * @return mixed + */ + public function getDefaultValue() { + return $this->defaultValue; + } + + /** + * @param mixed $defaultValue + * + * @return $this + */ + public function setDefaultValue($defaultValue) { + $this->defaultValue = $defaultValue; + + return $this; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription($description) { + $this->description = $description; + + return $this; + } + + /** + * @return bool + */ + public function isRequired() { + return $this->required; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequired($required) { + $this->required = $required; + + return $this; + } + + /** + * @return bool + */ + public function getRequiredIf() { + return $this->requiredIf; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequiredIf($requiredIf) { + $this->requiredIf = $requiredIf; + + return $this; + } + + /** + * @return string + */ + public function getDataType() { + return $this->dataType; + } + + /** + * @param $dataType + * + * @return $this + * @throws \Exception + */ + public function setDataType($dataType) { + if (array_key_exists($dataType, self::$typeAliases)) { + $dataType = self::$typeAliases[$dataType]; + } + + if (!in_array($dataType, $this->getValidDataTypes())) { + throw new \Exception(sprintf('Invalid data type "%s', $dataType)); + } + + $this->dataType = $dataType; + + return $this; + } + + /** + * @return int + */ + public function getSerialize() { + return $this->serialize; + } + + /** + * @param int|null $serialize + */ + public function setSerialize($serialize) { + $this->serialize = $serialize; + } + + /** + * Add valid types that are not not part of \CRM_Utils_Type::dataTypes + * + * @return array + */ + private function getValidDataTypes() { + $extraTypes = ['Boolean', 'Text', 'Float', 'Url']; + $extraTypes = array_combine($extraTypes, $extraTypes); + + return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes); + } + + /** + * @return array + */ + public function getOptions() { + if (!isset($this->options) || $this->options === TRUE) { + $fieldName = $this->getName(); + + if ($this instanceof CustomFieldSpec) { + // buildOptions relies on the custom_* type of field names + $fieldName = sprintf('custom_%d', $this->getCustomFieldId()); + } + + $dao = CoreUtil::getDAOFromApiName($this->getEntity()); + $options = $dao::buildOptions($fieldName); + + if (!is_array($options) || !$options) { + $options = FALSE; + } + + $this->setOptions($options); + } + return $this->options; + } + + /** + * @param array|bool $options + * + * @return $this + */ + public function setOptions($options) { + $this->options = $options; + return $this; + } + + /** + * @return string + */ + public function getFkEntity() { + return $this->fkEntity; + } + + /** + * @param string $fkEntity + * + * @return $this + */ + public function setFkEntity($fkEntity) { + $this->fkEntity = $fkEntity; + + return $this; + } + + /** + * @param array $values + * @return array + */ + public function toArray($values = []) { + $ret = []; + foreach (get_object_vars($this) as $key => $val) { + $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key)); + if (!$values || in_array($key, $values)) { + $ret[$key] = $val; + } + } + return $ret; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php new file mode 100644 index 00000000..660bfec9 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActionScheduleCreationSpecProvider.php @@ -0,0 +1,27 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class ActionScheduleCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('title')->setRequired(TRUE); + $spec->getFieldByName('mapping_id')->setRequired(TRUE); + $spec->getFieldByName('entity_value')->setRequired(TRUE); + $spec->getFieldByName('start_action_date')->setRequiredIf('empty($values.absolute_date)'); + $spec->getFieldByName('absolute_date')->setRequiredIf('empty($values.start_action_date)'); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'ActionSchedule' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php new file mode 100644 index 00000000..dc254342 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ActivityCreationSpecProvider.php @@ -0,0 +1,27 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class ActivityCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $sourceContactField = new FieldSpec('source_contact_id', 'Activity', 'Integer'); + $sourceContactField->setRequired(TRUE); + $sourceContactField->setFkEntity('Contact'); + + $spec->addFieldSpec($sourceContactField); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Activity' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php new file mode 100644 index 00000000..afba9c79 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/AddressCreationSpecProvider.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + + +class AddressCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Address' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php new file mode 100644 index 00000000..94c68d9d --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactCreationSpecProvider.php @@ -0,0 +1,32 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContactCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_type') + ->setRequired(TRUE) + ->setDefaultValue('Individual'); + + $spec->getFieldByName('is_opt_out')->setRequired(FALSE); + $spec->getFieldByName('is_deleted')->setRequired(FALSE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Contact' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php new file mode 100644 index 00000000..f55deb1c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContactTypeCreationSpecProvider.php @@ -0,0 +1,29 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContactTypeCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('label')->setRequired(TRUE); + $spec->getFieldByName('name')->setRequired(TRUE); + $spec->getFieldByName('parent_id')->setRequired(TRUE); + + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'ContactType' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php new file mode 100644 index 00000000..14861871 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/ContributionCreationSpecProvider.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class ContributionCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('financial_type_id')->setRequired(TRUE); + $spec->getFieldByName('receive_date')->setDefaultValue('now'); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Contribution' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php new file mode 100644 index 00000000..cd033754 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomGroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class CustomGroupCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + return $spec->getFieldByName('extends')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'CustomGroup' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php new file mode 100644 index 00000000..cd82d438 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/CustomValueSpecProvider.php @@ -0,0 +1,34 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\FieldSpec; +use Civi\Api4\Service\Spec\RequestSpec; + +class CustomValueSpecProvider implements SpecProviderInterface { + + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $action = $spec->getAction(); + if ($action !== 'create') { + $idField = new FieldSpec('id', $spec->getEntity(), 'Integer'); + $idField->setTitle(ts('Custom Value ID')); + $spec->addFieldSpec($idField); + } + $entityField = new FieldSpec('entity_id', $spec->getEntity(), 'Integer'); + $entityField->setTitle(ts('Entity ID')); + $entityField->setRequired($action === 'create'); + $entityField->setFkEntity('Contact'); + $spec->addFieldSpec($entityField); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return strstr($entity, 'Custom_'); + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php new file mode 100644 index 00000000..136b0e54 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EmailCreationSpecProvider.php @@ -0,0 +1,25 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class EmailCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('email')->setRequired(TRUE); + $spec->getFieldByName('on_hold')->setRequired(FALSE); + $spec->getFieldByName('is_bulkmail')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Email' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php new file mode 100644 index 00000000..42b74a6f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/EventCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class EventCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('is_template')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Event' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php new file mode 100644 index 00000000..8af69a0a --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/GroupCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class GroupCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('title')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Group' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php new file mode 100644 index 00000000..7d5fc270 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NavigationCreationSpecProvider.php @@ -0,0 +1,22 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class NavigationCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('domain_id')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Navigation' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php new file mode 100644 index 00000000..f12e592c --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/NoteCreationSpecProvider.php @@ -0,0 +1,28 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + + +class NoteCreationSpecProvider implements SpecProviderInterface { + + /** + * @param RequestSpec $spec + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('note')->setRequired(TRUE); + $spec->getFieldByName('entity_table')->setDefaultValue('civicrm_contact'); + } + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action) { + return $entity === 'Note' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php new file mode 100644 index 00000000..4ea634c1 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/OptionValueCreationSpecProvider.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class OptionValueCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('weight')->setRequired(FALSE); + $spec->getFieldByName('value')->setRequired(FALSE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'OptionValue' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php new file mode 100644 index 00000000..bb757d43 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/PhoneCreationSpecProvider.php @@ -0,0 +1,24 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +class PhoneCreationSpecProvider implements SpecProviderInterface { + /** + * @inheritDoc + */ + public function modifySpec(RequestSpec $spec) { + $spec->getFieldByName('contact_id')->setRequired(TRUE); + $spec->getFieldByName('location_type_id')->setRequired(TRUE); + $spec->getFieldByName('phone')->setRequired(TRUE); + } + + /** + * @inheritDoc + */ + public function applies($entity, $action) { + return $entity === 'Phone' && $action === 'create'; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php new file mode 100644 index 00000000..8be77e68 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/Provider/SpecProviderInterface.php @@ -0,0 +1,23 @@ +<?php + +namespace Civi\Api4\Service\Spec\Provider; + +use Civi\Api4\Service\Spec\RequestSpec; + +interface SpecProviderInterface { + /** + * @param RequestSpec $spec + * + * @return void + */ + public function modifySpec(RequestSpec $spec); + + /** + * @param string $entity + * @param string $action + * + * @return bool + */ + public function applies($entity, $action); + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php new file mode 100644 index 00000000..9437d930 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/RequestSpec.php @@ -0,0 +1,110 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +class RequestSpec { + + /** + * @var string + */ + protected $entity; + + /** + * @var string + */ + protected $action; + + /** + * @var FieldSpec[] + */ + protected $fields = []; + + /** + * @param string $entity + * @param string $action + */ + public function __construct($entity, $action) { + $this->entity = $entity; + $this->action = $action; + } + + public function addFieldSpec(FieldSpec $field) { + $this->fields[] = $field; + } + + /** + * @param $name + * + * @return FieldSpec|null + */ + public function getFieldByName($name) { + foreach ($this->fields as $field) { + if ($field->getName() === $name) { + return $field; + } + } + + return NULL; + } + + /** + * @return array + * Gets all the field names currently part of the specification + */ + public function getFieldNames() { + return array_map(function(FieldSpec $field) { + return $field->getName(); + }, $this->fields); + } + + /** + * @return array|FieldSpec[] + */ + public function getRequiredFields() { + return array_filter($this->fields, function (FieldSpec $field) { + return $field->isRequired(); + }); + } + + /** + * @return array|FieldSpec[] + */ + public function getConditionalRequiredFields() { + return array_filter($this->fields, function (FieldSpec $field) { + return $field->getRequiredIf(); + }); + } + + /** + * @param array $fieldNames + * Optional array of fields to return + * @return FieldSpec[] + */ + public function getFields($fieldNames = NULL) { + if (!$fieldNames) { + return $this->fields; + } + $fields = []; + foreach ($this->fields as $field) { + if (in_array($field->getName(), $fieldNames)) { + $fields[] = $field; + } + } + return $fields; + } + + /** + * @return string + */ + public function getEntity() { + return $this->entity; + } + + /** + * @return string + */ + public function getAction() { + return $this->action; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php new file mode 100644 index 00000000..c8e4d3da --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecFormatter.php @@ -0,0 +1,117 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use CRM_Utils_Array as ArrayHelper; +use CRM_Core_DAO_AllCoreTables as TableHelper; + +class SpecFormatter { + /** + * @param FieldSpec[] $fields + * @param array $return + * @param bool $includeFieldOptions + * + * @return array + */ + public static function specToArray($fields, $return = [], $includeFieldOptions = FALSE) { + $fieldArray = []; + + foreach ($fields as $field) { + if ($includeFieldOptions || in_array('options', $return)) { + $field->getOptions(); + } + $fieldArray[$field->getName()] = $field->toArray($return); + } + + return $fieldArray; + } + + /** + * @param array $data + * @param string $entity + * + * @return FieldSpec + */ + public static function arrayToField(array $data, $entity) { + $dataTypeName = self::getDataType($data); + + if (!empty($data['custom_group_id'])) { + $field = new CustomFieldSpec($data['name'], $entity, $dataTypeName); + if (strpos($entity, 'Custom_') !== 0) { + $field->setName($data['custom_group']['name'] . '.' . $data['name']); + } + else { + $field->setCustomTableName($data['custom_group']['table_name']); + $field->setCustomFieldColumnName($data['column_name']); + } + $field->setCustomFieldId(ArrayHelper::value('id', $data)); + $field->setCustomGroupName($data['custom_group']['name']); + $field->setTitle(ArrayHelper::value('label', $data)); + $field->setOptions(self::customFieldHasOptions($data)); + if (\CRM_Core_BAO_CustomField::isSerialized($data)) { + $field->setSerialize(\CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND); + } + } + else { + $name = ArrayHelper::value('name', $data); + $field = new FieldSpec($name, $entity, $dataTypeName); + $field->setRequired((bool) ArrayHelper::value('required', $data, FALSE)); + $field->setTitle(ArrayHelper::value('title', $data)); + $field->setOptions(!empty($data['pseudoconstant'])); + $field->setSerialize(ArrayHelper::value('serialize', $data)); + } + + $field->setDefaultValue(ArrayHelper::value('default', $data)); + $field->setDescription(ArrayHelper::value('description', $data)); + + $fkAPIName = ArrayHelper::value('FKApiName', $data); + $fkClassName = ArrayHelper::value('FKClassName', $data); + if ($fkAPIName || $fkClassName) { + $field->setFkEntity($fkAPIName ?: TableHelper::getBriefName($fkClassName)); + } + + return $field; + } + + /** + * Does this custom field have options + * + * @param array $field + * @return bool + */ + private static function customFieldHasOptions($field) { + // This will include boolean fields with Yes/No options. + if (in_array($field['html_type'], ['Radio', 'CheckBox'])) { + return TRUE; + } + // Do this before the "Select" string search because date fields have a "Select Date" html_type + // and contactRef fields have an "Autocomplete-Select" html_type - contacts are an FK not an option list. + if (in_array($field['data_type'], ['ContactReference', 'Date'])) { + return FALSE; + } + if (strpos($field['html_type'], 'Select')) { + return TRUE; + } + return !empty($field['option_group_id']); + } + + /** + * Get the data type from an array. Defaults to 'data_type' with fallback to + * mapping for the integer value 'type' + * + * @param array $data + * + * @return string + */ + private static function getDataType(array $data) { + if (isset($data['data_type'])) { + return $data['data_type']; + } + + $dataTypeInt = ArrayHelper::value('type', $data); + $dataTypeName = \CRM_Utils_Type::typeToString($dataTypeInt); + + return $dataTypeName; + } + +} diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php new file mode 100644 index 00000000..b1c83c89 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/Civi/Api4/Service/Spec/SpecGatherer.php @@ -0,0 +1,131 @@ +<?php + +namespace Civi\Api4\Service\Spec; + +use Civi\Api4\CustomField; +use Civi\Api4\Service\Spec\Provider\SpecProviderInterface; +use Civi\Api4\Utils\CoreUtil; + +class SpecGatherer { + + /** + * @var SpecProviderInterface[] + */ + protected $specProviders = []; + + /** + * A cache of DAOs based on entity + * + * @var \CRM_Core_DAO[] + */ + protected $DAONames; + + /** + * Returns a RequestSpec with all the fields available. Uses spec providers + * to add or modify field specifications. + * For an example @see CustomFieldSpecProvider. + * + * @param string $entity + * @param string $action + * @param $includeCustom + * + * @return \Civi\Api4\Service\Spec\RequestSpec + */ + public function getSpec($entity, $action, $includeCustom) { + $specification = new RequestSpec($entity, $action); + + // Real entities + if (strpos($entity, 'Custom_') !== 0) { + $this->addDAOFields($entity, $action, $specification); + if ($includeCustom && array_key_exists($entity, \CRM_Core_SelectValues::customGroupExtends())) { + $this->addCustomFields($entity, $specification); + } + } + // Custom pseudo-entities + else { + $this->getCustomGroupFields(substr($entity, 7), $specification); + } + + foreach ($this->specProviders as $provider) { + if ($provider->applies($entity, $action)) { + $provider->modifySpec($specification); + } + } + + return $specification; + } + + /** + * @param SpecProviderInterface $provider + */ + public function addSpecProvider(SpecProviderInterface $provider) { + $this->specProviders[] = $provider; + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addDAOFields($entity, $action, RequestSpec $specification) { + $DAOFields = $this->getDAOFields($entity); + + foreach ($DAOFields as $DAOField) { + if ($DAOField['name'] == 'id' && $action == 'create') { + continue; + } + if ($action !== 'create' || isset($DAOField['default'])) { + $DAOField['required'] = FALSE; + } + if ($DAOField['name'] == 'is_active' && empty($DAOField['default'])) { + $DAOField['default'] = '1'; + } + $field = SpecFormatter::arrayToField($DAOField, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entity + * @param RequestSpec $specification + */ + private function addCustomFields($entity, RequestSpec $specification) { + $extends = ($entity == 'Contact') ? ['Contact', 'Individual', 'Organization', 'Household'] : [$entity]; + $customFields = CustomField::get() + ->addWhere('custom_group.extends', 'IN', $extends) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, $entity); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $customGroup + * @param RequestSpec $specification + */ + private function getCustomGroupFields($customGroup, RequestSpec $specification) { + $customFields = CustomField::get() + ->addWhere('custom_group.name', '=', $customGroup) + ->setSelect(['custom_group.name', 'custom_group_id', 'name', 'label', 'data_type', 'html_type', 'is_searchable', 'is_search_range', 'weight', 'is_active', 'is_view', 'option_group_id', 'default_value', 'custom_group.table_name', 'column_name']) + ->execute(); + + foreach ($customFields as $fieldArray) { + $field = SpecFormatter::arrayToField($fieldArray, 'Custom_' . $customGroup); + $specification->addFieldSpec($field); + } + } + + /** + * @param string $entityName + * + * @return array + */ + private function getDAOFields($entityName) { + $dao = CoreUtil::getDAOFromApiName($entityName); + + return $dao::fields(); + } + +} |