Skip to content

Instantly share code, notes, and snippets.

@zviryatko
Created August 7, 2023 20:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zviryatko/a49930fdd04a6bd70e9277937c6a82b7 to your computer and use it in GitHub Desktop.
Save zviryatko/a49930fdd04a6bd70e9277937c6a82b7 to your computer and use it in GitHub Desktop.
Better REST export for Drupal 10 Views

Better REST Export for D10 Views

Put these files to src/Plugin/views/display/BetterRestExportNested.php and src/Plugin/views/style/BetterRestSerializer.php. Create views with display "Better REST export nested" and style "API Serializer". Feel free to connect it to React Tables or any. It has support of exposed filters, exposed sort and exposed pagination.

<?php
namespace Drupal\better_rest_api\Plugin\views\display;
use Drupal\Core\Render\RenderContext;
use Drupal\Component\Utility\Html;
use Drupal\rest\Plugin\views\display;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
/**
* The plugin that handles Data response callbacks for REST resources.
*
* This file copied from rest_export_nested project and integrated
* with BetterRestSerializer provided in this module.
*
* @see https://www.drupal.org/project/rest_export_nested
*
* @ingroup views_display_plugins
*
* @ViewsDisplay(
* id = "better_rest_api_export_nested",
* title = @Translation("Better REST export nested"),
* help = @Translation("Create a REST export resource which supports nested JSON."),
* uses_route = TRUE,
* admin = @Translation("Better REST export nested"),
* returns_response = TRUE
* )
*/
class BetterRestExportNested extends display\RestExport {
/**
* {@inheritdoc}
*/
public function render() {
$build = [];
$build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
return $this->view->style_plugin->render();
});
// Decode results.
$results = \GuzzleHttp\json_decode($build['#markup']);
// Loop through results and fields.
foreach ($results->rows as $key => $result) {
foreach ($result as $property => $value) {
// Check if the field can be decoded using PHP's json_decode().
if (is_string($value)) {
if (json_decode($value) !== NULL) {
// If so, use Guzzle to decode the JSON and add it to the results.
$results->rows[$key]->$property = \GuzzleHttp\json_decode($value);;
}
elseif (json_decode(Html::decodeEntities($value)) !== NULL){
$results->rows[$key]->$property = \GuzzleHttp\json_decode(Html::decodeEntities($value));
}
}
// Special null handling.
if (is_string($value) && $value === 'null') {
$results->rows[$key]->$property = NULL;
}
}
}
// Convert back to JSON.
$build['#markup'] = \GuzzleHttp\json_encode($results);
$this->view->element['#content_type'] = $this->getMimeType();
$this->view->element['#cache_properties'][] = '#content_type';
// Encode and wrap the output in a pre tag if this is for a live preview.
if (!empty($this->view->live_preview)) {
$build['#prefix'] = '<pre>';
$build['#plain_text'] = $build['#markup'];
$build['#suffix'] = '</pre>';
unset($build['#markup']);
}
elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
// This display plugin is primarily for returning non-HTML formats.
// However, we still invoke the renderer to collect cacheability metadata.
// Because the renderer is designed for HTML rendering, it filters
// #markup for XSS unless it is already known to be safe, but that filter
// only works for HTML. Therefore, we mark the contents as safe to bypass
// the filter. So long as we are returning this in a non-HTML response
// (checked above), this is safe, because an XSS attack only works when
// executed by an HTML agent.
// @todo Decide how to support non-HTML in the render API in
// https://www.drupal.org/node/2501313.
$build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
}
$this->applyDisplayCacheabilityMetadata($build);
return $build;
}
}
<?php
namespace Drupal\better_rest_api\Plugin\views\style;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormState;
use Drupal\rest\Plugin\views\style\Serializer;
use Drupal\views\Plugin\views\pager\None;
use Drupal\views\Plugin\views\pager\Some;
/**
* The style plugin for serialized output formats.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
* id = "better_rest_api_serializer",
* title = @Translation("API Serializer"),
* help = @Translation("Extends existing serializer styles to provide additional view data."),
* display_types = {"data"}
* )
*
* @see https://www.drupal.org/project/drupal/issues/2982729#comment-13599784
*/
class BetterRestSerializer extends Serializer {
/**
* {@inheritdoc}
*/
public function render() {
$data['endpoint'] = [
'path' => $this->view->getDisplay()->getPath(),
'args' => $this->view->args,
'requested' => $this->view->getUrl($this->view->args)->toString(),
];
$data['pager'] = $this->getPagerDetails();
$data['exposed_filters'] = $this->getExposedHandlers('filter');
$data['exposed_sorts'] = $this->getExposedHandlers('sort');
$data['rows'] = $this->getRows();
// Get the content type configured in the display or fallback to the default.
if ((empty($this->view->live_preview))) {
$content_type = $this->displayHandler->getContentType();
}
else {
$content_type = !empty($this->options['formats']) ? \reset($this->options['formats']) : 'json';
}
return $this->serializer->serialize($data, $content_type, ['views_style_plugin' => $this]);
}
/**
* Get the search results and process facets.
*
* @see FacetsSerializer::render();
*
* @return array
*/
private function getRows() {
$rows = [];
// If the Data Entity row plugin is used, this will be an array of entities
// which will pass through Serializer to one of the registered Normalizers,
// which will transform it to arrays/scalars. If the Data field row plugin
// is used, $rows will not contain objects and will pass directly to the
// Encoder.
foreach ($this->view->result as $row_index => $row) {
// Keep track of the current rendered row, like every style plugin has to
// do.
// @see \Drupal\views\Plugin\views\style\StylePluginBase::renderFields
$this->view->row_index = $row_index;
$rows[] = $this->view->rowPlugin->render($row);
}
// Remove native row index which was throwing off something in the rendering
// and avoid confusion. This is not relevant as the actually rows are being
// stored and rendered elsewhere. Can ask Jonathan to help clarify if need
// be.
unset($this->view->row_index);
return $rows;
}
/**
* Get pager and page details.
*
* @link https://www.drupal.org/project/drupal/issues/2982729
*
* @return array
*/
private function getPagerDetails() {
$details = ['active' => FALSE];
$pager = $this->view->pager;
if ($pager) {
$class = get_class($pager);
$total_pages = 0;
if (!in_array($class, [None::class, Some::class])) {
$total_pages = $pager->getPagerTotal();
}
$details = [
'active' => TRUE,
'current_page' => $pager->getCurrentPage(),
'total_items' => $pager->getTotalItems(),
'items_per_page' => $pager->getItemsPerPage(),
'total_pages' => $total_pages,
'options' => $pager->usesOptions() ? $pager->options : FALSE,
];
}
return $details;
}
/**
* Get exposed handler information and option values.
*
* @param string $type
* ID for a handler type.
*
* @return array
*/
private function getExposedHandlers($type) {
$exposed = [];
$handlers = $this->view->getHandlers($type);
$handler_objects = $this->view->display_handler->getHandlers($type);
$exposed_input = $this->view->getExposedInput();
foreach ($handlers as $id => $item) {
if (!empty($item['exposed'])) {
$info = $item['expose'];
if ($type === 'filter') {
unset($info['remember_roles']);
$form = []; $form_state = new FormState;
$form_state->set('exposed', TRUE);
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $plugin */
$plugin = $handler_objects[$id];
$identifier = $plugin->isAGroup() ? $item['group_info']['identifier'] : $item['expose']['identifier'];
$info['submitted_values'] = !empty($exposed_input[$identifier]) ? $exposed_input[$identifier] : [];
if ($plugin->isAGroup()) {
$plugin->groupForm($form, $form_state);
$info = $item['group_info'] + $item['expose'];
} else {
$plugin->buildExposedForm($form, $form_state);
}
$info['options'] = [];
if (!empty($form[$identifier]['#options'])) {
$info['options'] = $form[$identifier]['#options'];
}
// Remove "all"
unset($info['options']['All'], $info['options']['all']);
// Hide operator parameters if empty.
if (empty($info['use_operator'])) {
unset(
$info['use_operator'],
$info['operator'],
$info['operator_id'],
$info['operator_limit_selection'],
$info['operator_list'],
);
}
}
$exposed[] = $info;
}
}
return $exposed;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['url.path', 'url.query_args']);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment