diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index e1d0731..c9d4680 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -9,6 +9,7 @@ use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface; use Drupal\Component\Plugin\Discovery\DiscoveryCachedTrait; +use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator; use Drupal\Component\Plugin\PluginManagerBase; @@ -20,6 +21,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; use Drupal\Core\Plugin\Factory\ContainerFactory; +use \Symfony\Component\Debug\Exception\FlattenException; /** * Base class for plugin managers. @@ -83,6 +85,13 @@ class DefaultPluginManager extends PluginManagerBase implements PluginManagerInt protected $defaults = array(); /** + * Defines a fallback ID in case the plugin instance could not be created. + * + * @var string + */ + protected $fallbackPluginId; + + /** * Creates the discovery object. * * @param string|bool $subdir @@ -198,7 +207,6 @@ protected function setCachedDefinitions($definitions) { $this->definitions = $definitions; } - /** * Performs extra processing on plugin definitions. * @@ -212,6 +220,7 @@ public function processDefinition(&$definition, $plugin_id) { } } + /** * Finds plugin definitions. * @@ -241,4 +250,50 @@ protected function findDefinitions() { return $definitions; } + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = array()) { + try { + $instance = $this->factory->createInstance($plugin_id, $configuration); + } + catch (PluginException $exception) { + if ($fallback_plugin_id = $this->getFallbackPluginId($plugin_id, $configuration)) { + // Allow implementations show the exception message. + $configuration['_exception'] = new FlattenException($exception); + $instance = $this->factory->createInstance($this->getFallbackPluginId($plugin_id, $configuration), $configuration); + } + else { + throw $exception; + } + } + return $instance; + } + + /** + * Returns the fallback plugin ID to be used. + * + * @param string $original_plugin_id + * The plugin ID of the non-instantiable plugin. + * @param array $configuration + * The original configuration of the plugin. + * + * @return string|null + * The plugin ID of the fallback instance or NULL if there is no fallback + * set. + */ + protected function getFallbackPluginId($original_plugin_id, array $configuration) { + return $this->fallbackPluginId; + } + + /** + * Sets the fallback plugin ID. + * + * @param string $fallback_plugin_id + * The plugin ID to fall back to. + */ + public function setFallbackPluginId($fallback_plugin_id) { + $this->fallbackPluginId = $fallback_plugin_id; + } + } diff --git a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php index 342d0cc..0ad4ca4 100644 --- a/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php +++ b/core/modules/block_content/src/Plugin/Block/BlockContentBlock.php @@ -154,9 +154,8 @@ public function build() { } else { return array( - '#markup' => t('Block with uuid %uuid does not exist. Add custom block.', array( + '#markup' => t('Block with uuid %uuid does not exist.', array( '%uuid' => $uuid, - '!url' => url('block/add') )), '#access' => $this->account->hasPermission('administer blocks') ); diff --git a/core/modules/block_content/src/Tests/BlockContentPageViewTest.php b/core/modules/block_content/src/Tests/BlockContentPageViewTest.php index 9d57528..ed9eee7 100644 --- a/core/modules/block_content/src/Tests/BlockContentPageViewTest.php +++ b/core/modules/block_content/src/Tests/BlockContentPageViewTest.php @@ -22,7 +22,7 @@ class BlockContentPageViewTest extends BlockContentTestBase { public static $modules = array('block', 'block_content', 'block_content_test'); /** - * Checks block edit functionality. + * Checks block edit and fallback functionality. */ public function testPageEdit() { $this->drupalLogin($this->adminUser); @@ -33,6 +33,15 @@ public function testPageEdit() { // Assert response was '200' and not '403 Access denied'. $this->assertResponse('200', 'User was able the view the block'); - } + + $this->drupalGet(''); + $expected_text = t('Block with uuid %uuid does not exist.', array( + '%uuid' => 'foobar_gorilla', + )); + $this->assertRaw($expected_text); + + $this->drupalLogout(); + $this->assertNoRaw($expected_text); + } } diff --git a/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar.yml b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar.yml new file mode 100644 index 0000000..939779c --- /dev/null +++ b/core/modules/block_content/tests/modules/block_content_test/config/install/block.block.foobar.yml @@ -0,0 +1,35 @@ +id: foobar +weight: null +status: true +langcode: en +dependencies: + module: + - custom_block + theme: + - stark +theme: stark +region: sidebar_first +plugin: 'custom_block:foobar_gorilla' +settings: + label: Foobar + provider: custom_block + label_display: visible + cache: + max_age: -1 + contexts: { } + status: true + info: '' + view_mode: default + admin_label: '' + custom_block: + view_mode: default +visibility: + path: + visibility: 0 + pages: '' + role: + roles: { } + node_type: + types: + article: '0' + page: '0' diff --git a/core/modules/views/src/Plugin/ViewsHandlerManager.php b/core/modules/views/src/Plugin/ViewsHandlerManager.php index 7b7b76e..1cdaa33 100644 --- a/core/modules/views/src/Plugin/ViewsHandlerManager.php +++ b/core/modules/views/src/Plugin/ViewsHandlerManager.php @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\views\Plugin\views\HandlerBase; use Drupal\views\ViewsData; use Symfony\Component\DependencyInjection\Container; @@ -55,6 +56,7 @@ public function __construct($handler_type, \Traversable $namespaces, ViewsData $ parent::__construct("Plugin/views/$handler_type", $namespaces, $module_handler, $plugin_definition_annotation_name); $this->setCacheBackend($cache_backend, "views:$handler_type", array('extension' => array(TRUE, 'views'))); + $this->setFallbackPluginId('broken'); $this->viewsData = $views_data; $this->handlerType = $handler_type; @@ -97,26 +99,25 @@ public function getHandler($item, $override = NULL) { } } } - - // @todo This is crazy. Find a way to remove the override functionality. - $plugin_id = $override ? : $definition['id']; - // Try to use the overridden handler. - try { - return $this->createInstance($plugin_id, $definition); - } - catch (PluginException $e) { - // If that fails, use the original handler. - try { - return $this->createInstance($definition['id'], $definition); - } - catch (PluginException $e) { - // Deliberately empty, this case is handled generically below. - } - } + } + else { + $definition = $item; + $definition['id'] = 'non_existing'; } - // Finally, use the 'broken' handler. - return $this->createInstance('broken', array('original_configuration' => $item)); + // @todo This is crazy. Find a way to remove the override functionality. + $plugin_id = $override ?: $definition['id']; + // Try to use the overridden or the original handler. + try { + return $this->createInstance($plugin_id, $definition); + } + catch (PluginException $e) { + // If that fails, use the original handler or the broken fallback. + $this->setFallbackPluginId('broken'); + $result = $this->createInstance($definition['id'], $definition); + $this->setFallbackPluginId(NULL); + return $result; + } } /** diff --git a/core/modules/views/src/Plugin/views/BrokenHandlerTrait.php b/core/modules/views/src/Plugin/views/BrokenHandlerTrait.php index 4bd9948..0ecb10e 100644 --- a/core/modules/views/src/Plugin/views/BrokenHandlerTrait.php +++ b/core/modules/views/src/Plugin/views/BrokenHandlerTrait.php @@ -21,7 +21,7 @@ */ public function adminLabel($short = FALSE) { $args = array( - '@module' => $this->definition['original_configuration']['provider'], + '@module' => $this->definition['provider'], ); return t('Broken/missing handler (Module: @module) …', $args); } @@ -61,9 +61,9 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $description_top = t('The handler for this item is broken or missing. The following details are available:'); $items = array( - t('Module: @module', array('@module' => $this->definition['original_configuration']['provider'])), - t('Table: @table', array('@table' => $this->definition['original_configuration']['table'])), - t('Field: @field', array('@field' => $this->definition['original_configuration']['field'])), + t('Module: @module', array('@module' => $this->definition['provider'])), + t('Table: @table', array('@table' => $this->definition['table'])), + t('Field: @field', array('@field' => $this->definition['field'])), ); $description_bottom = t('Enabling the appropriate module will may solve this issue. Otherwise, check to see if there is a module update available.'); diff --git a/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php b/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php index 7f7d3ac..56bec56 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php @@ -7,12 +7,18 @@ namespace Drupal\Tests\Core\Plugin; +use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\plugin_test\Plugin\plugin_test\fruit\Apple; use Drupal\Tests\UnitTestCase; +use Symfony\Component\Debug\Exception\FlattenException; /** * Tests the DefaultPluginManager. * * @group Plugin + * @group Drupal + * + * @coversDefaultClass Drupal\Core\Plugin\DefaultPluginManager */ class DefaultPluginManagerTest extends UnitTestCase { @@ -58,6 +64,8 @@ protected function setUp() { /** * Tests the plugin manager with a disabled module. + * + * @covers ::getDefinition */ public function testDefaultPluginManagerWithDisabledModule() { $definitions = $this->expectedDefinitions; @@ -108,6 +116,9 @@ public function testDefaultPluginManagerWithObjects() { /** * Tests the plugin manager with no cache and altering. + * + * @covers ::getDefinitions + * @covers ::getDefinition */ public function testDefaultPluginManager() { $plugin_manager = new TestPluginManager($this->namespaces, $this->expectedDefinitions); @@ -117,6 +128,9 @@ public function testDefaultPluginManager() { /** * Tests the plugin manager with no cache and altering. + * + * @covers ::getDefinitions + * @covers ::getDefinition */ public function testDefaultPluginManagerWithAlter() { $module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandler') @@ -137,6 +151,9 @@ public function testDefaultPluginManagerWithAlter() { /** * Tests the plugin manager with caching and altering. + * + * @covers ::getDefinitions + * @covers ::getDefinition */ public function testDefaultPluginManagerWithEmptyCache() { $cid = $this->randomMachineName(); @@ -162,6 +179,8 @@ public function testDefaultPluginManagerWithEmptyCache() { /** * Tests the plugin manager with caching and altering. + * + * @covers ::getDefinitions */ public function testDefaultPluginManagerWithFilledCache() { $cid = $this->randomMachineName(); @@ -185,6 +204,9 @@ public function testDefaultPluginManagerWithFilledCache() { /** * Tests the plugin manager cache clear with tags. + * + * @covers ::getFallbackPluginId + * @covers ::createInstance */ public function testCacheClearWithTags() { $cid = $this->randomMachineName(); @@ -207,6 +229,46 @@ public function testCacheClearWithTags() { $plugin_manager->clearCachedDefinitions(); } + /** + * Tests the plugin manager without a fallback plugin. + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginException + */ + public function testCreateInstanceWithoutFallback() { + $factory = $this->getMock('\Drupal\Component\Plugin\Factory\FactoryInterface'); + $factory->expects($this->once()) + ->method('createInstance') + ->with('non_existing', array()) + ->will($this->throwException(new PluginException())); + + $plugin_manager = new TestPluginManager($this->namespaces, $this->expectedDefinitions, NULL, NULL); + $plugin_manager->setFactory($factory); + $plugin_manager->createInstance('non_existing'); + } + + /** + * Tests the plugin manager with a fallback plugin. + */ + public function testCreateInstanceWithFallback() { + $factory = $this->getMock('\Drupal\Component\Plugin\Factory\FactoryInterface'); + $apple = new Apple(); + $exception = new PluginException(); + $factory->expects($this->at(0)) + ->method('createInstance') + ->with('non_existing', array('key' => 'value')) + ->will($this->throwException($exception)); + $factory->expects($this->at(1)) + ->method('createInstance') + ->with('apple', array('_exception' => new FlattenException($exception), 'key' => 'value')) + ->will($this->returnValue($apple)); + + $plugin_manager = new TestPluginManager($this->namespaces, $this->expectedDefinitions); + $plugin_manager->setFallbackPluginId('apple'); + $plugin_manager->setFactory($factory); + $plugin = $plugin_manager->createInstance('non_existing', array('key' => 'value')); + $this->assertSame($apple, $plugin); + } + } if (!defined('DRUPAL_ROOT')) { diff --git a/core/tests/Drupal/Tests/Core/Plugin/TestPluginManager.php b/core/tests/Drupal/Tests/Core/Plugin/TestPluginManager.php index a7ea79d..4619640 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/TestPluginManager.php +++ b/core/tests/Drupal/Tests/Core/Plugin/TestPluginManager.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Plugin; use Drupal\Component\Plugin\Discovery\StaticDiscovery; +use Drupal\Component\Plugin\Factory\FactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; @@ -28,6 +29,8 @@ class TestPluginManager extends DefaultPluginManager { * (optional) The module handler to invoke the alter hook with. * @param string $alter_hook * (optional) Name of the alter hook. + * @param string $fallback_plugin_id + * (optional) The fallback plugin ID. */ public function __construct(\Traversable $namespaces, array $definitions, ModuleHandlerInterface $module_handler = NULL, $alter_hook = NULL) { // Create the object that can be used to return definitions for all the @@ -48,4 +51,14 @@ public function __construct(\Traversable $namespaces, array $definitions, Module } } + /** + * Sets the plugin factory. + * + * @param \Drupal\Component\Plugin\Factory\FactoryInterface $factory + * (optional) The plugin factory. + */ + public function setFactory(FactoryInterface $factory) { + $this->factory = $factory; + } + }