diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index 3f555cfec9fd0bc400255d97c35a3953e8972e39..3f55c28be780d38944978728ffab99ae4cf2cbc1 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -9,6 +9,7 @@ use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Plugin\views\wizard\WizardInterface; use Drupal\views\Render\ViewsRenderPipelineMarkup; +use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; /** @@ -508,9 +509,40 @@ public function renderGroupingSets($sets) { // Render as a record set. else { if ($this->usesRowPlugin()) { + $fibers = []; foreach ($set['rows'] as $index => $row) { - $this->view->row_index = $index; - $set['rows'][$index] = $this->view->rowPlugin->render($row); + $fibers[$index] = new \Fiber(function () use ($row, $index) { + $this->view->row_index = $index; + return $this->view->rowPlugin->render($row); + }); + } + $iterations = 0; + while (count($fibers) > 0) { + foreach ($fibers as $key => $fiber) { + if (!$fiber->isStarted()) { + $fiber->start(); + } + elseif ($fiber->isSuspended()) { + $fiber->resume(); + } + // If the Fiber hasn't terminated by this point, move onto the + // next placeholder, we'll resume this Fiber again when we get + // back here. + if (!$fiber->isTerminated()) { + // If we've gone through the placeholders once already, and + // they're still not finished, then start to allow code higher + // up the stack to get on with something else. + if ($iterations) { + $fiber = \Fiber::getCurrent(); + if ($fiber !== NULL) { + $fiber->suspend(); + } + } + continue; + } + $set['rows'][$key] = $fiber->getReturn(); + unset($fibers[$key]); + } } } @@ -669,84 +701,126 @@ protected function renderFields(array $result) { $this->view->row_index = 0; $field_ids = array_keys($this->view->field); - // Only tokens relating to field handlers preceding the one we invoke - // ::getRenderTokens() on are returned, so here we need to pick the last - // available field handler. - $render_tokens_field_id = end($field_ids); - // If all fields have a field::access FALSE there might be no fields, so // there is no reason to execute this code. if (!empty($field_ids)) { - $renderer = $this->getRenderer(); /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ - $cache_plugin = $this->view->display_handler->getPlugin('cache'); - $max_age = $cache_plugin->getCacheMaxAge(); /** @var \Drupal\views\ResultRow $row */ + $fibers = []; + $iterations = 0; foreach ($result as $index => $row) { - $this->view->row_index = $index; - - // Here we implement render caching for result rows. Since we never - // build a render array for single rows, given that style templates - // need individual field markup to support proper theming, we build - // a raw render array containing all field render arrays and cache it. - // This allows us to cache the markup of the various children, that is - // individual fields, which is then available for style template - // preprocess functions, later in the rendering workflow. - // @todo Fetch all the available cached row items in one single cache - // get operation, once https://www.drupal.org/node/2453945 is fixed. - $data = [ - '#pre_render' => [[$this, 'elementPreRenderRow']], - '#row' => $row, - '#cache' => [ - 'keys' => $cache_plugin->getRowCacheKeys($row), - 'tags' => $cache_plugin->getRowCacheTags($row), - 'max-age' => $max_age, - ], - '#cache_properties' => $field_ids, - ]; - $renderer->addCacheableDependency($data, $this->view->storage); - // Views may be rendered both inside and outside a render context: - // - HTML views are rendered inside a render context: then we want to - // use ::render(), so that attachments and cacheability are bubbled. - // - non-HTML views are rendered outside a render context: then we - // want to use ::renderInIsolation(), so that no bubbling happens - if ($renderer->hasRenderContext()) { - $renderer->render($data); - } - else { - $renderer->renderInIsolation($data); - } - - // Extract field output from the render array and post process it. - $fields = $this->view->field; - $rendered_fields = &$this->rendered_fields[$index]; - $post_render_tokens = []; - foreach ($field_ids as $id) { - $rendered_fields[$id] = $data[$id]['#markup']; - $tokens = $fields[$id]->postRender($row, $rendered_fields[$id]); - if ($tokens) { - $post_render_tokens += $tokens; + $fibers[$index] = new \Fiber(fn() => $this->renderFieldRow($index, $row, $field_ids)); + } + while (\count($fibers) > 0) { + foreach ($fibers as $key => $fiber) { + if (!$fiber->isStarted()) { + $fiber->start(); } - } - - // Populate row tokens. - $this->rowTokens[$index] = $this->view->field[$render_tokens_field_id]->getRenderTokens([]); - - // Replace post-render tokens. - if ($post_render_tokens) { - $placeholders = array_keys($post_render_tokens); - $values = array_values($post_render_tokens); - foreach ($this->rendered_fields[$index] as &$rendered_field) { - // Placeholders and rendered fields have been processed by the - // render system and are therefore safe. - $rendered_field = ViewsRenderPipelineMarkup::create(str_replace($placeholders, $values, $rendered_field)); + elseif ($fiber->isSuspended()) { + $fiber->resume(); + } + // If the Fiber hasn't terminated by this point, move onto the next + // placeholder, we'll resume this Fiber again when we get back here. + if (!$fiber->isTerminated()) { + // If we've gone through the placeholders once already, and + // they're still not finished, then start to allow code higher up + // the stack to get on with something else. + if ($iterations) { + $fiber = \Fiber::getCurrent(); + if ($fiber !== NULL) { + $fiber->suspend(); + } + } + continue; } + unset($fibers[$key]); } } + $iterations++; + } + } + + unset($this->view->row_index); + } + + /** + * Render an individual field row. + * + * @param int $index + * The index of this row within the result. + * @param Drupal\views\ResultRow $row + * The row to be rendered. + * @param array $field_ids + * The field IDs present in this row. + */ + protected function renderFieldRow(int $index, ResultRow $row, array $field_ids): void { + // Only tokens relating to field handlers preceding the one we invoke + // ::getRenderTokens() on are returned, so here we need to pick the last + // available field handler. + $render_tokens_field_id = end($field_ids); + + $cache_plugin = $this->view->display_handler->getPlugin('cache'); + $max_age = $cache_plugin->getCacheMaxAge(); + $renderer = $this->getRenderer(); + $this->view->row_index = $index; + + // Here we implement render caching for result rows. Since we never + // build a render array for single rows, given that style templates + // need individual field markup to support proper theming, we build + // a raw render array containing all field render arrays and cache it. + // This allows us to cache the markup of the various children, that is + // individual fields, which is then available for style template + // preprocess functions, later in the rendering workflow. + // @todo Fetch all the available cached row items in one single cache + // get operation, once https://www.drupal.org/node/2453945 is fixed. + $data = [ + '#pre_render' => [[$this, 'elementPreRenderRow']], + '#row' => $row, + '#cache' => [ + 'keys' => $cache_plugin->getRowCacheKeys($row), + 'tags' => $cache_plugin->getRowCacheTags($row), + 'max-age' => $max_age, + ], + '#cache_properties' => $field_ids, + ]; + $renderer->addCacheableDependency($data, $this->view->storage); + // Views may be rendered both inside and outside a render context: + // - HTML views are rendered inside a render context: then we want to + // use ::render(), so that attachments and cacheability are bubbled. + // - non-HTML views are rendered outside a render context: then we + // want to use ::renderInIsolation(), so that no bubbling happens + if ($renderer->hasRenderContext()) { + $renderer->render($data); + } + else { + $renderer->renderInIsolation($data); + } + + // Extract field output from the render array and post process it. + $fields = $this->view->field; + $rendered_fields = &$this->rendered_fields[$index]; + $post_render_tokens = []; + foreach ($field_ids as $id) { + $rendered_fields[$id] = $data[$id]['#markup']; + $tokens = $fields[$id]->postRender($row, $rendered_fields[$id]); + if ($tokens) { + $post_render_tokens += $tokens; } + } - unset($this->view->row_index); + // Populate row tokens. + $this->rowTokens[$index] = $this->view->field[$render_tokens_field_id]->getRenderTokens([]); + + // Replace post-render tokens. + if ($post_render_tokens) { + $placeholders = array_keys($post_render_tokens); + $values = array_values($post_render_tokens); + foreach ($this->rendered_fields[$index] as &$rendered_field) { + // Placeholders and rendered fields have been processed by the + // render system and are therefore safe. + $rendered_field = ViewsRenderPipelineMarkup::create(str_replace($placeholders, $values, $rendered_field)); + } } }