Skip to content

Commit

Permalink
Add generate:managed command to export managed entities and afforms
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Oct 2, 2023
1 parent ba584a1 commit 06bc8b0
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/CRM/CivixBundle/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use CRM\CivixBundle\Command\AddEntityCommand;
use CRM\CivixBundle\Command\AddEntityBoilerplateCommand;
use CRM\CivixBundle\Command\AddFormCommand;
use CRM\CivixBundle\Command\AddManagedEntityCommand;
use CRM\CivixBundle\Command\AddPageCommand;
use CRM\CivixBundle\Command\AddReportCommand;
use CRM\CivixBundle\Command\AddSearchCommand;
Expand Down Expand Up @@ -59,6 +60,7 @@ public function createCommands($context = 'default') {
$commands[] = new AddEntityCommand();
$commands[] = new AddEntityBoilerplateCommand();
$commands[] = new AddFormCommand();
$commands[] = new AddManagedEntityCommand();
$commands[] = new AddPageCommand();
$commands[] = new AddReportCommand();
$commands[] = new AddSearchCommand();
Expand Down
11 changes: 10 additions & 1 deletion src/CRM/CivixBundle/Builder/Dirs.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ class Dirs implements Builder {
// Note: Permissions will be further restricted by umask
const MODE = 0777;

public function __construct($paths) {
/**
* @var string[]
*/
private $paths;

public function __construct($paths = []) {
$this->paths = $paths;
}

public function addPath(string $path) {
$this->paths[] = $path;
}

public function loadInit(&$ctx) {
}

Expand Down
4 changes: 4 additions & 0 deletions src/CRM/CivixBundle/Builder/Info.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ public function getExtensionName() {
return empty($this->xml->name) ? 'FIXME' : $this->xml->name;
}

public function getExtensionUtilClass(): string {
return str_replace('/', '_', $this->getNamespace()) . '_ExtensionUtil';
}

/**
* Get the namespace into which civix should place files
* @return string
Expand Down
77 changes: 70 additions & 7 deletions src/CRM/CivixBundle/Builder/PhpData.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Symfony\Component\VarExporter\VarExporter;

/**
* Read/write a serialized data file based on PHP's var_export() format
* Write a data file in PHP format
*/
class PhpData implements Builder {

Expand All @@ -21,10 +21,20 @@ class PhpData implements Builder {
protected $data;

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

/**
* @var string[]
*/
private $keysToTranslate;

/**
* @var string
*/
private $extensionUtil;

public function __construct($path, $header = NULL) {
$this->path = $path;
$this->header = $header;
Expand Down Expand Up @@ -65,27 +75,80 @@ public function load(&$ctx) {
$this->data = include $this->path;
}

/**
* Specify fields that will be wrapped in E::ts()
*
* @param array $keysToTranslate
* @return void
*/
public function useTs(array $keysToTranslate) {
$this->keysToTranslate = $keysToTranslate;
}

/**
* Adds `use Foo_ExtensionUtil as E;` to the top of the file
*
* @param string $extensionUtilClass
* @return void
*/
public function useExtensionUtil(string $extensionUtilClass) {
$this->extensionUtil = $extensionUtilClass;
}

/**
* Write the xml document
*/
public function save(&$ctx, OutputInterface $output) {
$output->writeln("<info>Write</info> " . $this->path);

$content = "<?php\n";
if ($this->extensionUtil) {
$content .= "use $this->extensionUtil as E;\n";
}
if ($this->header) {
$content .= $this->header;
}
$content .= "\nreturn ";
$content .= preg_replace_callback('/^ +/m',
// VarExporter indents with 4x spaces. Civi/Drupal code standard is 2x spaces.
$data = $this->reduceIndentation(VarExporter::export($this->data));
$data = $this->ucConstants($data);
if ($this->keysToTranslate) {
$data = $this->translateStrings($data, $this->keysToTranslate);
}
$content .= "$data;\n";
file_put_contents($this->path, $content);
}

/**
* VarExporter indents with 4x spaces. Civi/Drupal code standard is 2x spaces.
*/
private function reduceIndentation(string $data): string {
return preg_replace_callback('/^ +/m',
function($m) {
$spaces = $m[0];
return substr($spaces, 0, ceil(strlen($spaces) / 2));
},
VarExporter::export($this->data)
$data
);
$content .= ";\n";
file_put_contents($this->path, $content);
}

/**
* Uppercase constants to match Civi/Drupal code standard
*/
private function ucConstants(string $data): string {
foreach (['null', 'false', 'true'] as $const) {
$uc = strtoupper($const);
$data = str_replace(" => $const,", " => $uc,", $data);
}
return $data;
}

/**
* Wrap strings in E::ts()
*/
private function translateStrings(string $data, array $keysToTranslate): string {
$keys = implode('|', array_unique($keysToTranslate));
$data = preg_replace("/'($keys)' => ('[^']+'),/", "'\$1' => E::ts(\$2),", $data);
return $data;
}

}
150 changes: 150 additions & 0 deletions src/CRM/CivixBundle/Command/AddManagedEntityCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
namespace CRM\CivixBundle\Command;

use CRM\CivixBundle\Builder\Content;
use CRM\CivixBundle\Builder\Mixins;
use CRM\CivixBundle\Services;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CRM\CivixBundle\Builder\Collection;
use CRM\CivixBundle\Builder\Dirs;
use CRM\CivixBundle\Builder\PhpData;
use CRM\CivixBundle\Utils\Path;

class AddManagedEntityCommand extends AbstractCommand {

protected function configure() {
parent::configure();
$this
->setName('generate:managed')
->setDescription('Exports a record in packaged format for distribution in this extension')
->addArgument('<EntityName>', InputArgument::REQUIRED, 'API entity name (Ex: "SavedSearch")')
->addArgument('<EntityId>', InputArgument::REQUIRED, 'Id of entity to be exported (or name if exporting an Afform)')
->setHelp('Uses APIv4 Export to save existing records as .mgd.php files.
Specify the name of the entity and the id.
The file will be saved to the managed directory.
This command also works to export Afforms to the ang directory.');
}

protected function execute(InputInterface $input, OutputInterface $output) {
$this->assertCurrentFormat();

$ctx = [];
$ctx['type'] = 'module';
$ctx['basedir'] = \CRM\CivixBundle\Application::findExtDir();
$basedir = new Path($ctx['basedir']);
$info = $this->getModuleInfo($ctx);

$entityName = $input->getArgument('<EntityName>');
$entityId = $input->getArgument('<EntityId>');

$ext = new Collection();
$ext->builders['dirs'] = new Dirs();

// Boot CiviCRM to use api4
Services::boot(['output' => $output]);

// Fields that most probably should be wrapped in E::ts()
$localizable = ['title', 'label', 'description', 'text'];

// Export Afform
if ($entityName === 'Afform') {
$ext->builders['dirs']->addPath($basedir->string('ang'));
$afformName = $entityId;

$fields = \civicrm_api4('Afform', 'getFields', [
'checkPermissions' => FALSE,
'where' => [['type', '=', 'Field']],
])->indexBy('name');
$afform = \civicrm_api4('Afform', 'get', [
'where' => [['name', '=', $afformName]],
'checkPermissions' => FALSE,
'select' => ['*', 'search_displays'],
'layoutFormat' => 'html',
])->first();
if (!$afform) {
$output->writeln("Error: Afform $afformName not found.");
return 1;
}

// An Afform consists of 2 files - a layout file and a meta file
$layoutFileName = $basedir->string('ang', $entityId . '.aff.html');
$metaFileName = $basedir->string('ang', $entityId . '.aff.php');

// Export layout file
$ext->builders['afformLayout' . $afformName] = new Content($afform['layout'], $layoutFileName, TRUE);

// If Afform contains embedded search displays, queue those to be exported as .mgd.php
if (!empty($afform['search_displays'])) {
$searchNames = array_map(function ($item) {
return explode('.', $item)[0];
}, $afform['search_displays']);
$entityId = (array) \civicrm_api4('SavedSearch', 'get', [
'where' => [['name', 'IN', $searchNames]],
'checkPermissions' => FALSE,
], ['id']);
if ($entityId) {
$entityName = 'SavedSearch';
}
}

// Export meta file
unset($afform['search_displays'], $afform['name'], $afform['layout']);
// Simplify meta file by removing values that match the defaults
foreach ($afform as $field => $value) {
if ($value === $fields[$field]['default_value']) {
unset($afform[$field]);
}
}
$phpData = new PhpData($metaFileName);
$phpData->useExtensionUtil($info->getExtensionUtilClass());
$phpData->useTs($localizable);
$phpData->set($afform);
$ext->builders['afformMeta' . $afformName] = $phpData;
}

// Export mgd.php
if ($entityName !== 'Afform') {
$ext->builders['mixins'] = new Mixins($info, $basedir->string('mixin'), ['[email protected]']);
$ext->builders['dirs']->addPath($basedir->string('managed'));
foreach ((array) $entityId as $id) {
$export = (array) \civicrm_api4($entityName, 'export', [
'id' => $id,
'checkPermissions' => FALSE,
]);
if (!$export) {
$output->writeln("Error: $entityName $id not found.");
return 1;
}
$managedName = $export[0]['name'];
$entityCount = count($export);

// Lookup entity-specific fields that should be wrapped in E::ts()
foreach ($export as $item) {
$fields = (array) \civicrm_api4($item['entity'], 'getFields', [
'where' => [['localizable', '=', TRUE]],
'checkPermissions' => FALSE,
], ['name']);
$localizable = array_merge($localizable, $fields);
}

$output->writeln("Exporting $managedName with $entityCount " . ($entityCount === 1 ? 'record.' : 'records.'));
$managedFileName = $basedir->string('managed', $managedName . '.mgd.php');
$phpData = new PhpData($managedFileName);
$phpData->useExtensionUtil($info->getExtensionUtilClass());
$phpData->useTs($localizable);
$phpData->set($export);
$ext->builders['mgd.php' . $id] = $phpData;
}
}

$ext->builders['info'] = $info;

$ext->loadInit($ctx);
$ext->save($ctx, $output);
return 0;
}

}

0 comments on commit 06bc8b0

Please sign in to comment.