Completed
Push — 3.4 ( d5402d...250a46 )
by Daniel
08:47
created

ModelAdmin::set_page_length()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Generates a three-pane UI for editing model classes, with an
4
 * automatically generated search panel, tabular results and edit forms.
5
 *
6
 * Relies on data such as {@link DataObject::$db} and {@link DataObject::getCMSFields()}
7
 * to scaffold interfaces "out of the box", while at the same time providing
8
 * flexibility to customize the default output.
9
 *
10
 * @uses SearchContext
11
 *
12
 * @package framework
13
 * @subpackage admin
14
 */
15
abstract class ModelAdmin extends LeftAndMain {
16
17
	private static $url_rule = '/$ModelClass/$Action';
18
19
	/**
20
	 * List of all managed {@link DataObject}s in this interface.
21
	 *
22
	 * Simple notation with class names only:
23
	 * <code>
24
	 * array('MyObjectClass','MyOtherObjectClass')
25
	 * </code>
26
	 *
27
	 * Extended notation with options (e.g. custom titles):
28
	 * <code>
29
	 * array(
30
	 *   'MyObjectClass' => array('title' => "Custom title")
31
	 * )
32
	 * </code>
33
	 *
34
	 * Available options:
35
	 * - 'title': Set custom titles for the tabs or dropdown names
36
	 *
37
	 * @config
38
	 * @var array|string
39
	 */
40
	private static $managed_models = null;
41
42
	/**
43
	 * Override menu_priority so that ModelAdmin CMSMenu objects
44
	 * are grouped together directly above the Help menu item.
45
	 * @var float
46
	 */
47
	private static $menu_priority = -0.5;
48
49
	private static $menu_icon = 'framework/admin/images/menu-icons/16x16/db.png';
50
51
	private static $allowed_actions = array(
52
		'ImportForm',
53
		'SearchForm',
54
	);
55
56
	private static $url_handlers = array(
57
		'$ModelClass/$Action' => 'handleAction'
58
	);
59
60
	/**
61
	 * @var String
62
	 */
63
	protected $modelClass;
64
65
	/**
66
	 * Change this variable if you don't want the Import from CSV form to appear.
67
	 * This variable can be a boolean or an array.
68
	 * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo')
69
	 */
70
	public $showImportForm = true;
71
72
	/**
73
	 * Change this variable if you don't want the search form to appear.
74
	 * This variable can be a boolean or an array.
75
	 * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo')
76
	 */
77
	public $showSearchForm = true;
78
79
	/**
80
	 * List of all {@link DataObject}s which can be imported through
81
	 * a subclass of {@link BulkLoader} (mostly CSV data).
82
	 * By default {@link CsvBulkLoader} is used, assuming a standard mapping
83
	 * of column names to {@link DataObject} properties/relations.
84
	 *
85
	 * e.g. "BlogEntry" => "BlogEntryCsvBulkLoader"
86
	 *
87
	 * @config
88
	 * @var array
89
	 */
90
	private static $model_importers = null;
91
92
	/**
93
	 * Amount of results showing on a single page.
94
	 *
95
	 * @config
96
	 * @var int
97
	 */
98
	private static $page_length = 30;
99
100
	/**
101
	 * Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins.
102
	 */
103
	public function init() {
104
		parent::init();
105
106
		$models = $this->getManagedModels();
107
108
		if($this->getRequest()->param('ModelClass')) {
109
			$this->modelClass = $this->unsanitiseClassName($this->getRequest()->param('ModelClass'));
110
		} else {
111
			reset($models);
112
			$this->modelClass = key($models);
113
		}
114
115
		// security check for valid models
116
		if(!array_key_exists($this->modelClass, $models)) {
117
			user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR);
118
		}
119
120
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ModelAdmin.js');
121
	}
122
123
	public function Link($action = null) {
124
		if(!$action) $action = $this->sanitiseClassName($this->modelClass);
125
		return parent::Link($action);
126
	}
