Skip to content

Commit

Permalink
Component: add Activities (Draft for Jepthe)
Browse files Browse the repository at this point in the history
  • Loading branch information
klees committed Oct 16, 2024
1 parent 06aba7c commit 70bf31b
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 0 deletions.
68 changes: 68 additions & 0 deletions components/ILIAS/Component/src/Activities/Activity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

namespace ILIAS\Component\Activities;

use ILIAS\Component\Dependencies\Name;
use ILIAS\UI\Component\Input\Control\Form\FormInput;
use ILIAS\Data\Result;

/**
* An Activity is an action on the domain layer action of a component.
*
* This defines the interface to any activity. When implementing Activities,
* you should use one of these base classes:
*
*
*
*/
interface Activity
{
public function getName(): Name;
public function getType(): ActivityType;
public function getDescription(): string; // shall be TextHandling/Markdown some day
public function getInputDescription(): FormInput; // might better be ILIAS/UI/Input/Input, but we would need to promote many properties there before.

/**
* This shall check if the given user is allowed to perform the activity based
* on business rules of this component. This shall, for example, check if the
* given user may add this other user to a course based on RBAC and position
* permissions, but this shall not check overall business rules such as: root
* may do everything. This shall not cause any observable side effects.
*
* @param mixed $parameters whatever the `FormInput` from `getInputDescription` produces.
*/
public function isAllowedToPerform(int $usr_id, mixed $parameters): bool;

/**
* This shall perform the activity. This shall not check if a user is allowed to perform the activity.
*
* @throws any SPL Exception (https://www.php.net/manual/en/spl.exceptions.php)
* @param mixed $parameters whatever the `FormInput` from `getInputDescription` produces.
*/
public function perform(mixed $parameters): mixed;

/**
* Grinds the $raw_parameters through the input description, checks if the users
* is allowed to perform the action as requested and, if so, then attempts to
* performs it. Wraps the result and possible errors in the `Result` type.
*/
public function maybePerformAs(int $usr_id, array $raw_parameters): Result;
}
27 changes: 27 additions & 0 deletions components/ILIAS/Component/src/Activities/ActivityType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

namespace ILIAS\Component\Activities;

enum ActivityType: string
{
case Command = "command";
case Query = "query";
}
154 changes: 154 additions & 0 deletions components/ILIAS/Component/src/Activities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Activities

## Abstract

The aim of the facilities in this folder is to allow to build a discernible layer
where developers can find "Activities" with objects and services in the system.

**An Activity is a way in which a user would want to use our system or the a
subsystem thereof.** Hence, Activities target the domain layer of our system
and the various subsystems and make the things that user do there referenceable.
Examples for activities would be things like: "Create a Course", "Add Members to
a Group", "Make a Test available.".

These examples, superficially, might look as if they could be mapped onto according
calls on existing objects, such as `ilObjCourse::create`, `ilParticipants::addMember`
or `ilObjTest::setOnline`. But in reality this is not the case. When users say they
want to create a course, they do no only mean to programmatically "Create a ilObjCourse"
PHP-object. The object needs to be put in the Repository somewhere. Permissions need
to be checked. Same for "Add Members to a Group". This is what the "Domain" of our
software is, in difference to the programming that we use to implement that domain.

By explicitly introducing Activities via a common framework, the system and its
components gain a lot of benefits:

* Activities offer a way to present a domain level API to other developers, webservices
and end users in a machine-readable way.
* Activities offer a documentation in the ILIAS GUI for end users to take advantage
of the available services e.g. in webservice integrations or workflow definitions.
* Activities to maintain such APIs in the components where they arise but still
present them to the complete system in a consumer agnostic way.

## Design

From the perspective of a component, every [**Activity**](./Activity.php) is provided
as the general way to use the component, together with according information and
facilities to make actual use of the activity. Activities have a name that is the
class name of the activity. They have a description in markdown format that shall
give additional information about the Activity that can not be derived from its name.
Activities describe their input as a FormInput from the UI framework, which is a
common and widely useful way to talk about user inputs to the system. The central
functions of any `Activity` are one function to check the permission for a given
user to perform a certain activity and the method to actually perform it.

