Completed
Pull Request — master (#6354)
by Will
08:07
created

ModelAdmin::getEditForm()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 51
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 36
nc 4
nop 2
dl 0
loc 51
rs 9.4109
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\HTTPResponse;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Dev\BulkLoader;
10
use SilverStripe\Forms\FieldList;
11
use SilverStripe\Forms\Form;
12
use SilverStripe\Forms\FormAction;
13
use SilverStripe\Forms\GridField\GridFieldDetailForm;
14
use SilverStripe\Forms\ResetFormAction;
15
use SilverStripe\Forms\RequiredFields;
16
use SilverStripe\Forms\HiddenField;
17
use SilverStripe\Forms\FileField;
18
use SilverStripe\Forms\LiteralField;
19
use SilverStripe\Forms\CheckboxField;
20
use SilverStripe\Forms\DatetimeField;
21
use SilverStripe\Forms\CompositeField;
22
use SilverStripe\Forms\GridField\GridFieldImportButton;
23
use SilverStripe\Forms\GridField\GridFieldExportButton;
24
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
25
use SilverStripe\Forms\GridField\GridFieldPrintButton;
26
use SilverStripe\Forms\GridField\GridField;
27
use SilverStripe\ORM\ArrayLib;
28
use SilverStripe\ORM\ArrayList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\Search\SearchContext;
31
use SilverStripe\ORM\SS_List;
32
use SilverStripe\Security\Member;
33
use SilverStripe\View\Requirements;
34
use SilverStripe\View\ArrayData;
35
36
/**
37
 * Generates a three-pane UI for editing model classes, with an
38
 * automatically generated search panel, tabular results and edit forms.
39
 *
40
 * Relies on data such as {@link DataObject::$db} and {@link DataObject::getCMSFields()}
41
 * to scaffold interfaces "out of the box", while at the same time providing
42
 * flexibility to customize the default output.
43
 *
44
 * @uses SearchContext
45
 */
46
abstract class ModelAdmin extends LeftAndMain {
47
48
	private static $url_rule = '/$ModelClass/$Action';
49
50
	/**
51
	 * List of all managed {@link DataObject}s in this interface.
52
	 *
53
	 * Simple notation with class names only:
54
	 * <code>
55
	 * array('MyObjectClass','MyOtherObjectClass')
56
	 * </code>
57
	 *
58
	 * Extended notation with options (e.g. custom titles):
59
	 * <code>
60
	 * array(
61
	 *   'MyObjectClass' => array('title' => "Custom title")
62
	 * )
63
	 * </code>
64
	 *
65
	 * Available options:
66
	 * - 'title': Set custom titles for the tabs or dropdown names
67
	 *
68
	 * @config
69
	 * @var array|string
70
	 */
71
	private static $managed_models = null;
72
73
	/**
74
	 * Override menu_priority so that ModelAdmin CMSMenu objects
75
	 * are grouped together directly above the Help menu item.
76
	 * @var float
77
	 */
78
	private static $menu_priority = -0.5;
79
80
	private static $menu_icon = 'framework/admin/client/src/sprites/menu-icons/16x16/db.png';
81
82
	private static $allowed_actions = array(
83
		'ImportForm',
84
		'SearchForm',
85
	);
86
87
	private static $url_handlers = array(
88
		'$ModelClass/$Action' => 'handleAction'
89
	);
90
91
	/**
92
	 * @var String
93
	 */
94
	protected $modelClass;
95
96
	/**
97
	 * Change this variable if you don't want the Import from CSV form to appear.
98
	 * This variable can be a boolean or an array.
99
	 * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo')
100
	 */
101
	public $showImportForm = true;
102
103
	/**
104
	 * Change this variable if you don't want the search form to appear.
105
	 * This variable can be a boolean or an array.
106
	 * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo')
107
	 */
108
	public $showSearchForm = true;
109
110
	/**
111
	 * List of all {@link DataObject}s which can be imported through
112
	 * a subclass of {@link BulkLoader} (mostly CSV data).
113
	 * By default {@link CsvBulkLoader} is used, assuming a standard mapping
114
	 * of column names to {@link DataObject} properties/relations.
115
	 *
116
	 * e.g. "BlogEntry" => "BlogEntryCsvBulkLoader"
117
	 *
118
	 * @config
119
	 * @var array
120
	 */
121
	private static $model_importers = null;
122
123
	/**
124
	 * Amount of results showing on a single page.
125
	 *
126
	 * @config
127
	 * @var int
128
	 */
129
	private static $page_length = 30;
130
131
	/**
132
	 * Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins.
133
	 */
134
	protected function init() {
135
		parent::init();
136
137
		$models = $this->getManagedModels();
138
139
		if($this->getRequest()->param('ModelClass')) {
140
			$this->modelClass = $this->unsanitiseClassName($this->getRequest()->param('ModelClass'));
141
		} else {
142
			reset($models);
143
			$this->modelClass = key($models);
144
		}
145
146
		// security check for valid models
147
		if(!array_key_exists($this->modelClass, $models)) {
148
			user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR);
149
		}
150
	}
151
152
	public function Link($action = null) {
153
		if(!$action) $action = $this->sanitiseClassName($this->modelClass);
154
		return parent::Link($action);
155
	}
156
157
	public function getEditForm($id = null, $fields = null) {
158
		$list = $this->getList();
159
		$exportButton = new GridFieldExportButton('buttons-before-left');
160
		$exportButton->setExportColumns($this->getExportFields());
161
		$listField = GridField::create(
162
			$this->sanitiseClassName($this->modelClass),
163
			false,
164
			$list,
165
			$fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length'))
166
				->addComponent($exportButton)
167
				->removeComponentsByType('SilverStripe\\Forms\\GridField\\GridFieldFilterHeader')
168
				->addComponents(new GridFieldPrintButton('buttons-before-left'))
169
		);
170
171
		// Validation
172
		if(singleton($this->modelClass)->hasMethod('getCMSValidator')) {
173
			$detailValidator = singleton($this->modelClass)->getCMSValidator();
174
			/** @var GridFieldDetailForm $detailform */
175
			$detailform = $listField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDetailForm');
176
			$detailform->setValidator($detailValidator);
177
		}
178
179
        if($this->showImportForm) {
180
            $import = CompositeField::create(array(
181
                new LiteralField(
182
                    'ImportForm',
183
                    $this->customise(new ArrayData(array(
184
185
                    )))->renderWith('SilverStripe\\Forms\\GridField\\GridFieldImportButton_Modal')
186
                )
187
            ));
188
189
            $fieldConfig->addComponent(new GridFieldImportButton('buttons-before-left', $import));
190
        }
191
192
		$form = Form::create(
193
			$this,
194
			'EditForm',
195
			new FieldList($listField),
196
			new FieldList()
197
		)->setHTMLID('Form_EditForm');
198
		$form->addExtraClass('cms-edit-form cms-panel-padded center flexbox-area-grow');
199
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
200
		$editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm');
201
		$form->setFormAction($editFormAction);
202
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
203
204
		$this->extend('updateEditForm', $form);
205
206
		return $form;
207
	}
208
209
	/**
210
	 * Define which fields are used in the {@link getEditForm} GridField export.
211
	 * By default, it uses the summary fields from the model definition.
212
	 *
213
	 * @return array
214
	 */
215
	public function getExportFields() {
216
		return singleton($this->modelClass)->summaryFields();
217
	}
218
219
	/**
220
	 * @return SearchContext
221
	 */
222
	public function getSearchContext() {
223
		$context = DataObject::singleton($this->modelClass)->getDefaultSearchContext();
224
225
		// Namespace fields, for easier detection if a search is present
226
		foreach($context->getFields() as $field) {
227
			$field->setName(sprintf('q[%s]', $field->getName()));
228
		}
229
		foreach($context->getFilters() as $filter) {
230
			$filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
231
		}
232
233
		$this->extend('updateSearchContext', $context);
234
235
		return $context;
236
	}
237
238
	/**
239
	 * @return Form|bool
240
	 */
241
	public function SearchForm() {
242
		if(!$this->showSearchForm ||
243
			(is_array($this->showSearchForm) && !in_array($this->modelClass, $this->showSearchForm))
244
		) {
245
			return false;
246
		}
247
		$context = $this->getSearchContext();
248
		/** @skipUpgrade */
249
		$form = new Form($this, "SearchForm",
250
			$context->getSearchFields(),
251
			new FieldList(
252
				FormAction::create('search', _t('MemberTableField.APPLY_FILTER', 'Apply Filter'))
253
					->setUseButtonTag(true)->addExtraClass('btn-primary'),
254
				ResetFormAction::create('clearsearch', _t('ModelAdmin.RESET','Reset'))
255
					->setUseButtonTag(true)->addExtraClass('btn-secondary')
256
			),
257
			new RequiredFields()
258
		);
259
		$form->setFormMethod('get');
260
		$form->setFormAction($this->Link($this->sanitiseClassName($this->modelClass)));
261
		$form->addExtraClass('cms-search-form');
262
		$form->disableSecurityToken();
263
		$form->loadDataFrom($this->getRequest()->getVars());
264
265
		$this->extend('updateSearchForm', $form);
266
267
		return $form;
268
	}
269
270
	public function getList() {
271
		$context = $this->getSearchContext();
272
		$params = $this->getRequest()->requestVar('q');
273
274
		if(is_array($params)) {
275
			$params = ArrayLib::array_map_recursive('trim', $params);
276
277
			// Parse all DateFields to handle user input non ISO 8601 dates
278
			foreach($context->getFields() as $field) {
279
				if($field instanceof DatetimeField && !empty($params[$field->getName()])) {
280
					$params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()]));
281
				}
282
			}
