ce29ba64bf6c19620e02428b7ba37e07a4910c4a
[yaffs-website] / src / Plugin / Alter / ThemeSuggestions.php
1 <?php
2 /**
3  * @file
4  * Contains \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions.
5  */
6
7 namespace Drupal\bootstrap\Plugin\Alter;
8
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;
15
16 /**
17  * Implements hook_theme_suggestions_alter().
18  *
19  * @ingroup plugins_alter
20  *
21  * @BootstrapAlter("theme_suggestions")
22  */
23 class ThemeSuggestions extends PluginBase implements AlterInterface {
24
25   /**
26    * @var array
27    */
28   protected $bootstrapPanelTypes = ['details', 'fieldset'];
29
30   /**
31    * An element object provided in the variables array, may not be set.
32    *
33    * @var \Drupal\bootstrap\Utility\Element|false
34    */
35   protected $element;
36
37   /**
38    * The theme hook invoked.
39    *
40    * @var string
41    */
42   protected $hook;
43
44   /**
45    * The theme hook suggestions, exploded by the "__" delimiter.
46    *
47    * @var array
48    */
49   protected $hookSuggestions;
50
51   /**
52    * The types of elements to ignore for the "input__form_control" suggestion.
53    *
54    * @var array
55    */
56   protected $ignoreFormControlTypes = ['checkbox', 'hidden', 'radio'];
57
58   /**
59    * The original "hook" value passed via hook_theme_suggestions_alter().
60    *
61    * @var string
62    */
63   protected $originalHook;
64
65   /**
66    * The array of suggestions to return.
67    *
68    * @var array
69    */
70   protected $suggestions;
71
72   /**
73    * The variables array object passed via hook_theme_suggestions_alter().
74    *
75    * @var \Drupal\bootstrap\Utility\Variables
76    */
77   protected $variables;
78
79   /**
80    * {@inheritdoc}
81    */
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;
92
93     // Processes the necessary theme hook suggestions.
94     $this->processSuggestions();
95
96     // Ensure the list of suggestions is unique.
97     $suggestions = array_unique($this->suggestions);
98   }
99
100   /***************************************************************************
101    * Dynamic alter methods.
102    ***************************************************************************/
103
104   /**
105    * Dynamic alter method for "input".
106    */
107   protected function alterInput() {
108     if ($this->element && $this->element->isButton()) {
109       $hook = 'input__button';
110       if ($this->element->getProperty('split')) {
111         $hook .= '__split';
112       }
113       $this->addSuggestion($hook);
114     }
115     elseif ($this->element && !$this->element->isType($this->ignoreFormControlTypes)) {
116       $this->addSuggestion('input__form_control');
117     }
118   }
119
120   /**
121    * Dynamic alter method for "links__dropbutton".
122    */
123   protected function alterLinksDropbutton() {
124     // Remove the 'dropbutton' suggestion.
125     array_shift($this->hookSuggestions);
126
127     $this->addSuggestion('bootstrap_dropdown');
128   }
129
130   /**
131    * Dynamic alter method for "user".
132    *
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.
136    */
137   protected function alterUser() {
138     $this->addSuggestionsForEntity('user');
139   }
140
141   /***************************************************************************
142    * Protected methods.
143    ***************************************************************************/
144
145   /**
146    * Add a suggestion to the list of suggestions.
147    *
148    * @param string $hook
149    *   The theme hook suggestion to add.
150    */
151   protected function addSuggestion($hook) {
152     $suggestions = $this->buildSuggestions($hook);
153     foreach ($suggestions as $suggestion) {
154       $this->suggestions[] = $suggestion;
155     }
156   }
157
158   /**
159    * Adds "bundle" and "view mode" suggestions for an entity.
160    *
161    * This is a helper method because core's implementation of theme hook
162    * suggestions on entities is inconsistent.
163    *
164    * @see https://www.drupal.org/node/2808481
165    *
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.
171    *
172    * @todo Remove/refactor once core issue is resolved.
173    */
174   protected function addSuggestionsForEntity($entity_type = 'entity', $prefix = '') {
175     // Immediately return if there is no element.
176     if (!$this->element) {
177       return;
178     }
179
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)) {
185         $prefix .= '__';
186         $suggestions[] = $prefix . '__' . $entity_type_id;
187       }
188
189       // View mode.
190       if ($view_mode = preg_replace('/[^A-Za-z0-9]+/', '_', $this->element->getProperty('view_mode'))) {
191         $suggestions[] = $prefix . $entity_type_id . '__' . $view_mode;
192
193         // Bundle.
194         if ($entity->getEntityType()->hasKey('bundle')) {
195           $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle();
196           $suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle() . '__' . $view_mode;
197         }
198       }
199     }
200   }
201
202   /**
203    * Builds a list of suggestions.
204    *
205    * @param string $hook
206    *   The theme hook suggestion to build.
207    *
208    * @return array
209    */
210   protected function buildSuggestions($hook) {
211     $suggestions = [];
212
213     $hook_suggestions = $this->hookSuggestions;
214
215     // Replace the first hook suggestion with $hook.
216     array_shift($hook_suggestions);
217     array_unshift($suggestions, $hook);
218
219     $suggestions = [];
220     while ($hook_suggestions) {
221       $suggestions[] = $hook . '__' . implode('__', $hook_suggestions);
222       array_pop($hook_suggestions);
223     }
224
225     // Append the base hook.
226     $suggestions[] = $hook;
227
228     // Return the suggestions, reversed.
229     return array_reverse($suggestions);
230   }
231
232   /**
233    * Retrieves the methods to invoke to process the theme hook suggestion.
234    *
235    * @return array
236    *   An indexed array of methods to be invoked.
237    */
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);
243     }
244
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);
249     }
250
251     $methods = [];
252     while ($hook_suggestions) {
253       $method = 'alter' . implode('', $hook_suggestions);
254       if (method_exists($this, $method)) {
255         $methods[] = $method;
256       }
257       array_pop($hook_suggestions);
258     }
259
260     // Reverse the methods.
261     $methods = array_reverse($methods);
262
263     // Cache the methods.
264     $cache->set($this->hook, $methods);
265
266     return $methods;
267   }
268
269   /**
270    * Extracts the entity from the element(s) passed in the Variables object.
271    *
272    * @param string $entity_type
273    *   Optional. The entity type to attempt to retrieve.
274    *
275    * @return \Drupal\Core\Entity\EntityInterface|null
276    *   The extracted entity, NULL if entity could not be found.
277    */
278   protected function getEntityObject($entity_type = 'entity') {
279     // Immediately return if there is no element.
280     if (!$this->element) {
281       return NULL;
282     }
283
284     // Attempt to retrieve the provided element type.
285     $entity = $this->element->getProperty($entity_type);
286
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');
291     }
292
293     // Only return the entity if it's the proper object.
294     return $entity instanceof EntityInterface ? $entity : NULL;
295   }
296
297   /**
298    * Processes the necessary theme hook suggestions.
299    */
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');
304     }
305
306     // Retrieve any dynamic alter methods.
307     $methods = $this->getAlterMethods();
308     foreach ($methods as $method) {
309       $this->$method();
310     }
311   }
312
313   /***************************************************************************
314    * Deprecated methods (DO NOT USE).
315    ***************************************************************************/
316
317   /**
318    * Adds "bundle" and "view mode" suggestions for an entity.
319    *
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.
329    *
330    * @deprecated Since 8.x-3.2. Will be removed in a future release.
331    *
332    * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::addSuggestionsForEntity
333    */
334   public function addEntitySuggestions(array &$suggestions, Variables $variables, $entity_type = 'entity', $prefix = '') {
335     Bootstrap::deprecated();
336     $this->addSuggestionsForEntity($entity_type, $prefix);
337   }
338
339   /**
340    * Extracts the entity from the element(s) passed in the Variables object.
341    *
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.
346    *
347    * @return \Drupal\Core\Entity\EntityInterface|null
348    *   The extracted entity, NULL if entity could not be found.
349    *
350    * @deprecated Since 8.x-3.2. Will be removed in a future release.
351    *
352    * @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::getEntityObject
353    */
354   public function getEntity(Variables $variables, $entity_type = 'entity') {
355     Bootstrap::deprecated();
356     return $this->getEntityObject($entity_type);
357   }
358
359 }