Skip to content

[FEATURE] KeyValueStorage: Add new component for global and subject-scoped key/value pairs - #12146

Open
mjansenDatabay wants to merge 19 commits into
ILIAS-eLearning:trunkfrom
mjansenDatabay:improvement/12/kv-storage-with-subjects
Open

mjansenDatabay wants to merge 19 commits into
ILIAS-eLearning:trunkfrom
mjansenDatabay:improvement/12/kv-storage-with-subjects

Conversation

@mjansenDatabay

@mjansenDatabay mjansenDatabay commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Provenance and AI Usage

This PR builds on and supersedes #11650. It keeps the KeyValueStorage introduced there and closes the subject question that PR left open. It is based on discussions around several UI / Removing of Legacy-UIComponents-Service and Table (LUI) topics between @thibsy, @oliversamoila, @fhelfer and @mjansenDatabay. @thibsy provided important feedback and impulses throughout. The implementation was produced with AI assistance; the tool followed the architectural direction of those contributors and was not an independent author of the design. A substantial simplification review by @chfsx is incorporated, as is his review of the subject model: the provider is part of the subject, in the API and in the table.

Summary

This PR introduces KeyValueStorage: namespace-scoped key-value storage for application state (for example table sort column, wizard step, collapsed panels).

It is intentionally distinct from ilSetting, ILIAS\Cache, and ILIAS\User\Settings. Please read components/ILIAS/KeyValueStorage/README.md for the API, delimitation, privacy notes, and what is still missing.

Consumers use Services::session(), Services::persistent(), or Services::persistentFor() and receive a namespace-scoped Store. Values are JSON. An in-request memoization avoids repeated session or database reads within the same request, and skips the write when the value equals what this request has already read or written.

The 3 Base Changes

1) The KeyValueStorage Component

Owns the consumer API (Services / Store), key and value rules, the persistent repository (kvs_store), the subject model, and setup. Session persistence is the one scope it cannot implement itself, because the session belongs to Authentication: it therefore defines SessionRepository and uses Authentication's implementation.

2) Session backend and the authenticated user — Authentication

Implements SessionRepository on top of the ILIAS session. Session scope is the natural fit for state that must not survive logout, including anonymous actors.

Authentication also contributes the subject provider authentication. The subject id is the user id. When a user account is deleted, Authentication calls Services::purgeSubject() for that subject. The listener still reaches the services through global $DIC, because ilAppEventListener is static; that is noted in the Authentication roadmap.

3) Persistent backend — Database

Provides ILIAS\Database\Connection (lazy accessor) so components can obtain ilDBInterface during wiring without reaching into $DIC themselves. KeyValueStorage uses that connection for the persistent scope. Database no longer owns the KV table.

Subjects

kvs_store has the primary key (provider, subject, namespace, keyword). Global rows use an empty provider and an empty subject. A subject's rows are addressed by the provider that named it and by the id that provider assigned.

A component that names subjects contributes a SubjectProvider. The provider name is read from that object (name()), for example authentication. Two providers with the same name fail the build (composer dump-autoload / php cli/setup.php build), so a collision is rejected before any rows can be mixed. A provider is constructed without arguments for that check.

new SubjectId($provider, $id) takes the provider object. persistentFor() and purgeSubject() accept only a subject of a contributed provider, and only when the class that built the id is the class that was contributed under that name. Purging one subject is DELETE WHERE provider = ? AND subject = ? and reaches only that provider's rows.

An anonymous subject cannot be persisted. That state stays in the session scope.

Subject rows of provider authentication are personal data (the subject id is the user id). They are removed on deleteUser. Global rows have no user reference. Details are in components/ILIAS/KeyValueStorage/PRIVACY.md.

UI Storage

UI\Storage is still adapted in Authentication; moving the adapter into UI is a follow-up, so that the UI owns the namespace and the keys.

The adapter chooses the store on every access. A fully authenticated user keeps UI state in the persistent subject scope. Everyone else keeps it in the session. The stores are memoized by Services, and a value that did not change is not written again, which matters because tables store their view-control state on every rendering.

Keys may contain characters that appear in PHP class names (for example \), so existing UI table storage ids continue to work. Colon and control characters remain rejected because repositories use : when composing identifiers.

Overview

consumer overview public-api repositories subjects

We'd appreciate input on the preferred directions for the following next steps:

  • UI table state — we should agree on how the UI framework finally wants to handle table state: sort direction and sort column, pagination, filters, row settings, selection of visible columns, and related view-control persistence.

mjansenDatabay and others added 13 commits September 18, 2026 11:35
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…nsumers