127
128
	public function getEditForm($id = null, $fields = null) {
129
		$list = $this->getList();
130
		$exportButton = new GridFieldExportButton('buttons-before-left');
131
		$exportButton->setExportColumns($this->getExportFields());
132
		$listField = GridField::create(
133
			$this->sanitiseClassName($this->modelClass),
134
			false,
135
			$list,
136
			$fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length'))
137
				->addComponent($exportButton)
138
				->removeComponentsByType('GridFieldFilterHeader')
139
				->addComponents(new GridFieldPrintButton('buttons-before-left'))
140
		);
141
142
		// Validation
143
		if(singleton($this->modelClass)->hasMethod('getCMSValidator')) {
144
			$detailValidator = singleton($this->modelClass)->getCMSValidator();
145
			$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...
146
		}
147
148
		$form = CMSForm::create(
149
			$this,
150
			'EditForm',
151
			new FieldList($listField),
152
			new FieldList()
153
		)->setHTMLID('Form_EditForm');
154
		$form->setResponseNegotiator($this->getResponseNegotiator());
155
		$form->addExtraClass('cms-edit-form cms-panel-padded center');
156
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
157
		$editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm');
158
		$form->setFormAction($editFormAction);
159
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
160
161
		$this->extend('updateEditForm', $form);
162
163
		return $form;
164
	}
165
166
	/**
167
	 * Define which fields are used in the {@link getEditForm} GridField export.
168
	 * By default, it uses the summary fields from the model definition.
169
	 *
170
	 * @return array
171
	 */
172
	public function getExportFields() {
173
		return singleton($this->modelClass)->summaryFields();
174
	}
175
176
	/**
177
	 * @return SearchContext
178
	 */
179
	public function getSearchContext() {
180
		$context = singleton($this->modelClass)->getDefaultSearchContext();
181
182
		// Namespace fields, for easier detection if a search is present
183
		foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName()));
184
		foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
185
186
		$this->extend('updateSearchContext', $context);
187
188
		return $context;
189
	}
190
191
	/**
192
	 * @return Form|bool
193
	 */
194
	public function SearchForm() {
195
		if(!$this->showSearchForm ||
196
			(is_array($this->showSearchForm) && !in_array($this->modelClass, $this->showSearchForm))
197
		) {
198
			return false;
199
		}
200
		$context = $this->getSearchContext();
201
		$form = new Form($this, "SearchForm",
202
			$context->getSearchFields(),
203
			new FieldList(
204
				Object::create('FormAction', 'search', _t('MemberTableField.APPLY_FILTER', 'Apply Filter'))
205
				->setUseButtonTag(true)->addExtraClass('ss-ui-action-constructive'),
206
				Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.RESET','Reset'))
207
					->setUseButtonTag(true)
208
			),
209
			new RequiredFields()
210
		);
211
		$form->setFormMethod('get');
212
		$form->setFormAction($this->Link($this->sanitiseClassName($this->modelClass)));
213
		$form->addExtraClass('cms-search-form');
214
		$form->disableSecurityToken();
215
		$form->loadDataFrom($this->getRequest()->getVars());
216
217
		$this->extend('updateSearchForm', $form);
218
219
		return $form;
220
	}
221
222
	public function getList() {
223
		$context = $this->getSearchContext();
224
		$params = $this->getRequest()->requestVar('q');
225
226
		if(is_array($params)) {
227
			$params = ArrayLib::array_map_recursive('trim', $params);
228
		}
229
230
		// Parse all DateFields to handle user input non ISO 8601 dates
231
		foreach($context->getFields() as $field) {
232
			if($field instanceof DatetimeField) {
233
				$params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()]));
234
			}
235
		}
236
237
		$list = $context->getResults($params);
238
239
		$this->extend('updateList', $list);
240
241
		return $list;
242
	}
243
244
245
	/**
246
	 * Returns managed models' create, search, and import forms
247
	 * @uses SearchContext
248
	 * @uses SearchFilter
249
	 * @return SS_List of forms
250
	 */
251
	protected function getManagedModelTabs() {
252
		$models = $this->getManagedModels();
253
		$forms  = new ArrayList();
254
255
		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...
256
			$forms->push(new ArrayData(array (
257
				'Title'     => $options['title'],
258
				'ClassName' => $class,
259
				'Link' => $this->Link($this->sanitiseClassName($class)),
260
				'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link'
261
			)));
262
		}
263
264
		return $forms;
265
	}
