Completed
Push — master ( 2fdc96...4f1f24 )
by Damian
12:09
created

GridFieldDetailForm::getURLHandlers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
use SilverStripe\Framework\Core\Extensible;
3
4
/**
5
 * Provides view and edit forms at GridField-specific URLs.
6
 *
7
 * These can be placed into pop-ups by an appropriate front-end.
8
 *
9
 * Usually added to a {@link GridField} alongside of a
10
 * {@link GridFieldEditButton} which takes care of linking the
11
 * individual rows to their edit view.
12
 *
13
 * The URLs provided will be off the following form:
14
 *  - <FormURL>/field/<GridFieldName>/item/<RecordID>
15
 *  - <FormURL>/field/<GridFieldName>/item/<RecordID>/edit
16
 *
17
 * @package forms
18
 * @subpackage fields-gridfield
19
 */
20
class GridFieldDetailForm implements GridField_URLHandler {
21
22
	use Extensible;
23
24
	/**
25
	 * @var string
26
	 */
27
	protected $template = 'GridFieldDetailForm';
28
29
	/**
30
	 *
31
	 * @var string
32
	 */
33
	protected $name;
34
35
	/**
36
	 * @var Validator The form validator used for both add and edit fields.
37
	 */
38
	protected $validator;
39
40
	/**
41
	 * @var FieldList Falls back to {@link DataObject->getCMSFields()} if not defined.
42
	 */
43
	protected $fields;
44
45
	/**
46
	 * @var string
47
	 */
48
	protected $itemRequestClass;
49
50
	/**
51
	 * @var callable With two parameters: $form and $component
52
	 */
53
	protected $itemEditFormCallback;
54
55
	public function getURLHandlers($gridField) {
56
		return array(
57
			'item/$ID' => 'handleItem',
58
			'autocomplete' => 'handleAutocomplete',
59
		);
60
	}
61
62
	/**
63
	 * Create a popup component. The two arguments will specify how the popup form's HTML and
64
	 * behaviour is created.  The given controller will be customised, putting the edit form into the
65
	 * template with the given name.
66
	 *
67
	 * The arguments are experimental API's to support partial content to be passed back to whatever
68
	 * controller who wants to display the getCMSFields
69
	 *
70
	 * @param string $name The name of the edit form to place into the pop-up form
71
	 */
72
	public function __construct($name = 'DetailForm') {
73
		$this->name = $name;
74
		$this->constructExtensions();
75
	}
76
77
	/**
78
	 *
79
	 * @param GridField $gridField
80
	 * @param SS_HTTPRequest $request
81
	 * @return GridFieldDetailForm_ItemRequest
82
	 */
83
	public function handleItem($gridField, $request) {
84
		// Our getController could either give us a true Controller, if this is the top-level GridField.
85
		// It could also give us a RequestHandler in the form of GridFieldDetailForm_ItemRequest if this is a
86
		// nested GridField.
87
		$requestHandler = $gridField->getForm()->getController();
88
89
		if(is_numeric($request->param('ID'))) {
90
			$record = $gridField->getList()->byId($request->param("ID"));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method byId() does only exist in the following implementations of said interface: ArrayList, DataList, FieldList, HasManyList, ManyManyList, Member_GroupSet, PolymorphicHasManyList, RelationList, UnsavedRelationList.

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...
91
		} else {
92
			$record = Object::create($gridField->getModelClass());
93
		}
94
95
		$handler = $this->getItemRequestHandler($gridField, $record, $requestHandler);
0 ignored issues
show
Bug introduced by
It seems like $requestHandler defined by $gridField->getForm()->getController() on line 87 can be null; however, GridFieldDetailForm::getItemRequestHandler() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
96
97
		// if no validator has been set on the GridField and the record has a
98
		// CMS validator, use that.
99
		if(!$this->getValidator() && (method_exists($record, 'getCMSValidator') || $record instanceof Object && $record->hasMethod('getCMSValidator'))) {
100
			$this->setValidator($record->getCMSValidator());
101
		}
102
103
		return $handler->handleRequest($request, DataModel::inst());
104
	}
105
106
	/**
107
	 * Build a request handler for the given record
108
	 *
109
	 * @param GridField $gridField
110
	 * @param DataObject $record
111
	 * @param Controller $requestHandler
112
	 * @return GridFieldDetailForm_ItemRequest
113
	 */
114
	protected function getItemRequestHandler($gridField, $record, $requestHandler) {
115
		$class = $this->getItemRequestClass();
116
		$this->extend('updateItemRequestClass', $class, $gridField, $record, $requestHandler);
117
		$handler = \Injector::inst()->createWithArgs(
118
			$class,
119
			array($gridField, $this, $record, $requestHandler, $this->name)
120
		);
121
		$handler->setTemplate($this->template);
122
		$this->extend('updateItemRequestHandler', $handler);
123
		return $handler;
124
	}
125
126
	/**
127
	 * @param String
128
	 */
129
	public function setTemplate($template) {
130
		$this->template = $template;
131
		return $this;
132
	}
133
134
	/**
135
	 * @return String
136
	 */
137
	public function getTemplate() {
138
		return $this->template;
139
	}
140
141
	/**
142
	 * @param String
143
	 */
144
	public function setName($name) {
145
		$this->name = $name;
146
		return $this;
147
	}
148
149
	/**
150
	 * @return String
151
	 */
152
	public function getName() {
153
		return $this->name;
154
	}
155
156
	/**
157
	 * @param Validator $validator
158
	 */
159
	public function setValidator(Validator $validator) {
160
		$this->validator = $validator;
161
		return $this;
162
	}
163
164
	/**
165
	 * @return Validator
166
	 */
167
	public function getValidator() {
168
		return $this->validator;
169
	}
170
171
	/**
172
	 * @param FieldList $fields
173
	 */
174
	public function setFields(FieldList $fields) {
175
		$this->fields = $fields;
176
		return $this;
177
	}
178
179
	/**
180
	 * @return FieldList
181
	 */
182
	public function getFields() {
183
		return $this->fields;
184
	}
185
186
	/**
187
	 * @param String
188
	 */
189
	public function setItemRequestClass($class) {
190
		$this->itemRequestClass = $class;
191
		return $this;
192
	}
193
194
	/**
195
	 * @return String
196
	 */
197
	public function getItemRequestClass() {
198
		if($this->itemRequestClass) {
199
			return $this->itemRequestClass;
200
		} else if(ClassInfo::exists(get_class($this) . "_ItemRequest")) {
201
			return get_class($this) . "_ItemRequest";
202
		} else {
203
			return 'GridFieldDetailForm_ItemRequest';
204
		}
205
	}
206
207
	/**
208
	 * @param Closure $cb Make changes on the edit form after constructing it.
209
	 */
210
	public function setItemEditFormCallback(Closure $cb) {
211
		$this->itemEditFormCallback = $cb;
212
		return $this;
213
	}
214
215
	/**
216
	 * @return Closure
217
	 */
218
	public function getItemEditFormCallback() {
219
		return $this->itemEditFormCallback;
220
	}
221
222
}
223
224
/**
225
 * @package forms
226
 * @subpackage fields-gridfield
227
 */
