Completed
Push — 3.4 ( cbd3aa...1ec56a )
by Daniel
12:12
created

ModelAdmin   F

Complexity

Total Complexity 57

Size/Duplication

Total Lines 479
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 28

Importance

Changes 0
Metric Value
dl 0
loc 479
rs 1.3043
c 0
b 0
f 0
wmc 57
lcom 1
cbo 28

17 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 19 3
A Link() 0 4 2
A sanitiseClassName() 0 3 1
B getEditForm() 0 37 2
A getExportFields() 0 3 1
A getSearchContext() 0 11 3
A SearchForm() 0 22 1
A getManagedModelTabs() 0 15 3
A unsanitiseClassName() 0 3 1
B getManagedModels() 0 24 5
A getModelImporters() 0 18 4
C ImportForm() 0 63 9
C import() 0 45 13
A Breadcrumbs() 0 16 2
A set_page_length() 0 4 1
A get_page_length() 0 4 1
B getList() 0 21 5

How to fix   Complexity   

Complex Class

Complex classes like ModelAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelAdmin, and based on these observations, apply Extract Interface, too.

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