graphql_compose-1.0.0-beta20/modules/graphql_compose_views/src/Plugin/views/row/GraphQLFieldRow.php
modules/graphql_compose_views/src/Plugin/views/row/GraphQLFieldRow.php
<?php
declare(strict_types=1);
namespace Drupal\graphql_compose_views\Plugin\views\row;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\field\FieldHandlerInterface;
use Drupal\views\Plugin\views\row\RowPluginBase;
use function Symfony\Component\String\u;
/**
* Plugin which displays fields as raw data.
*
* @ViewsRow(
* id = "graphql_field",
* title = @Translation("Fields"),
* help = @Translation("Use fields as row data."),
* display_types = {"graphql"},
* )
*/
class GraphQLFieldRow extends RowPluginBase {
/**
* {@inheritdoc}
*/
protected $usesFields = TRUE;
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['field_options'] = ['default' => []];
return $options;
}
/**
* Returns the field options.
*
* @return array
* The field options.
*/
protected function getFieldOptions(): array {
return (array) ($this->options['field_options'] ?? []);
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$fields = $this->view->display_handler->getOption('fields') ?: [];
// Filter out the excluded fields.
$fields = array_filter(
$fields,
fn(array $field) => empty($field['exclude'])
);
$form['field_options'] = [
'#type' => 'table',
'#header' => [
$this->t('Field'),
$this->t('Alias'),
$this->t('Raw'),
$this->t('Nullable'),
$this->t('Type'),
],
'#empty' => $this->t('You have no fields. Add some to your view.'),
'#tree' => TRUE,
];
foreach ($fields as $id => $field) {
$form['field_options'][$id]['field'] = [
'#markup' => $id,
];
$form['field_options'][$id]['alias'] = [
'#title' => $this->t('Alias for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#default_value' => $this->getAlias($id, FALSE),
'#element_validate' => [[$this, 'validateAlias']],
];
$form['field_options'][$id]['raw_output'] = [
'#title' => $this->t('Raw output for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#default_value' => $this->isRaw($id),
];
$form['field_options'][$id]['nullable'] = [
'#title' => $this->t('Nullable output for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#default_value' => $this->isNullable($id),
];
$form['field_options'][$id]['type'] = [
'#type' => 'select',
'#options' => [
'String' => $this->t('String'),
'Int' => $this->t('Int'),
'Float' => $this->t('Float'),
'Boolean' => $this->t('Boolean'),
'Scalar' => $this->t('Custom Scalar'),
],
'#default_value' => $this->getType($id),
];
}
}
/**
* Form element validation handler.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function validateAlias(array $element, FormStateInterface $form_state) {
$value = $element['#value'] ?: '';
if ($value && !preg_match('/^[a-z]([A-Za-z0-9]+)?$/', $value)) {
$message = $this->t('@name must start with a lowercase letter and contain only letters and numbers.', [
'@name' => $element['#title'] ?? 'Field name',
]);
$form_state->setError($element, $message);
}
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
$options = $form_state->getValue(['row_options', 'field_options']) ?: [];
$aliases = array_filter(array_column($options, 'alias'));
$unique_aliases = array_unique($aliases);
if (count($aliases) !== count($unique_aliases)) {
$form_state->setErrorByName('field_options', $this->t('All field aliases must be unique.'));
}
}
/**
* Return an alias for a field ID, as set in the options form.
*
* @param string $id
* The field id to lookup an alias for.
* @param bool $with_default
* Whether to return the default alias if none is set.
*
* @return string
* The matches user entered alias, or null when an empty.
*/
public function getAlias(string $id, bool $with_default = TRUE): string {
$options = $this->getFieldOptions();
$alias = $options[$id]['alias'] ?? NULL;
$alias = $alias ? trim($alias) : NULL;
if ($with_default && empty($alias)) {
$alias = $this->getDefaultAlias($id);
}
return $alias ?: '';
}
/**
* Return a GraphQL field type, as set in the options form.
*
* @param string $id
* The field id to lookup a type for.
*
* @return string
* The matches user entered type, or String.
*/
public function getType(string $id): string {
$options = $this->getFieldOptions();
return $options[$id]['type'] ?? 'String';
}
/**
* Checks the field should return raw value (without a render).
*
* @param string $id
* Field name.
*
* @return bool
* TRUE if the field is set to raw.
*/
public function isRaw(string $id): bool {
$options = $this->getFieldOptions();
return (bool) ($options[$id]['raw_output'] ?? FALSE);
}
/**
* Checks the field is nullable.
*
* @param string $id
* Field name.
*
* @return bool
* TRUE if the field can be set to null.
*/
public function isNullable(string $id): bool {
$options = $this->getFieldOptions();
return (bool) ($options[$id]['nullable'] ?? FALSE);
}
/**
* Returns a default field alias for a given field ID.
*
* @param string $id
* The field ID to generate an alias for.
*
* @return string
* The default alias for the field ID.
*/
protected function getDefaultAlias(string $id): string {
return u($id)
->trimPrefix('field_')
->camel()
->toString();
}
/**
* {@inheritdoc}
*/
public function render($row) {
$output = [];
/** @var array<string,FieldHandlerInterface> $fields */
$fields = $this->view->field ?: [];
// Filter out excluded fields.
$fields = array_filter(
$fields,
fn (FieldHandlerInterface $field) => empty($field->options['exclude'])
);
foreach ($fields as $id => $field) {
// Get the field type.
$field_type = $this->getType($id);
if ($this->isRaw($id)) {
$value = $field->getValue($row);
}
else {
// Renders a field using "advanced" settings.
// This also wraps safe markup in MarkupInterface.
$markup = $field->advancedRender($row);
// Post render to support un-cacheable fields.
$field->postRender($row, $markup);
$value = $field->last_render;
}
// If the value is a MarkupInterface, convert it to a string.
$value = $value instanceof MarkupInterface ? (string) $value : $value;
// Prep for conversion. Strip and trim Int/Float/Boolean strings.
if (is_string($value) && in_array($field_type, ['Int', 'Float', 'Boolean'])) {
$value = trim(strip_tags($value));
}
// Convert the value to the appropriate type.
switch ($field_type) {
case 'String':
$value = Xss::filter((string) $value);
break;
case 'Int':
$value = (int) $value;
break;
case 'Float':
$value = (float) filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
break;
case 'Boolean':
$truthy = [
(string) $this->t('yes'),
(string) $this->t('true'),
(string) $this->t('on'),
(string) $this->t('enabled'),
'y',
'yes',
'true',
'on',
'enabled',
'1',
1,
TRUE,
];
$value = is_string($value) ? strtolower($value) : $value;
$value = in_array($value, $truthy, TRUE);
break;
case 'Scalar':
if ($this->isRaw($id) && is_string($value)) {
try {
// We can attempt to convert from JSON.
$json = Json::decode($value);
// Because false/null are valid JSON values
// we should check for errors. If none, great!
$value = json_last_error() ? $value : $json;
}
catch (InvalidDataTypeException) {
// Couldn't convert to JSON.
// Don't change the value further.
}
}
elseif (is_string($value)) {
$value = Xss::filter($value);
}
break;
default:
throw new \InvalidArgumentException(
sprintf('Invalid field type "%s" for field "%s".', $field_type, $id)
);
}
if ($this->isNullable($id) && empty($value)) {
$value = NULL;
}
$output[$this->getAlias($id)] = $value;
}
return $output;
}
}
