Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Installing default blocks #212

Merged
merged 19 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# LocalGov Drupal core functionality

LocalGov Drupal Core module, for helper functions and core dependencies.

## Default blocks
This module contains a mechanism that will place default blocks into your site's
active theme when other localgov modules are installed. This is intended to
reduce the work that site owners need to do when installing new features
provided by localgov modules. If you don't want this to happen, you can turn it
off by adding this to your site's settings.php file:

```php
$config['localgov_core.settings']['install_default_blocks'] = FALSE;
```

If you're a module maintainer and would like to use this feature, create a file
in your module at config/localgov/block.description.yml. The description part of
the filename can be anything you like.

In that file, place the exported config yaml for a single block, and remove the
following keys:
* uuid
* id
* theme

The default block installer will read the file, and create an instance of the
block in the current active theme, along with localgov_base and
localgov_scarfolk, if they exist and are enabled. An id for each instance will
be generated from the combination of theme and block plugin name.

Using this feature lets your blocks appear automatically in the right place in
existing localgov sites with custom themes. It also saves you having to manage
multiple block config files for localgov_base and localgov_scarfolk.
7 changes: 7 additions & 0 deletions config/schema/localgov_core_settings.schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
localgov_core.settings:
type: config_object
label: 'LocalGov Core settings'
mapping:
install_default_blocks:
type: boolean
label: 'Install default blocks when modules provide them'
37 changes: 37 additions & 0 deletions localgov_core.module
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* LocalGovDrupal Core module file.
*/

use Drupal\Core\Installer\InstallerKernel;

/**
* Implements hook_theme().
*/
Expand Down Expand Up @@ -32,3 +34,38 @@ function localgov_core_template_preprocess_default_variables_alter(&$variables)
$variables['localgov_base_remove_js'] = TRUE;
}
}

/**
* Implements hook_modules_installed().
*
* This installs default blocks for modules when they're enabled on an existing
* site. (IE, not in the installer.)
*/
function localgov_core_modules_installed(array $modules): void {
// If we're in the installer, do nothing.
if (InstallerKernel::installationAttempted()) {
return;
}

/** @var \Drupal\localgov_core\Service\DefaultBlockInstaller $defaultBlockInstaller */
$defaultBlockInstaller = \Drupal::service('localgov_core.default_block_installer');
$defaultBlockInstaller->install($modules);
}

/**
* Implements hook_localgov_post_install().
*
* This installs default blocks as part of the installation of a new LocalGov
* site. All LocalGov modules (IE ones whose name starts with 'localgov_') will
* have default blocks that they define installed.
*/
function localgov_core_localgov_post_install(): void {
$moduleList = \Drupal::moduleHandler()->getModuleList();
$localgovModules = array_filter(array_keys($moduleList), function ($moduleName) {
return str_starts_with($moduleName, 'localgov_');
});

/** @var \Drupal\localgov_core\Service\DefaultBlockInstaller $defaultBlockInstaller */
$defaultBlockInstaller = \Drupal::service('localgov_core.default_block_installer');
$defaultBlockInstaller->install($localgovModules);
}
4 changes: 4 additions & 0 deletions localgov_core.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
localgov_core.default_block_installer:
class: Drupal\localgov_core\Service\DefaultBlockInstaller
arguments: ['@config.factory', '@entity_type.manager', '@file_system', '@module_handler', '@theme_handler', '@theme.manager']
185 changes: 185 additions & 0 deletions src/Service/DefaultBlockInstaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

namespace Drupal\localgov_core\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\Yaml\Yaml;