283
		}
284
285
		$list = $context->getResults($params);
286
287
		$this->extend('updateList', $list);
288
289
		return $list;
290
	}
291
292
293
	/**
294
	 * Returns managed models' create, search, and import forms
295
	 * @uses SearchContext
296
	 * @uses SearchFilter
297
	 * @return SS_List of forms
298
	 */
299
	protected function getManagedModelTabs() {
300
		$models = $this->getManagedModels();
301
		$forms  = new ArrayList();
302
303
		foreach($models as $class => $options) {
0 ignored issues
show
Bug introduced by
The expression $models of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
304
			$forms->push(new ArrayData(array (
305
				'Title'     => $options['title'],
306
				'ClassName' => $class,
307
				'Link' => $this->Link($this->sanitiseClassName($class)),
308
				'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link'
309
			)));
310
		}
311
312
		return $forms;
313
	}
314
315
	/**
316
	 * Sanitise a model class' name for inclusion in a link
317
	 *
318
	 * @param string $class
319
	 * @return string
320
	 */
321
	protected function sanitiseClassName($class) {
322
		return str_replace('\\', '-', $class);
323
	}
324
325
	/**
326
	 * Unsanitise a model class' name from a URL param
327
	 *
328
	 * @param string $class
329
	 * @return string
330
	 */
