custom_action_links-1.0.x-dev/src/Element/ActionLink.php
src/Element/ActionLink.php
<?php
namespace Drupal\custom_action_links\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\ParamConverter\ParamConverterManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Render\Element\Details;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Provides an action link element.
*
* Properties:
* - #action_link: Sets the value of the inner form elements. An array with the
* following keys:
* - 'route_name': A string for the route name.
* - 'route_parameters': An array of route parameters.
* - 'link_title': A string used as the link text.
* - #route_access_check: Determines if the route should be access checked.
* Defaults to TRUE.
* - #title: The title of the details container. Defaults to "Details".
* - #open: Indicates whether the container should be open by default.
* Defaults to FALSE.
* - #summary_attributes: An array of attributes to apply to the <summary>
* element.
*
* @RenderElement("custom_action_link")
*/
class ActionLink extends Details {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
$class = static::class;
$info = parent::getInfo();
$info['#input'] = TRUE;
$info['#route_access_check'] = TRUE;
array_unshift($info['#process'], [$class, 'processActionLink']);
$info['#element_validate'][] = [$class, 'validateActionLink'];
$info['#value_callback'] = [$class, 'valueCallback'];
unset($info['#value']);
return $info;
}
/**
* Expands a action link element type into input elements.
*
* @param array $element
* The form element whose value is being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The form element whose value has been processed.
*
* @see \Drupal\Core\Datetime\DateFormatterInterface::format()
*/
public static function processActionLink(array &$element, FormStateInterface $form_state, array &$complete_form): array {
// The value callback has populated the #value array.
$values = $element['#value'];
unset($element['#value']);
$user_token = Crypt::hmacBase64(static::getCurrentUser()->id(), Settings::getHashSalt() . static::getPrivateKey());
$element['route_name'] = [
'#type' => 'textfield',
'#title' => new TranslatableMarkup('Route name'),
'#default_value' => $values['route_name'],
'#description' => new TranslatableMarkup('The route name. For example: "node.add". To remove the route delete the value.'),
'#autocomplete_route_name' => 'custom_action_links.route_names.autocomplete',
'#autocomplete_route_parameters' => ['user_token' => $user_token],
];
$element['route_parameters'] = [
'#type' => 'textfield',
'#title' => new TranslatableMarkup('The route parameters'),
'#default_value' => static::paramArrayToString($values['route_parameters']),
'#description' => new TranslatableMarkup('The route parameter values separate by a comma. For example, "node_type=page".'),
];
$element['link_title'] = [
'#type' => 'textfield',
'#title' => new TranslatableMarkup('The text for action link'),
'#default_value' => $values['link_title'],
];
return $element;
}
/**
* Validation callback for a action link element.
*
* @param array $element
* The form element whose value is being validated.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*/
public static function validateActionLink(array &$element, FormStateInterface $form_state, array &$complete_form) {
$input_exists = FALSE;
$input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
if ($input_exists && !empty($input['route_name'])) {
if (empty($input['link_title'])) {
$form_state->setError($element['link_title'], new TranslatableMarkup('The text for action link is required.'));
}
try {
$route = static::getRouteProvider()->getRouteByName($input['route_name']);
// ParamConverterManager relies on the route name and object being
// available from the parameters array.
if (class_exists('Drupal\Core\Routing\RouteObjectInterface')) {
$input['route_parameters'][RouteObjectInterface::ROUTE_NAME] = $input['route_name'];
$input['route_parameters'][RouteObjectInterface::ROUTE_OBJECT] = $route;
}
else {
// Drupal 8 code.
// @todo remove once Drupal 8 is no longer supported.
$input['route_parameters'][\Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_NAME] = $input['route_name'];
$input['route_parameters'][\Symfony\Cmf\Component\Routing\RouteObjectInterface::ROUTE_OBJECT] = $route;
}
$upcasted_parameters = static::getParamConverter()->convert($input['route_parameters'] + $route->getDefaults());
$route_match = new RouteMatch($input['route_name'], $route, $upcasted_parameters, $input['route_parameters']);
if ($element['#route_access_check'] && !static::getAccessManager()->check($route_match)) {
$form_state->setError($element['route_name'], new TranslatableMarkup('Access to the route is denied.'));
}
}
catch (RouteNotFoundException $e) {
$form_state->setError($element['route_name'], new TranslatableMarkup('The route does not exist.'));
}
catch (ParamNotConvertedException $e) {
$form_state->setError($element['route_parameters'], new TranslatableMarkup('The route parameters are incorrect: %message', ['%message' => $e->getMessage()]));
}
}
}
/**
* Determines how user input is mapped to an element's #value property.
*
* @param array $element
* An associative array containing the properties of the element.
* @param mixed $input
* The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return mixed
* The value to assign to the element.
*
* @see \Drupal\Core\Render\Element\FormElementInterface::valueCallback()
*/
public static function valueCallback(array &$element, $input, FormStateInterface $form_state): array {
if ($input) {
$input['route_parameters'] = static::paramStringToArray($input['route_parameters']);
}
else {
$input = $element['#action_link'] ?? [
'route_name' => '',
'route_parameters' => [],
'link_title' => ''
];
}
return $input;
}
/**
* Converts a route parameter string into an array.
*
* @param string|null $param_string
* The route parameter string.
*
* @return array
* The array representation.
*/
private static function paramStringToArray(string $param_string = NULL) : array {
$params = [];
$params_to_process = !empty($param_string) ? explode(',', $param_string) : [];
foreach ($params_to_process as $param) {
[$value1, $value2] = explode('=', $param, 2);
if (!empty($value2)) {
$params[$value1] = $value2;
}
else {
$params[] = $value1;
}
}
return $params;
}
/**
* Converts an array of route parameters into a string.
*
* @param array $params
* The array of route parameters.
*
* @return string
* The string representation.
*/
private static function paramArrayToString(array $params = []) : string {
$strings = [];
foreach ($params as $key => $param) {
$strings[] = "$key=$param";
}
return implode(', ', $strings);
}
/**
* Gets the access manager for route checking.
*
* @return \Drupal\Core\Access\AccessManagerInterface
*/
private static function getAccessManager(): AccessManagerInterface {
return \Drupal::service('access_manager');
}
/**
* Gets the route provider.
*
* @return \Drupal\Core\Routing\RouteProviderInterface
*/
private static function getRouteProvider(): RouteProviderInterface {
return \Drupal::service('router.route_provider');
}
/**
* Gets the param convertor.
*
* @return \Drupal\Core\ParamConverter\ParamConverterManagerInterface
*/
private static function getParamConverter(): ParamConverterManagerInterface{
return \Drupal::service('paramconverter_manager');
}
/**
* The current user.
*
* @return \Drupal\Core\Session\AccountProxyInterface
*/
private static function getCurrentUser(): AccountProxyInterface {
return \Drupal::currentUser();
}
/**
* Gets the private key.
*
* @return string
* The private key.
*/
private static function getPrivateKey(): string {
return \Drupal::service('private_key')->get();
}
}
