3 namespace Drupal\bootstrap\Plugin\Alter;
5 use Drupal\bootstrap\Bootstrap;
6 use Drupal\bootstrap\Plugin\PluginBase;
7 use Drupal\bootstrap\Utility\Unicode;
8 use Drupal\bootstrap\Utility\Variables;
9 use Drupal\Core\Entity\EntityInterface;
12 * Implements hook_theme_suggestions_alter().
14 * @ingroup plugins_alter
16 * @BootstrapAlter("theme_suggestions")
18 class ThemeSuggestions extends PluginBase implements AlterInterface {
21 * The element types that should be converted into Panel markup.
25 protected $bootstrapPanelTypes = ['details', 'fieldset'];
28 * An element object provided in the variables array, may not be set.
30 * @var \Drupal\bootstrap\Utility\Element|false
35 * The theme hook invoked.
42 * The theme hook suggestions, exploded by the "__" delimiter.
46 protected $hookSuggestions;
49 * The types of elements to ignore for the "input__form_control" suggestion.
53 protected $ignoreFormControlTypes = ['checkbox', 'hidden', 'radio'];
56 * The original "hook" value passed via hook_theme_suggestions_alter().
60 protected $originalHook;
63 * The array of suggestions to return.
67 protected $suggestions;
70 * The variables array object passed via hook_theme_suggestions_alter().
72 * @var \Drupal\bootstrap\Utility\Variables
79 public function alter(&$suggestions, &$variables = [], &$hook = NULL) {
80 // This is intentionally backwards. The "original" theme hook is actually
81 // the hook being invoked. The provided $hook (to the alter) is the watered
82 // down version of said original hook.
83 $this->hook = !empty($variables['theme_hook_original']) ? $variables['theme_hook_original'] : $hook;
84 $this->hookSuggestions = explode('__', $this->hook);
85 $this->originalHook = $hook;
86 $this->suggestions = $suggestions;
87 $this->variables = Variables::create($variables);
88 $this->element = $this->variables->element;
90 // Processes the necessary theme hook suggestions.
91 $this->processSuggestions();
93 // Ensure the list of suggestions is unique.
94 $suggestions = array_unique($this->suggestions);
97 /***************************************************************************
98 * Dynamic alter methods.
99 ***************************************************************************/
102 * Dynamic alter method for "input".
104 protected function alterInput() {
105 if ($this->element && $this->element->isButton()) {
106 $hook = 'input__button';
107 if ($this->element->getProperty('split')) {
110 $this->addSuggestion($hook);
112 elseif ($this->element && !$this->element->isType($this->ignoreFormControlTypes)) {
113 $this->addSuggestion('input__form_control');
118 * Dynamic alter method for "links__dropbutton".
120 protected function alterLinksDropbutton() {
121 // Remove the 'dropbutton' suggestion.
122 array_shift($this->hookSuggestions);
124 $this->addSuggestion('bootstrap_dropdown');
128 * Dynamic alter method for "user".
130 * @see https://www.drupal.org/node/2828634
131 * @see https://www.drupal.org/node/2808481
132 * @todo Remove/refactor once core issue is resolved.
134 protected function alterUser() {
135 $this->addSuggestionsForEntity('user');
138 /***************************************************************************
140 ***************************************************************************/
143 * Adds suggestions based on an array of hooks.
145 * @param string|string[] $hook
146 * A single theme hook suggestion or an array of theme hook suggestions.
148 protected function addSuggestion($hook) {
149 $hooks = (array) $hook;
150 foreach ($hooks as $hook) {
151 $suggestions = $this->buildSuggestions($hook);
152 foreach ($suggestions as $suggestion) {
153 $this->suggestions[] = $suggestion;
159 * Adds "bundle" and "view mode" suggestions for an entity.
161 * This is a helper method because core's implementation of theme hook
162 * suggestions on entities is inconsistent.
164 * @param string $entity_type
165 * Optional. A specific type of entity to look for.
166 * @param string $prefix
167 * Optional. A prefix (like "entity") to use. It will automatically be
168 * appended with the "__" separator.
170 * @see https://www.drupal.org/node/2808481
172 * @todo Remove/refactor once core issue is resolved.
174 protected function addSuggestionsForEntity($entity_type = 'entity', $prefix = '') {
175 // Immediately return if there is no element.
176 if (!$this->element) {
180 // Extract the entity.
181 if ($entity = $this->getEntityObject($entity_type)) {
182 $entity_type_id = $entity->getEntityTypeId();
185 // Only add the entity type identifier if there's a prefix.
186 if (!empty($prefix)) {
188 $suggestions[] = $prefix . '__' . $entity_type_id;
192 if ($view_mode = preg_replace('/[^A-Za-z0-9]+/', '_', $this->element->getProperty('view_mode'))) {
193 $suggestions[] = $prefix . $entity_type_id . '__' . $view_mode;
196 if ($entity->getEntityType()->hasKey('bundle')) {
197 $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle();
198 $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle() . '__' . $view_mode;
204 $this->addSuggestion($suggestions);
210 * Builds a list of suggestions.
212 * @param string $hook
213 * The theme hook suggestion to build.
216 * An list of theme hook suggestions.
218 protected function buildSuggestions($hook) {
221 $hook_suggestions = $this->hookSuggestions;
223 // Replace the first hook suggestion with $hook.
224 array_shift($hook_suggestions);
225 array_unshift($suggestions, $hook);
228 while ($hook_suggestions) {
229 $suggestions[] = $hook . '__' . implode('__', $hook_suggestions);
230 array_pop($hook_suggestions);
233 // Append the base hook.
234 $suggestions[] = $hook;
236 // Return the suggestions, reversed.
237 return array_reverse($suggestions);
241 * Retrieves the methods to invoke to process the theme hook suggestion.
244 * An indexed array of methods to be invoked.
246 protected function getAlterMethods() {
247 // Retrieve cached theme hook suggestion alter methods.
248 $cache = $this->theme->getCache('theme_hook_suggestions');
249 if ($cache->has($this->hook)) {
250 return $cache->get($this->hook);
253 // Convert snake_cased hook suggestions into lowerCamelCase alter methods.
255 $hook_suggestions = array_map('\Drupal\Component\Utility\Unicode::ucfirst', $this->hookSuggestions);
256 while ($hook_suggestions) {
257 // In order to provide backwards compatibility with sub-themes that used
258 // the previous malformed method names, both of the method names need to
260 // @see https://www.drupal.org/project/bootstrap/issues/3008004
261 // @todo Only use the last method name and remove array in 8.x-4.x.
263 'alter' . implode('', $hook_suggestions),
264 'alter' . implode('', array_map('\Drupal\Component\Utility\Unicode::ucfirst', explode('_', implode('', $hook_suggestions)))),
266 foreach (array_unique($methodNames) as $method) {
267 if (method_exists($this, $method)) {
268 $methods[] = $method;
271 array_pop($hook_suggestions);
274 // Reverse the methods.
275 $methods = array_reverse($methods);
277 // Cache the methods.
278 $cache->set($this->hook, $methods);
284 * Extracts the entity from the element(s) passed in the Variables object.
286 * @param string $entity_type
287 * Optional. The entity type to attempt to retrieve.
289 * @return \Drupal\Core\Entity\EntityInterface|null
290 * The extracted entity, NULL if entity could not be found.
292 protected function getEntityObject($entity_type = 'entity') {
293 // Immediately return if there is no element.
294 if (!$this->element) {
298 // Attempt to retrieve the provided element type.
299 $entity = $this->element->getProperty($entity_type);
301 // If the provided entity type doesn't exist, check to see if a generic
302 // "entity" property was used instead.
303 if ($entity_type !== 'entity' && (!$entity || !($entity instanceof EntityInterface))) {
304 $entity = $this->element->getProperty('entity');
307 // Only return the entity if it's the proper object.
308 return $entity instanceof EntityInterface ? $entity : NULL;
312 * Processes the necessary theme hook suggestions.
314 protected function processSuggestions() {
315 // Add special hook suggestions for Bootstrap panels.
316 if ((in_array($this->originalHook, $this->bootstrapPanelTypes)) && $this->element && $this->element->getProperty('bootstrap_panel', TRUE)) {
317 $this->addSuggestion('bootstrap_panel');
320 // Retrieve any dynamic alter methods.
321 $methods = $this->getAlterMethods();
322 foreach ($methods as $method) {
327 /***************************************************************************
328 * Deprecated methods (DO NOT USE).
329 ***************************************************************************/
332 * Adds "bundle" and "view mode" suggestions for an entity.
334 * @param array $suggestions
335 * The suggestions array, this is ignored.
336 * @param \Drupal\bootstrap\Utility\Variables $variables
337 * The variables object, this is ignored.
338 * @param string $entity_type
339 * Optional. A specific type of entity to look for.
340 * @param string $prefix
341 * Optional. A prefix (like "entity") to use. It will automatically be
342 * appended with the "__" separator.
344 * @deprecated Since 8.x-3.2. Will be removed in a future release.
346 * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::addSuggestionsForEntity
348 public function addEntitySuggestions(array &$suggestions, Variables $variables, $entity_type = 'entity', $prefix = '') {
349 Bootstrap::deprecated();
350 $this->addSuggestionsForEntity($entity_type, $prefix);
354 * Extracts the entity from the element(s) passed in the Variables object.
356 * @param \Drupal\bootstrap\Utility\Variables $variables
357 * The Variables object, this is ignored.
358 * @param string $entity_type
359 * Optional. The entity type to attempt to retrieve.
361 * @return \Drupal\Core\Entity\EntityInterface|null
362 * The extracted entity, NULL if entity could not be found.
364 * @deprecated Since 8.x-3.2. Will be removed in a future release.
366 * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::getEntityObject
368 public function getEntity(Variables $variables, $entity_type = 'entity') {
369 Bootstrap::deprecated();
370 return $this->getEntityObject($entity_type);