Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session GridField state manager #11288

Open
wants to merge 5 commits into
base: 5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Forms/GridField/GridFieldDetailForm_ItemRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,13 @@ protected function getFormActions()

$gridState = $this->gridField->getState(false);
$actions->push(HiddenField::create($manager->getStateKey($this->gridField), null, $gridState));
if (ClassInfo::hasMethod($manager, 'getStateRequestVar')) {
$stateRequestVar = $manager->getStateRequestVar();
$stateValue = $this->getRequest()->requestVar($stateRequestVar);
if ($stateValue) {
$actions->push(HiddenField::create($stateRequestVar, '', $stateValue));
Comment on lines 456 to +461
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both of these hidden fields? Can you please give me a short rundown of what they each do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not really need both no.
The old one is there for backwards compatibility with the default GridStateManager. That one contains the whole grid field state as json data.
The new one (from the getStateRequestVar method) needs a different value, i.e. the given state key value from the request. It is needed with SessionGridFieldStateManager to retain the session state key when for example saving a record.

}
}
Comment on lines 456 to +463
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per #11288 (comment)

Suggested change
$actions->push(HiddenField::create($manager->getStateKey($this->gridField), null, $gridState));
if (ClassInfo::hasMethod($manager, 'getStateRequestVar')) {
$stateRequestVar = $manager->getStateRequestVar();
$stateValue = $this->getRequest()->requestVar($stateRequestVar);
if ($stateValue) {
$actions->push(HiddenField::create($stateRequestVar, '', $stateValue));
}
}
if (ClassInfo::hasMethod($manager, 'getStateRequestVar')) {
$stateRequestVar = $manager->getStateRequestVar();
$stateValue = $this->getRequest()->requestVar($stateRequestVar);
if ($stateValue) {
$actions->push(HiddenField::create($stateRequestVar, '', $stateValue));
}
} else {
$actions->push(HiddenField::create($manager->getStateKey($this->gridField), null, $gridState));
}


