Completed
Push — master ( bbb282...43d0b8 )
by Daniel
25s
created

ModelAdmin::Link()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Admin;
4
5
6
use SilverStripe\ORM\ArrayList;
7
use SilverStripe\ORM\SS_List;
8
use SilverStripe\Security\Member;
9
use Requirements;
10
use GridFieldExportButton;
11
use GridField;
12
use GridFieldConfig_RecordEditor;
13
use GridFieldPrintButton;
14
use Form;
15
use FieldList;
16
use Controller;
17
use Object;
18
use RequiredFields;
19
use ArrayLib;
20
use ArrayData;
21
use HiddenField;
22
use FileField;
23
use Convert;
24
use LiteralField;
25
use CheckboxField;
26
use FormAction;
27
use Deprecation;
28
29
30
/**
31
 * Generates a three-pane UI for editing model classes, with an
32
 * automatically generated search panel, tabular results and edit forms.
33
 *
34
 * Relies on data such as {@link DataObject::$db} and {@link DataObject::getCMSFields()}
35
 * to scaffold interfaces "out of the box", while at the same time providing
36
 * flexibility to customize the default output.
37
 *
38
 * @uses SearchContext
39
 *
40
 * @package framework
41
 * @subpackage admin
42
 */
43
abstract class ModelAdmin extends LeftAndMain {
44
45
	private static $url_rule = '/$ModelClass/$Action';
46
47
	/**
48
	 * List of all managed {@link DataObject}s in this interface.
49
	 *
50
	 * Simple notation with class names only:
51
	 * <code>
52
	 * array('MyObjectClass','MyOtherObjectClass')
53
	 * </code>
54
	 *
55
	 * Extended notation with options (e.g. custom titles):
56
	 * <code>
57
	 * array(
58
	 *   'MyObjectClass' => array('title' => "Custom title")
59
	 * )
60
	 * </code>
61
	 *
62
	 * Available options:
63
	 * - 'title': Set custom titles for the tabs or dropdown names
64
	 *
65
	 * @config
66
	 * @var array|string
67
	 */
68
	private static $managed_models = null;
69
70
	/**
71
	 * Override menu_priority so that ModelAdmin CMSMenu objects
72
	 * are grouped together directly above the Help menu item.
73
	 * @var float
74
	 */
75
	private static $menu_priority = -0.5;
76
77
	private static $menu_icon = 'framework/admin/client/src/sprites/menu-icons/16x16/db.png';
78
79
	private static $allowed_actions = array(
80
		'ImportForm',
81
		'SearchForm',
82
	);
83
84
	private static $url_handlers = array(
85
		'$ModelClass/$Action' => 'handleAction'
86
	);
87
88
	/**
89
	 * @var String
90
	 */
91
	protected $modelClass;
92
93
	/**
94
	 * Change this variable if you don't want the Import from CSV form to appear.
95
	 * This variable can be a boolean or an array.
96
	 * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClasstwo')
97
	 */
98
	public $showImportForm = true;
99
100
	/**
101
	 * List of all {@link DataObject}s which can be imported through
102
	 * a subclass of {@link BulkLoader} (mostly CSV data).
103
	 * By default {@link CsvBulkLoader} is used, assuming a standard mapping
104
	 * of column names to {@link DataObject} properties/relations.
105
	 *
106
	 * e.g. "BlogEntry" => "BlogEntryCsvBulkLoader"
107
	 *
108
	 * @config
109
	 * @var array
110
	 */
111
	private static $model_importers = null;
112
113
	/**
114
	 * Amount of results showing on a single page.
115
	 *
116
	 * @config
117
	 * @var int
118
	 */
119
	private static $page_length = 30;
120
121
	/**
122
	 * Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins.
123
	 */
124
	protected function init() {
125
		parent::init();
126
127
		$models = $this->getManagedModels();
128
129
		if($this->getRequest()->param('ModelClass')) {
130
			$this->modelClass = $this->unsanitiseClassName($this->getRequest()->param('ModelClass'));
131
		} else {
132
			reset($models);
133
			$this->modelClass = key($models);
134
		}
135
136
		// security check for valid models
137
		if(!array_key_exists($this->modelClass, $models)) {
138
			user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR);
139
		}
140
141
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/ModelAdmin.js');
142
	}
143
144
	public function Link($action = null) {
145
		if(!$action) $action = $this->sanitiseClassName($this->modelClass);
146
		return parent::Link($action);
147
	}
