Skip to content

Commit

Permalink
Merge pull request #9192 from sminnee/fix-9163
Browse files Browse the repository at this point in the history
NEW: Support dot syntax in form field names
  • Loading branch information
chillu committed May 20, 2021
2 parents a5fc61a + 8c9e203 commit ad4e488
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/en/02_Developer_Guides/03_Forms/00_Introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ class PageController extends ContentController

```

See [how_tos/handle_nested_data](How to: Handle nested form data) for more advanced use cases.

## Validation

Form validation is handled by the [Validator](api:SilverStripe\Forms\Validator) class and the `validator` property on the `Form` object. The validator
Expand Down
279 changes: 279 additions & 0 deletions docs/en/02_Developer_Guides/03_Forms/How_Tos/06_Handle_Nested_data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
---
title: How to handle nested data in forms
summary: Forms can save into arrays, including has_one relations
iconBrand: wpforms
---

# How to: Save nested data

## Overview

Forms often save into fields `DataObject` records, through [Form::saveInto()](api:Form::saveInto()).
There are a number of ways to save nested data into those records, including their relationships.

Let's take the following data structure, and walk through different approaches.

```php
<?php
use SilverStripe\ORM\DataObject;

class Player extends DataObject
{
private static $db = [
'Name' => 'Varchar',
];

private static $has_one = [
'HometownTeam' => Team::class,
];

private static $many_many = [
'Teams' => Team::class,
];
}
```

```
<?php
use SilverStripe\ORM\DataObject;
class Team extends DataObject
{
private static $db = [
'Name' => 'Varchar',
];
private static $belongs_many_many = [
'Players' => Player::class,
];
}
```

## Form fields

Some form fields like [MultiSelectField](api:MultiSelectField) and [CheckboxSetField](api:CheckboxSetField)
support saving lists of identifiers into a relation. Naming the field by the relation name will
trigger the form field to write into the relationship.

Example: Select teams for an existing player

```php
<?php

use SilverStripe\Control\Controller;
use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;

class MyController extends Controller
{
private static $allowed_actions = ['Form'];

private static $url_segment = 'MyController';

public function Form()
{
$player = Player::get()->byID(1);
return Form::create(
$this,
'Form',
FieldList::create([
TextField::create('Name'),
CheckboxSetField::create('Teams')
->setSource(Team::get()->map()),
HiddenField::create('ID'),
]),
FieldList::create([
FormAction::create('doSubmitForm', 'Submit')
]),
RequiredFields::create([
'Name',
'Teams',
'ID',
])
)->loadDataFrom($player);
}

public function doSubmitForm($data, $form)
{
$player = Player::get()->byID($data['ID']);

// Only works for updating existing records
if (!$player) {
return false;
}

// Check permissions for the current user.
if (!$player->canEdit()) {
return false;
}

// Automatically writes Teams() relationship
$form->saveInto($player);

$form->sessionMessage('Saved!', 'good');

return $this->redirectBack();
}
}
```


## Dot notation

For single record relationships (e.g. `has_one`),
forms can automatically traverse into this relationship by using dot notation
in the form field name. This also works with custom getters returning
`DataObject` instances.

Example: Update team name (via a `has_one` relationship) on an existing player.

```php
<?php

use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;