Activities are contributed to the system via the [according mechanisms of the
component framework](docs/development/components-and-directories.md#contribute-to-service-or-functionality).
Activities can use then use the mechanisms from the component framework to use
facilities from other components. An Activity can and should also use activities
from other components, if it requires cooperation from other domains. Depending
on the case at hand, the implementor should then decide if a permission check for
the Activity at hand should or should not delegate to checks to sub permissions.
Any dependency of an Activity should be introduced via dependency injection.

Activities come in two flavors:

* A `Command` changes the state of the system, but does not return data from the
system. It can and should of course return information e.g. regarding the success
of the state change. And it can and should of course use data from the system
to perform the command.
* A `Query` returns data from the system, but does not induce state changes. A state
change is understood as a change in the business data of the system, even queries
could e.g. cause entries in logs.

This differentiation is important to allow the system to make various decisions,
such as:

* If the Activity is performed via an HTTP endpoint: Would it be GET or POST?
* Can I reorder this sequence of Activities without changing meaning?
* Can I cache the results of that Activity? Do I expect the same results upon
performing the same Activity twice?

That means that implementors of Activities should carefully decide which type an
Activity actually has to prevent side effects.

The flavors are represented as a property on activities to allow the framework to
combine activities. Some `Query` that returns a list of users and some `Command`
that acts on one user could be combined to a `Command` that act on multiple users.
Facilities to allow for combinations won't be implemented in the first iterations
on the framework, though.

## Usage

### As User of a Specific Activity

For many use cases you will want to perform a specific activity. A table of members,
for example, will need a specific `Query` to get the data for this exact table. If
we want to do something with a row in that table, we also should know which exact
`Command` it is that should be performed.

For this cases, you most likely should [pull](docs/development/components-and-directories.md#pull-code)
the required Activity as a dependency into the location where you need it, e.g.
a GUI class. You will need this exact `Activity` after all, not any similar `Activity`
or a some stand in.

If the `Activity` at hand is an activity of your component, or some very closely
related component, you might already have the correct `$parameters` at hand anyway
and you could use `isAllowedToPerform` and `perform` in appropriate locations. An
activity "Remove Member" for example, belongs to the component that shows the table
of members.

If the components are only losely related, you might be better of using `maybePerformAs`
with the parameters provided as primitives in an array. This method will internally
mangle the parameters accordingly via the `InputDescription`. This should be more
robust with regards to changes in the other component, such as changes in internal
data representation or inputs that are accepted. "Send Mail to Member" for example,
could be considered an activity from a losely related component for our member table.

### As User of Generic Activities

For some use cases, you won't be interested in any specific `Activity` but instead
you will want to have access to all activities provided by the system. Just similar
to the Kitchen Sink, we may want to build an automated documentation about all
available activities some day.

For this case, you most likely will want to use the [`Repository`](components/ILIAS/Component/src/Activities/Repository.php)
of activities. The Repository will allow you to select some or all Activities from
the system.

To deal with a single but generic `Activity` from the `Repository`, keep in mind
that the input to the `Activity` as described by `getInputDescription` is a quite
generic tool, although being defined in terms of the UI framework:

* the `FormInput` can be used verbatim to collect input from users via the GUI
* `FormInput` supports input in JSON format, so it should be simple to use it when
serialization comes into play
* `FormInputs` provide information about the single fields that can be used as
a description of the expected inputs
* `FormInput` can be used to get appropriatly represented data to be used as
`$parameters` for `isAllowedToPerform` and `perform` without needing to know
the actual content or representation of said data

By using the `Repository` and the information and methods from a single `Activity`,
you should be able to get a firm grip on each and every `Activity` in the system
without the requirement to actually know any `Activity` individually.

### As Implementor

Please take the documentation in `Activity` seriously. The methods in every `Activity`
have a specific relation to each other that need to be maintained to allow the
coherent usage of activities, be it specific activities or generic activities as
described above. This e.g. means:

* If an input is accepted by `getInputDescription` it shall not make `isAllowedToPerform` or `perform` crash.
* `maybePerformAs` shall use `getInputDescription`, `isAllowedToPerform` and `perform`
in a very specific pattern.
* If `getType` is `Query`, perform shouldn't cause side effects.

To simplify the implementation, the documentation for the class lists some base
classes that could be used for the implementation.

If you have implemented activities for your component you should integrate them
with the system according to the [existing integration mechanisms](docs/development/components-and-directories.md):

* You should [provide them with as specific name](docs/development/components-and-directories.md#pull-code), so other components can pull them.
* You should [contribute them to the system](docs/development/components-and-directories.md#contribute-to-service-or-functionality) so generic facilities such as the `Repository` can discover them as well.
32 changes: 32 additions & 0 deletions components/ILIAS/Component/src/Activities/Repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

namespace ILIAS\Component\Activities;

interface Repository
{
/**
* Get all activities where the name matches the provided regexp.
*
* @param string $name_matcher as preg_match can understand
* @return Iterator<string, Activity> where keys are the name
*/
public function getActivitiesByName(string $name_matcher, ?ActivityType $type = null, ?Range $range = null): Iterator;
}

0 comments on commit 70bf31b

Please sign in to comment.