148
149
	public function getEditForm($id = null, $fields = null) {
150
		$list = $this->getList();
151
		$exportButton = new GridFieldExportButton('buttons-before-left');
152
		$exportButton->setExportColumns($this->getExportFields());
153
		$listField = GridField::create(
154
			$this->sanitiseClassName($this->modelClass),
155
			false,
156
			$list,
157
			$fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length'))
158
				->addComponent($exportButton)
159
				->removeComponentsByType('GridFieldFilterHeader')
160
				->addComponents(new GridFieldPrintButton('buttons-before-left'))
161
		);
162
163
		// Validation
164
		if(singleton($this->modelClass)->hasMethod('getCMSValidator')) {
165
			$detailValidator = singleton($this->modelClass)->getCMSValidator();
166
			$listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setValidator() does only exist in the following implementations of said interface: GridFieldDetailForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
167
		}
168
169
		$form = Form::create(
170
			$this,
171
			'EditForm',
172
			new FieldList($listField),
173
			new FieldList()
174
		)->setHTMLID('Form_EditForm');
175
		$form->addExtraClass('cms-edit-form cms-panel-padded center');
176
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
177
		$editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm');
178
		$form->setFormAction($editFormAction);
179
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
180
181
		$this->extend('updateEditForm', $form);
182
183
		return $form;
184
	}
185
186
	/**
187
	 * Define which fields are used in the {@link getEditForm} GridField export.
188
	 * By default, it uses the summary fields from the model definition.
189
	 *
190
	 * @return array
191
	 */
192
	public function getExportFields() {
193
		return singleton($this->modelClass)->summaryFields();
194
	}
195
196
	/**
197
	 * @return SearchContext
198
	 */
199
	public function getSearchContext() {
200
		$context = singleton($this->modelClass)->getDefaultSearchContext();
201
202
		// Namespace fields, for easier detection if a search is present
203
		foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName()));
204
		foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
205
206
		$this->extend('updateSearchContext', $context);
207
208
		return $context;
209
	}
210
211
	/**
212
	 * @return Form
213
	 */
214
	public function SearchForm() {
215
		$context = $this->getSearchContext();
216
		/** @skipUpgrade */
217
		$form = new Form($this, "SearchForm",
218
			$context->getSearchFields(),
219
			new FieldList(
220
				Object::create('FormAction', 'search', _t('MemberTableField.APPLY_FILTER', 'Apply Filter'))
221
				->setUseButtonTag(true)->addExtraClass('ss-ui-action-constructive'),
222
				Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.RESET','Reset'))
223
					->setUseButtonTag(true)
224
			),
225
			new RequiredFields()
226
		);
227
		$form->setFormMethod('get');
228
		$form->setFormAction($this->Link($this->sanitiseClassName($this->modelClass)));
229
		$form->addExtraClass('cms-search-form');
230
		$form->disableSecurityToken();
231
		$form->loadDataFrom($this->getRequest()->getVars());
232
233
		$this->extend('updateSearchForm', $form);
234
235
		return $form;
236
	}
237
238
	public function getList() {
239
		$context = $this->getSearchContext();
240
		$params = $this->getRequest()->requestVar('q');
241
242
		if(is_array($params)) {
243
			$params = ArrayLib::array_map_recursive('trim', $params);
244
		}
245
246
		$list = $context->getResults($params);
247
248
		$this->extend('updateList', $list);
249
250
		return $list;
251
	}
252
253
254
	/**
255
	 * Returns managed models' create, search, and import forms
256
	 * @uses SearchContext
257
	 * @uses SearchFilter
258
	 * @return SS_List of forms
259
	 */
260
	protected function getManagedModelTabs() {
261
		$models = $this->getManagedModels();
262
		$forms  = new ArrayList();
263
264
		foreach($models as $class => $options) {
0 ignored issues
show
Bug introduced by
The expression $models of type array|integer|double|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...
265
			$forms->push(new ArrayData(array (
266
				'Title'     => $options['title'],
267
				'ClassName' => $class,
268
				'Link' => $this->Link($this->sanitiseClassName($class)),
269
				'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link'
270
			)));
271
		}
272
273
		return $forms;
274
	}
275
276
	/**
277
	 * Sanitise a model class' name for inclusion in a link
278
	 *
279
	 * @param string $class
280
	 * @return string
281
	 */