class MyController extends Controller
{
private static $allowed_actions = ['Form'];

private static $url_segment = 'MyController';

public function Form()
{
return Form::create(
$this,
'Form',
FieldList::create([
TextField::create('Name'),
TextField::create('HometownTeam.Name'),
HiddenField::create('ID'),
]),
FieldList::create([
FormAction::create('doSubmitForm', 'Submit')
]),
RequiredFields::create([
'Name',
'HometownTeam.Name',
'ID',
])
);
}

public function doSubmitForm($data, $form)
{
$player = Player::get()->byID($data['ID']);

// Only works for updating existing records
if (!$player) {
return false;
}

// Check permissions for the current user.
if (!$player->canEdit() || !$player->HometownTeam()->canEdit()) {
return false;
}

$form->saveInto($player);

// Write relationships *before* the original object
// to avoid changes being lost when flush() is called after write().
// CAUTION: This will create a new record if none is set on the relationship.
// This might or might not be desired behaviour.
$player->HometownTeam()->write();
$player->write();

$form->sessionMessage('Saved!', 'good');

return $this->redirectBack();
}
}
```

## Array notation

This is the most advanced technique, since it works with the form submission directly,
rather than relying on form field logic.

Example: Create one or more new teams for existing player

```
<?php
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;
class MyController extends Controller
{
private static $allowed_actions = ['Form'];
private static $url_segment = 'MyController';
public function Form()
{
$player = Player::get()->byID(1);
return Form::create(
$this,
'Form',
FieldList::create([
TextField::create('Name'),
// The UI could duplicate this field to allow creating multiple fields
TextField::create('NewTeams[]', 'New Teams'),
HiddenField::create('ID'),
]),
FieldList::create([
FormAction::create('doSubmitForm', 'Submit')
]),
RequiredFields::create([
'Name',
'MyTeams[]',
'ID',
])
)->loadDataFrom($player);
}
public function doSubmitForm($data, $form)
{
$player = Player::get()->byID($data['ID']);
// Only works for updating existing records
if (!$player) {
return false;
}
// Check permissions for the current user.
// if (!$player->canEdit()) {
// return false;
// }
$form->saveInto($player);
// Manually create teams based on provided data
foreach ($data['NewTeams'] as $teamName) {
// Caution: Requires data validation on model
$team = Team::create()->update(['Name' => $teamName]);
$team->write();
$player->Teams()->add($team);
}
$form->sessionMessage('Saved!', 'good');
return $this->redirectBack();
}
}
```
6 changes: 6 additions & 0 deletions docs/en/04_Changelogs/4.9.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 4.9.0 (Unreleased)


## New features

* [Dot notation support in form fields](https://github.com/silverstripe/silverstripe-framework/pull/9192): Save directly into nested has_one relationships (see [docs](/developer_guides/forms/how_tos/handle_nested_data)).
5 changes: 4 additions & 1 deletion src/Forms/FieldList.php
Original file line number Diff line number Diff line change
Expand Up @@ -511,14 +511,17 @@ public function findOrMakeTab($tabName, $title = null)
*/
public function fieldByName($name)
{
$fullName = $name;
if (strpos($name, '.') !== false) {
list($name, $remainder) = explode('.', $name, 2);
} else {
$remainder = null;
}

foreach ($this as $child) {
if (trim($name) == trim($child->getName()) || $name == $child->id) {
if (trim($fullName) == trim($child->getName()) || $fullName == $child->id) {
return $child;
} elseif (trim($name) == trim($child->getName()) || $name == $child->id) {
if ($remainder) {
if ($child instanceof CompositeField) {
return $child->fieldByName($remainder);
Expand Down
36 changes: 28 additions & 8 deletions src/Forms/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -1462,19 +1462,39 @@ public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null)
$val = null;

if (is_object($data)) {
$exists = (
isset($data->$name) ||
$data->hasMethod($name) ||
($data->hasMethod('hasField') && $data->hasField($name))
);

if ($exists) {
$val = $data->__get($name);
// Allow dot-syntax traversal of has-one relations fields
if (strpos($name, '.') !== false) {
$exists = (
$data->hasMethod('relField')
);
try {
$val = $data->relField($name);
} catch (\LogicException $e) {
// There's no other way to tell whether the relation actually exists
$exists = false;
}
// Regular ViewableData access
} else {
$exists = (
isset($data->$name) ||
$data->hasMethod($name) ||
($data->hasMethod('hasField') && $data->hasField($name))
);

if ($exists) {
$val = $data->__get($name);
}
}

// Regular array access. Note that dot-syntax not supported here
} elseif (is_array($data)) {
if (array_key_exists($name, $data)) {
$exists = true;
$val = $data[$name];
// PHP turns the '.'s in POST vars into '_'s
} elseif (array_key_exists($altName = str_replace('.', '_', $name), $data)) {
$exists = true;
$val = $data[$altName];
} elseif (preg_match_all('/(.*)\[(.*)\]/U', $name, $matches)) {
// If field is in array-notation we need to access nested data
//discard first match which is just the whole string
Expand Down
14 changes: 12 additions & 2 deletions src/Forms/FormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,18 @@ public function Value()
*/
public function saveInto(DataObjectInterface $record)
{
if ($this->name) {
$record->setCastedField($this->name, $this->dataValue());
$component = $record;
$fieldName = $this->name;

// Allow for dot syntax
if (($pos = strrpos($this->name, '.')) !== false) {
$relation = substr($this->name, 0, $pos);
$fieldName = substr($this->name, $pos + 1);
$component = $record->relObject($relation);
}

if ($fieldName) {
$component->setCastedField($fieldName, $this->dataValue());
}
}

Expand Down

0 comments on commit ad4e488

Please sign in to comment.