266
267
	/**
268
	 * Sanitise a model class' name for inclusion in a link
269
	 * @return string
270
	 */
271
	protected function sanitiseClassName($class) {
272
		return str_replace('\\', '-', $class);
273
	}
274
275
	/**
276
	 * Unsanitise a model class' name from a URL param
277
	 * @return string
278
	 */
279
	protected function unsanitiseClassName($class) {
280
		return str_replace('-', '\\', $class);
281
	}
282
283
	/**
284
	 * @return array Map of class name to an array of 'title' (see {@link $managed_models})
285
	 */
286
	public function getManagedModels() {
287
		$models = $this->stat('managed_models');
288
		if(is_string($models)) {
289
			$models = array($models);
290
		}
291
		if(!count($models)) {
292
			user_error(
293
				'ModelAdmin::getManagedModels():
294
				You need to specify at least one DataObject subclass in public static $managed_models.
295
				Make sure that this property is defined, and that its visibility is set to "public"',
296
				E_USER_ERROR
297
			);
298
		}
299
300
		// Normalize models to have their model class in array key
301
		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...
302
			if(is_numeric($k)) {
303
				$models[$v] = array('title' => singleton($v)->i18n_singular_name());
304
				unset($models[$k]);
305
			}
306
		}
307
308
		return $models;
309
	}
310
311
	/**
312
	 * Returns all importers defined in {@link self::$model_importers}.
313
	 * If none are defined, we fall back to {@link self::managed_models}
314
	 * with a default {@link CsvBulkLoader} class. In this case the column names of the first row
315
	 * in the CSV file are assumed to have direct mappings to properties on the object.
316
	 *
317
	 * @return array Map of model class names to importer instances
318
	 */
319
	public function getModelImporters() {
320
		$importerClasses = $this->stat('model_importers');
321
322
		// fallback to all defined models if not explicitly defined
323
		if(is_null($importerClasses)) {
324
			$models = $this->getManagedModels();
325
			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...
326
				$importerClasses[$modelName] = 'CsvBulkLoader';
327
			}
328
		}
329
330
		$importers = array();
331
		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...
332
			$importers[$modelClass] = new $importerClass($modelClass);
333
		}
334
335
		return $importers;
336
	}
337
338
	/**
339
	 * Generate a CSV import form for a single {@link DataObject} subclass.
340
	 *
341
	 * @return Form|bool
342
	 */
343
	public function ImportForm() {
344
		$modelSNG = singleton($this->modelClass);
345
		$modelName = $modelSNG->i18n_singular_name();
346
		// check if a import form should be generated
347
		if(!$this->showImportForm ||
348
			(is_array($this->showImportForm) && !in_array($this->modelClass, $this->showImportForm))
349
		) {
350
			return false;
351
		}
352
353
		$importers = $this->getModelImporters();
354
		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...
355
356
		if(!$modelSNG->canCreate(Member::currentUser())) return false;
357
358
		$fields = new FieldList(
359
			new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
360
			new FileField('_CsvFile', false)
361
		);
362
363
		// get HTML specification for each import (column names etc.)
364
		$importerClass = $importers[$this->modelClass];
365
		$importer = new $importerClass($this->modelClass);
366
		$spec = $importer->getImportSpec();
367
		$specFields = new ArrayList();
368
		foreach($spec['fields'] as $name => $desc) {
369
			$specFields->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
370
		}
371
		$specRelations = new ArrayList();
372
		foreach($spec['relations'] as $name => $desc) {
373
			$specRelations->push(new ArrayData(array('Name' => $name, 'Description' => $desc)));
374
		}
375
		$specHTML = $this->customise(array(
376
			'ClassName' => $this->sanitiseClassName($this->modelClass),
377
			'ModelName' => Convert::raw2att($modelName),
378
			'Fields' => $specFields,
379
			'Relations' => $specRelations,
380
		))->renderWith('ModelAdmin_ImportSpec');
381
382
		$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
383
		$fields->push(
384
			new CheckboxField('EmptyBeforeImport', _t('ModelAdmin.EMPTYBEFOREIMPORT', 'Replace data'),
385
				false)
386
		);
387
388
		$actions = new FieldList(
389
			new FormAction('import', _t('ModelAdmin.IMPORT', 'Import from CSV'))
390
		);
391
392
		$form = new Form(
393
			$this,
394
			"ImportForm",
395
			$fields,
396
			$actions
397
		);
398
		$form->setFormAction(
399
			Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'ImportForm')
400
		);