228
class GridFieldDetailForm_ItemRequest extends RequestHandler {
229
230
	private static $allowed_actions = array(
231
		'edit',
232
		'view',
233
		'ItemEditForm'
234
	);
235
236
	/**
237
	 *
238
	 * @var GridField
239
	 */
240
	protected $gridField;
241
242
	/**
243
	 *
244
	 * @var GridField_URLHandler
245
	 */
246
	protected $component;
247
248
	/**
249
	 *
250
	 * @var DataObject
251
	 */
252
	protected $record;
253
254
	/**
255
	 * This represents the current parent RequestHandler (which does not necessarily need to be a Controller).
256
	 * It allows us to traverse the RequestHandler chain upwards to reach the Controller stack.
257
	 *
258
	 * @var RequestHandler
259
	 */
260
	protected $popupController;
261
262
	/**
263
	 *
264
	 * @var string
265
	 */
266
	protected $popupFormName;
267
268
	/**
269
	 * @var String
270
	 */
271
	protected $template = 'GridFieldItemEditView';
272
273
	private static $url_handlers = array(
274
		'$Action!' => '$Action',
275
		'' => 'edit',
276
	);
277
278
	/**
279
	 *
280
	 * @param GridFIeld $gridField
281
	 * @param GridField_URLHandler $component
282
	 * @param DataObject $record
283
	 * @param RequestHandler $requestHandler
284
	 * @param string $popupFormName
285
	 */
286
	public function __construct($gridField, $component, $record, $requestHandler, $popupFormName) {
287
		$this->gridField = $gridField;
288
		$this->component = $component;
289
		$this->record = $record;
290
		$this->popupController = $requestHandler;
291
		$this->popupFormName = $popupFormName;
292
		parent::__construct();
293
	}
294
295
	public function Link($action = null) {
296
		return Controller::join_links($this->gridField->Link('item'),
297
			$this->record->ID ? $this->record->ID : 'new', $action);
298
	}
299
300
	public function view($request) {
301
		if(!$this->record->canView()) {
302
			$this->httpError(403);
303
		}
304
305
		$controller = $this->getToplevelController();
306
307
		$form = $this->ItemEditForm($this->gridField, $request);
0 ignored issues
show
Unused Code introduced by
The call to GridFieldDetailForm_ItemRequest::ItemEditForm() has too many arguments starting with $this->gridField.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
308
		$form->makeReadonly();
309
310
		$data = new ArrayData(array(
311
			'Backlink'     => $controller->Link(),
312
			'ItemEditForm' => $form
313
		));
314
		$return = $data->renderWith($this->template);
315
316
		if($request->isAjax()) {
317
			return $return;
318
		} else {
319
			return $controller->customise(array('Content' => $return));
320
		}
321
	}
322
323
	public function edit($request) {
324
		$controller = $this->getToplevelController();
325
		$form = $this->ItemEditForm($this->gridField, $request);
0 ignored issues
show
Unused Code introduced by
The call to GridFieldDetailForm_ItemRequest::ItemEditForm() has too many arguments starting with $this->gridField.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
326
327
		$return = $this->customise(array(
328
			'Backlink' => $controller->hasMethod('Backlink') ? $controller->Backlink() : $controller->Link(),
329
			'ItemEditForm' => $form,
330
		))->renderWith($this->template);
331
332
		if($request->isAjax()) {
333
			return $return;
334
		} else {
335
			// If not requested by ajax, we need to render it within the controller context+template
336
			return $controller->customise(array(
337
				// TODO CMS coupling
338
				'Content' => $return,
339
			));
340
		}
341
	}
342
343
	/**
344
	 * Builds an item edit form.  The arguments to getCMSFields() are the popupController and
345
	 * popupFormName, however this is an experimental API and may change.
346
	 *
347
	 * @todo In the future, we will probably need to come up with a tigher object representing a partially
348
	 * complete controller with gaps for extra functionality.  This, for example, would be a better way
349
	 * of letting Security/login put its log-in form inside a UI specified elsewhere.
350
	 *
351
	 * @return Form
352
	 */
353
	public function ItemEditForm() {
354
		$list = $this->gridField->getList();
355
356
		if (empty($this->record)) {
357
			$controller = $this->getToplevelController();
358
			$url = $controller->getRequest()->getURL();
359
			$noActionURL = $controller->removeAction($url);
360
			$controller->getResponse()->removeHeader('Location');   //clear the existing redirect
361
			return $controller->redirect($noActionURL, 302);
362
		}
363
364
		$canView = $this->record->canView();
365
		$canEdit = $this->record->canEdit();
366
		$canDelete = $this->record->canDelete();
367
		$canCreate = $this->record->canCreate();
368
369
		if(!$canView) {
370
			$controller = $this->getToplevelController();
371
			// TODO More friendly error
372
			return $controller->httpError(403);
373
		}
374
375
		// Build actions
376
		$actions = $this->getFormActions();
377
378
		$fields = $this->component->getFields();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridField_URLHandler as the method getFields() 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...
379
		if(!$fields) $fields = $this->record->getCMSFields();
380
381
		// If we are creating a new record in a has-many list, then
382
		// pre-populate the record's foreign key. Also disable the form field as
383
		// it has no effect.
384
		if($list instanceof HasManyList) {
385
			$key = $list->getForeignKey();
386
			$id = $list->getForeignID();
387
388
			if(!$this->record->isInDB()) {
389
				$this->record->$key = $id;
390
			}
391
392
			if($field = $fields->dataFieldByName($key)) {
393
				$fields->makeFieldReadonly($field);
394
			}
395
		}
396
397
		// Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead.
398
		// Thanks to this however, we are able to nest GridFields, and also access the initial Controller by
399
		// dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController
400
		// below.
401
		$form = new Form(
402
			$this,
403
			'ItemEditForm',
404
			$fields,
405
			$actions,
406
			$this->component->getValidator()
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridField_URLHandler as the method getValidator() 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...
407
		);
408
409
		$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
410
411
		if($this->record->ID && !$canEdit) {
412
			// Restrict editing of existing records
413
			$form->makeReadonly();
414
			// Hack to re-enable delete button if user can delete
415
			if ($canDelete) {
416
				$form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
417
			}
418
		} elseif(!$this->record->ID && !$canCreate) {
419
			// Restrict creation of new records
420
			$form->makeReadonly();
421
		}
422
423
		// Load many_many extraData for record.
424
		// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
425
		if($list instanceof ManyManyList) {
426
			$extraData = $list->getExtraData('', $this->record->ID);
427
			$form->loadDataFrom(array('ManyMany' => $extraData));
428
		}
429
430
		// TODO Coupling with CMS
431
		$toplevelController = $this->getToplevelController();
432
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
433
			// Always show with base template (full width, no other panels),
434
			// regardless of overloaded CMS controller templates.
435
			// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
436
			$form->setTemplate('LeftAndMain_EditForm');
437
			$form->addExtraClass('cms-content cms-edit-form center');
438
			$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
439
			if($form->Fields()->hasTabset()) {
440
				$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
441
				$form->addExtraClass('cms-tabset');
442
			}
443
444
			$form->Backlink = $this->getBackLink();
0 ignored issues
show
Documentation introduced by
The property Backlink does not exist on object<Form>. 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...
445
		}
446
447
		$cb = $this->component->getItemEditFormCallback();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridField_URLHandler as the method getItemEditFormCallback() 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...
448
		if($cb) $cb($form, $this);
449
		$this->extend("updateItemEditForm", $form);
450
		return $form;
451
	}
