-
Notifications
You must be signed in to change notification settings - Fork 56
/
AddEntityBoilerplateCommand.php
233 lines (208 loc) · 8.62 KB
/
AddEntityBoilerplateCommand.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
<?php
namespace CRM\CivixBundle\Command;
use Civix;
use CRM\CivixBundle\Utils\Files;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CRM\CivixBundle\Builder\Info;
use CRM\CivixBundle\Builder\Module;
use CRM\CivixBundle\Utils\Path;
use Exception;
class AddEntityBoilerplateCommand extends AbstractCommand {
const API_VERSION = 3;
protected function configure() {
parent::configure();
$this
->setName('generate:entity-boilerplate')
->setDescription('Generates boilerplate code for entities based on xml schema definition files (*EXPERIMENTAL AND INCOMPLETE*)')
->setHelp(
"Creates DAOs, mysql install and uninstall instructions.\n" .
"\n" .
"Typically you will run this command after creating or updating one or more\n" .
"xml/schema/CRM/NameSpace/EntityName.xml files.\n"
);
}
/**
* Note: this function replicates a fair amount of the functionality of
* CRM_Core_CodeGen_Specification (which is a bit messy and hard to interact
* with). It's tempting to completely rewrite / rethink entity generation. Until
* then...
*/
protected function execute(InputInterface $input, OutputInterface $output) {
Civix::boot(['output' => $output]);
$civicrm_api3 = Civix::api3();
if (!$civicrm_api3 || !$civicrm_api3->local) {
$output->writeln("<error>Require access to local CiviCRM source tree. Configure civicrm_api3_conf_path.</error>");
return 1;
}
if (version_compare(\CRM_Utils_System::version(), '4.7.0', '<=')) {
$output->writeln("<error>This command requires CiviCRM 4.7+.</error>");
return 1;
}
$this->assertCurrentFormat();
$ctx = [];
$ctx['type'] = 'module';
$ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir();
$basedir = new Path($ctx['basedir']);
$info = new Info($basedir->string('info.xml'));
$info->load($ctx);
$attrs = $info->get()->attributes();
if ($attrs['type'] != 'module') {
$output->writeln('<error>Wrong extension type: ' . $attrs['type'] . '</error>');
return;
}
$xmlSchemaGlob = "xml/schema/{$ctx['namespace']}/*.xml";
$absXmlSchemaGlob = $basedir->string($xmlSchemaGlob);
$xmlSchemas = glob($absXmlSchemaGlob);
if (!count($xmlSchemas)) {
throw new Exception("Could not find files matching '$xmlSchemaGlob'. You may want to run `civix generate:entity` before running this command.");
}
$specification = new \CRM_Core_CodeGen_Specification();
$specification->buildVersion = \CRM_Utils_System::majorVersion();
$config = new \stdClass();
$config->phpCodePath = $basedir->string('');
$config->sqlCodePath = $basedir->string('sql/');
$config->database = $this->getDefaultDatabase();
foreach ($xmlSchemas as $xmlSchema) {
$dom = new \DomDocument();
$xmlString = file_get_contents($xmlSchema);
$dom->loadXML($xmlString);
$xml = simplexml_import_dom($dom);
if (!$xml) {
$output->writeln('<error>There is an error in the XML for ' . $xmlSchema . '</error>');
continue;
}
/** @var array $tables */
$specification->getTable($xml, $config->database, $tables);
$name = (string) $xml->name;
$tables[$name]['name'] = $name;
$sourcePath = strstr($xmlSchema, "/xml/schema/{$ctx['namespace']}/");
$tables[$name]['sourceFile'] = $ctx['fullName'] . $sourcePath;
}
$config->tables = $tables;
$_namespace = ' ' . preg_replace(':/:', '_', $ctx['namespace']);
$this->orderTables($tables, $output);
$this->resolveForeignKeys($tables);
$config->tables = $tables;
foreach ($tables as $table) {
$dao = new \CRM_Core_CodeGen_DAO($config, (string) $table['name'], "{$_namespace}_ExtensionUtil::ts");
// Don't display gencode's output
ob_start();
$dao->run();
ob_end_clean();
$daoFileName = $basedir->string("{$table['base']}{$table['fileName']}");
$output->writeln("<info>Write</info>" . Files::relativize($daoFileName));
}
$schema = new \CRM_Core_CodeGen_Schema($config);
\CRM_Core_CodeGen_Util_File::createDir($config->sqlCodePath);
/**
* @param string $generator
* The desired $schema->$generator() function which will produce the file.
* @param string $fileName
* The desired basename of the SQL file.
*/
$createSql = function($generator, $fileName) use ($output, $schema, $config) {
$filePath = $config->sqlCodePath . $fileName;
// We're poking into an internal class+function (`$schema->$generator()`) that changed in v5.23.
// Beginning in 5.23: $schema->$function() returns an array with file content.
// Before 5.23: $schema->$function($fileName) creates $fileName and returns void.
$output->writeln("<info>Write</info> " . Files::relativize($filePath));
if (version_compare(\CRM_Utils_System::version(), '5.23.alpha1', '>=')) {
$data = $schema->$generator();
if (!file_put_contents($filePath, reset($data))) {
$output->writeln("<error>Failed to write data to {$filePath}</error>");
}
}
else {
$output->writeln("<error>WARNING</error>: Support for generating entities on <5.23 is deprecated.");
// Don't display gencode's output
ob_start();
$schema->$generator($fileName);
ob_end_clean();
}
};
$createSql('generateCreateSql', 'auto_install.sql');
$createSql('generateDropSql', 'auto_uninstall.sql');
$module = new Module(Civix::templating());
$module->loadInit($ctx);
$module->save($ctx, $output);
$upgraderClass = str_replace('/', '_', $ctx['namespace']) . '_Upgrader';
if (!class_exists($upgraderClass)) {
$output->writeln('<comment>You are missing an upgrader class. Your generated SQL files will not be executed on enable and uninstall. Fix this by running `civix generate:upgrader`.</comment>');
}
return 0;
}
private function orderTables(&$tables, $output) {
$ordered = [];
$abort = count($tables);
while (count($tables)) {
// Safety valve
if ($abort-- == 0) {
$output->writeln("<error>Cannot determine FK ordering of tables.</error> Do you have circular Foreign Keys? Change your FK's or fix your auto_install.sql");
break;
}
// Consider each table
foreach ($tables as $k => $table) {
// No FK's? Easy - add now
if (!isset($table['foreignKey'])) {
$ordered[$k] = $table;
unset($tables[$k]);
}
if (isset($table['foreignKey'])) {
// If any FK references a table still in our list (but is not a self-reference),
// skip this table for now
foreach ($table['foreignKey'] as $fKey) {
if (in_array($fKey['table'], array_keys($tables)) && $fKey['table'] != $table['name']) {
continue 2;
}
}
// If we get here, all FK's reference already added tables or external tables so add now
$ordered[$k] = $table;
unset($tables[$k]);
}
}
}
$tables = $ordered;
}
private function resolveForeignKeys(&$tables) {
foreach ($tables as &$table) {
if (isset($table['foreignKey'])) {
foreach ($table['foreignKey'] as &$key) {
if (isset($tables[$key['table']])) {
$key['className'] = $tables[$key['table']]['className'];
$key['fileName'] = $tables[$key['table']]['fileName'];
$table['fields'][$key['name']]['FKClassName'] = $key['className'];
}
else {
$key['className'] = \CRM_Core_DAO_AllCoreTables::getClassForTable($key['table']);
$key['fileName'] = $key['className'] . '.php';
$table['fields'][$key['name']]['FKClassName'] = $key['className'];
}
}
}
}
}
/**
* Get general/default database options (eg character set, collation).
*
* In civicrm-core, the `database` definition comes from
* `xml/schema/Schema.xml` and `$spec->getDatabase($dbXml)`.
*
* Civix uses different defaults. Explanations are inlined below.
*
* @return array
*/
private function getDefaultDatabase(): array {
return [
'name' => '',
'attributes' => '',
'tableAttributes_modern' => 'ENGINE=InnoDB',
'tableAttributes_simple' => 'ENGINE=InnoDB',
// ^^ Set very limited defaults.
// Existing deployments may be inconsistent with respect to charsets and collations, and
// it's hard to attune with static code. This represents a compromise (until we can
// rework the process in a way that clearly addresses the inconsistencies among deployments).
'comment' => '',
];
}
}