How to: save nested data
Overview
Forms often save into fields DataObject
records, through 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.
namespace App\Model;
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,
];
}
namespace App\Model;
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 and 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
namespace App\Control;
use App\Model\Player;
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\TextField;
use SilverStripe\Forms\Validation\RequiredFieldsValidator;
class MyController extends Controller
{
private static $url_segment = 'MyController';
private static $allowed_actions = [
'getForm',
];
private static $url_handlers = [
'Form' => 'getForm',
];
public function getForm()
{
$player = Player::get()->byID(1);
return Form::create(
$this,
__FUNCTION__,
FieldList::create([
TextField::create('Name'),
CheckboxSetField::create('Teams')
->setSource(Team::get()->map()),
HiddenField::create('ID'),
]),
FieldList::create([
FormAction::create('doSubmitForm', 'Submit'),
]),
RequiredFieldsValidator::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.
namespace App\Control;
use App\Model\Player;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\Validation\RequiredFieldsValidator;
class MyController extends Controller
{
private static $url_segment = 'MyController';
private static $allowed_actions = [
'getForm',
];
private static $url_handlers = [
'Form' => 'getForm',
];
public function getForm()
{
return Form::create(
$this,
'Form',
FieldList::create([
TextField::create('Name'),
TextField::create('HometownTeam.Name'),
HiddenField::create('ID'),
]),
FieldList::create([
FormAction::create('doSubmitForm', 'Submit'),
]),
RequiredFieldsValidator::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
namespace App\Control;
use App\Model\Player;
use App\Model\Team;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\Validation\RequiredFieldsValidator;
class MyController extends Controller
{
private static $url_segment = 'MyController';
private static $allowed_actions = [
'getForm',
];
private static $url_handlers = [
'Form' => 'getForm',
];
public function getForm()
{
$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'),
]),
RequiredFieldsValidator::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();
}
}