331
	protected function unsanitiseClassName($class) {
332
		return str_replace('-', '\\', $class);
333
	}
334
335
	/**
336
	 * @return array Map of class name to an array of 'title' (see {@link $managed_models})
337
	 */
338
	public function getManagedModels() {
339
		$models = $this->stat('managed_models');
340
		if(is_string($models)) {
341
			$models = array($models);
342
		}
343
		if(!count($models)) {
344
			user_error(
345
				'ModelAdmin::getManagedModels():
346
				You need to specify at least one DataObject subclass in public static $managed_models.
347
				Make sure that this property is defined, and that its visibility is set to "public"',
348
				E_USER_ERROR
349
			);
350
		}
351
352
		// Normalize models to have their model class in array key
353
		foreach($models as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $models of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
354
			if(is_numeric($k)) {
355
				$models[$v] = array('title' => singleton($v)->i18n_plural_name());
356
				unset($models[$k]);
357
			}
358
		}
359
360
		return $models;
361
	}
362
363
	/**
364
	 * Returns all importers defined in {@link self::$model_importers}.
365
	 * If none are defined, we fall back to {@link self::managed_models}
366
	 * with a default {@link CsvBulkLoader} class. In this case the column names of the first row
367
	 * in the CSV file are assumed to have direct mappings to properties on the object.
368
	 *
369
	 * @return array Map of model class names to importer instances
370
	 */
371
	public function getModelImporters() {
372
		$importerClasses = $this->stat('model_importers');
373
374
		// fallback to all defined models if not explicitly defined
375
		if(is_null($importerClasses)) {
376
			$models = $this->getManagedModels();
377
			foreach($models as $modelName => $options) {
0 ignored issues
show
Bug introduced by
The expression $models of type object|integer|double|null|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
378
				$importerClasses[$modelName] = 'SilverStripe\\Dev\\CsvBulkLoader';
379
			}
380
		}
381
382
		$importers = array();
383
		foreach($importerClasses as $modelClass => $importerClass) {
0 ignored issues
show
Bug introduced by
The expression $importerClasses of type object|integer|double|string|array|boolean|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
384
			$importers[$modelClass] = new $importerClass($modelClass);
385
		}
386
387
		return $importers;
388
	}
389
390
	/**
391
	 * Generate a CSV import form for a single {@link DataObject} subclass.
392
	 *
393
	 * @return Form|false
394
	 */