452
453
	/**
454
	 * Build the set of form field actions for this DataObject
455
	 *
456
	 * @return FieldList
457
	 */
458
	protected function getFormActions() {
459
		$canEdit = $this->record->canEdit();
460
		$canDelete = $this->record->canDelete();
461
		$actions = new FieldList();
462
		if($this->record->ID !== 0) {
463
			if($canEdit) {
464
				$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
465
					->setUseButtonTag(true)
466
					->addExtraClass('ss-ui-action-constructive')
467
					->setAttribute('data-icon', 'accept'));
468
			}
469
470
			if($canDelete) {
471
				$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
472
					->setUseButtonTag(true)
473
					->addExtraClass('ss-ui-action-destructive action-delete'));
474
			}
475
476
		} else { // adding new record
477
			//Change the Save label to 'Create'
478
			$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
479
				->setUseButtonTag(true)
480
				->addExtraClass('ss-ui-action-constructive')
481
				->setAttribute('data-icon', 'add'));
482
483
			// Add a Cancel link which is a button-like link and link back to one level up.
484
			$crumbs = $this->Breadcrumbs();
485
			if($crumbs && $crumbs->count() >= 2){
486
				$oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2);
487
				$text = sprintf(
488
					"<a class=\"%s\" href=\"%s\">%s</a>",
489
					"crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
490
					$oneLevelUp->Link, // url
491
					_t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
492
				);
493
				$actions->push(new LiteralField('cancelbutton', $text));
494
			}
495
		}
496
		$this->extend('updateFormActions', $actions);
497
		return $actions;
498
	}
499
500
	/**
501
	 * Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest.
502
	 * This allows us to access the Controller responsible for invoking the top-level GridField.
503
	 * This should be equivalent to getting the controller off the top of the controller stack via Controller::curr(),
504
	 * but allows us to avoid accessing the global state.
505
	 *
506
	 * GridFieldDetailForm_ItemRequests are RequestHandlers, and as such they are not part of the controller stack.
507
	 *
508
	 * @return Controller
509
	 */
510
	protected function getToplevelController() {
511
		$c = $this->popupController;
512
		while($c && $c instanceof GridFieldDetailForm_ItemRequest) {
513
			$c = $c->getController();
514
		}
515
		return $c;
516
	}
517
518
	protected function getBackLink(){
519
		// TODO Coupling with CMS
520
		$backlink = '';
521
		$toplevelController = $this->getToplevelController();
522
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
523
			if($toplevelController->hasMethod('Backlink')) {
524
				$backlink = $toplevelController->Backlink();
525
			} elseif($this->popupController->hasMethod('Breadcrumbs')) {
526
				$parents = $this->popupController->Breadcrumbs(false)->items;
527
				$backlink = array_pop($parents)->Link;
528
			}
529
		}
