Forms + Actions
###############
Actions are the primary way users interact with an Elgg site. Elgg forms submit to actions. Actions define the behavior for form submission.
Overview
========
An action in Elgg is the code that runs to make changes to the database when a user does something. For example, logging in, posting a comment, and making a blog post are actions. The action script processes input, makes the appropriate modifications to the database, and provides feedback to the user about the action.
Action Handler
==============
Actions are registered during the boot process by calling ``elgg_register_action()``. All actions URLs start with ``action/`` and are served by Elgg's front end controller through the routing service. The default action middleware performs :doc:`CSRF security checks `.
This guide assumes basic familiarity with:
- :doc:`/admin/plugins`
- :doc:`views`
- :doc:`i18n`
.. contents:: Contents
:local:
:depth: 2
Registering actions
===================
Actions must be registered before use.
There are two ways to register actions:
Using ``elgg_register_action()``
.. code-block:: php
elgg_register_action("example", __DIR__ . "/actions/example.php");
The ``mod/example/actions/example.php`` script will now be run whenever a form is submitted to ``http://localhost/elgg/action/example``.
Use ``elgg-plugin.php``
.. code-block:: php
return [
'actions' => [
// defaults to using an action file in /actions/myplugin/action_a.php
'myplugin/action_a' => [
'access' => 'public',
],
// define custom action path
'myplugin/action_b' => [
'access' => 'admin',
'filename' => __DIR__ . '/actions/action.php'
],
// define a controller
'myplugin/action_c' => [
'controller' => \MyPlugin\Actions\ActionC::class,
],
],
];
.. warning::
A stumbling point for many new developers is the URL for actions. The URL always uses ``/action/`` (singular) and
never ``/actions/`` (plural). However, action script files are usually saved under the directory ``/actions/`` (plural)
and always have an extension. Use ``elgg_generate_action_url()`` to avoid confusion.
Registering actions using plugin config file
--------------------------------------------
You can also register actions via the :doc:`elgg-plugin` config file.
To do this you need to provide an action section in the config file.
The location of the action files are assumed to be in the plugin folder ``/actions``.
.. code-block:: php
[
'blog/save' => [], // all defaults
'blog/delete' => [ // all custom
'access' => 'admin',
'filename' => __DIR__ . 'actions/blog/remove.php',
],
],
];
Permissions
-----------
By default, actions are only available to logged in users.
To make an action available to logged out users, pass ``"public"`` as the third parameter:
.. code-block:: php
elgg_register_action("example", $filepath, "public");
To restrict an action to only administrators, pass ``"admin"`` for the last parameter:
.. code-block:: php
elgg_register_action("example", $filepath, "admin");
To restrict an action to only logged out users, pass ``"logged_out"`` for the last parameter:
.. code-block:: php
elgg_register_action("example", $filepath, "logged_out");
Writing action files
--------------------
Use the ``get_input()`` function to get access to request parameters:
.. code-block:: php
$field = get_input('input_field_name', 'default_value');
You can then use the :doc:`database` api to load entities and perform actions on them accordingly.
To indicate a successful action, use ``elgg_ok_response()``. This function accepts data that you want to make available
to the client for XHR calls (this data will be ignored for non-XHR calls)
.. code-block:: php
$user = get_entity($guid);
// do something
$action_data = [
'entity' => $user,
'stats' => [
'friends_count' => $user->getEntitiesFromRelationship([
'type' => 'user',
'relationship' => 'friend',
'count' => true,
]);
],
];
return elgg_ok_response($action_data, 'Action was successful', 'url/to/forward/to');
To indicate an error, use ``elgg_error_response()``
.. code-block:: php
$user = elgg_get_logged_in_user_entity();
if (!$user) {
// show an error and forward the user to the referring page
// send 404 error code on AJAX calls
return elgg_error_response('User not found', REFERRER, ELGG_HTTP_NOT_FOUND);
}
if (!$user->canEdit()) {
// show an error and forward to user's profile
// send 403 error code on AJAX calls
return elgg_error_response('You are not allowed to perform this action', $user->getURL(), ELGG_HTTP_FORBIDDEN);
}
Writing action controllers
--------------------------
As an alternative to action files it is also possible to write the action code in a controller class.
You can extend a generic action class called ``\Elgg\Controllers\GenericAction`` to have it invoked with a set of functions in the following order:
* ``sanitize()`` - sanitize your input
* ``validate()`` - validate the input or check permissions
* ``executeBefore()`` - a preparation before the main execution
* ``execute()`` - the main execution of the action
* ``executeAfter()`` - executed after the main execution
* ``success()`` - if nothing went wrong handle a successful response
* ``error()`` - if one of the previous steps throws an exception they will be handled by the error step by showing the error as a system message and forwarding back to the ``REFERER``
For entity save/edit actions there is an additional helper controller ``\Elgg\Controllers\EntityEditAction``.
This controller will do most generic checks based on the fields configuration of an entity.
It will also create a river item on create of the new entity.
When using/extending this controller make sure to configure the entity type/subtype when registering the action.
.. code-block:: php
// elgg-plugin.php
return [
'actions' => [
'my_entity/save' => [
'controller' => \Elgg\Controllers\EntityEditAction::class,
'options' => [
'entity_type' => 'object',
'entity_subtype' => 'my_entity',
],
],
],
];
Customizing actions
-------------------
Before executing any action, Elgg triggers an event:
.. code-block:: php
$result = elgg_trigger_event_results('action:validate', $action, [], true);
Where ``$action`` is the action being called. If the event returns ``false`` then the action will not be executed. Don't return anything
if your validation passes.
Example: Captcha
^^^^^^^^^^^^^^^^
The captcha module uses this to intercept the ``register`` and ``user/requestnewpassword`` actions and redirect them to a
function which checks the captcha code. This check returns ``false`` if the captcha validation fails (which prevents the associated
action from executing).
This is done as follows:
.. code-block:: php
elgg_register_event_handler("action:validate", "register", "captcha_verify_action_event");
elgg_register_event_handler("action:validate", "user/requestnewpassword", "captcha_verify_action_event");
...
function captcha_verify_action_event(\Elgg\Event $event) {
$token = get_input('captcha_token');
$input = get_input('captcha_input');
if (($token) && (captcha_verify_captcha($input, $token))) {
return;
}
elgg_register_error_message(elgg_echo('captcha:captchafail'));
return false;
}
This lets a plugin extend an existing action without the need to replace the whole action. In the case of the captcha plugin it
allows the plugin to provide captcha support in a very loosely coupled way.
Actions available in core
=========================
``entity/delete``
-----------------
If your plugin does not implement any custom logic when deleting an entity, you can use bundled delete action
.. code-block:: php
$guid = 123;
// You can provide optional forward path as a URL query parameter
$forward_url = 'path/to/forward/to';
echo elgg_view('output/url', array(
'text' => elgg_echo('delete'),
'href' => elgg_generate_action_url('entity/delete', [
'guid' => $guid,
'forward_url' => $forward_url,
]),
'confirm' => true,
));
You can customize the success message keys for your entity type and subtype, using ``"entity:delete:$type:$subtype:success"``
and ``"entity:delete:$type:success"`` keys.
.. code-block:: php
// to add a custom message when a blog post or file is deleted
// add the translations keys in your language files
return [
'entity:delete:object:blog:success' => 'Blog post has been deleted,
'entity:delete:object:file:success' => 'File titled %s has been deleted',
];
Fields
======
Forms and actions for storing (save/edit) entities use various fields to provide input to users, but also to validate the input.
Forms and actions need to keep these field configurations in line. To help with this a generic ``FieldsService``` is available
to access the configured fields for a certain entity type/subtype.
These fields can be requested using one of the following functions:
* ``$entity->getFields()`` - retrieves the field configuration for a entity
* ``elgg()->fields->get('entity_type', 'entity_subtype')`` - retrieves the field configuration for a given type/subtype
They will both return an array of field configurations directly usable for passing to ``elgg_view_field($field_config)``.
Developers can configure a default set of fields for their own entities in the related entity class or provide them via an event.
Configure the default fields in your entity class.
.. code-block:: php
class MyEntity extends \ElggObject {
/**
* {@inheritdoc}
*/
public static function getDefaultFields(): array {
return [
[
'#type' => 'text',
'#label' => elgg_echo('title'),
'name' => 'title',
'required' => true,
],
...
];
}
}
You can also configure your fields in an event callback or use the callback to extend other field configurations.
.. code-block:: php
/**
* Register the fields for my entity type/subtype
*
* @param \Elgg\Event $event 'fields', 'object:my_entity'
*
* @return array
*/
public function __invoke(\Elgg\Event $event): array {
$result = (array) $event->getValue();
$result[] = [
'#type' => 'text',
'#label' => elgg_echo('title'),
'name' => 'title',
'required' => true,
];
return $result;
}
Forms
=====
To output a form, use the elgg_view_form function like so:
.. code-block:: php
echo elgg_view_form('example');
Doing this generates something like the following markup:
.. code-block:: html
Elgg does some things automatically for you when you generate forms this way:
1. It sets the action to the appropriate URL based on the name of the action you pass to it
2. It adds some anti-csrf tokens (``__elgg_ts`` and ``__elgg_token``) to help keep your actions secure
3. It automatically looks for the body of the form in the ``forms/example`` view.
Put the content of your form in your plugin’s ``forms/example`` view:
.. code-block:: php
// /mod/example/views/default/forms/example.php
echo elgg_view('input/text', array('name' => 'example'));
// defer form footer rendering
// this will allow other plugins to extend forms/example view
elgg_set_form_footer(elgg_view('input/submit'));
Now when you call ``elgg_view_form('example')``, Elgg will produce:
.. code-block:: html
Inputs
------
To render a form input, use one of the bundled input views, which cover all standard
HTML input elements. See individual view files for a list of accepted parameters.
.. code-block:: php
echo elgg_view('input/select', array(
'required' => true,
'name' => 'status',
'options_values' => [
'draft' => elgg_echo('status:draft'),
'published' => elgg_echo('status:published'),
],
// most input views will render additional parameters passed to the view
// as tag attributes
'data-rel' => 'blog',
));
The above example will render a dropdown select input:
.. code-block:: html
To ensure consistency in field markup, use ``elgg_view_field()``, which accepts
all the parameters of the input being rendered, as well as ``#label`` and ``#help``
parameters (both of which are optional and accept HTML or text).
.. code-block:: php
echo elgg_view_field([
'#type' => 'select',
'#label' => elgg_echo('blog:status:label'),
'#help' => elgg_view_icon('help') . elgg_echo('blog:status:help'),
'required' => true,
'name' => 'status',
'options_values' => [
'draft' => elgg_echo('status:draft'),
'published' => elgg_echo('status:published'),
],
'data-rel' => 'blog',
]);
The above will generate the following markup:
.. code-block:: html
This indicates whether or not the blog is visible in the feed
Input types
-----------
A list of bundled input types/views:
* ``input/text`` - renders a text input ````
* ``input/plaintext`` - renders a textarea ````
* ``input/longtext`` - renders a WYSIWYG text input
* ``input/url`` - renders a url input ````
* ``input/email`` - renders an email input ````
* ``input/checkbox`` - renders a single checkbox ````
* ``input/checkboxes`` - renders a set of checkboxes with the same name
* ``input/radio`` - renders one or more radio buttons ````
* ``input/submit`` - renders a submit button ``