Commit 5e49de6f authored by casey's avatar casey Committed by marcvangend

Issue #2513246 by casey, tedbow, marcvangend: New approach for fields as block, first iteration

parent 71aac1fb
<?php
///**
// * Implements hook_uninstall().
// */
//function fieldblock_uninstall() {
// // Delete variables.
// $entities = entity_get_info();
// // Loop over the entity types.
// foreach ($entities as $entity_type => $entity_info) {
// // Loop over each entity type's bundles.
// foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
// $view_modes = field_view_mode_settings($entity_type, $bundle);
// // Treat the default settings as a real view mode with custom settings.
// $view_modes['default']['custom_settings'] = true;
// // Loop over the bundle's view modes.
// foreach ($view_modes as $view_mode => $view_mode_info) {
// // Delete the variable, if it exists.
// $variable_name = 'fieldblock-'. $entity_type .'-'. $bundle .'-'. $view_mode;
// variable_del($variable_name);
// }
// }
// }
//}
//
///**
// * Legacy helper function to undo drupal core schema alter.
// */
//function _fieldblock_db_alter_block_delta_length($length) {
// // Alter block table.
// db_drop_unique_key('block', 'tmd');
// db_change_field('block', 'delta', 'delta',
// array(
// 'type' => 'varchar',
// 'length' => $length,
// 'not null' => TRUE,
// 'default' => '0',
// 'description' => 'Unique ID for block within a module.',
// ),
// array(
// 'unique keys' => array(
// 'tmd' => array('theme', 'module', 'delta'),
// )
// )
// );
//
// // Alter block_role table.
// db_drop_primary_key('block_role');
// db_change_field('block_role', 'delta', 'delta',
// array(
// 'type' => 'varchar',
// 'length' => $length,
// 'not null' => TRUE,
// 'description' => "The block's unique delta within module, from {block}.delta.",
// ),
// array(
// 'primary key' => array('module', 'delta', 'rid'),
// )
// );
//
// // Alter block_node_type table.
// db_drop_primary_key('block_node_type');
// db_change_field('block_node_type', 'delta', 'delta',
// array(
// 'type' => 'varchar',
// 'length' => $length,
// 'not null' => TRUE,
// 'description' => "The block's unique delta within module, from {block}.delta.",
// ),
// array(
// 'primary key' => array('module', 'delta', 'type'),
// )
// );
//}
//
///**
// * Update legacy fieldblock deltas to use md5 identifier.
// * Reset drupal core block schema.
// */
//function fieldblock_update_7100() {
// $blocks = db_query("SELECT bid, delta FROM {block} WHERE module = 'fieldblock'");
// foreach ($blocks as $block) {
// db_query("UPDATE {block} SET delta = :new_delta WHERE bid = :bid AND delta = :old_delta AND module = 'fieldblock'", array(':new_delta' => md5($block->delta), ':bid' => $block->bid, ':old_delta' => $block->delta));
// }
// _fieldblock_db_alter_block_delta_length(32);
//}
<?php
/**
* @file
* Allow fields to be rendered in blocks.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
/**
* Implements hook_form_alter().
*
* Adds a column to the "display fields" table-form, with a checkbox for each
* field.
*/
function fieldblock_form_field_ui_display_overview_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
$entity_type = $form['#entity_type'];
$bundle = $form['#bundle'];
$mode = $form['#mode'];
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity_view_display */
$entity_view_display = EntityViewDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
// Add a column header.
$form['fields']['#header'][] = t('Display as block');
// Add checkboxes.
$field_names = array_merge($form['#fields'], $form['#extra']);
foreach ($field_names as $field_name) {
$form['fields'][$field_name]['fieldblock'] = array(
'#type' => 'checkbox',
'#default_value' => $entity_view_display->getThirdPartySetting('fieldblock', $field_name) ? true : false,
'#title' => '',
);
}
// Add a submit handler.
$form['#submit'][] = 'fieldblock_field_display_submit';
}
/**
* Form submit handler for field_ui_display_overview_form.
* Stores which fields are published as blocks as a third_party_settings array
* in the EntityViewDisplay object of the entity type / bundle / view mode.
*
* @param mixed[] $form
* A form API array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the submitted form.
*/
function fieldblock_field_display_submit($form, FormStateInterface $form_state) {
$entity_type = $form['#entity_type'];
$bundle = $form['#bundle'];
$mode = $form['#mode'];
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity_view_display */
$entity_view_display = EntityViewDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
$fields = $form_state->getValue('fields');
foreach ($fields as $field_name => $field) {
if (isset($field['fieldblock']) && $field['fieldblock'] == 1) {
$entity_view_display->setThirdPartySetting('fieldblock', $field_name, $form['fields'][$field_name]['human_name']['#markup']);
}
else if ($entity_view_display->getThirdPartySetting('fieldblock', $field_name)) {
$entity_view_display->unsetThirdPartySetting('fieldblock', $field_name);
}
}
$entity_view_display->save();
// Invalidate the block cache to update fielblock derivatives.
if (\Drupal::moduleHandler()->moduleExists('block')) {
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
}
/**
* Implements hook_entity_view_alter().
*
* Takes fields out of the current entity and caches them in a post render cache
* context. The #post_render_cache callback makes this data available to the
* fieldblock when it is built, We also hide the field from the render array.
*
* @param array &$build
* A renderable array representing the entity content.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object being rendered. Not used in this implementation.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The entity view display holding the display options configured for the
* entity components.
*
* @see \Drupal\fieldblock\Plugin\Block\FieldBlock::fieldBlockPostRenderCache
* @see https://www.drupal.org/node/2151609
*/
function fieldblock_entity_view_alter(array &$build, $entity, EntityViewDisplayInterface $display) {
$fieldblock_settings = $display->getThirdPartySettings('fieldblock');
$display_id = $display->get('id');
foreach ($fieldblock_settings as $field_name => $field_label) {
$fieldblock_id = $display_id . ':' . $field_name;
if (count(\Drupal\Core\Render\Element::children($build[$field_name]))) {
// This is where we take out the field and cache it in a post render
// cache context.
$build['#post_render_cache']['Drupal\fieldblock\Plugin\Block\FieldBlock::fieldBlockPostRenderCache'][] = array(
'build' => $build[$field_name],
'fieldblock_id' => $fieldblock_id,
);
hide($build[$field_name]);
}
}
}
# Schema for the configuration files of the Field as Block module.
block.settings.fieldblock:*:
type: block_settings
label: 'Field as Block'
mapping:
label_from_field:
type: boolean
lable: 'Use field label as block title'
field_name:
type: string
label: 'Field name'
formatter_id:
type: string
label: 'Format type machine name'
formatter_settings:
type: field.formatter.settings.[%parent.formatter_id]
label: 'Settings'
......@@ -6,82 +6,358 @@
*/
namespace Drupal\fieldblock\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FormatterInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\FormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a fieldblock.
*
* @Block(
* id = "fieldblock",
* admin_label = @Translation("Fieldblock"),
* deriver = "Drupal\fieldblock\Plugin\Derivative\FieldBlock"
* admin_label = @Translation("Field as Block"),
* deriver = "Drupal\fieldblock\Plugin\Derivative\FieldBlockDeriver"
* )
*/
class FieldBlock extends BlockBase {
class FieldBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The field formatter plugin manager.
*
* @var \Drupal\Core\Field\FormatterPluginManager
*/
protected $formatterPluginManager;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager
* The field formatter plugin manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function build() {
$block_id = $this->getDerivativeId();
$block = $this::getFieldBlock($block_id);
return $block;
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, FormatterPluginManager $formatter_plugin_manager, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
$this->formatterPluginManager = $formatter_plugin_manager;
$this->routeMatch = $route_match;
}
/**
* @var array[]
* Static storage for fields that are grabbed from the entity's render
* array, to be retrieved when fieldblocks are built.
* {@inheritdoc}
*/
protected static $fieldBlocks;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager'),
$container->get('plugin.manager.field.formatter'),
$container->get('current_route_match'));
}
/**
* @param string $fieldblock_id
* The identifier of the fieldblock.
* @return mixed[]|false
* The render array of the field that is published as block or false if the
* field is not available.
* {@inheritdoc}
*/
public static function getFieldBlock($fieldblock_id) {
if (isset(self::$fieldBlocks[$fieldblock_id])) {
return self::$fieldBlocks[$fieldblock_id];
public function defaultConfiguration() {
return [
'label_from_field' => TRUE,
'field_name' => '',
'formatter_id' => '',
'formatter_settings' => []
];
}
protected function getFieldOptions() {
$field_definitions = $this->entityManager->getFieldStorageDefinitions($this->getDerivativeId());
$options = [];
foreach ($field_definitions as $definition) {
$options[$definition->getName()] = $definition->getLabel();
}
return $options;
}
protected function getFormatterOptions(FieldDefinitionInterface $field_definition) {
$options = $this->formatterPluginManager->getOptions($field_definition->getType());
foreach ($options as $id => $label) {
$definition = $this->formatterPluginManager->getDefinition($id, FALSE);
$formatter_plugin_class = isset($definition['class']) ? $definition['class'] : NULL;
$applicable = $formatter_plugin_class instanceof FormatterInterface && $formatter_plugin_class::isApplicable($field_definition);
if ($applicable) {
unset($options[$id]);
}
}
return $options;
}
/**
* Gets the field definition.
*
* A FieldBlock works on an entity type across bundles, and thus only has access to
* field storage definitions. In order to be able to use formatters, we create a
* generic field definition out of that storage definition.
*
* @param string $field_name
*
* @see BaseFieldDefinition::createFromFieldStorageDefinition()
* @see \Drupal\views\Plugin\views\field\Field::getFieldDefinition()
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition used by this block.
*/
protected function getFieldDefinition($field_name) {
$field_storage_config = $this->getFieldStorageDefinition($field_name);
return BaseFieldDefinition::createFromFieldStorageDefinition($field_storage_config);
}
/**
* Gets the field storage definition.
*
* @param string $field_name
*
* @return \Drupal\field\FieldStorageConfigInterface
* The field storage definition used by this block.
*/
protected function getFieldStorageDefinition($field_name) {
$field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->getDerivativeId());
return $field_storage_definitions[$field_name];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['label_from_field'] = [
'#title' => $this->t('Use field label as block title'),
'#type' => 'checkbox',
'#default_value' => $this->configuration['label_from_field'],
];
$form['field_name'] = [
'#title' => $this->t('Field'),
'#type' => 'select',
'#options' => $this->getFieldOptions(),
'#default_value' => $this->configuration['field_name'],
'#required' => TRUE,
'#ajax' => [
'callback' => [$this, 'blockFormChangeFieldOrFormatterAjax'],
'wrapper' => 'edit-block-formatter-wrapper',
],
];
$form['formatter'] = [
'#type' => 'container',
'#id' => 'edit-block-formatter-wrapper'
];
$field_name = $form_state->getValue(['settings', 'field_name'], $this->configuration['field_name']);
$field_definition = NULL;
$formatter_id = $form_state->getValue(['settings', 'formatter', 'id'], $this->configuration['formatter_id']);
if ($field_name) {
$field_definition = $this->getFieldDefinition($field_name);
$formatter_options = $this->getFormatterOptions($field_definition);
if (empty($formatter_options)) {
$formatter_id = '';
}
else {
return FALSE;
if (empty($formatter_id)) {
$formatter_id = reset($formatter_options);
}
$form['formatter']['id'] = [
'#title' => $this->t('Formatter'),
'#type' => 'select',
'#options' => $formatter_options,
'#default_value' => $this->configuration['formatter_id'],
'#required' => TRUE,
'#ajax' => [
'callback' => [$this, 'blockFormChangeFieldOrFormatterAjax'],
'wrapper' => 'edit-block-formatter-wrapper',
],
];
}
}
$form['formatter']['change'] = [
'#type' => 'submit',
'#name' => 'fieldblock_change_field',
'#value' => $this->t('Change field'),
'#attributes' => ['class' => ['js-hide']],
'#limit_validation_errors' => [['settings']],
'#submit' => [[get_class($this), 'blockFormChangeFieldOrFormatter']],
];
if ($formatter_id) {
$formatter_settings = $this->configuration['formatter_settings'] + $this->formatterPluginManager->getDefaultSettings($formatter_id);
$formatter_options = [
'field_definition' => $field_definition,
'view_mode' => '_custom',
'configuration' => [
'type' => $formatter_id,
'settings' => $formatter_settings,
'label' => '',
'weight' => 0,
]
];
if ($formatter_plugin = $this->formatterPluginManager->getInstance($formatter_options)) {
$formatter_settings_form = $formatter_plugin->settingsForm($form, $form_state);
// Convert field UI selector states to work in the block configuration form.
FormHelper::rewriteStatesSelector($formatter_settings_form,
"fields[{$field_name}][settings_edit_form]",
'settings[formatter][settings]');
}
if (!empty($formatter_settings_form)) {
$form['formatter']['settings'] = $formatter_settings_form;
$form['formatter']['settings']['#type'] = 'fieldset';
$form['formatter']['settings']['#title'] = $this->t('Formatter settings');
}
}
return $form;
}
/**
* @param string $fieldblock_id
* The identifier of the fieldblock.
* @param mixed[] $render_array
* The render array of the field that will be published as block.
* Element submit handler for non-JS field/formatter changes.
*
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*/
public static function setFieldBlock($fieldblock_id, array $render_array) {
self::$fieldBlocks[$fieldblock_id] = $render_array;
public static function blockFormChangeFieldOrFormatter(array $form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
/**
* #post_render_cache callback, temporarily stores a field's render array in a
* static variable and returns the original element as post render cache
* callbacks are supposed to do.
* Ajax callback on changing field_name or formatter_id form element.
*
* @param $form
*
* Note that this is an atypical way to use the post render cache mechanism.
* Post render cache is meant to allow modules to dynamically alter pieces of
* cached content. Here we use it as some kind of context-aware cache, because
* the cached field will only be retrieved and displayed as a block when the
* entity is viewed.
* @return array
* The part of the form that has changed.
*/
public function blockFormChangeFieldOrFormatterAjax($form) {
return $form['settings']['formatter'];
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
parent::blockValidate($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['label_from_field'] = $form_state->getValue('label_from_field');
$this->configuration['field_name'] = $form_state->getValue('field_name');
$this->configuration['formatter_id'] = $form_state->getValue(['formatter', 'id'], '');
$this->configuration['formatter_settings'] = $form_state->getValue(['formatter', 'settings'], []);
}
/**
* {@inheritdoc}
*
* @param mixed[] $element
* The render array being rendered.
* @param mixed[] $context
* Array containing the fieldblock ID and the field's render array.
* @return mixed[]
* The render array being rendered.
* @see \Drupal\views\Plugin\views\field\Field::calculateDependencies()
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
// Add the module providing the configured field storage as a dependency.
if (($field_storage_definition = $this->getFieldStorageDefinition($this->configuration['field_name'])) && $field_storage_definition instanceof EntityInterface) {
$dependencies['config'][] = $field_storage_definition->getConfigDependencyName();
}
// Add the module providing the formatter.
if (!empty($this->configuration['formatter_id'])) {
$dependencies['module'][] = $this->formatterPluginManager->getDefinition($this->configuration['formatter_id'])['provider'];
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$entity_type = $this->getDerivativeId();
$entity = $this->routeMatch->getParameter($entity_type);
if ($entity instanceof ContentEntityInterface && $entity->getEntityTypeId() === $entity_type && $entity->hasField($this->configuration['field_name'])) {
$field = $entity->get($this->configuration['field_name']);
return AccessResult::allowedIf(!$field->isEmpty() && $field->access('view', $account));
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$build = [];
$entity_type = $this->getDerivativeId();
$entity = $this->routeMatch->getParameter($entity_type);
if ($entity instanceof ContentEntityInterface) {
$build['field'] = $entity->get($this->configuration['field_name'])->view([
'label' => 'hidden',
'type' => $this->configuration['formatter_id'],
'settings' => $this->configuration['formatter_settings']
]);
if ($this->configuration['label_from_field'] && !empty($build['field']['#title'])) {
$build['#title'] = $build['field']['#title'];
}
}
return $build;
}
/**
* {@inheritdoc}
*/
public static function fieldBlockPostRenderCache(array $element, array $context) {
self::setFieldBlock($context['fieldblock_id'], $context['build']);
return $element;
public function getCacheTags() {
$entity_type = $this->getDerivativeId();
$entity = $this->routeMatch->getParameter($entity_type);
if ($entity instanceof ContentEntityInterface) {
return $entity->getCacheTags();
}
return parent::getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// This block must be cached per URL: every entity has its own canonical url
// and its own fields.
return ['url'];
}
}
<?php
/**
* @file
* Contains \Drupal\fieldblock\Plugin\Derivative\FieldBlock.
*/
namespace Drupal\fieldblock\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides block plugin definitions for fieldblock blocks.
*
* @see \Drupal\fieldblock\Plugin\Block\FieldBlock
*/
class FieldBlock extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity view display storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $entityViewDisplayStorage;
/**
* Constructs a FieldBlock deriver object.
*
* @param \Drupal\Core\Entity\Entity\EntityViewDisplay $entity_view_display
* The entity view display storage.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
*/
public function __construct($entity_view_display, TranslationInterface $string_translation) {
$this->entityViewDisplayStorage = $entity_view_display;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
/** @var EntityManagerInterface $entity_manager */
$entity_manager = $container->get('entity.manager');
return new static(
$entity_manager->getStorage('entity_view_display'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$blocks = $this->fieldBlockGetBlockList();
foreach ($blocks as $fieldblock_id => $description) {
$this->derivatives[$fieldblock_id] = $base_plugin_definition;
$this->derivatives[$fieldblock_id]['admin_label'] = $description;
}
return $this->derivatives;
}
/**
* Builds a list of fields that have been made available as a block.
*
* @return string[]
* An array of fieldblocks in the form of fieldblock_id => admin label.
*/
protected function fieldBlockGetBlockList() {
$fieldblocks = array();
// Get all EntityViewDisplay config entities and iterate over them.
$entity_view_displays = $this->entityViewDisplayStorage->loadMultiple();
/** @var \Drupal\Core\Entity\EntityDisplayModeInterface $entity_view_display */
foreach ($entity_view_displays as $display_id => $entity_view_display) {
$view_display_fieldblocks = $entity_view_display->getThirdPartySettings('fieldblock');
$entity_type = $entity_view_display->get('targetEntityType');
$bundle = $entity_view_display->get('bundle');
$mode = $entity_view_display->get('mode');
foreach ($view_display_fieldblocks as $field_name => $field_label) {
$fieldblock_id = $display_id . ':' . $field_name;
$fieldblocks[$fieldblock_id] = $this->t('@field field (from @type: @bundle: @mode)', array(
'@field' => $field_label,
'@type' => $entity_type,
'@bundle' => $bundle,
'@mode' => $mode,
));
}
}
return $fieldblocks;
}
}
<?php
/**
* @file
* Contains \Drupal\fieldblock\Plugin\Derivative\FieldBlockDeriver.
*/
namespace Drupal\fieldblock\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;