entity.views.inc
26.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
<?php
/**
* @file
* Provide views data for modules making use of the entity CRUD API.
*/
/**
* Implements hook_views_data().
*
* Provides Views integration for entities if they satisfy one of these
* conditions:
* - hook_entity_info() specifies a 'views controller class' key.
* - hook_entity_info() specifies a 'module' key, and the module does not
* implement hook_views_data().
*
* @see entity_crud_hook_entity_info()
* @see entity_views_table_definition()
*/
function entity_views_data() {
$data = array();
foreach (entity_crud_get_info() as $type => $info) {
// Provide default integration with the basic controller class if we know
// the module providing the entity and it does not provide views integration.
if (!isset($info['views controller class'])) {
$info['views controller class'] = isset($info['module']) && !module_hook($info['module'], 'views_data') ? 'EntityDefaultViewsController' : FALSE;
}
if ($info['views controller class']) {
$controller = new $info['views controller class']($type);
// Relationship data may return views data for already existing tables,
// so merge results on the second level.
foreach ($controller->views_data() as $table => $table_data) {
$data += array($table => array());
$data[$table] = array_merge($data[$table], $table_data);
}
}
}
// Add tables based upon data selection "queries" for all entity types.
foreach (entity_get_info() as $type => $info) {
$table = entity_views_table_definition($type);
if ($table) {
$data['entity_' . $type] = $table;
}
// Generally expose properties marked as 'entity views field'.
$data['views_entity_' . $type] = array();
foreach (entity_get_all_property_info($type) as $key => $property) {
if (!empty($property['entity views field'])) {
entity_views_field_definition($key, $property, $data['views_entity_' . $type]);
}
}
}
// Expose generally usable entity-related fields.
foreach (entity_get_info() as $entity_type => $info) {
if (entity_type_supports($entity_type, 'view')) {
// Expose a field allowing to display the rendered entity.
$data['views_entity_' . $entity_type]['rendered_entity'] = array(
'title' => t('Rendered @entity-type', array('@entity-type' => $info['label'])),
'help' => t('The @entity-type of the current relationship rendered using a view mode.', array('@entity-type' => $info['label'])),
'field' => array(
'handler' => 'entity_views_handler_field_entity',
'type' => $entity_type,
// The EntityFieldHandlerHelper treats the 'entity object' data
// selector as special case for loading the base entity.
'real field' => 'entity object',
),
);
}
}
$data['entity__global']['table']['group'] = t('Entity');
$data['entity__global']['table']['join'] = array(
// #global let's it appear all the time.
'#global' => array(),
);
$data['entity__global']['entity'] = array(
'title' => t('Rendered entity'),
'help' => t('Displays a single chosen entity.'),
'area' => array(
'handler' => 'entity_views_handler_area_entity',
),
);
return $data;
}
/**
* Helper function for getting data selection based entity Views table definitions.
*
* This creates extra tables for each entity type that are not associated with a
* query plugin (and thus are not base tables) and just rely on the entities to
* retrieve the displayed data. To obtain the entities corresponding to a
* certain result set, the field handlers defined on the table use a generic
* interface defined for query plugins that are based on entity handling, and
* which is described in the entity_views_example_query class.
*
* These tables are called "data selection tables".
*
* Other modules providing Views integration with new query plugins that are
* based on entities can then use these tables as a base for their own tables
* (by directly using this method and modifying the returned table) and/or by
* specifying relationships to them. The tables returned here already specify
* relationships to each other wherever an entity contains a reference to
* another (e.g., the node author constructs a relationship from nodes to
* users).
*
* As filtering and other query manipulation is potentially more plugin-specific
* than the display, only field handlers and relationships are provided with
* these tables. By providing a add_selector_orderby() method, the query plugin
* can, however, support click-sorting for the field handlers in these tables.
*
* For a detailed discussion see http://drupal.org/node/1266036
*
* For example use see the Search API views module in the Search API project:
* http://drupal.org/project/search_api
*
* @param $type
* The entity type whose table definition should be returned.
* @param $exclude
* Whether properties already exposed as 'entity views field' should be
* excluded. Defaults to TRUE, as they are available for all views tables for
* the entity type anyways.
*
* @return
* An array containing the data selection Views table definition for the
* entity type.
*
* @see entity_views_field_definition()
*/
function entity_views_table_definition($type, $exclude = TRUE) {
// As other modules might want to copy these tables as a base for their own
// Views integration, we statically cache the tables to save some time.
$tables = &drupal_static(__FUNCTION__, array());
if (!isset($tables[$type])) {
// Work-a-round to fix updating, see http://drupal.org/node/1330874.
// Views data might be rebuilt on update.php before the registry is rebuilt,
// thus the class cannot be auto-loaded.
if (!class_exists('EntityFieldHandlerHelper')) {
module_load_include('inc', 'entity', 'views/handlers/entity_views_field_handler_helper');
}
$info = entity_get_info($type);
$tables[$type]['table'] = array(
'group' => $info['label'],
'entity type' => $type,
);
foreach (entity_get_all_property_info($type) as $key => $property) {
if (!$exclude || empty($property['entity views field'])) {
entity_views_field_definition($key, $property, $tables[$type]);
}
}
}
return $tables[$type];
}
/**
* Helper function for adding a Views field definition to data selection based Views tables.
*
* @param $field
* The data selector of the field to add. E.g. "title" would derive the node
* title property, "body:summary" the node body's summary.
* @param array $property_info
* The property information for which to create a field definition.
* @param array $table
* The table into which the definition should be inserted.
* @param $title_prefix
* Internal use only.
*
* @see entity_views_table_definition()
*/
function entity_views_field_definition($field, array $property_info, array &$table, $title_prefix = '') {
$additional = array();
$additional_field = array();
// Create a valid Views field identifier (no colons, etc.). Keep the original
// data selector as real field though.
$key = _entity_views_field_identifier($field, $table);
if ($key != $field) {
$additional['real field'] = $field;
}
$field_name = EntityFieldHandlerHelper::get_selector_field_name($field);
$field_handlers = entity_views_get_field_handlers();
$property_info += entity_property_info_defaults();
$type = entity_property_extract_innermost_type($property_info['type']);
$title = $title_prefix . $property_info['label'];
if ($info = entity_get_info($type)) {
$additional['relationship'] = array(
'handler' => $field_handlers['relationship'],
'base' => 'entity_' . $type,
'base field' => $info['entity keys']['id'],
'relationship field' => $field,
'label' => $title,
);
if ($property_info['type'] != $type) {
// This is a list of entities, so we should mark the relationship as such.
$additional['relationship']['multiple'] = TRUE;
}
// Implementers of the field handlers alter hook could add handlers for
// specific entity types.
if (!isset($field_handlers[$type])) {
$type = 'entity';
}
}
elseif (!empty($property_info['field'])) {
$type = 'field';
// Views' Field API field handler needs some extra definitions to work.
$additional_field['field_name'] = $field_name;
$additional_field['entity_tables'] = array();
$additional_field['entity type'] = $table['table']['entity type'];
$additional_field['is revision'] = FALSE;
}
// Copied from EntityMetadataWrapper::optionsList()
elseif (isset($property_info['options list']) && is_callable($property_info['options list'])) {
// If this is a nested property, we need to get rid of all prefixes first.
$type = 'options';
$additional_field['options callback'] = array(
'function' => $property_info['options list'],
'info' => $property_info,
);
}
elseif ($type == 'decimal') {
$additional_field['float'] = TRUE;
}
if (isset($field_handlers[$type])) {
$table += array($key => array());
$table[$key] += array(
'title' => $title,
'help' => empty($property_info['description']) ? t('(No information available)') : $property_info['description'],
'field' => array(),
);
$table[$key]['field'] += array(
'handler' => $field_handlers[$type],
'type' => $property_info['type'],
);
$table[$key] += $additional;
$table[$key]['field'] += $additional_field;
}
if (!empty($property_info['property info'])) {
foreach ($property_info['property info'] as $nested_key => $nested_property) {
entity_views_field_definition($field . ':' . $nested_key, $nested_property, $table, $title . ' » ');
}
}
}
/**
* @return array
* The handlers to use for the data selection based Views tables.
*
* @see hook_entity_views_field_handlers_alter()
*/
function entity_views_get_field_handlers() {
$field_handlers = drupal_static(__FUNCTION__);
if (!isset($field_handlers)) {
// Field handlers for the entity tables, by type.
$field_handlers = array(
'text' => 'entity_views_handler_field_text',
'token' => 'entity_views_handler_field_text',
'integer' => 'entity_views_handler_field_numeric',
'decimal' => 'entity_views_handler_field_numeric',
'date' => 'entity_views_handler_field_date',
'duration' => 'entity_views_handler_field_duration',
'boolean' => 'entity_views_handler_field_boolean',
'uri' => 'entity_views_handler_field_uri',
'options' => 'entity_views_handler_field_options',
'field' => 'entity_views_handler_field_field',
'entity' => 'entity_views_handler_field_entity',
'relationship' => 'entity_views_handler_relationship',
);
drupal_alter('entity_views_field_handlers', $field_handlers);
}
return $field_handlers;
}
/**
* Helper function for creating valid Views field identifiers out of data selectors.
*
* Uses $table to test whether the identifier is already used, and also
* recognizes if a definition for the same field is already present and returns
* that definition's identifier.
*
* @return string
* A valid Views field identifier that is not yet used as a key in $table.
*/
function _entity_views_field_identifier($field, array $table) {
$key = $base = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field);
$i = 0;
// The condition checks whether this sanitized field identifier is already
// used for another field in this table (and whether the identifier is
// "table", which can never be used).
// If $table[$key] is set, the identifier is already used, but this might be
// already for the same field. To test that, we need the original field name,
// which is either $table[$key]['real field'], if set, or $key. If this
// original field name is equal to $field, we can use that key. Otherwise, we
// append numeric suffixes until we reach an unused key.
while ($key == 'table' || (isset($table[$key]) && (isset($table[$key]['real field']) ? $table[$key]['real field'] : $key) != $field)) {
$key = $base . '_' . ++$i;
}
return $key;
}
/**
* Implements hook_views_plugins().
*/
function entity_views_plugins() {
// Have views cache the table list for us so it gets
// cleared at the appropriate times.
$data = views_cache_get('entity_base_tables', TRUE);
if (!empty($data->data)) {
$base_tables = $data->data;
}
else {
$base_tables = array();
foreach (views_fetch_data() as $table => $data) {
if (!empty($data['table']['entity type']) && !empty($data['table']['base'])) {
$base_tables[] = $table;
}
}
views_cache_set('entity_base_tables', $base_tables, TRUE);
}
if (!empty($base_tables)) {
return array(
'module' => 'entity',
'row' => array(
'entity' => array(
'title' => t('Rendered entity'),
'help' => t('Renders a single entity in a specific view mode (e.g. teaser).'),
'handler' => 'entity_views_plugin_row_entity_view',
'uses fields' => FALSE,
'uses options' => TRUE,
'type' => 'normal',
'base' => $base_tables,
),
),
);
}
}
/**
* Default controller for generating basic views integration.
*
* The controller tries to generate suiting views integration for the entity
* based upon the schema information of its base table and the provided entity
* property information.
* For that it is possible to map a property name to its schema/views field
* name by adding a 'schema field' key with the name of the field as value to
* the property info.
*/
class EntityDefaultViewsController {
protected $type, $info, $relationships;
public function __construct($type) {
$this->type = $type;
$this->info = entity_get_info($type);
}
/**
* Defines the result for hook_views_data().
*/
public function views_data() {
$data = array();
$this->relationships = array();
if (!empty($this->info['base table'])) {
$table = $this->info['base table'];
// Define the base group of this table. Fields that don't
// have a group defined will go into this field by default.
$data[$table]['table']['group'] = drupal_ucfirst($this->info['label']);
$data[$table]['table']['entity type'] = $this->type;
// If the plural label isn't available, use the regular label.
$label = isset($this->info['plural label']) ? $this->info['plural label'] : $this->info['label'];
$data[$table]['table']['base'] = array(
'field' => $this->info['entity keys']['id'],
'access query tag' => $this->type . '_access',
'title' => drupal_ucfirst($label),
'help' => isset($this->info['description']) ? $this->info['description'] : '',
);
$data[$table]['table']['entity type'] = $this->type;
$data[$table] += $this->schema_fields();
// Add in any reverse-relationships which have been determined.
$data += $this->relationships;
}
if (!empty($this->info['revision table']) && !empty($this->info['entity keys']['revision'])) {
$revision_table = $this->info['revision table'];
$data[$table]['table']['default_relationship'] = array(
$revision_table => array(
'table' => $revision_table,
'field' => $this->info['entity keys']['revision'],
),
);
// Define the base group of this table. Fields that don't
// have a group defined will go into this field by default.
$data[$revision_table]['table']['group'] = drupal_ucfirst($this->info['label']) . ' ' . t('Revisions');
$data[$revision_table]['table']['entity type'] = $this->type;
// If the plural label isn't available, use the regular label.
$label = isset($this->info['plural label']) ? $this->info['plural label'] : $this->info['label'];
$data[$revision_table]['table']['base'] = array(
'field' => $this->info['entity keys']['revision'],
'access query tag' => $this->type . '_access',
'title' => drupal_ucfirst($label) . ' ' . t('Revisions'),
'help' => (isset($this->info['description']) ? $this->info['description'] . ' ' : '') . t('Revisions'),
);
$data[$revision_table]['table']['entity type'] = $this->type;
$data[$revision_table] += $this->schema_revision_fields();
// Add in any reverse-relationships which have been determined.
$data += $this->relationships;
// For other base tables, explain how we join.
$data[$revision_table]['table']['join'] = array(
// Directly links to base table.
$table => array(
'left_field' => $this->info['entity keys']['revision'],
'field' => $this->info['entity keys']['revision'],
),
);
$data[$revision_table]['table']['default_relationship'] = array(
$table => array(
'table' => $table,
'field' => $this->info['entity keys']['id'],
),
);
}
return $data;
}
/**
* Try to come up with some views fields with the help of the schema and
* the entity property information.
*/
protected function schema_fields() {
$schema = drupal_get_schema($this->info['base table']);
$properties = entity_get_property_info($this->type) + array('properties' => array());
$data = array();
foreach ($properties['properties'] as $name => $property_info) {
if (isset($property_info['schema field']) && isset($schema['fields'][$property_info['schema field']])) {
if ($views_info = $this->map_from_schema_info($name, $schema['fields'][$property_info['schema field']], $property_info)) {
$data[$name] = $views_info;
}
}
}
return $data;
}
/**
* Try to come up with some views fields with the help of the revision schema
* and the entity property information.
*/
protected function schema_revision_fields() {
$data = array();
if (!empty($this->info['revision table'])) {
$schema = drupal_get_schema($this->info['revision table']);
$properties = entity_get_property_info($this->type) + array('properties' => array());
foreach ($properties['properties'] as $name => $property_info) {
if (isset($property_info['schema field']) && isset($schema['fields'][$property_info['schema field']])) {
if ($views_info = $this->map_from_schema_info($name, $schema['fields'][$property_info['schema field']], $property_info)) {
$data[$name] = $views_info;
}
}
}
}
return $data;
}
/**
* Comes up with views information based on the given schema and property
* info.
*/
protected function map_from_schema_info($property_name, $schema_field_info, $property_info) {
$type = isset($property_info['type']) ? $property_info['type'] : 'text';
$views_field_name = $property_info['schema field'];
$return = array();
if (!empty($schema_field_info['serialize'])) {
return FALSE;
}
$description = array(
'title' => $property_info['label'],
'help' => isset($property_info['description']) ? $property_info['description'] : NULL,
);
// Add in relationships to related entities.
if (($info = entity_get_info($type)) && !empty($info['base table'])) {
// Prepare reversed relationship data.
$label_lowercase = drupal_strtolower($this->info['label'][0]) . drupal_substr($this->info['label'], 1);
$property_label_lowercase = drupal_strtolower($property_info['label'][0]) . drupal_substr($property_info['label'], 1);
// We name the field of the first reverse-relationship just with the
// base table to be backward compatible, for subsequents relationships we
// append the views field name in order to get a unique name.
$name = !isset($this->relationships[$info['base table']][$this->info['base table']]) ? $this->info['base table'] : $this->info['base table'] . '_' . $views_field_name;
$this->relationships[$info['base table']][$name] = array(
'title' => $this->info['label'],
'help' => t("Associated @label via the @label's @property.", array('@label' => $label_lowercase, '@property' => $property_label_lowercase)),
'relationship' => array(
'label' => $this->info['label'],
'handler' => $this->getRelationshipHandlerClass($this->type, $type),
'base' => $this->info['base table'],
'base field' => $views_field_name,
'relationship field' => isset($info['entity keys']['name']) ? $info['entity keys']['name'] : $info['entity keys']['id'],
),
);
$return['relationship'] = array(
'label' => drupal_ucfirst($info['label']),
'handler' => $this->getRelationshipHandlerClass($type, $this->type),
'base' => $info['base table'],
'base field' => isset($info['entity keys']['name']) ? $info['entity keys']['name'] : $info['entity keys']['id'],
'relationship field' => $views_field_name,
);
// Add in direct field/filters/sorts for the id itself too.
$type = isset($info['entity keys']['name']) ? 'token' : 'integer';
// Append the views-field-name to the title if it is different to the
// property name.
if ($property_name != $views_field_name) {
$description['title'] .= ' ' . $views_field_name;
}
}
switch ($type) {
case 'token':
case 'text':
$return += $description + array(
'field' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_field',
'click sortable' => TRUE,
),
'sort' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_sort',
),
'filter' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_filter_string',
),
'argument' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_argument_string',
),
);
break;
case 'decimal':
case 'integer':
$return += $description + array(
'field' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_field_numeric',
'click sortable' => TRUE,
'float' => ($type == 'decimal'),
),
'sort' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_sort',
),
'filter' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_filter_numeric',
),
'argument' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_argument_numeric',
),
);
break;
case 'date':
$return += $description + array(
'field' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_field_date',
'click sortable' => TRUE,
),
'sort' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_sort_date',
),
'filter' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_filter_date',
),
'argument' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_argument_date',
),
);
break;
case 'uri':
$return += $description + array(
'field' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_field_url',
'click sortable' => TRUE,
),
'sort' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_sort',
),
'filter' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_filter_string',
),
'argument' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_argument_string',
),
);
break;
case 'boolean':
$return += $description + array(
'field' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_field_boolean',
'click sortable' => TRUE,
),
'sort' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_sort',
),
'filter' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_filter_boolean_operator',
),
'argument' => array(
'real field' => $views_field_name,
'handler' => 'views_handler_argument_string',
),
);
break;
}
// If there is an options list callback, add to the filter and field.
if (isset($return['filter']) && !empty($property_info['options list'])) {
$return['filter']['handler'] = 'views_handler_filter_in_operator';
$return['filter']['options callback'] = array('EntityDefaultViewsController', 'optionsListCallback');
$return['filter']['options arguments'] = array($this->type, $property_name, 'view');
}
// @todo: This class_exists is needed until views 3.2.
if (isset($return['field']) && !empty($property_info['options list']) && class_exists('views_handler_field_machine_name')) {
$return['field']['handler'] = 'views_handler_field_machine_name';
$return['field']['options callback'] = array('EntityDefaultViewsController', 'optionsListCallback');
$return['field']['options arguments'] = array($this->type, $property_name, 'view');
}
return $return;
}
/**
* Determines the handler to use for a relationship to an entity type.
*
* @param $entity_type
* The entity type to join to.
* @param $left_type
* The data type from which to join.
*/
function getRelationshipHandlerClass($entity_type, $left_type) {
// Look for an entity type which is used as bundle for the given entity
// type. If there is one, allow filtering the relation by bundle by using
// our own handler.
foreach (entity_get_info() as $type => $info) {
// In case we already join from the bundle entity we do not need to filter
// by bundle entity any more, so we stay with the general handler.
if (!empty($info['bundle of']) && $info['bundle of'] == $entity_type && $type != $left_type) {
return 'entity_views_handler_relationship_by_bundle';
}
}
return 'views_handler_relationship';
}
/**
* A callback returning property options, suitable to be used as views options callback.
*/
public static function optionsListCallback($type, $selector, $op = 'view') {
$wrapper = entity_metadata_wrapper($type, NULL);
$parts = explode(':', $selector);
foreach ($parts as $part) {
$wrapper = $wrapper->get($part);
}
return $wrapper->optionsList($op);
}
}