$actions->push($this->getRightGroupField());
} else { // adding new record
Expand Down
7 changes: 6 additions & 1 deletion src/Forms/GridField/GridState.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private function mergeValues(GridState_Data $data, array $array): void
public function getData()
{
if (!$this->data) {
$this->data = new GridState_Data();
$this->data = new GridState_Data([], $this);
}

return $this->data;
Expand All @@ -99,6 +99,11 @@ public function getList()
return $this->grid->getList();
}

public function getGridField(): GridField
{
return $this->grid;
}

/**
* Returns a json encoded string representation of this state.
*
Expand Down
24 changes: 20 additions & 4 deletions src/Forms/GridField/GridState_Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace SilverStripe\Forms\GridField;

use SilverStripe\Core\ClassInfo;

/**
* Simple set of data, similar to stdClass, but without the notice-level
* errors.
Expand All @@ -10,29 +12,33 @@
*/
class GridState_Data
{
use GridFieldStateAware;

/**
* @var array
*/
protected $data;

protected ?GridState $state;

protected $defaults = [];

public function __construct($data = [])
public function __construct($data = [], ?GridState $state = null)
{
$this->data = $data;
$this->state = $state;
}

public function __get($name)
{
return $this->getData($name, new GridState_Data());
return $this->getData($name, new GridState_Data([], $this->state));
}

public function __call($name, $arguments)
{
// Assume first parameter is default value
if (empty($arguments)) {
$default = new GridState_Data();
$default = new GridState_Data([], $this->state);
} else {
$default = $arguments[0];
}
Expand Down Expand Up @@ -72,16 +78,25 @@ public function getData($name, $default = null)
$this->data[$name] = $default;
} else {
if (is_array($this->data[$name])) {
$this->data[$name] = new GridState_Data($this->data[$name]);
$this->data[$name] = new GridState_Data($this->data[$name], $this->state);
}
}

return $this->data[$name];
}

public function storeData()
{
$stateManager = $this->getStateManager();
if (ClassInfo::hasMethod($stateManager, 'storeState') && $this->state) {
$stateManager->storeState($this->state->getGridField(), $this->state->Value());
}
}

public function __set($name, $value)
{
$this->data[$name] = $value;
$this->storeData();
}

public function __isset($name)
Expand All @@ -92,6 +107,7 @@ public function __isset($name)
public function __unset($name)
{
unset($this->data[$name]);
$this->storeData();
}

public function __toString()
Expand Down
101 changes: 101 additions & 0 deletions src/Forms/GridField/SessionGridFieldStateManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace SilverStripe\Forms\GridField;

use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;

/**
* Creates a unique key for managing GridField states in user Session, for both storage and retrieval.
* Only stores states and generates a session key if a state is requested to be stored
* (i.e. the state is changed from the default).
* If a session state key is present in the request, it will always be used instead of generating a new one.
*/
class SessionGridFieldStateManager implements GridFieldStateManagerInterface
{
protected static $state_ids = [];

protected function getStateID(GridField $gridField, $create = false): ?string
{
$requestVar = $this->getStateRequestVar();
$sessionStateID = $gridField->getForm()?->getRequestHandler()->getRequest()->requestVar($requestVar);
if (!$sessionStateID) {
$sessionStateID = Controller::curr()->getRequest()->requestVar($requestVar);
}
if ($sessionStateID) {
return $sessionStateID;
}
$stateKey = $this->getStateKey($gridField);
if (isset(self::$state_ids[$stateKey])) {
$sessionStateID = self::$state_ids[$stateKey];
} elseif ($create) {
$sessionStateID = substr(md5(time()), 0, 8);
// we don't want session state id to be strictly numeric, since this is used as a session key,
// and session keys in php has to be usable as variable names
if (is_numeric($sessionStateID)) {
$sessionStateID .= 'a';
}
self::$state_ids[$stateKey] = $sessionStateID;
}
return $sessionStateID;
}

public function storeState(GridField $gridField, $value = null)
{
$sessionStateID = $this->getStateID($gridField, true);
$sessionState = Controller::curr()->getRequest()->getSession()->get($sessionStateID);
if (!$sessionState) {
$sessionState = [];
}
$stateKey = $this->getStateKey($gridField);
$sessionState[$stateKey] = $value ?? $gridField->getState(false)->Value();
Controller::curr()->getRequest()->getSession()->set($sessionStateID, $sessionState);
}

public function getStateRequestVar(): string
{
return 'gridSessionState';
}

/**
* @param GridField $gridField
* @return string
*/
public function getStateKey(GridField $gridField): string
{
$record = $gridField->getForm()?->getRecord();
return $gridField->getName() . '-' . ($record ? $record->ID : 0);
}

/**
* @param GridField $gridField
* @param string $url
* @return string
*/
public function addStateToURL(GridField $gridField, string $url): string
{
$sessionStateID = $this->getStateID($gridField);
if ($sessionStateID) {
return Controller::join_links($url, '?' . $this->getStateRequestVar() . '=' . $sessionStateID);
}
return $url;
}

/**
* @param GridField $gridField
* @param HTTPRequest $request
* @return string|null
*/
public function getStateFromRequest(GridField $gridField, HTTPRequest $request): ?string
{
$gridSessionStateID = $request->requestVar($this->getStateRequestVar());
if ($gridSessionStateID) {
$sessionState = $request->getSession()->get($gridSessionStateID);
$stateKey = $this->getStateKey($gridField);
if ($sessionState && isset($sessionState[$stateKey])) {
return $sessionState[$stateKey];
}
}
return null;
}
}
140 changes: 140 additions & 0 deletions tests/php/Forms/GridField/SessionGridFieldStateManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

namespace SilverStripe\Forms\Tests\GridField;

use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldStateManagerInterface;
use SilverStripe\Forms\GridField\SessionGridFieldStateManager;
use SilverStripe\Forms\Tests\GridField\GridFieldPrintButtonTest\TestObject;

class SessionGridFieldStateManagerTest extends SapphireTest
{
protected function setUp(): void
{
parent::setUp();
// configure the injector to use the session grid field state manager
Injector::inst()->registerService(new SessionGridFieldStateManager(), GridFieldStateManagerInterface::class);
}

public function testStateKey()
{
$manager = new SessionGridFieldStateManager();
$controller = new Controller();
$form1 = new Form($controller, 'form1', new FieldList(), new FieldList());
$testObject = new TestObject();
$testObject->ID = 1;
$form2 = new Form($controller, 'form2', new FieldList(), new FieldList());
$form2->loadDataFrom($testObject);

$grid1 = new GridField('A');
$grid2 = new GridField('B');
$grid1->setForm($form1);
$grid2->setForm($form2);
$this->assertEquals('A-0', $manager->getStateKey($grid1));
$this->assertEquals('B-1', $manager->getStateKey($grid2));
}

public function testAddStateToURL()
{
$manager = new SessionGridFieldStateManager();
$grid = new GridField('TestGrid');
$grid->getState()->testValue = 'foo';
$stateRequestVar = $manager->getStateRequestVar();
$link = '/link-to/something';
$this->assertTrue(
preg_match(
"|^$link\?{$stateRequestVar}=[a-zA-Z0-9]+$|",
$manager->addStateToURL($grid, $link)
) == 1
);

$link = '/link-to/something-else?someParam=somevalue';
$this->assertTrue(
preg_match(
"|^/link-to/something-else\?someParam=somevalue&{$stateRequestVar}=[a-zA-Z0-9]+$|",
$manager->addStateToURL($grid, $link)
) == 1
);
}

public function testGetStateFromRequest()
{
$manager = new SessionGridFieldStateManager();

$session = new Session([]);
$request = new HTTPRequest(
'GET',
'/link-to/something',
[
$manager->getStateRequestVar() => 'testGetStateFromRequest'
]
);
$request->setSession($session);

$controller = new Controller();
$controller->setRequest($request);
$controller->pushCurrent();
$form = new Form($controller, 'form1', new FieldList(), new FieldList());
$grid = new GridField('TestGrid');
$grid->setForm($form);

$grid->getState()->testValue = 'foo';
$state = $grid->getState(false)->Value() ?? '{}';
$result = $manager->getStateFromRequest($grid, $request);

$this->assertEquals($state, $result);
$controller->popCurrent();
}

public function testDefaultStateLeavesURLUnchanged()
{
$manager = new SessionGridFieldStateManager();
$grid = new GridField('DefaultStateGrid');
$grid->getState(false)->getData()->testValue->initDefaults(['foo' => 'bar']);
$link = '/link-to/something';

$this->assertEquals('{}', $grid->getState(false)->Value());

$this->assertEquals(
'/link-to/something',
$manager->addStateToURL($grid, $link)
);
}

public function testStoreState()
{
$manager = new SessionGridFieldStateManager();

$session = new Session([]);
$request = new HTTPRequest(
'GET',
'/link-to/something',
[
$manager->getStateRequestVar() => 'testStoreState'
]
);
$request->setSession($session);

$controller = new Controller();
$controller->setRequest($request);
$controller->pushCurrent();
$form = new Form($controller, 'form1', new FieldList(), new FieldList());
$grid = new GridField('TestGrid');
$grid->setForm($form);

$grid->getState()->testValue = 'foo';
$state = $grid->getState(false)->Value() ?? '{}';

$manager->storeState($grid);
$this->assertEquals($state, $session->get('testStoreState')['TestGrid-0']);

$controller->popCurrent();
}
}
Loading