401
402
		$this->extend('updateImportForm', $form);
403
404
		return $form;
405
	}
406
407
	/**
408
	 * Imports the submitted CSV file based on specifications given in
409
	 * {@link self::model_importers}.
410
	 * Redirects back with a success/failure message.
411
	 *
412
	 * @todo Figure out ajax submission of files via jQuery.form plugin
413
	 *
414
	 * @param array $data
415
	 * @param Form $form
416
	 * @param SS_HTTPRequest $request
417
	 * @return bool|null
418
	 */
419
	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...
420
		if(!$this->showImportForm || (is_array($this->showImportForm)
421
				&& !in_array($this->modelClass,$this->showImportForm))) {
422
423
			return false;
424
		}
425
426
		$importers = $this->getModelImporters();
427
		$loader = $importers[$this->modelClass];
428
429
		// File wasn't properly uploaded, show a reminder to the user
430
		if(
431
			empty($_FILES['_CsvFile']['tmp_name']) ||
432
			file_get_contents($_FILES['_CsvFile']['tmp_name']) == ''
433
		) {
434
			$form->sessionMessage(_t('ModelAdmin.NOCSVFILE', 'Please browse for a CSV file to import'), 'good');
435
			$this->redirectBack();
436
			return false;
437
		}
438
439
		if (!empty($data['EmptyBeforeImport']) && $data['EmptyBeforeImport']) { //clear database before import
440
			$loader->deleteExistingRecords = true;
441
		}
442
		$results = $loader->load($_FILES['_CsvFile']['tmp_name']);
443
444
		$message = '';
445
		if($results->CreatedCount()) $message .= _t(
446
			'ModelAdmin.IMPORTEDRECORDS', "Imported {count} records.",
447
			array('count' => $results->CreatedCount())
448
		);
449
		if($results->UpdatedCount()) $message .= _t(
450
			'ModelAdmin.UPDATEDRECORDS', "Updated {count} records.",
451
			array('count' => $results->UpdatedCount())
452
		);
453
		if($results->DeletedCount()) $message .= _t(
454
			'ModelAdmin.DELETEDRECORDS', "Deleted {count} records.",
455
			array('count' => $results->DeletedCount())
456
		);
457
		if(!$results->CreatedCount() && !$results->UpdatedCount()) {
458
			$message .= _t('ModelAdmin.NOIMPORT', "Nothing to import");
459
		}
460
461
		$form->sessionMessage($message, 'good');
462
		$this->redirectBack();
463
	}
464
465
	/**
466
	 * @return ArrayList
467
	 */
468
	public function Breadcrumbs($unlinked = false) {
469
		$items = parent::Breadcrumbs($unlinked);
470
471
		// Show the class name rather than ModelAdmin title as root node
472
		$models = $this->getManagedModels();
473
		$params = $this->getRequest()->getVars();
474
		if(isset($params['url'])) unset($params['url']);
475
476
		$items[0]->Title = $models[$this->modelClass]['title'];
477
		$items[0]->Link = Controller::join_links(
478
			$this->Link($this->sanitiseClassName($this->modelClass)),
479
			'?' . http_build_query($params)
480
		);
481
482
		return $items;
483
	}
484
485
	/**
486
	 * overwrite the static page_length of the admin panel,
487
	 * should be called in the project _config file.
488
	 *
489
	 * @deprecated 4.0 Use "ModelAdmin.page_length" config setting
490
	 */
491
	public static function set_page_length($length){
492
		Deprecation::notice('4.0', 'Use "ModelAdmin.page_length" config setting');
493
		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...
494
	}
495
496
	/**
497
	 * Return the static page_length of the admin, default as 30
498
	 *
499
	 * @deprecated 4.0 Use "ModelAdmin.page_length" config setting
500
	 */
501
	public static function get_page_length(){
502
		Deprecation::notice('4.0', 'Use "ModelAdmin.page_length" config setting');
503
		return self::config()->page_length;
504
	}
505
506
}
507