4 * Contains \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions.
7 namespace Drupal\bootstrap\Plugin\Alter;
9 use Drupal\bootstrap\Annotation\BootstrapAlter;
10 use Drupal\bootstrap\Bootstrap;
11 use Drupal\bootstrap\Plugin\PluginBase;
12 use Drupal\bootstrap\Utility\Unicode;
13 use Drupal\bootstrap\Utility\Variables;
14 use Drupal\Core\Entity\EntityInterface;
17 * Implements hook_theme_suggestions_alter().
19 * @ingroup plugins_alter
21 * @BootstrapAlter("theme_suggestions")
23 class ThemeSuggestions extends PluginBase implements AlterInterface {
28 protected $bootstrapPanelTypes = ['details', 'fieldset'];
31 * An element object provided in the variables array, may not be set.
33 * @var \Drupal\bootstrap\Utility\Element|false
38 * The theme hook invoked.
45 * The theme hook suggestions, exploded by the "__" delimiter.
49 protected $hookSuggestions;
52 * The types of elements to ignore for the "input__form_control" suggestion.
56 protected $ignoreFormControlTypes = ['checkbox', 'hidden', 'radio'];
59 * The original "hook" value passed via hook_theme_suggestions_alter().
63 protected $originalHook;
66 * The array of suggestions to return.
70 protected $suggestions;
73 * The variables array object passed via hook_theme_suggestions_alter().
75 * @var \Drupal\bootstrap\Utility\Variables
82 public function alter(&$suggestions, &$variables = [], &$hook = NULL) {
83 // This is intentionally backwards. The "original" theme hook is actually
84 // the hook being invoked. The provided $hook (to the alter) is the watered
85 // down version of said original hook.
86 $this->hook = !empty($variables['theme_hook_original']) ? $variables['theme_hook_original'] : $hook;
87 $this->hookSuggestions = explode('__', $this->hook);
88 $this->originalHook = $hook;
89 $this->suggestions = $suggestions;
90 $this->variables = Variables::create($variables);
91 $this->element = $this->variables->element;
93 // Processes the necessary theme hook suggestions.
94 $this->processSuggestions();
96 // Ensure the list of suggestions is unique.
97 $suggestions = array_unique($this->suggestions);
100 /***************************************************************************
101 * Dynamic alter methods.
102 ***************************************************************************/
105 * Dynamic alter method for "input".
107 protected function alterInput() {
108 if ($this->element && $this->element->isButton()) {
109 $hook = 'input__button';
110 if ($this->element->getProperty('split')) {
113 $this->addSuggestion($hook);
115 elseif ($this->element && !$this->element->isType($this->ignoreFormControlTypes)) {
116 $this->addSuggestion('input__form_control');
121 * Dynamic alter method for "links__dropbutton".
123 protected function alterLinksDropbutton() {
124 // Remove the 'dropbutton' suggestion.
125 array_shift($this->hookSuggestions);
127 $this->addSuggestion('bootstrap_dropdown');
131 * Dynamic alter method for "user".
133 * @see https://www.drupal.org/node/2828634
134 * @see https://www.drupal.org/node/2808481
135 * @todo Remove/refactor once core issue is resolved.
137 protected function alterUser() {
138 $this->addSuggestionsForEntity('user');
141 /***************************************************************************
143 ***************************************************************************/
146 * Add a suggestion to the list of suggestions.
148 * @param string $hook
149 * The theme hook suggestion to add.
151 protected function addSuggestion($hook) {
152 $suggestions = $this->buildSuggestions($hook);
153 foreach ($suggestions as $suggestion) {
154 $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 * @see https://www.drupal.org/node/2808481
166 * @param string $entity_type
167 * Optional. A specific type of entity to look for.
168 * @param string $prefix
169 * Optional. A prefix (like "entity") to use. It will automatically be
170 * appended with the "__" separator.
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();
183 // Only add the entity type identifier if there's a prefix.
184 if (!empty($prefix)) {
186 $suggestions[] = $prefix . '__' . $entity_type_id;
190 if ($view_mode = preg_replace('/[^A-Za-z0-9]+/', '_', $this->element->getProperty('view_mode'))) {
191 $suggestions[] = $prefix . $entity_type_id . '__' . $view_mode;
194 if ($entity->getEntityType()->hasKey('bundle')) {
195 $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle();
196 $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle() . '__' . $view_mode;
203 * Builds a list of suggestions.
205 * @param string $hook
206 * The theme hook suggestion to build.
210 protected function buildSuggestions($hook) {
213 $hook_suggestions = $this->hookSuggestions;
215 // Replace the first hook suggestion with $hook.
216 array_shift($hook_suggestions);
217 array_unshift($suggestions, $hook);
220 while ($hook_suggestions) {
221 $suggestions[] = $hook . '__' . implode('__', $hook_suggestions);
222 array_pop($hook_suggestions);
225 // Append the base hook.
226 $suggestions[] = $hook;
228 // Return the suggestions, reversed.
229 return array_reverse($suggestions);
233 * Retrieves the methods to invoke to process the theme hook suggestion.
236 * An indexed array of methods to be invoked.
238 protected function getAlterMethods() {
239 // Retrieve cached theme hook suggestion alter methods.
240 $cache = $this->theme->getCache('theme_hook_suggestions');
241 if ($cache->has($this->hook)) {
242 return $cache->get($this->hook);
245 // Uppercase each theme hook suggestion to be used in the method name.
246 $hook_suggestions = $this->hookSuggestions;
247 foreach ($hook_suggestions as $key => $suggestion) {
248 $hook_suggestions[$key] = Unicode::ucfirst($suggestion);
252 while ($hook_suggestions) {
253 $method = 'alter' . implode('', $hook_suggestions);
254 if (method_exists($this, $method)) {
255 $methods[] = $method;
257 array_pop($hook_suggestions);
260 // Reverse the methods.
261 $methods = array_reverse($methods);
263 // Cache the methods.
264 $cache->set($this->hook, $methods);
270 * Extracts the entity from the element(s) passed in the Variables object.
272 * @param string $entity_type
273 * Optional. The entity type to attempt to retrieve.
275 * @return \Drupal\Core\Entity\EntityInterface|null
276 * The extracted entity, NULL if entity could not be found.
278 protected function getEntityObject($entity_type = 'entity') {
279 // Immediately return if there is no element.
280 if (!$this->element) {
284 // Attempt to retrieve the provided element type.
285 $entity = $this->element->getProperty($entity_type);
287 // If the provided entity type doesn't exist, check to see if a generic
288 // "entity" property was used instead.
289 if ($entity_type !== 'entity' && (!$entity || !($entity instanceof EntityInterface))) {
290 $entity = $this->element->getProperty('entity');
293 // Only return the entity if it's the proper object.
294 return $entity instanceof EntityInterface ? $entity : NULL;
298 * Processes the necessary theme hook suggestions.
300 protected function processSuggestions() {
301 // Add special hook suggestions for Bootstrap panels.
302 if ((in_array($this->originalHook, $this->bootstrapPanelTypes)) && $this->element && $this->element->getProperty('bootstrap_panel', TRUE)) {
303 $this->addSuggestion('bootstrap_panel');
306 // Retrieve any dynamic alter methods.
307 $methods = $this->getAlterMethods();
308 foreach ($methods as $method) {
313 /***************************************************************************
314 * Deprecated methods (DO NOT USE).
315 ***************************************************************************/
318 * Adds "bundle" and "view mode" suggestions for an entity.
320 * @param array $suggestions
321 * The suggestions array, this is ignored.
322 * @param \Drupal\bootstrap\Utility\Variables $variables
323 * The variables object, this is ignored.
324 * @param string $entity_type
325 * Optional. A specific type of entity to look for.
326 * @param string $prefix
327 * Optional. A prefix (like "entity") to use. It will automatically be
328 * appended with the "__" separator.
330 * @deprecated Since 8.x-3.2. Will be removed in a future release.
332 * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::addSuggestionsForEntity
334 public function addEntitySuggestions(array &$suggestions, Variables $variables, $entity_type = 'entity', $prefix = '') {
335 Bootstrap::deprecated();
336 $this->addSuggestionsForEntity($entity_type, $prefix);
340 * Extracts the entity from the element(s) passed in the Variables object.
342 * @param \Drupal\bootstrap\Utility\Variables $variables
343 * The Variables object, this is ignored.
344 * @param string $entity_type
345 * Optional. The entity type to attempt to retrieve.
347 * @return \Drupal\Core\Entity\EntityInterface|null
348 * The extracted entity, NULL if entity could not be found.
350 * @deprecated Since 8.x-3.2. Will be removed in a future release.
352 * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::getEntityObject
354 public function getEntity(Variables $variables, $entity_type = 'entity') {
355 Bootstrap::deprecated();
356 return $this->getEntityObject($entity_type);