diff --git a/demos/_includes/Session.php b/demos/_includes/Session.php
index 8fa9ad2605..09fcb06049 100644
--- a/demos/_includes/Session.php
+++ b/demos/_includes/Session.php
@@ -4,11 +4,19 @@
namespace Atk4\Ui\Demos;
+use Atk4\Core\AppScopeTrait;
use Atk4\Core\NameTrait;
-use Atk4\Core\SessionTrait;
+use Atk4\Ui\App;
+use Atk4\Ui\SessionTrait;
class Session
{
+ use AppScopeTrait;
use NameTrait;
use SessionTrait;
+
+ public function __construct(App $app)
+ {
+ $this->setApp($app);
+ }
}
diff --git a/demos/interactive/modal.php b/demos/interactive/modal.php
index 586072356f..5bcff81a8d 100644
--- a/demos/interactive/modal.php
+++ b/demos/interactive/modal.php
@@ -11,7 +11,7 @@
\Atk4\Ui\Header::addTo($app, ['Modal View']);
-$session = new Session();
+$session = new Session($app);
// Re-usable component implementing counter
\Atk4\Ui\Header::addTo($app, ['Static Modal Dialog']);
diff --git a/demos/interactive/popup.php b/demos/interactive/popup.php
index e17f94e48e..5036e50082 100644
--- a/demos/interactive/popup.php
+++ b/demos/interactive/popup.php
@@ -20,7 +20,8 @@
/** @var \Atk4\Ui\Lister $cartClass */
$cartClass = AnonymousClassNameCache::get_class(fn () => new class() extends \Atk4\Ui\Lister {
- use \Atk4\Core\SessionTrait;
+ use \Atk4\Ui\SessionTrait;
+
public $items = [];
public $defaultTemplate = 'lister.html';
diff --git a/docs/session.rst b/docs/session.rst
new file mode 100644
index 0000000000..de5dde6ad7
--- /dev/null
+++ b/docs/session.rst
@@ -0,0 +1,62 @@
+=============
+Session Trait
+=============
+
+.. php:trait:: SessionTrait
+
+
+Introduction
+============
+
+SessionTrait is a simple way to let object store relevant data in the session. Specifically used in ATK UI
+some objects want to memorize data. (see https://github.com/atk4/ui/blob/develop/src/Wizard.php#L12)
+
+You would need 3 things. First make use of session trait::
+
+ use \Atk4\Ui\SessionTrait;
+
+next you may memorize any value, which will be stored independently from any other object (even of a same class)::
+
+ $this->memorize('dsn', $dsn);
+
+Later when you need the value, you can simply recall it::
+
+ $dsn = $this->recall('dsn');
+
+
+Properties
+==========
+
+.. php:attr:: session_key
+
+ Internal property to make sure that all session data will be stored in one
+ "container" (array key).
+
+Methods
+=======
+
+.. php:method:: startSession($options = [])
+
+ Create new session.
+
+.. php:method:: destroySession()
+
+ Destroy existing session.
+
+.. php:method:: memorize($key, $value)
+
+ Remember data in object-relevant session data.
+
+.. php:method:: learn($key, $default = null)
+
+ Similar to memorize, but if value for key exist, will return it.
+
+.. php:method:: recall($key, $default = null)
+
+ Returns session data for this object. If not previously set, then $default
+ is returned.
+
+.. php:method:: forget($key = null)
+
+ Forget session data for arg $key. If $key is omitted will forget all
+ associated session data.
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 8aada406cd..a3f9f2158d 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -211,6 +211,10 @@ parameters:
-
path: 'src/Panel/Right.php'
message: '~^Call to an undefined method Atk4\\Ui\\JsExpression::addPanel\(\)\.$~'
+ -
+ path: 'src/SessionTrait.php'
+ count: 4
+ message: '~^Access to an undefined property Atk4\\Ui\\Tests\\SessionAbstractMock::\$name\.$~'
-
path: 'src/Table/Column.php'
message: '~^Call to an undefined method Atk4\\Ui\\AbstractView::setHoverable\(\)\.$~'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d59efca385..60055a9dde 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -13,6 +13,7 @@
demos_http
+ require_session
diff --git a/src/App.php b/src/App.php
index 6665f425d0..dc045afe8a 100644
--- a/src/App.php
+++ b/src/App.php
@@ -105,6 +105,9 @@ class App
/** @var Persistence|Persistence\Sql */
public $db;
+ /** @var App\Session */
+ public $session;
+
/** @var string[] Extra HTTP headers to send on exit. */
protected $response_headers = [
self::HEADER_STATUS_CODE => '200',
@@ -136,39 +139,11 @@ class App
public $templateClass = HtmlTemplate::class;
- /**
- * @param array $defaults
- */
- public function __construct($defaults = [])
+ public function __construct(array $defaults = [])
{
$this->setApp($this);
- // Process defaults
- if (is_string($defaults)) {
- $defaults = ['title' => $defaults];
- }
-
- if (isset($defaults[0])) {
- $defaults['title'] = $defaults[0];
- unset($defaults[0]);
- }
-
- /*
- if (is_array($defaults)) {
- throw (new Exception('Constructor requires array argument'))
- ->addMoreInfo('arg', $defaults);
- }*/
$this->setDefaults($defaults);
- /*
-
- foreach ($defaults as $key => $val) {
- if (is_array($val)) {
- $this->{$key} = array_merge(is_array($this->{$key} ?? null) ? $this->{$key} : [], $val);
- } elseif ($val !== null) {
- $this->{$key} = $val;
- }
- }
- */
$this->setupTemplateDirs();
@@ -208,11 +183,14 @@ static function (int $severity, string $msg, string $file, int $line): bool {
$this->setupAlwaysRun();
}
- // Set up UI persistence
if ($this->ui_persistence === null) {
$this->ui_persistence = new UiPersistence();
}
+ if ($this->session === null) {
+ $this->session = new App\Session();
+ }
+
// setting up default executor factory.
$this->executorFactory = Factory::factory([ExecutorFactory::class]);
}
diff --git a/src/App/Session.php b/src/App/Session.php
new file mode 100644
index 0000000000..45f680b606
--- /dev/null
+++ b/src/App/Session.php
@@ -0,0 +1,121 @@
+startSession();
+
+ $_SESSION[$this->session_key][$namespace][$key] = $value;
+
+ return $value;
+ }
+
+ /**
+ * Similar to memorize, but if value for key exist, will return it.
+ *
+ * @param mixed $default
+ *
+ * @return mixed Previously memorized data or $default
+ */
+ public function learn(string $namespace, string $key, $default = null)
+ {
+ $this->startSession();
+
+ if (!isset($_SESSION[$this->session_key][$namespace][$key])) {
+ if ($default instanceof \Closure) {
+ $default = $default($key);
+ }
+
+ return $this->memorize($namespace, $key, $default);
+ }
+
+ return $this->recall($namespace, $key);
+ }
+
+ /**
+ * Returns session data for this object. If not previously set, then
+ * $default is returned.
+ *
+ * @param mixed $default
+ *
+ * @return mixed Previously memorized data or $default
+ */
+ public function recall(string $namespace, string $key, $default = null)
+ {
+ $this->startSession();
+
+ if (!isset($_SESSION[$this->session_key][$namespace][$key])) {
+ if ($default instanceof \Closure) {
+ $default = $default($key);
+ }
+
+ return $default;
+ }
+
+ return $_SESSION[$this->session_key][$namespace][$key];
+ }
+
+ /**
+ * Forget session data for $key. If $key is omitted will forget all
+ * associated session data.
+ *
+ * @param string $key Optional key of data to forget
+ */
+ public function forget(string $namespace, string $key = null): void
+ {
+ $this->startSession();
+
+ if ($key === null) {
+ unset($_SESSION[$this->session_key][$namespace]);
+ } else {
+ unset($_SESSION[$this->session_key][$namespace][$key]);
+ }
+ }
+}
diff --git a/src/SessionTrait.php b/src/SessionTrait.php
new file mode 100644
index 0000000000..557d18204a
--- /dev/null
+++ b/src/SessionTrait.php
@@ -0,0 +1,72 @@
+getApp()->session;
+ }
+
+ /**
+ * Remember data in object-relevant session data.
+ *
+ * @param mixed $value
+ *
+ * @return mixed $value
+ */
+ public function memorize(string $key, $value)
+ {
+ return $this->getSession()->memorize($this->name, $key, $value);
+ }
+
+ /**
+ * Similar to memorize, but if value for key exist, will return it.
+ *
+ * @param mixed $default
+ *
+ * @return mixed Previously memorized data or $default
+ */
+ public function learn(string $key, $default = null)
+ {
+ return $this->getSession()->learn($this->name, $key, $default);
+ }
+
+ /**
+ * Returns session data for this object. If not previously set, then
+ * $default is returned.
+ *
+ * @param mixed $default
+ *
+ * @return mixed Previously memorized data or $default
+ */
+ public function recall(string $key, $default = null)
+ {
+ return $this->getSession()->recall($this->name, $key, $default);
+ }
+
+ /**
+ * Forget session data for $key. If $key is omitted will forget all
+ * associated session data.
+ *
+ * @param string $key Optional key of data to forget
+ *
+ * @return $this
+ */
+ public function forget(string $key = null)
+ {
+ $this->getSession()->forget($this->name, $key);
+
+ return $this;
+ }
+}
diff --git a/src/Table/Column/FilterModel.php b/src/Table/Column/FilterModel.php
index d19256a675..e14749a70d 100644
--- a/src/Table/Column/FilterModel.php
+++ b/src/Table/Column/FilterModel.php
@@ -4,19 +4,22 @@
namespace Atk4\Ui\Table\Column;
+use Atk4\Core\AppScopeTrait;
use Atk4\Core\NameTrait;
-use Atk4\Core\SessionTrait;
use Atk4\Data\Field;
use Atk4\Data\Model;
use Atk4\Data\Persistence;
use Atk4\Data\Types\Types as CustomTypes;
+use Atk4\Ui\App;
+use Atk4\Ui\SessionTrait;
use Doctrine\DBAL\Types\Types;
/**
* Implement a generic filter model for filtering column data.
*/
-class FilterModel extends Model
+abstract class FilterModel extends Model
{
+ use AppScopeTrait; // needed for SessionTrait
use NameTrait; // needed for SessionTrait
use SessionTrait;
@@ -32,13 +35,20 @@ class FilterModel extends Model
/** @var Field The field where this filter need to query data. */
public $lookupField;
+ public function __construct(App $app, array $defaults = [])
+ {
+ $this->setApp($app);
+
+ $persistence = new Persistence\Array_();
+
+ parent::__construct($persistence, $defaults);
+ }
+
/**
* Factory method that will return a FilterModel Type class.
*/
- public static function factoryType(Field $field): self
+ public static function factoryType(App $app, Field $field): self
{
- $persistence = new Persistence\Array_();
-
$class = [
Types::STRING => FilterModel\TypeString::class,
Types::TEXT => FilterModel\TypeString::class,
@@ -69,7 +79,9 @@ public static function factoryType(Field $field): self
$class = $field->filterModel;
}
- return new $class($persistence, ['lookupField' => $field]);
+ $filterModel = new $class($app, ['lookupField' => $field]);
+
+ return $filterModel;
}
protected function init(): void
@@ -118,10 +130,7 @@ public function recallData(): array
*
* @return Model
*/
- public function setConditionForModel(Model $model)
- {
- return $model;
- }
+ abstract public function setConditionForModel(Model $model);
/**
* Method that will set Field display condition in a form.
diff --git a/src/Table/Column/FilterPopup.php b/src/Table/Column/FilterPopup.php
index 4229089c36..d58655ceb9 100644
--- a/src/Table/Column/FilterPopup.php
+++ b/src/Table/Column/FilterPopup.php
@@ -38,10 +38,11 @@ class FilterPopup extends Popup
protected function init(): void
{
parent::init();
+
$this->setOption('delay', ['hide' => 1500]);
$this->setHoverable();
- $model = FilterModel::factoryType($this->field);
+ $model = FilterModel::factoryType($this->getApp(), $this->field);
$model = $model->createEntity();
$this->form = Form::addTo($this)->addClass('');
diff --git a/src/Wizard.php b/src/Wizard.php
index f881436b0f..6bab78d541 100644
--- a/src/Wizard.php
+++ b/src/Wizard.php
@@ -11,7 +11,7 @@
*/
class Wizard extends View
{
- use \Atk4\Core\SessionTrait;
+ use SessionTrait;
public $defaultTemplate = 'wizard.html';
public $ui = 'steps';
diff --git a/tests/SessionTraitTest.php b/tests/SessionTraitTest.php
new file mode 100644
index 0000000000..d3605d3b8c
--- /dev/null
+++ b/tests/SessionTraitTest.php
@@ -0,0 +1,156 @@
+app = new App([
+ 'catch_exceptions' => false,
+ 'always_run' => false,
+ ]);
+
+ session_abort();
+ $sessionDir = sys_get_temp_dir() . '/atk4_test__ui__session';
+ if (!file_exists($sessionDir)) {
+ mkdir($sessionDir);
+ }
+ ini_set('session.save_path', $sessionDir);
+ }
+
+ protected function tearDown(): void
+ {
+ session_abort();
+ $sessionDir = ini_get('session.save_path');
+ foreach (scandir($sessionDir) as $f) {
+ if (!in_array($f, ['.', '..'], true)) {
+ unlink($sessionDir . '/' . $f);
+ }
+ }
+
+ rmdir($sessionDir);
+
+ parent::tearDown();
+ }
+
+ public function testException1(): void
+ {
+ // when try to start session without NameTrait
+ $this->expectException(Exception::class);
+ $m = new SessionWithoutNameMock($this->app);
+ $m->memorize('test', 'foo');
+ }
+
+ public function testConstructor(): void
+ {
+ $m = new SessionMock($this->app);
+
+ $this->assertFalse(isset($_SESSION));
+ $m->getApp()->session->startSession();
+ $this->assertTrue(isset($_SESSION));
+ $m->getApp()->session->destroySession();
+ $this->assertFalse(isset($_SESSION));
+ }
+
+ /**
+ * Test memorize().
+ */
+ public function testMemorize(): void
+ {
+ $m = new SessionMock($this->app);
+ $m->name = 'test';
+
+ // value as string
+ $m->memorize('foo', 'bar');
+ $this->assertSame('bar', $_SESSION['__atk_session'][$m->name]['foo']);
+
+ // value as null
+ $m->memorize('foo', null);
+ $this->assertNull($_SESSION['__atk_session'][$m->name]['foo']);
+
+ // value as object
+ $o = new \stdClass();
+ $m->memorize('foo', $o);
+ $this->assertSame($o, $_SESSION['__atk_session'][$m->name]['foo']);
+
+ $m->getApp()->session->destroySession();
+ }
+
+ /**
+ * Test learn(), recall(), forget().
+ */
+ public function testLearnRecallForget(): void
+ {
+ $m = new SessionMock($this->app);
+ $m->name = 'test';
+
+ // value as string
+ $m->learn('foo', 'bar');
+ $this->assertSame('bar', $m->recall('foo'));
+
+ $m->learn('foo', 'qwerty');
+ $this->assertSame('bar', $m->recall('foo'));
+
+ $m->forget('foo');
+ $this->assertSame('undefined', $m->recall('foo', 'undefined'));
+
+ // value as callback
+ $m->learn('foo', function ($key) {
+ return $key . '_bar';
+ });
+ $this->assertSame('foo_bar', $m->recall('foo'));
+
+ $m->learn('foo_2', 'another');
+ $this->assertSame('another', $m->recall('foo_2'));
+
+ $v = $m->recall('foo_3', function ($key) {
+ return $key . '_bar';
+ });
+ $this->assertSame('foo_3_bar', $v);
+ $this->assertSame('undefined', $m->recall('foo_3', 'undefined'));
+
+ $m->forget();
+ $this->assertSame('undefined', $m->recall('foo', 'undefined'));
+ $this->assertSame('undefined', $m->recall('foo_2', 'undefined'));
+ $this->assertSame('undefined', $m->recall('foo_3', 'undefined'));
+ }
+}
+
+abstract class SessionAbstractMock
+{
+ use AppScopeTrait;
+ use SessionTrait;
+
+ public function __construct(App $app)
+ {
+ $this->setApp($app);
+ }
+}
+
+class SessionMock extends SessionAbstractMock
+{
+ use NameTrait;
+}
+
+class SessionWithoutNameMock extends SessionAbstractMock
+{
+}