395
	public function ImportForm() {
396
		$modelSNG = singleton($this->modelClass);
397
		$modelName = $modelSNG->i18n_singular_name();
398
		// check if a import form should be generated
399
		if(!$this->showImportForm ||
400
			(is_array($this->showImportForm) && !in_array($this->modelClass, $this->showImportForm))
401
		) {
402
			return false;
403
		}
404
405
		$importers = $this->getModelImporters();
406
		if(!$importers || !isset($importers[$this->modelClass])) return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression $importers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
407
408
		if(!$modelSNG->canCreate(Member::currentUser())) return false;
409
410
		$fields = new FieldList(
411
			new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
412
			new FileField('_CsvFile', false)
413
		);
414
415
		// get HTML specification for each import (column names etc.)
416
		$importerClass = $importers[$this->modelClass];
417
		/** @var BulkLoader $importer */
418
		$importer = new $importerClass($this->modelClass);
419
		$spec = $importer->getImportSpec();
420
		$specFields = new ArrayList();
421
		foreach($spec['fields'] as $name => $desc) {
422
			$specFields->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
423
		}
424
		$specRelations = new ArrayList();
425
		foreach($spec['relations'] as $name => $desc) {
426
			$specRelations->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
427
		}
428
		$specHTML = $this->customise(array(
429
			'ClassName' => $this->sanitiseClassName($this->modelClass),
430
			'ModelName' => Convert::raw2att($modelName),
431
			'Fields' => $specFields,
432
			'Relations' => $specRelations,
433
		))->renderWith($this->getTemplatesWithSuffix('_ImportSpec'));
434
435
		$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
436
		$fields->push(
437
			new CheckboxField('EmptyBeforeImport', _t('ModelAdmin.EMPTYBEFOREIMPORT', 'Replace data'),
438
				false)
439
		);
440
441
		$actions = new FieldList(
442
			FormAction::create('import', _t('ModelAdmin.IMPORT', 'Import from CSV'))
443
				->addExtraClass('btn btn-secondary-outline font-icon-upload')
444
		);
445
446
		$form = new Form(
447
			$this,
448
			"ImportForm",
449
			$fields,
450
			$actions
451
		);
452
		$form->setFormAction(
453
			Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'ImportForm')
454
		);
455
456
		$this->extend('updateImportForm', $form);
457
458
		return $form;
459
	}
460
461
	/**
462
	 * Imports the submitted CSV file based on specifications given in
463
	 * {@link self::model_importers}.
464
	 * Redirects back with a success/failure message.
465
	 *
466
	 * @todo Figure out ajax submission of files via jQuery.form plugin
467
	 *
468
	 * @param array $data
469
	 * @param Form $form
470
	 * @param HTTPRequest $request
471
	 * @return bool|HTTPResponse
472
	 */
473
	public function import($data, $form, $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Coding Style introduced by
import uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
474
		if(!$this->showImportForm || (is_array($this->showImportForm)
475
				&& !in_array($this->modelClass,$this->showImportForm))) {
476
477
			return false;
478
		}
479
480
		$importers = $this->getModelImporters();
481
		/** @var BulkLoader $loader */
482
		$loader = $importers[$this->modelClass];
483
484
		// File wasn't properly uploaded, show a reminder to the user
485
		if(
486
			empty($_FILES['_CsvFile']['tmp_name']) ||
487
			file_get_contents($_FILES['_CsvFile']['tmp_name']) == ''
488
		) {
489
			$form->sessionMessage(_t('ModelAdmin.NOCSVFILE', 'Please browse for a CSV file to import'), 'good');
490
			$this->redirectBack();
491
			return false;
492
		}
493
494
		if (!empty($data['EmptyBeforeImport']) && $data['EmptyBeforeImport']) { //clear database before import
495
			$loader->deleteExistingRecords = true;
496
		}
497
		$results = $loader->load($_FILES['_CsvFile']['tmp_name']);
498
499
		$message = '';
500
		if($results->CreatedCount()) $message .= _t(
501
			'ModelAdmin.IMPORTEDRECORDS', "Imported {count} records.",
502
			array('count' => $results->CreatedCount())
503
		);
504
		if($results->UpdatedCount()) $message .= _t(
505
			'ModelAdmin.UPDATEDRECORDS', "Updated {count} records.",
506
			array('count' => $results->UpdatedCount())
507
		);
508
		if($results->DeletedCount()) $message .= _t(
509
			'ModelAdmin.DELETEDRECORDS', "Deleted {count} records.",
510
			array('count' => $results->DeletedCount())
511
		);
512
		if(!$results->CreatedCount() && !$results->UpdatedCount()) {
513
			$message .= _t('ModelAdmin.NOIMPORT', "Nothing to import");
514
		}
515
516
		$form->sessionMessage($message, 'good');
517
		return $this->redirectBack();
518
	}
519
520
	/**
521
	 * @param bool $unlinked
522
	 * @return ArrayList
523
	 */
524
	public function Breadcrumbs($unlinked = false) {
525
		$items = parent::Breadcrumbs($unlinked);
526
527
		// Show the class name rather than ModelAdmin title as root node
528
		$models = $this->getManagedModels();
529
		$params = $this->getRequest()->getVars();
530
		if(isset($params['url'])) unset($params['url']);
531
532
		$items[0]->Title = $models[$this->modelClass]['title'];
533
		$items[0]->Link = Controller::join_links(
534
			$this->Link($this->sanitiseClassName($this->modelClass)),
535
			'?' . http_build_query($params)
536
		);
537
538
		return $items;
539
	}
540
541
}
542