282
	protected function sanitiseClassName($class) {
283
		return str_replace('\\', '-', $class);
284
	}
285
286
	/**
287
	 * Unsanitise a model class' name from a URL param
288
	 *
289
	 * @param string $class
290
	 * @return string
291
	 */
292
	protected function unsanitiseClassName($class) {
293
		return str_replace('-', '\\', $class);
294
	}
295
296
	/**
297
	 * @return array Map of class name to an array of 'title' (see {@link $managed_models})
298
	 */
299
	public function getManagedModels() {
300
		$models = $this->stat('managed_models');
301
		if(is_string($models)) {
302
			$models = array($models);
303
		}
304
		if(!count($models)) {
305
			user_error(
306
				'ModelAdmin::getManagedModels():
307
				You need to specify at least one DataObject subclass in public static $managed_models.
308
				Make sure that this property is defined, and that its visibility is set to "public"',
309
				E_USER_ERROR
310
			);
311
		}
312
313
		// Normalize models to have their model class in array key
314
		foreach($models as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $models of type array|integer|double|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...
315
			if(is_numeric($k)) {
316
				$models[$v] = array('title' => singleton($v)->i18n_plural_name());
317
				unset($models[$k]);
318
			}
319
		}
320
321
		return $models;
322
	}
323
324
	/**
325
	 * Returns all importers defined in {@link self::$model_importers}.
326
	 * If none are defined, we fall back to {@link self::managed_models}
327
	 * with a default {@link CsvBulkLoader} class. In this case the column names of the first row
328
	 * in the CSV file are assumed to have direct mappings to properties on the object.
329
	 *
330
	 * @return array Map of model class names to importer instances
331
	 */
332
	public function getModelImporters() {
333
		$importerClasses = $this->stat('model_importers');
334
335
		// fallback to all defined models if not explicitly defined
336
		if(is_null($importerClasses)) {
337
			$models = $this->getManagedModels();
338
			foreach($models as $modelName => $options) {
0 ignored issues
show
Bug introduced by
The expression $models of type array|integer|double|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...
339
				$importerClasses[$modelName] = 'CsvBulkLoader';
340
			}
341
		}
342
343
		$importers = array();
344
		foreach($importerClasses as $modelClass => $importerClass) {
0 ignored issues
show
Bug introduced by
The expression $importerClasses of type array|integer|double|string|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...
345
			$importers[$modelClass] = new $importerClass($modelClass);
346
		}
347
348
		return $importers;
349
	}
350
351
	/**
352
	 * Generate a CSV import form for a single {@link DataObject} subclass.
353
	 *
354
	 * @return Form
355
	 */
356
	public function ImportForm() {
357
		$modelSNG = singleton($this->modelClass);
358
		$modelName = $modelSNG->i18n_singular_name();
359
		// check if a import form should be generated
360
		if(!$this->showImportForm ||
361
			(is_array($this->showImportForm) && !in_array($this->modelClass, $this->showImportForm))
362
		) {
363
			return false;
364
		}
365
366
		$importers = $this->getModelImporters();
367
		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...
368
369
		if(!$modelSNG->canCreate(Member::currentUser())) return false;
370
371
		$fields = new FieldList(
372
			new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
373
			new FileField('_CsvFile', false)
374
		);
375
376
		// get HTML specification for each import (column names etc.)
377
		$importerClass = $importers[$this->modelClass];
378
		$importer = new $importerClass($this->modelClass);
379
		$spec = $importer->getImportSpec();
380
		$specFields = new ArrayList();
381
		foreach($spec['fields'] as $name => $desc) {
382
			$specFields->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
383
		}
384
		$specRelations = new ArrayList();
385
		foreach($spec['relations'] as $name => $desc) {
386
			$specRelations->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
387
		}
388
		$specHTML = $this->customise(array(
389
			'ClassName' => $this->sanitiseClassName($this->modelClass),
390
			'ModelName' => Convert::raw2att($modelName),
391
			'Fields' => $specFields,
392
			'Relations' => $specRelations,
393
		))->renderWith($this->getTemplatesWithSuffix('_ImportSpec'));
394
395
		$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
396
		$fields->push(
397
			new CheckboxField('EmptyBeforeImport', _t('ModelAdmin.EMPTYBEFOREIMPORT', 'Replace data'),
398
				false)
399
		);
400
401
		$actions = new FieldList(
402
			new FormAction('import', _t('ModelAdmin.IMPORT', 'Import from CSV'))
403
		);
404
405
		$form = new Form(
406
			$this,
407
			"ImportForm",
408
			$fields,
409
			$actions
410
		);
411
		$form->setFormAction(
412
			Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'ImportForm')
413
		);
