4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
20 namespace Civi\Api4\Service\Schema
;
23 use Civi\Api4\Event\Events
;
24 use Civi\Api4\Event\SchemaMapBuildEvent
;
25 use Civi\Api4\Service\Schema\Joinable\CustomGroupJoinable
;
26 use Civi\Api4\Service\Schema\Joinable\Joinable
;
27 use Symfony\Component\EventDispatcher\EventDispatcherInterface
;
28 use CRM_Core_DAO_AllCoreTables
as AllCoreTables
;
30 class SchemaMapBuilder
{
32 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
34 protected $dispatcher;
38 protected $apiEntities;
41 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
43 public function __construct(EventDispatcherInterface
$dispatcher) {
44 $this->dispatcher
= $dispatcher;
45 $this->apiEntities
= array_keys((array) Entity
::get(FALSE)->addSelect('name')->execute()->indexBy('name'));
51 public function build() {
52 $map = new SchemaMap();
53 $this->loadTables($map);
55 $event = new SchemaMapBuildEvent($map);
56 $this->dispatcher
->dispatch(Events
::SCHEMA_MAP_BUILD
, $event);
62 * Add all tables and joins
64 * @param SchemaMap $map
66 private function loadTables(SchemaMap
$map) {
67 /** @var \CRM_Core_DAO $daoName */
68 foreach (AllCoreTables
::get() as $daoName => $data) {
69 $table = new Table($data['table']);
70 foreach ($daoName::fields() as $fieldData) {
71 $this->addJoins($table, $fieldData['name'], $fieldData);
73 $map->addTable($table);
74 if (in_array($data['name'], $this->apiEntities
)) {
75 $this->addCustomFields($map, $table, $data['name']);
79 $this->addBackReferences($map);
84 * @param string $field
87 private function addJoins(Table
$table, $field, array $data) {
88 $fkClass = $data['FKClassName'] ??
NULL;
90 // can there be multiple methods e.g. pseudoconstant and fkclass
92 $tableName = AllCoreTables
::getTableForClass($fkClass);
93 $fkKey = $data['FKKeyColumn'] ??
'id';
94 $alias = str_replace('_id', '', $field);
95 $joinable = new Joinable($tableName, $fkKey, $alias);
96 $joinable->setJoinType($joinable::JOIN_TYPE_MANY_TO_ONE
);
97 $table->addTableLink($field, $joinable);
102 * Loop through existing links and provide link from the other side
104 * @param SchemaMap $map
106 private function addBackReferences(SchemaMap
$map) {
107 foreach ($map->getTables() as $table) {
108 foreach ($table->getTableLinks() as $link) {
109 $target = $map->getTableByName($link->getTargetTable());
110 $tableName = $link->getBaseTable();
111 // Exclude custom field tables
112 if (strpos($link->getTargetTable(), 'civicrm_value_') !== 0 && strpos($link->getBaseTable(), 'civicrm_value_') !== 0) {
113 $plural = str_replace('civicrm_', '', $this->getPlural($tableName));
114 $joinable = new Joinable($tableName, $link->getBaseColumn(), $plural);
115 $joinable->setJoinType($joinable::JOIN_TYPE_ONE_TO_MANY
);
116 $target->addTableLink($link->getTargetColumn(), $joinable);
123 * Simple implementation of pluralization.
124 * Could be replaced with symfony/inflector
126 * @param string $singular
130 private function getPlural($singular) {
131 $last_letter = substr($singular, -1);
132 switch ($last_letter) {
134 return substr($singular, 0, -1) . 'ies';
137 return $singular . 'es';
140 return $singular . 's';
145 * @param \Civi\Api4\Service\Schema\SchemaMap $map
146 * @param \Civi\Api4\Service\Schema\Table $baseTable
147 * @param string $entity
149 private function addCustomFields(SchemaMap
$map, Table
$baseTable, $entity) {
151 if (!array_key_exists($entity, \CRM_Core_SelectValues
::customGroupExtends())) {
154 $queryEntity = (array) $entity;
155 if ($entity == 'Contact') {
156 $queryEntity = ['Contact', 'Individual', 'Organization', 'Household'];
158 $fieldData = \CRM_Utils_SQL_Select
::from('civicrm_custom_field f')
159 ->join('custom_group', 'INNER JOIN civicrm_custom_group g ON g.id = f.custom_group_id')
160 ->select(['g.name as custom_group_name', 'g.table_name', 'g.is_multiple', 'f.name', 'label', 'column_name', 'option_group_id'])
161 ->where('g.extends IN (@entity)', ['@entity' => $queryEntity])
162 ->where('g.is_active')
163 ->where('f.is_active')
168 while ($fieldData->fetch()) {
169 $tableName = $fieldData->table_name
;
171 $customTable = $map->getTableByName($tableName);
173 $customTable = new Table($tableName);
176 $map->addTable($customTable);
178 $alias = $fieldData->custom_group_name
;
179 $links[$alias]['tableName'] = $tableName;
180 $links[$alias]['isMultiple'] = !empty($fieldData->is_multiple
);
181 $links[$alias]['columns'][$fieldData->name
] = $fieldData->column_name
;
184 if (!empty($fieldData->is_multiple
)) {
185 $joinable = new Joinable($baseTable->getName(), 'id', AllCoreTables
::convertEntityNameToLower($entity));
186 $customTable->addTableLink('entity_id', $joinable);
190 foreach ($links as $alias => $link) {
191 $joinable = new CustomGroupJoinable($link['tableName'], $alias, $link['isMultiple'], $link['columns']);
192 $baseTable->addTableLink('id', $joinable);