/**
* Service to install default blocks.
*/
class DefaultBlockInstaller {

/**
* Array of regions in each theme.
*
* @var array
*/
protected $themeRegions = [];

/**
* Constructor.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected EntityTypeManagerInterface $entityTypeManager,
protected FileSystemInterface $fileSystem,
protected ModuleHandlerInterface $moduleHandler,
protected ThemeHandlerInterface $themeHandler,
protected ThemeManagerInterface $themeManager,
) {
}

/**
* Read the yaml files provided by modules.
*/
protected function blockDefinitions(string $module): array {

$modulePath = $this->moduleHandler->getModule($module)->getPath();
$moduleBlockDefinitionsPath = $modulePath . '/config/localgov';
$blocks = [];

if (is_dir($moduleBlockDefinitionsPath)) {
$files = $this->fileSystem->scanDirectory($moduleBlockDefinitionsPath, '/block\..+\.yml$/');
foreach ($files as $file) {
$blocks[] = Yaml::parseFile($moduleBlockDefinitionsPath . '/' . $file->filename);
}
}

return $blocks;
}

/**
* The themes we'll be installing blocks into.
*/
protected function targetThemes(): array {

$themes = ['localgov_base', 'localgov_scarfolk'];

$activeTheme = $this->getActiveThemeName();
if ($activeTheme && !in_array($activeTheme, $themes)) {
$themes[] = $activeTheme;
}

// Don't try to use themes that don't exist.
foreach ($themes as $i => $theme) {
if (!$this->themeHandler->themeExists($theme)) {
unset($themes[$i]);
}
}

return $themes;
}

/**
* Gets the name of the active theme, if there is one.
*/
protected function getActiveThemeName(): ?string {
$activeTheme = $this->themeManager->getActiveTheme()->getName();
if ($activeTheme === 'core') {
// 'core' is what Drupal returns when there's no themes available.
// See \Drupal\Core\Theme\ThemeInitialization::getActiveThemeByName().
// This mainly happens in the installer.
return NULL;
}
return $activeTheme;
}

/**
* Installs the default blocks for the given modules.
*
* If the site is configured not to allow default blocks to be installed, this
* method will do nothing.
*/
public function install(array $modules): void {

// If localgov_core.settings.install_default_blocks is set to FALSE, don't
// install default blocks. This lets site owners opt out if desired.
$config = $this->configFactory->get('localgov_core.settings');
if ($config && ($config->get('install_default_blocks') === FALSE)) {
return;
}

foreach ($modules as $module) {
$this->installForModule($module);
}
}

/**
* Installs the default blocks for the given module.
*/
protected function installForModule(string $module): void {

$blocks = $this->blockDefinitions($module);

// Loop over every theme and block definition, so we set up all the blocks
// in all the relevant themes.
foreach ($this->targetThemes() as $theme) {
foreach ($blocks as $block) {

// Verify that the theme we're using has the requested region.
if (!$this->themeHasRegion($theme, $block['region'])) {
continue;
}

$block['id'] = $this->sanitiseId($theme . '_' . $block['plugin']);
$block['theme'] = $theme;

$this->entityTypeManager
->getStorage('block')
->create($block)
->save();
}
}
}

/**
* Replace characters that aren't allowed in config IDs.
*
* This is partly based on
* \Drupal\Core\Block\BlockPluginTrait::getMachineNameSuggestion().
*/
protected function sanitiseId(string $id): string {

// Shift to lower case.
$id = mb_strtolower($id);

// Limit to alphanumeric chars, dot and underscore.
$id = preg_replace('@[^a-z0-9_.]+@', '_', $id);

// Remove non-alphanumeric chars from the beginning and end of the id.
$id = preg_replace('@^([^a-z0-9]+)|([^a-z0-9]+)$@', '', $id);

return $id;
}

/**
* Does the given theme have the given region?
*/
protected function themeHasRegion(string $theme, string $region): bool {
return in_array($region, $this->themeRegions($theme));
}

/**
* Gets the regions for the given theme.
*/
protected function themeRegions(string $theme): array {
if (!isset($this->themeRegions[$theme])) {
Polynya marked this conversation as resolved.
Show resolved Hide resolved
$themeInfo = $this->themeHandler->getTheme($theme);
if (empty($themeInfo)) {
$regions = [];
}
else {
$regions = array_keys($themeInfo->info['regions']);
}
$this->themeRegions[$theme] = $regions;
}
return $this->themeRegions[$theme];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugin: 'localgov_core_test_block'
region: bad_region
status: true
weight: 1
settings:
label: 'Block in a bad region.'
visibility: { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugin: 'localgov_core_test_block'
region: content_top
status: true
weight: 1
settings:
label: 'Block in a good region.'
visibility: { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: 'Default blocks test'
type: module
package: Testing
core_version_requirement: ^9 || ^10
description: Testing module for default blocks.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Drupal\localgov_core_default_blocks_test\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
* Provides a block for testing default block placement.
*
* @Block(
* id = "localgov_core_test_block",
* admin_label = @Translation("Default block test block")
* )
*/
class TestBlock extends BlockBase {

/**
* {@inheritdoc}
*/
public function build(): array {
return ['#markup' => 'Default block has been placed!'];
}

}
34 changes: 34 additions & 0 deletions tests/src/Functional/DefaultBlockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Drupal\Tests\localgov_core\Functional;

use Drupal\Tests\BrowserTestBase;

/**
* Tests for the default blocks mechanism.
*/
class DefaultBlockTest extends BrowserTestBase {

/**
* {@inheritdoc}
*/
protected $defaultTheme = 'localgov_base';

/**
* {@inheritdoc}
*/
protected static $modules = [
'localgov_core',
'localgov_core_default_blocks_test',
];

/**
* Test block display.
*/
public function testBlockDisplay() {
$this->drupalGet('<front>');
$this->assertSession()->pageTextContains('Block in a good region.');
$this->assertSession()->pageTextNotContains('Block in a bad region.');
}

}
Loading