414
415
		$this->extend('updateImportForm', $form);
416
417
		return $form;
418
	}
419
420
	/**
421
	 * Imports the submitted CSV file based on specifications given in
422
	 * {@link self::model_importers}.
423
	 * Redirects back with a success/failure message.
424
	 *
425
	 * @todo Figure out ajax submission of files via jQuery.form plugin
426
	 *
427
	 * @param array $data
428
	 * @param Form $form
429
	 * @param SS_HTTPRequest $request
430
	 */
431
	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...
432
		if(!$this->showImportForm || (is_array($this->showImportForm)
433
				&& !in_array($this->modelClass,$this->showImportForm))) {
434
435
			return false;
436
		}
437
438
		$importers = $this->getModelImporters();
439
		$loader = $importers[$this->modelClass];
440
441
		// File wasn't properly uploaded, show a reminder to the user
442
		if(
443
			empty($_FILES['_CsvFile']['tmp_name']) ||
444
			file_get_contents($_FILES['_CsvFile']['tmp_name']) == ''
445
		) {
446
			$form->sessionMessage(_t('ModelAdmin.NOCSVFILE', 'Please browse for a CSV file to import'), 'good');
447
			$this->redirectBack();
448
			return false;
449
		}
450
451
		if (!empty($data['EmptyBeforeImport']) && $data['EmptyBeforeImport']) { //clear database before import
452
			$loader->deleteExistingRecords = true;
453
		}
454
		$results = $loader->load($_FILES['_CsvFile']['tmp_name']);
455
456
		$message = '';
457
		if($results->CreatedCount()) $message .= _t(
458
			'ModelAdmin.IMPORTEDRECORDS', "Imported {count} records.",
459
			array('count' => $results->CreatedCount())
460
		);
461
		if($results->UpdatedCount()) $message .= _t(
462
			'ModelAdmin.UPDATEDRECORDS', "Updated {count} records.",
463
			array('count' => $results->UpdatedCount())
464
		);
465
		if($results->DeletedCount()) $message .= _t(
466
			'ModelAdmin.DELETEDRECORDS', "Deleted {count} records.",
467
			array('count' => $results->DeletedCount())
468
		);
469
		if(!$results->CreatedCount() && !$results->UpdatedCount()) {
470
			$message .= _t('ModelAdmin.NOIMPORT', "Nothing to import");
471
		}
472
473
		$form->sessionMessage($message, 'good');
474
		$this->redirectBack();
475
	}
476
477
	/**
478
	 * @param bool $unlinked
479
	 * @return ArrayList
480
	 */
481
	public function Breadcrumbs($unlinked = false) {
482
		$items = parent::Breadcrumbs($unlinked);
483
484
		// Show the class name rather than ModelAdmin title as root node
485
		$models = $this->getManagedModels();
486
		$params = $this->getRequest()->getVars();
487
		if(isset($params['url'])) unset($params['url']);
488
489
		$items[0]->Title = $models[$this->modelClass]['title'];
490
		$items[0]->Link = Controller::join_links(
491
			$this->Link($this->sanitiseClassName($this->modelClass)),
492
			'?' . http_build_query($params)
493
		);
494
495
		return $items;
496
	}
497
498
	/**
499
	 * overwrite the static page_length of the admin panel,
500
	 * should be called in the project _config file.
501
	 *
502
	 * @deprecated 4.0 Use "ModelAdmin.page_length" config setting
503
	 */
504
	public static function set_page_length($length){
505
		Deprecation::notice('4.0', 'Use "ModelAdmin.page_length" config setting');
506
		self::config()->page_length = $length;
0 ignored issues
show
Documentation introduced by
The property page_length does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
507
	}
508
509
	/**
510
	 * Return the static page_length of the admin, default as 30
511
	 *
512
	 * @deprecated 4.0 Use "ModelAdmin.page_length" config setting
513
	 */
514
	public static function get_page_length(){
515
		Deprecation::notice('4.0', 'Use "ModelAdmin.page_length" config setting');
516
		return self::config()->page_length;
517
	}
518
519
}
520