Refactored `StorageNamespace` to use a list of segments
instead of string-based namespaces. Updated all
relevant usages, tests, and documentation to align with
the new implementation, ensuring separation of namespace
segments and proper validation.
…t()`

This commit suggests enhancing the KeyValueStorage to
integrate Refinery transformations in the `get()` method.
Updated the API, documentation, and tests to support default
transformations and validation.
…any`

Extended KeyValueStorage with methods to retrieve all keys
in a namespace (`keys`), read all entries (`readAll`), and
fetch multiple keys with Refinery transformations (`getMany`).
…urging

Introduced "subject" as a new storage scope in KeyValueStorage,
enabling per-subject keyed storage alongside session and global
persistence. Enhanced Authentication to purge subject-specific
data upon user deletion. Updated database schema, documentation,
and tests to reflect these changes.
…urging

Introduced "subject" as a new storage scope in KeyValueStorage,
enabling per-subject keyed storage alongside session and global
persistence. Enhanced Authentication to purge subject-specific
data upon user deletion. Updated database schema, documentation,
and tests to reflect these changes.

@chfsx chfsx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mjansenDatabay, keeping the subject out of namespace and key and letting the owning component
name it through a SubjectResolver reads well, and purging on user deletion closes the gap #11650
left open.

One thing I would like to see changed before this goes in: a subject does not know where it comes
from. SubjectId is a free segment, and u plus the user id belonging to Authentication is a
convention nothing enforces. If a second provider ever produces the same segment, both share the
same rows: they read and overwrite each other's values, because (subject, namespace, keyword) is
identical, and purgeSubject() for one deletes the data of the other.

I would make the origin part of the subject, in the API and in the table:

kvs_store
  provider   -- e.g. 'authentication', '' for global state
  subject    -- e.g. '42'
  namespace
  keyword
  value
  PK (provider, subject, namespace, keyword)
  • Providers are contributed, e.g. $contribute[SubjectProvider::class] in Authentication, and
    KeyValueStorage seeks them and rejects a provider name that is used twice, so a collision fails
    the build instead of mixing data.
  • The provider of a SubjectId comes from the registered provider, not from the consumer, and
    persistentFor() / purgeSubject() only accept subjects of registered providers.
  • Purging becomes DELETE WHERE provider = ? AND subject = ? and can only reach the provider's
    own rows; removing everything of a provider (e.g. when a plugin is uninstalled) is
    DELETE WHERE provider = ?. Both are covered by the primary key.
  • The u prefix is no longer needed, the subject is just the user id.

Since the table is not on trunk yet, now is the cheapest moment for this.

Three smaller remarks inline.

One question on the way forward: this PR contains all of #11650 and changes its update step in
place. Is #11650 superseded by this one? If so, closing it would avoid both being merged, which the
in-place change of step_1 would not survive.

Comment thread components/ILIAS/Authentication/src/KeyValueStorage/UiStorageAdapter.php Outdated
Comment thread components/ILIAS/Authentication/src/Domain/AuthenticatedUserSubjectId.php Outdated
Comment thread components/ILIAS/KeyValueStorage/src/Internal/NamespacedStore.php Outdated
@mjansenDatabay

Copy link
Copy Markdown
Contributor Author

Hi @chfsx ,

thx for the review. I will try to address your remarks ASAP, but I cannot guarantee that it is going to be done before the next JF.

One question on the way forward: this PR contains all of #11650 and changes its update step in
place. Is #11650 superseded by this one? If so, closing it would avoid both being merged, which the
in-place change of step_1 would not survive.

Yes, this PR contains all of/is based on #11650 . If we agree to move on with the suggested approach here + your requested changes, we can close #11650 .

Best regards,
Michael

@chfsx

chfsx commented Oct 1, 2026

Copy link
Copy Markdown
Member

I try to implement the chabges asap and will make a Pr against your branch

@chfsx

chfsx commented Oct 1, 2026

Copy link
Copy Markdown
Member

@mjansenDatabay the changes from my review are in mjansenDatabay#32, against your branch, one commit per topic.

@mjansenDatabay

Copy link
Copy Markdown
Contributor Author

@mjansenDatabay the changes from my review are in mjansenDatabay#32, against your branch, one commit per topic.

Faster than the speed of light 😉. I'll have a look tomorrow.

@mjansenDatabay mjansenDatabay added the authorities Pull requests that contain updates to the authorities of a component. label Oct 5, 2026
@mjansenDatabay
mjansenDatabay requested a review from chfsx October 5, 2026 07:01
@matthiaskunkel

matthiaskunkel commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Jour Fixe, 05 OCT 2026: @mjansenDatabay gave an comprehensive presentation of this PR. We highly appreciate this suggestion and accept it for ILIAS 12. The authorities for the new component "KeyValuePairs" has already been accepted by the Technical Board.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authorities Pull requests that contain updates to the authorities of a component. improvement php Pull requests that update Php code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants