Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

GridFieldDetailForm_ItemRequest::Breadcrumbs()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 19
rs 8.8571
cc 5
eloc 13
nc 4
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"));
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
		// If we are creating a new record in a has-many list, then
379
		// pre-populate the record's foreign key.
380
		if($list instanceof HasManyList && !$this->record->isInDB()) {
381
			$key = $list->getForeignKey();
382
			$id = $list->getForeignID();
383
			$this->record->$key = $id;
384
		}
385
386
		$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...
387
		if(!$fields) $fields = $this->record->getCMSFields();
388
389
		// If we are creating a new record in a has-many list, then
390
		// Disable the form field as it has no effect.
391
		if($list instanceof HasManyList) {
392
			$key = $list->getForeignKey();
393
394
			if($field = $fields->dataFieldByName($key)) {
395
				$fields->makeFieldReadonly($field);
396
			}
397
		}
398
399
		// Caution: API violation. Form expects a Controller, but we are giving it a RequestHandler instead.
400
		// Thanks to this however, we are able to nest GridFields, and also access the initial Controller by
401
		// dereferencing GridFieldDetailForm_ItemRequest->getController() multiple times. See getToplevelController
402
		// below.
403
		$form = new Form(
404
			$this,
405
			'ItemEditForm',
406
			$fields,
407
			$actions,
408
			$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...
409
		);
410
411
		$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
412
413
		if($this->record->ID && !$canEdit) {
414
			// Restrict editing of existing records
415
			$form->makeReadonly();
416
			// Hack to re-enable delete button if user can delete
417
			if ($canDelete) {
418
				$form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
419
			}
420
		} elseif(!$this->record->ID && !$canCreate) {
421
			// Restrict creation of new records
422
			$form->makeReadonly();
423
		}
424
425
		// Load many_many extraData for record.
426
		// Fields with the correct 'ManyMany' namespace need to be added manually through getCMSFields().
427
		if($list instanceof ManyManyList) {
428
			$extraData = $list->getExtraData('', $this->record->ID);
429
			$form->loadDataFrom(array('ManyMany' => $extraData));
430
		}
431
432
		// TODO Coupling with CMS
433
		$toplevelController = $this->getToplevelController();
434
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
435
			// Always show with base template (full width, no other panels),
436
			// regardless of overloaded CMS controller templates.
437
			// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
438
			$form->setTemplate('LeftAndMain_EditForm');
439
			$form->addExtraClass('cms-content cms-edit-form center');
440
			$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
441
			if($form->Fields()->hasTabset()) {
442
				$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
443
				$form->addExtraClass('cms-tabset');
444
			}
445
446
			$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...
447
		}
448
449
		$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...
450
		if($cb) $cb($form, $this);
451
		$this->extend("updateItemEditForm", $form);
452
		return $form;
453
	}
454
455
	/**
456
	 * Build the set of form field actions for this DataObject
457
	 *
458
	 * @return FieldList
459
	 */
460
	protected function getFormActions() {
461
		$canEdit = $this->record->canEdit();
462
		$canDelete = $this->record->canDelete();
463
		$actions = new FieldList();
464
		if($this->record->ID !== 0) {
465
			if($canEdit) {
466
				$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
467
					->setUseButtonTag(true)
468
					->addExtraClass('ss-ui-action-constructive')
469
					->setAttribute('data-icon', 'accept'));
470
			}
471
472
			if($canDelete) {
473
				$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
474
					->setUseButtonTag(true)
475
					->addExtraClass('ss-ui-action-destructive action-delete'));
476
			}
477
478
		} else { // adding new record
479
			//Change the Save label to 'Create'
480
			$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
481
				->setUseButtonTag(true)
482
				->addExtraClass('ss-ui-action-constructive')
483
				->setAttribute('data-icon', 'add'));
484
485
			// Add a Cancel link which is a button-like link and link back to one level up.
486
			$crumbs = $this->Breadcrumbs();
487
			if($crumbs && $crumbs->count() >= 2){
488
				$oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2);
489
				$text = sprintf(
490
					"<a class=\"%s\" href=\"%s\">%s</a>",
491
					"crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
492
					$oneLevelUp->Link, // url
493
					_t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
494
				);
495
				$actions->push(new LiteralField('cancelbutton', $text));
496
			}
497
		}
498
		$this->extend('updateFormActions', $actions);
499
		return $actions;
500
	}
501
502
	/**
503
	 * Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest.
504
	 * This allows us to access the Controller responsible for invoking the top-level GridField.
505
	 * This should be equivalent to getting the controller off the top of the controller stack via Controller::curr(),
506
	 * but allows us to avoid accessing the global state.
507
	 *
508
	 * GridFieldDetailForm_ItemRequests are RequestHandlers, and as such they are not part of the controller stack.
509
	 *
510
	 * @return Controller
511
	 */
512
	protected function getToplevelController() {
513
		$c = $this->popupController;
514
		while($c && $c instanceof GridFieldDetailForm_ItemRequest) {
515
			$c = $c->getController();
516
		}
517
		return $c;
518
	}
519
520
	protected function getBackLink(){
521
		// TODO Coupling with CMS
522
		$backlink = '';
523
		$toplevelController = $this->getToplevelController();
524
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
525
			if($toplevelController->hasMethod('Backlink')) {
526
				$backlink = $toplevelController->Backlink();
527
			} elseif($this->popupController->hasMethod('Breadcrumbs')) {
528
				$parents = $this->popupController->Breadcrumbs(false)->items;
529
				$backlink = array_pop($parents)->Link;
530
			}
531
		}
532
		if(!$backlink) $backlink = $toplevelController->Link();
533
534
		return $backlink;
535
	}
536
537
	/**
538
	 * Get the list of extra data from the $record as saved into it by
539
	 * {@see Form::saveInto()}
540
	 *
541
	 * Handles detection of falsey values explicitly saved into the
542
	 * DataObject by formfields
543
	 *
544
	 * @param DataObject $record
545
	 * @param SS_List $list
546
	 * @return array List of data to write to the relation
547
	 */
548
	protected function getExtraSavedData($record, $list) {
549
		// Skip extra data if not ManyManyList
550
		if(!($list instanceof ManyManyList)) {
551
			return null;
552
		}
553
554
		$data = array();
555
		foreach($list->getExtraFields() as $field => $dbSpec) {
556
			$savedField = "ManyMany[{$field}]";
557
			if($record->hasField($savedField)) {
558
				$data[$field] = $record->getField($savedField);
559
			}
560
		}
561
		return $data;
562
	}
563
564
	public function doSave($data, $form) {
565
		$isNewRecord = $this->record->ID == 0;
566
567
		// Check permission
568
		if (!$this->record->canEdit()) {
569
			return $this->httpError(403);
570
		}
571
572
		// Save from form data
573
		try {
574
			$this->saveFormIntoRecord($data, $form);
575
		} catch (ValidationException $e) {
576
			return $this->generateValidationResponse($form, $e);
577
		}
578
579
		$link = '<a href="' . $this->Link('edit') . '">"'
580
			. htmlspecialchars($this->record->Title, ENT_QUOTES)
581
			. '"</a>';
582
		$message = _t(
583
			'GridFieldDetailForm.Saved',
584
			'Saved {name} {link}',
585
			array(
586
				'name' => $this->record->i18n_singular_name(),
587
				'link' => $link
588
			)
589
		);
590
591
		$form->sessionMessage($message, 'good', false);
592
593
		// Redirect after save
594
		return $this->redirectAfterSave($isNewRecord);
595
	}
596
597
	/**
598
	 * Response object for this request after a successful save
599
	 *
600
	 * @param bool $isNewRecord True if this record was just created
601
	 * @return SS_HTTPResponse|HTMLText
602
	 */
603
	protected function redirectAfterSave($isNewRecord) {
604
		$controller = $this->getToplevelController();
605
		if($isNewRecord) {
606
			return $controller->redirect($this->Link());
607
		} elseif($this->gridField->getList()->byId($this->record->ID)) {
608
			// Return new view, as we can't do a "virtual redirect" via the CMS Ajax
609
			// to the same URL (it assumes that its content is already current, and doesn't reload)
610
			return $this->edit($controller->getRequest());
611
		} else {
612
			// Changes to the record properties might've excluded the record from
613
			// a filtered list, so return back to the main view if it can't be found
614
			$url = $controller->getRequest()->getURL();
615
			$noActionURL = $controller->removeAction($url);
616
			$controller->getRequest()->addHeader('X-Pjax', 'Content');
617
			return $controller->redirect($noActionURL, 302);
618
		}
619
	}
620
621
	public function httpError($errorCode, $errorMessage = null) {
622
		$controller = $this->getToplevelController();
623
		return $controller->httpError($errorCode, $errorMessage);
624
	}
625
626
	/**
627
	 * Loads the given form data into the underlying dataobject and relation
628
	 *
629
	 * @param array $data
630
	 * @param Form $form
631
	 * @throws ValidationException On error
632
	 * @return DataObject Saved record
633
	 */
634
	protected function saveFormIntoRecord($data, $form) {
635
		$list = $this->gridField->getList();
636
637
		// Check object matches the correct classname
638
		if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
639
			$newClassName = $data['ClassName'];
640
			// The records originally saved attribute was overwritten by $form->saveInto($record) before.
641
			// This is necessary for newClassInstance() to work as expected, and trigger change detection
642
			// on the ClassName attribute
643
			$this->record->setClassName($this->record->ClassName);
644
			// Replace $record with a new instance
645
			$this->record = $this->record->newClassInstance($newClassName);
646
		}
647
648
		// Save form and any extra saved data into this dataobject
649
		$form->saveInto($this->record);
650
		$this->record->write();
651
		$extraData = $this->getExtraSavedData($this->record, $list);
652
		$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...
653
654
		return $this->record;
655
	}
656
657
	/**
658
	 * Generate a response object for a form validation error
659
	 *
660
	 * @param Form $form The source form
661
	 * @param ValidationException $e The validation error message
662
	 * @return SS_HTTPResponse
663
	 * @throws SS_HTTPResponse_Exception
664
	 */
665
	protected function generateValidationResponse($form, $e) {
666
		$controller = $this->getToplevelController();
667
668
		$form->sessionMessage($e->getResult()->message(), 'bad', false);
669
		$responseNegotiator = new PjaxResponseNegotiator(array(
670
			'CurrentForm' => function() use(&$form) {
671
				return $form->forTemplate();
672
			},
673
			'default' => function() use(&$controller) {
674
				return $controller->redirectBack();
675
			}
676
		));
677
		if($controller->getRequest()->isAjax()){
678
			$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
679
		}
680
		return $responseNegotiator->respond($controller->getRequest());
681
	}
682
683
684
	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...
685
		$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...
686
		try {
687
			if (!$this->record->canDelete()) {
688
				throw new ValidationException(
689
					_t('GridFieldDetailForm.DeletePermissionsFailure',"No delete permissions"),0);
690
			}
691
692
			$this->record->delete();
693
		} catch(ValidationException $e) {
694
			$form->sessionMessage($e->getResult()->message(), 'bad', false);
695
			return $this->getToplevelController()->redirectBack();
696
		}
697
698
		$message = sprintf(
699
			_t('GridFieldDetailForm.Deleted', 'Deleted %s %s'),
700
			$this->record->i18n_singular_name(),
701
			htmlspecialchars($title, ENT_QUOTES)
702
		);
703
704
		$toplevelController = $this->getToplevelController();
705
		if($toplevelController && $toplevelController instanceof LeftAndMain) {
706
			$backForm = $toplevelController->getEditForm();
707
			$backForm->sessionMessage($message, 'good', false);
708
		} else {
709
			$form->sessionMessage($message, 'good', false);
710
		}
711
712
		//when an item is deleted, redirect to the parent controller
713
		$controller = $this->getToplevelController();
714
		$controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
715
716
		return $controller->redirect($this->getBacklink(), 302); //redirect back to admin section
717
	}
718
719
	/**
720
	 * @param String
721
	 */
722
	public function setTemplate($template) {
723
		$this->template = $template;
724
		return $this;
725
	}
726
727
	/**
728
	 * @return String
729
	 */
730
	public function getTemplate() {
731
		return $this->template;
732
	}
733
734
	/**
735
	 * @return Controller
736
	 */
737
	public function getController() {
738
		return $this->popupController;
739
	}
740
741
	/**
742
	 * @return GridField
743
	 */
744
	public function getGridField() {
745
		return $this->gridField;
746
	}
747
748
	/**
749
	 * @return DataObject
750
	 */
751
	public function getRecord() {
752
		return $this->record;
753
	}
754
755
	/**
756
	 * CMS-specific functionality: Passes through navigation breadcrumbs
757
	 * to the template, and includes the currently edited record (if any).
758
	 * see {@link LeftAndMain->Breadcrumbs()} for details.
759
	 *
760
	 * @param boolean $unlinked
761
	 * @return ArrayData
762
	 */
763
	public function Breadcrumbs($unlinked = false) {
764
		if(!$this->popupController->hasMethod('Breadcrumbs')) return;
765
766
		$items = $this->popupController->Breadcrumbs($unlinked);
767
		if($this->record && $this->record->ID) {
768
			$title = ($this->record->Title) ? $this->record->Title : "#{$this->record->ID}";
769
			$items->push(new ArrayData(array(
770
				'Title' => $title,
771
				'Link' => $this->Link()
772
			)));
773
		} else {
774
			$items->push(new ArrayData(array(
775
				'Title' => sprintf(_t('GridField.NewRecord', 'New %s'), $this->record->i18n_singular_name()),
776
				'Link' => false
777
			)));
778
		}
779
780
		return $items;
781
	}
782
}
783