530
		if(!$backlink) $backlink = $toplevelController->Link();
531
532
		return $backlink;
533
	}
534
535
	/**
536
	 * Get the list of extra data from the $record as saved into it by
537
	 * {@see Form::saveInto()}
538
	 *
539
	 * Handles detection of falsey values explicitly saved into the
540
	 * DataObject by formfields
541
	 *
542
	 * @param DataObject $record
543
	 * @param SS_List $list
544
	 * @return array List of data to write to the relation
545
	 */
546
	protected function getExtraSavedData($record, $list) {
547
		// Skip extra data if not ManyManyList
548
		if(!($list instanceof ManyManyList)) {
549
			return null;
550
		}
551
552
		$data = array();
553
		foreach($list->getExtraFields() as $field => $dbSpec) {
554
			$savedField = "ManyMany[{$field}]";
555
			if($record->hasField($savedField)) {
556
				$data[$field] = $record->getField($savedField);
557
			}
558
		}
559
		return $data;
560
	}
561
562
	public function doSave($data, $form) {
563
		$isNewRecord = $this->record->ID == 0;
564
565
		// Check permission
566
		if (!$this->record->canEdit()) {
567
			return $this->httpError(403);
568
		}
569
570
		// Save from form data
571
		try {
572
			$this->saveFormIntoRecord($data, $form);
573
		} catch (ValidationException $e) {
574
			return $this->generateValidationResponse($form, $e);
575
		}
576
577
		$link = '<a href="' . $this->Link('edit') . '">"'
578
			. htmlspecialchars($this->record->Title, ENT_QUOTES)
579
			. '"</a>';
580
		$message = _t(
581
			'GridFieldDetailForm.Saved',
582
			'Saved {name} {link}',
583
			array(
584
				'name' => $this->record->i18n_singular_name(),
585
				'link' => $link
586
			)
587
		);
588
589
		$form->sessionMessage($message, 'good', false);
590
591
		// Redirect after save
592
		return $this->redirectAfterSave($isNewRecord);
593
	}
594
595
	/**
596
	 * Response object for this request after a successful save
597
	 *
598
	 * @param bool $isNewRecord True if this record was just created
599
	 * @return SS_HTTPResponse|HTMLText
600
	 */
601
	protected function redirectAfterSave($isNewRecord) {
602
		$controller = $this->getToplevelController();
603
		if($isNewRecord) {
604
			return $controller->redirect($this->Link());
605
		} elseif($this->gridField->getList()->byId($this->record->ID)) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method byId() does only exist in the following implementations of said interface: ArrayList, DataList, FieldList, HasManyList, ManyManyList, Member_GroupSet, PolymorphicHasManyList, RelationList, UnsavedRelationList.

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...
606
			// Return new view, as we can't do a "virtual redirect" via the CMS Ajax
607
			// to the same URL (it assumes that its content is already current, and doesn't reload)
608
			return $this->edit($controller->getRequest());
609
		} else {
610
			// Changes to the record properties might've excluded the record from
611
			// a filtered list, so return back to the main view if it can't be found
612
			$url = $controller->getRequest()->getURL();
613
			$noActionURL = $controller->removeAction($url);
614
			$controller->getRequest()->addHeader('X-Pjax', 'Content');
615
			return $controller->redirect($noActionURL, 302);
616
		}
617
	}
618
619
	public function httpError($errorCode, $errorMessage = null) {
620
		$controller = $this->getToplevelController();
621
		return $controller->httpError($errorCode, $errorMessage);
622
	}
623
624
	/**
625
	 * Loads the given form data into the underlying dataobject and relation
626
	 *
627
	 * @param array $data
628
	 * @param Form $form
629
	 * @throws ValidationException On error
630
	 * @return DataObject Saved record
631
	 */
632
	protected function saveFormIntoRecord($data, $form) {
633
		$list = $this->gridField->getList();
634
635
		// Check object matches the correct classname
636
		if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
637
			$newClassName = $data['ClassName'];
638
			// The records originally saved attribute was overwritten by $form->saveInto($record) before.
639
			// This is necessary for newClassInstance() to work as expected, and trigger change detection
640
			// on the ClassName attribute
641
			$this->record->setClassName($this->record->ClassName);
642
			// Replace $record with a new instance
643
			$this->record = $this->record->newClassInstance($newClassName);
644
		}
