diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index b1861f7b36..f13c0e752d 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -4,7 +4,6 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Tags; -use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\views\display\DisplayRouterInterface; @@ -17,9 +16,14 @@ * * An object to contain all of the data to generate a view, plus the member * functions to build the view query, execute the query and render the output. + * + * This class does not implement the Serializable interface since problems + * occurred when using the serialize method. + * + * @see https://www.drupal.org/node/2849674 + * @see https://bugs.php.net/bug.php?id=66052 */ -class ViewExecutable implements \Serializable { - use DependencySerializationTrait; +class ViewExecutable { /** * The config entity in which the view is stored. @@ -435,6 +439,13 @@ class ViewExecutable implements \Serializable { protected $baseEntityType; /** + * Holds all necessary data for proper unserialization. + * + * @var array + */ + protected $serializationData; + + /** * Constructs a new ViewExecutable object. * * @param \Drupal\views\ViewEntityInterface $storage @@ -2466,52 +2477,68 @@ public function getDependencies() { } /** - * {@inheritdoc} - */ - public function serialize() { - return serialize([ - // Only serialize the storage entity ID. - $this->storage->id(), - $this->current_display, - $this->args, - $this->current_page, - $this->exposed_input, - $this->exposed_raw_input, - $this->exposed_data, - $this->dom_id, - $this->executed, - ]); + * Magic method implementation to serialize the view executable. + * + * @return array + * The names of all variables that should be serialized. + */ + public function __sleep() { + // Limit to only the required data which is needed to properly restore the + // state during unserialization. + $this->serializationData = [ + 'storage' => $this->storage->id(), + 'views_data' => $this->viewsData->_serviceId, + 'route_provider' => $this->routeProvider->_serviceId, + 'current_display' => $this->current_display, + 'args' => $this->args, + 'current_page' => $this->current_page, + 'exposed_input' => $this->exposed_input, + 'exposed_raw_input' => $this->exposed_raw_input, + 'exposed_data' => $this->exposed_data, + 'dom_id' => $this->dom_id, + 'executed' => $this->executed, + ]; + return ['serializationData']; } /** - * {@inheritdoc} + * Magic method implementation to unserialize the view executable. */ - public function unserialize($serialized) { - list($storage, $current_display, $args, $current_page, $exposed_input, $exposed_raw_input, $exposed_data, $dom_id, $executed) = unserialize($serialized); - - // There are cases, like in testing, where we don't have a container + public function __wakeup() { + // There are cases, like in testing where we don't have a container // available. - if (\Drupal::hasContainer()) { - $this->setRequest(\Drupal::request()); - $this->user = \Drupal::currentUser(); + if (\Drupal::hasContainer() && !empty($this->serializationData)) { + // Load and reference the storage. + $this->storage = \Drupal::entityTypeManager()->getStorage('view') + ->load($this->serializationData['storage']); + $this->storage->set('executable', $this); - $this->storage = \Drupal::entityManager()->getStorage('view')->load($storage); + // Attach all necessary services. + $this->user = \Drupal::currentUser(); + $this->viewsData = \Drupal::service($this->serializationData['views_data']); + $this->routeProvider = \Drupal::service($this->serializationData['route_provider']); - $this->setDisplay($current_display); - $this->setArguments($args); - $this->setCurrentPage($current_page); - $this->setExposedInput($exposed_input); - $this->exposed_data = $exposed_data; - $this->exposed_raw_input = $exposed_raw_input; - $this->dom_id = $dom_id; + // Restore the state of this executable. + if ($request = \Drupal::request()) { + $this->setRequest($request); + } + $this->setDisplay($this->serializationData['current_display']); + $this->setArguments($this->serializationData['args']); + $this->setCurrentPage($this->serializationData['current_page']); + $this->setExposedInput($this->serializationData['exposed_input']); + $this->exposed_data = $this->serializationData['exposed_data']; + $this->exposed_raw_input = $this->serializationData['exposed_raw_input']; + $this->dom_id = $this->serializationData['dom_id']; $this->initHandlers(); // If the display was previously executed, execute it now. - if ($executed) { + if ($this->serializationData['executed']) { $this->execute($this->current_display); } } + // Unset serializationData since it serves no further purpose. + unset($this->serializationData); } } diff --git a/core/modules/views/tests/modules/user_batch_action_test/config/install/system.action.user_batch_action_test_action.yml b/core/modules/views/tests/modules/user_batch_action_test/config/install/system.action.user_batch_action_test_action.yml new file mode 100644 index 0000000000..6b256637a7 --- /dev/null +++ b/core/modules/views/tests/modules/user_batch_action_test/config/install/system.action.user_batch_action_test_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - user +id: user_batch_action_test_action +label: 'Process user in batch' +type: user +plugin: user_batch_action_test_action +configuration: { } diff --git a/core/modules/views/tests/modules/user_batch_action_test/config/schema/user_batch_action_test.schema.yml b/core/modules/views/tests/modules/user_batch_action_test/config/schema/user_batch_action_test.schema.yml new file mode 100644 index 0000000000..0730941986 --- /dev/null +++ b/core/modules/views/tests/modules/user_batch_action_test/config/schema/user_batch_action_test.schema.yml @@ -0,0 +1,3 @@ +action.configuration.user_batch_action_test_action: + type: action_configuration_default + label: 'Process user in batch' diff --git a/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php new file mode 100644 index 0000000000..e4bed32f4d --- /dev/null +++ b/core/modules/views/tests/modules/user_batch_action_test/src/Plugin/Action/BatchUserAction.php @@ -0,0 +1,87 @@ + $entity->getEntityTypeId(), + 'entity_id' => $entity->id(), + ]], + ]; + } + + if ($operations) { + $batch = [ + 'operations' => $operations, + 'finished' => [get_class($this), 'finishBatch'], + ]; + batch_set($batch); + } + } + + /** + * {@inheritdoc} + */ + public function execute(ContentEntityInterface $entity = NULL) { + $this->executeMultiple([$entity]); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + return TRUE; + } + + /** + * Processes the batch item. + * + * @param array $data + * Keyed array of data to process. + * @param array $context + * The batch context. + */ + public static function processBatch($data, &$context) { + if (!isset($context['results']['processed'])) { + $context['results']['processed'] = 0; + } + $context['results']['processed']++; + } + + /** + * Finish batch. + * + * @param bool $success + * Indicates whether the batch process was successful. + * @param array $results + * Results information passed from the processing callback. + */ + public static function finishBatch($success, $results) { + drupal_set_message(\Drupal::translation() + ->formatPlural($results['processed'], 'One item has been processed.', '@count items have been processed.')); + } + +} diff --git a/core/modules/views/tests/modules/user_batch_action_test/user_batch_action_test.info.yml b/core/modules/views/tests/modules/user_batch_action_test/user_batch_action_test.info.yml new file mode 100644 index 0000000000..931dfc2b6c --- /dev/null +++ b/core/modules/views/tests/modules/user_batch_action_test/user_batch_action_test.info.yml @@ -0,0 +1,9 @@ +name: 'User batch action test' +type: module +description: 'Support module for user batch action testing.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - views + - user diff --git a/core/modules/views/tests/src/Functional/UserBatchActionTest.php b/core/modules/views/tests/src/Functional/UserBatchActionTest.php new file mode 100644 index 0000000000..7b59dcc579 --- /dev/null +++ b/core/modules/views/tests/src/Functional/UserBatchActionTest.php @@ -0,0 +1,45 @@ +drupalLogin($this->rootUser); + + $themes = ['classy', 'seven', 'bartik']; + + /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */ + $theme_installer = $this->container->get('theme_installer'); + $theme_installer->install($themes); + + foreach ($themes as $theme) { + \Drupal::configFactory()->getEditable('system.theme')->set('default', $theme)->save(); + $this->drupalGet('admin/people'); + $edit = [ + 'user_bulk_form[0]' => TRUE, + 'action' => 'user_batch_action_test_action', + ]; + $this->drupalPostForm(NULL, $edit, t('Apply to selected items')); + $this->checkForMetaRefresh(); + $this->assertSession()->pageTextContains('One item has been processed.'); + } + } + +} diff --git a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php index f843892e5d..577ed03e98 100644 --- a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php +++ b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php @@ -487,6 +487,35 @@ public function testSerialization() { $this->assertIdentical($unserialized->current_display, 'page_1', 'The expected display was set on the unserialized view.'); $this->assertIdentical($unserialized->args, ['test'], 'The expected argument was set on the unserialized view.'); $this->assertIdentical($unserialized->getCurrentPage(), 2, 'The expected current page was set on the unserialized view.'); + + // Get the definition of node's nid field, for example. Only get it not from + // the field manager directly, but from the item data definition. It should + // be the same base field definition object (the field and item definitions + // refer to each other). + // See https://bugs.php.net/bug.php?id=66052 + $field_manager = $this->container->get('entity_field.manager'); + $nid_definition_before = $field_manager->getBaseFieldDefinitions('node')['nid'] + ->getItemDefinition() + ->getFieldDefinition(); + + // Load and execute a view. + $view_entity = View::load('content'); + $view_executable = $view_entity->getExecutable(); + $view_executable->execute('page_1'); + + // Reset static cache, but leave possibility to use database cache. + $field_manager->useCaches(FALSE); + $field_manager->useCaches(TRUE); + + // Magic line. + unserialize(serialize(['SOMETHING UNEXPECTED', $view_executable])); + + // Make sure the serialisation of the ViewExecutable didn't influence the + // field definitions. + $nid_definition_after = $field_manager->getBaseFieldDefinitions('node')['nid'] + ->getItemDefinition() + ->getFieldDefinition(); + $this->assertEquals($nid_definition_before->getPropertyDefinitions(), $nid_definition_after->getPropertyDefinitions()); } }