localgov_directories-3.3.1/src/ConfigurationHelper.php
src/ConfigurationHelper.php
<?php
declare(strict_types=1);
namespace Drupal\localgov_directories;
use Drupal\Core\Config\ConfigInstallerInterface;
use Drupal\Core\Config\FileStorage as ConfigFileStorage;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\block\BlockInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\Field as SearchIndexField;
use Drupal\search_api\Utility\PluginHelperInterface;
use Drupal\views\ViewEntityInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Update index and block configurations for changed entities and fields.
*/
class ConfigurationHelper implements ContainerInjectionInterface {
/**
* The Search API directory index.
*
* @var \Drupal\search_api\IndexInterface
*/
protected ?IndexInterface $index;
/**
* The directory view.
*
* @var \Drupal\views\ViewEntityInterface
*/
protected ?ViewEntityInterface $view;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The Localgov Directories logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* The module extension list service.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* The configuration installer.
*
* @var \Drupal\Core\Config\ConfigInstallerInterface
*/
protected ConfigInstallerInterface $configInstaller;
/**
* Search API Plugin Helper utility.
*
* @var \Drupal\search_api\Utility\PluginHelperInterface
*/
protected PluginHelperInterface $searchApiPluginHelper;
/**
* DirectoryExtraFieldDisplay constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* Entity field manager.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* Logger channel factory.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
* Module extension list.
* @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
* Config installer.
* @param \Drupal\search_api\Utility\PluginHelperInterface $search_api_plugin_helper
* Search API Plugin helper.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, LoggerChannelFactoryInterface $logger_factory, ModuleExtensionList $module_extension_list, ConfigInstallerInterface $config_installer, PluginHelperInterface $search_api_plugin_helper) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->logger = $logger_factory->get('localgov_directories');
$this->moduleExtensionList = $module_extension_list;
$this->configInstaller = $config_installer;
$this->searchApiPluginHelper = $search_api_plugin_helper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('logger.factory'),
$container->get('extension.list.module'),
$container->get('config.installer'),
$container->get('search_api.plugin_helper'),
);
}
/**
* Get index to work on.
*/
public function getIndex(): ?IndexInterface {
return $this->index ?? $this->entityTypeManager->getStorage('search_api_index')->load(Constants::DEFAULT_INDEX);
}
/**
* Get directory view to work on.
*/
public function getView(): ?ViewEntityInterface {
return $this->view ?? $this->entityTypeManager->getStorage('view')->load('localgov_directory_channel');
}
/**
* Act on a Directory channel field being added.
*/
public function insertedDirectoryChannelField(FieldConfigInterface $field): void {
$entity_type_id = $field->getTargetEntityTypeId();
$entity_bundle = $field->getTargetBundle();
// Index changes.
if ($index = $this->getIndex()) {
$this->indexAddBundle($index, $entity_type_id, $entity_bundle);
$this->renderedItemAddBundle($index, $entity_type_id, $entity_bundle);
$this->indexAddChannelsField($index);
// The Channel is also the trigger for adding/removing from the index.
// So also handle fields already existing on the entity that should be
// included in the index.
$entity_fields = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $entity_bundle);
if (array_key_exists(Constants::FACET_SELECTION_FIELD, $entity_fields)) {
$this->indexAddFacetField($index);
}
if (array_key_exists(Constants::TITLE_SORT_FIELD, $entity_fields)) {
$this->indexAddTitleSortField($index);
}
$index->save();
}
if ($view = $this->getView()) {
$this->viewSetViewMode($view, $entity_type_id, $entity_bundle);
$view->save();
}
$this->blockAddContentType(Constants::CHANNEL_SEARCH_BLOCK, $entity_bundle);
}
/**
* Act on Directory channel field being removed.
*/
public function deletedDirectoryChannelField(FieldConfigInterface $field): void {
// Only working for nodes at the moment.
$entity_type_id = $field->getTargetEntityTypeId();
$entity_bundle = $field->getTargetBundle();
// Index changes.
if ($index = $this->getIndex()) {
$this->indexRemoveBundle($index, $entity_type_id, $entity_bundle);
}
$this->blockRemoveContentType(Constants::CHANNEL_SEARCH_BLOCK, $entity_bundle);
}
/**
* Act on Directory facet field being added.
*/
public function insertedFacetField(FieldConfigInterface $field): void {
if ($index = $this->getIndex()) {
$this->indexAddFacetField($index);
$index->save();
}
if ($index && $index->status()) {
$this->createFacet(Constants::FACET_CONFIG_ENTITY_ID, Constants::FACET_CONFIG_FILE);
}
}
/**
* Act on Directory title sort field being added.
*/
public function insertedTitleSortField(FieldConfigInterface $field): void {
if ($index = $this->getIndex()) {
$this->indexAddTitleSortField($index);
$index->save();
}
}
/**
* Create new config entity from given config file.
*
* @param string $entity_type
* Example: facets_facet.
* @param string $config_path
* Example: modules/foo/config/bar.
* @param string $config_filename
* Example: views.view.bar.
*/
public function importConfigEntity(string $entity_type, string $config_path, string $config_filename): bool {
$config_src = new ConfigFileStorage($config_path);
if (!$config_src instanceof ConfigFileStorage) {
return FALSE;
}
$config_values = $config_src->read($config_filename);
if (empty($config_values)) {
return FALSE;
}
try {
$this->entityTypeManager->getStorage($entity_type)->create($config_values)->save();
}
catch (\Exception $e) {
$this->logger->error('Failed to create new config entity: %filename. Error: %msg', [
'%filename' => $config_filename,
'%msg' => $e->getMessage(),
]);
return FALSE;
}
return TRUE;
}
/**
* Setup indexing on the Facet selection field of Directory entries.
*
* This assumes that the localgov_directory_facets_select field is part of a
* Directory entry content type.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to add the facet field to.
*/
protected function indexAddFacetField(IndexInterface $index): void {
if ($index->getField(Constants::FACET_INDEXING_FIELD)) {
return;
}
$field = new SearchIndexField($index, Constants::FACET_INDEXING_FIELD);
$field->setLabel('Facets');
$field->setDataSourceId('entity:node');
$field->setPropertyPath(Constants::FACET_SELECTION_FIELD);
$field->setType('integer');
$field->setDependencies([
'config' => [
'field.storage.node.' . Constants::FACET_SELECTION_FIELD,
],
]);
$index->addField($field);
}
/**
* Setup indexing on the Title Sort field of Directory entries.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to the title sort field field to.
*/
protected function indexAddTitleSortField(IndexInterface $index): void {
if ($index->getField(Constants::TITLE_SORT_FIELD)) {
return;
}
$field = new SearchIndexField($index, Constants::TITLE_SORT_FIELD);
$field->setLabel('Title (sort)');
$field->setDataSourceId('entity:node');
$field->setPropertyPath(Constants::TITLE_SORT_FIELD);
$field->setType('string');
$field->setDependencies([
'config' => [
'field.storage.node.' . Constants::TITLE_SORT_FIELD,
],
]);
$index->addField($field);
}
/**
* Setup indexing on the Directory channels field of Directory entries.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to the channel field to.
*/
protected function indexAddChannelsField(IndexInterface $index): void {
if ($index->getField(Constants::CHANNEL_SELECTION_FIELD)) {
return;
}
$field = new SearchIndexField($index, Constants::CHANNEL_SELECTION_FIELD);
$field->setLabel('Directory channels');
$field->setDataSourceId('entity:node');
$field->setPropertyPath(Constants::CHANNEL_SELECTION_FIELD);
$field->setType('string');
$field->setDependencies([
'config' => [
'field.storage.node.' . Constants::CHANNEL_SELECTION_FIELD,
],
]);
$index->addField($field);
}
/**
* Import config entity for the directory Facet.
*/
public function createFacet(string $facet_id, string $facet_cfg_file): void {
if ($this->entityTypeManager->getStorage('facets_facet')->load($facet_id)) {
return;
}
$conditional_config_path = $this->moduleExtensionList->getPath('localgov_directories') . '/config/conditional';
if ($this->importConfigEntity('facets_facet', $conditional_config_path, $facet_cfg_file)) {
$this->configInstaller->installOptionalConfig(NULL, [
'config' => 'facets.facet.localgov_directories_facets',
]);
}
}
/**
* Update a block's visibility to add to content type.
*
* The given block should appear in the sidebars of pages for the given
* content type.
*
* @param string $block_id
* The block to update visibility for.
* @param string $content_type
* The content type on which the block should be visible.
*
* @return bool
* True on success.
*/
public function blockAddContentType(string $block_id, string $content_type): bool {
$block_config = $this->entityTypeManager->getStorage('block')->load($block_id);
if (!$block_config instanceof BlockInterface) {
return FALSE;
}
try {
$visibility = $block_config->getVisibility();
$visibility['entity_bundle:node']['bundles'][$content_type] = $content_type;
$block_config->setVisibilityConfig('entity_bundle:node', $visibility['node_type']);
$block_config->save();
}
catch (\Exception $e) {
$this->logger->error('Failed to add %content-type content type to %block-id block: %error-msg', [
'%content-type' => $content_type,
'%block-id' => $block_id,
'%error-msg' => $e->getMessage(),
]);
return FALSE;
}
$this->logger->notice('Added %content-type content type to %block-id block.', [
'%content-type' => $content_type,
'%block-id' => $block_id,
]);
return TRUE;
}
/**
* Update a block's visibility to remove from content type.
*
* The given block should no longer appear in the sidebars of pages for the
* given content type.
*
* @param string $block_id
* The block to update visibility for.
* @param string $content_type
* The content type on which the block should no longer be visible.
*
* @return bool
* True on success.
*/
public function blockRemoveContentType(string $block_id, string $content_type): bool {
$block_config = $this->entityTypeManager->getStorage('block')->load($block_id);
if (!$block_config instanceof BlockInterface) {
$this->logger->error('Block %block-id is missing. Cannot update its visibility settings.', [
'%block-id' => $block_id,
]);
return FALSE;
}
try {
$visibility = $block_config->getVisibility();
if (empty($visibility['entity_bundle:node']['bundles'][$content_type])) {
return FALSE;
}
unset($visibility['entity_bundle:node']['bundles'][$content_type]);
$block_config->setVisibilityConfig('entity_bundle_node', $visibility['node_type']);
$block_config->save();
}
catch (\Exception $e) {
$this->logger->error('Failed to remove %content-type content type to %block-id block: %error-msg', [
'%content-type' => $content_type,
'%block-id' => $block_id,
'%error-msg' => $e->getMessage(),
]);
return FALSE;
}
$this->logger->notice('Removed %content-type content type to %block-id block.', [
'%content-type' => $content_type,
'%block-id' => $block_id,
]);
return TRUE;
}
/**
* Add entity bundle to index datasource.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to add bundle to.
* @param string $entity_type_id
* Entity type ID.
* @param string $entity_bundle
* The bundle ID.
*/
protected function indexAddBundle(IndexInterface $index, string $entity_type_id, string $entity_bundle): void {
$datasource = $this->indexGetDatasource($index, $entity_type_id);
if (!$datasource) {
$this->logger->error('Failed to update the directories search index with new bundle');
return;
}
$configuration = $datasource->getConfiguration();
$configuration['bundles']['default'] = FALSE;
if (!in_array($entity_bundle, $configuration['bundles']['selected'], TRUE)) {
$configuration['bundles']['selected'][] = $entity_bundle;
}
$datasource->setConfiguration($configuration);
}
/**
* Remove entity bundle to index datasource.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to remove bundle from.
* @param string $entity_type_id
* Entity type ID.
* @param string $entity_bundle
* The bundle ID.
*/
protected function indexRemoveBundle(IndexInterface $index, string $entity_type_id, string $entity_bundle): void {
$datasource = $this->indexGetDatasource($index, $entity_type_id);
if (!$datasource) {
$this->logger->error('Failed to update the directories search index with new bundle');
return;
}
$configuration = $datasource->getConfiguration();
$configuration['bundles']['default'] = FALSE;
if (($key = array_search($entity_bundle, $configuration['bundles']['selected'], TRUE)) !== FALSE) {
unset($configuration['bundles']['selected'][$key]);
}
$datasource->setConfiguration($configuration);
}
/**
* Set entity bundle default view mode in view.
*
* @param \Drupal\views\ViewEntityInterface $view
* The view to update.
* @param string $entity_type_id
* Entity type ID.
* @param string $entity_bundle
* The bundle ID.
*/
protected function viewSetViewMode(ViewEntityInterface $view, string $entity_type_id, string $entity_bundle): void {
// Also set the default view mode for the directory view listing.
$display = $view->get('display');
if (isset($display['node_embed']['display_options']['row'])) {
$display['node_embed']['display_options']['row']['options']['view_modes']['entity:' . $entity_type_id][$entity_bundle] = 'teaser';
}
elseif (isset($display['default']['display_options']['row'])) {
$display['default']['display_options']['row']['options']['view_modes']['entity:' . $entity_type_id][$entity_bundle] = 'teaser';
}
$view->set('display', $display);
}
/**
* Add entity bundle to index rendered item field.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to add bundle to.
* @param string $entity_type_id
* Entity type ID.
* @param string $entity_bundle
* The bundle ID.
*/
protected function renderedItemAddBundle(IndexInterface $index, string $entity_type_id, string $entity_bundle): void {
$index_field = $index->getField('rendered_item');
if ($index_field) {
$configuration = $index_field->getConfiguration();
$configuration['view_mode']['entity:' . $entity_type_id][$entity_bundle] = 'directory_index';
$index_field->setConfiguration($configuration);
}
}
/**
* Get index entity datasource.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to retrieve the datasource from.
* @param string $entity_type_id
* The entity type ID.
*
* @return \Drupal\search_api\Datasource\DatasourceInterface
* The datasource.
*/
protected function indexGetDatasource(IndexInterface $index, string $entity_type_id): DatasourceInterface {
$datasource = $index->getDatasource('entity:' . $entity_type_id);
if (!$datasource) {
// If the content:node datasource has been lost so have the fields most
// probably and it's more of a mess. But leaving this here anyway.
$datasource = $this->searchApiPluginHelper->createDatasourcePlugin($index, 'entity:' . $entity_type_id);
}
return $datasource;
}
}