645
646
		// Save form and any extra saved data into this dataobject
647
		$form->saveInto($this->record);
648
		$this->record->write();
649
		$extraData = $this->getExtraSavedData($this->record, $list);
650
		$list->add($this->record, $extraData);
0 ignored issues
show
Unused Code introduced by
The call to SS_List::add() has too many arguments starting with $extraData.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
651
652
		return $this->record;
653
	}
654
655
	/**
656
	 * Generate a response object for a form validation error
657
	 *
658
	 * @param Form $form The source form
659
	 * @param ValidationException $e The validation error message
660
	 * @return SS_HTTPResponse
661
	 * @throws SS_HTTPResponse_Exception
662
	 */
663
	protected function generateValidationResponse($form, $e) {
664
		$controller = $this->getToplevelController();
665
666
		$form->sessionMessage($e->getResult()->message(), 'bad', false);
667
		$responseNegotiator = new PjaxResponseNegotiator(array(
668
			'CurrentForm' => function() use(&$form) {
669
				return $form->forTemplate();
670
			},
671
			'default' => function() use(&$controller) {
672
				return $controller->redirectBack();
673
			}
674
		));
675
		if($controller->getRequest()->isAjax()){
676
			$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
677
		}
678
		return $responseNegotiator->respond($controller->getRequest());
679
	}
680
681
682
	public function doDelete($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
683
		$title = $this->record->Title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<DataObject>. 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...
684
		try {
685
			if (!$this->record->canDelete()) {
686
				throw new ValidationException(
687
					_t('GridFieldDetailForm.DeletePermissionsFailure',"No delete permissions"),0);
688
			}
689
690
			$this->record->delete();
691
		} catch(ValidationException $e) {
692
			$form->sessionMessage($e->getResult()->message(), 'bad', false);
693
			return $this->getToplevelController()->redirectBack();
694
		}
695
696
		$message = sprintf(
697
			_t('GridFieldDetailForm.Deleted', 'Deleted %s %s'),
698
			$this->record->i18n_singular_name(),
699
			htmlspecialchars($title, ENT_QUOTES)
700
		);
701
702
		$toplevelController = $this->getToplevelController();
703
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
704
			$backForm = $toplevelController->getEditForm();
705
			$backForm->sessionMessage($message, 'good', false);
0 ignored issues
show
Bug introduced by
The method sessionMessage does only exist in CMSForm, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
706
		} else {
707
			$form->sessionMessage($message, 'good', false);
708
		}
709
710
		//when an item is deleted, redirect to the parent controller
711
		$controller = $this->getToplevelController();
712
		$controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
713
714
		return $controller->redirect($this->getBacklink(), 302); //redirect back to admin section
715
	}
716
717
	/**
718
	 * @param String
719
	 */
720
	public function setTemplate($template) {
721
		$this->template = $template;
722
		return $this;
723
	}
724
725
	/**
726
	 * @return String
727
	 */
728
	public function getTemplate() {
729
		return $this->template;
730
	}
731
732
	/**
733
	 * @return Controller
734
	 */
735
	public function getController() {
736
		return $this->popupController;
737
	}
738
739
	/**
740
	 * @return GridField
741
	 */
742
	public function getGridField() {
743
		return $this->gridField;
744
	}
745
746
	/**
747
	 * @return DataObject
748
	 */
749
	public function getRecord() {
750
		return $this->record;
751
	}
752
753
	/**
754
	 * CMS-specific functionality: Passes through navigation breadcrumbs
755
	 * to the template, and includes the currently edited record (if any).
756
	 * see {@link LeftAndMain->Breadcrumbs()} for details.
757
	 *
758
	 * @param boolean $unlinked
759
	 * @return ArrayData
760
	 */
761
	public function Breadcrumbs($unlinked = false) {
762
		if(!$this->popupController->hasMethod('Breadcrumbs')) return;
763
764
		$items = $this->popupController->Breadcrumbs($unlinked);
765
		if($this->record && $this->record->ID) {
766
			$items->push(new ArrayData(array(
767
				'Title' => $this->record->Title,
768
				'Link' => $this->Link()
769
			)));
770
		} else {
771
			$items->push(new ArrayData(array(
772
				'Title' => sprintf(_t('GridField.NewRecord', 'New %s'), $this->record->i18n_singular_name()),
773
				'Link' => false
774
			)));
775
		}
776
777
		return $items;
778
	}
779
}
780