Skip to content

Instantly share code, notes, and snippets.

@ewinslow
Last active August 29, 2015 14:01
Show Gist options
  • Save ewinslow/a9482cbde29b208a8165 to your computer and use it in GitHub Desktop.
Save ewinslow/a9482cbde29b208a8165 to your computer and use it in GitHub Desktop.
Elgg OO Actions

Actions

Actions define the behavior of forms.

Defining an action

Map the action name to the action controller in your plugin's actions.php:

namespace My\Namespace\Actions;

// actions.php
return array(
  'do/work' => DoWork::class,
);

Define a class that extends Elgg\Actions\Action and implement the execute() function:

namespace My\Namespace\Actions;

use Elgg\Actions\Action;
use Elgg\Actions\Response;
use Elgg\Actions\Input;
use Elgg\Some\Service;

class DoWork extends Action {

  public function __constructor(Service $someService) {
    $this->theService = $someService;
  }

  public function execute(Input $input) {
     var $result = $this->theService->doWork();

     return new Response(200, ['result' => $result]);
  }
}

Note

Elgg's DI system will inject dependencies into the constructor automagically.

Write a unit test:

namespace My\Namespace\Actions;

use Elgg\Actions\Input;
use Elgg\Some\Service;

class DoWorkTest extends PHPUnit_Framework_TestCase {
  public function testRespondsWithTheResultOfServicesDoWorkMethod() {
    $serviceMock = $this->createMock(Service::class);

    $action = new DoWork($serviceMock);

    $response = $action->execute(new Input());

    $this->assertEquals(200, $response->httpStatus);
    $this->assertEquals('foo', $response->body->result);
  }
}

Elgg\Actions\Action

A single action representing the behavior of a single form. Noop by default.

hasPermission()

Called during permissions phase to check whether the current session can perform the requested action.

getFields()

Return the complete list of inputs this action accepts along with the validation rules.

execute(Input $input)

Contains the business logic for the action.

Example

namespace Elgg\Users\Actions;

use Elgg\Actions\NullAction;

// Potential implementation of the "register" action
class Register extends NullAction {

  // primitives injected automatically from $config service
  public function __constructor($allow_registrations, Users $users, Passwords $passwords, Notifications $notifications) {
    $this->allow_registrations = $allow_registrations;

    $this->users = $users;
    $this->passwords = $passwords;
    $this->notifications = $notifications;
  }

  public function getFields() {
    return array(
      "email" => input()->type('email')->validate('unique', function($value, Users $users) {
        return count($users->where(['email' => $value])->ignoreAccess()) == 0;
      }),
      "name" => input()->type('text'),

      // input field validation is transferable knowledge because it's based on HTML5
      "username" => input()
        ->type('text')
        ->pattern('[a-zA-Z0-9_.-]+')
        ->minlength(4)
        // named validation rules allow for custom error messages when rules are broken
        // Return true for valid input. False for invalid input.
        // This callback is dependency-injected including with the value of the input
        ->validate('unique', function($value, Users $users) {
          return count($users->where(['username' => $value])->ignoreAccess()) == 0;
        }),
      "password" => input()->type('password')->minlength(6),
      "password2" => input()->type('password')->validate('matches', function($value, Input $input) {
        return $value == $input->get('password');
      }),
      "friend" => entity()->type('user')->required(false),
      "invitecode" => input()->type('hidden')->required(false)
    );
  }

  public function hasPermission(Input $input) {
    return $this->allow_registrations;
  }

  public function execute(Input $input) {
     $user = $this->users->create([
       'username' => $input->get('username'),
       'email' => $input->get('email'),
       'name' => $input->get('name'),
       'access_id' => ACCESS_PUBLIC,
       'owner_guid' => 0,
       'container_guid' => 0,
       'language' => $this->language,
     ]);

     $this->passwords->set($user->guid, $input->get('password'));

     // Turn on email notifications by default
     $this->notifications->setUserSetting($user->guid, 'email', true);

     return new EntityResponse($user);
  }
}

Elgg\Actions\ActionDecorator

Tweak the inputs and behaviors of actions with Action Decorators.

Implementation

namespace Elgg\Actions;

class ActionDecorator implements Action {
  private $decoratedAction;

  public function __construct(Action $decoratedAction) {
    $this->decoratedAction = $decoratedAction;
  }

  public function getFields() {
    return $this->decoratedAction->getFields();
  }

  public function execute(Input $input) {
    return $this->decoratedAction->execute($input)
  }
}

Example usage

namespace Elgg\Captcha\Actions;

use Elgg\Actions\Action;
use Elgg\Actions\ActionDecorator;
use Elgg\Inputs\InputField;

/**
 * Validates that the user filled out a captcha before submitting
 */
class CaptchaActionDecorator extends ActionDecorator {
  // Must be named exactly "decoratedAction" to inject correctly
  public function __construct(Action $decoratedAction, Session $session) {
    parent::__construct($decoratedAction);

    $this->session = $session;
  }

  // Change behavior of injected action by overriding methods
  public function getFields() {
    // Never make logged in users fill out captcha
    if ($this->session->isLoggedIn()) {
      return parent::getFields();
    }

    // Utilize existing behavior by calling parent methods
    return array_merge(parent::getFields(), array(
      'captcha' => input()->type('captcha')->options([3]),
    ));
  }
}

Registering decorators

namespace Elgg\Captcha\Actions;

// actions.php
return array(
  "users/register" => array(
    Decorator::class => 500,
  ),
);

Unregistering decorators

use Elgg\Captcha\Actions\Decorator as CaptchaDecorator;

// actions.php
return array(
  "users/register" => array(
    CaptchaDecorator::class => false,
  ),
);

Lifecycle of an Action Request

  • Parse the action name from the URL
  • Get the action configuration based on the name
  • Instantiate the default action class
  • Instantiate decorators
  • Validate inputs (Your getFields() is called here)
  • Check permissions (Your hasPermission() is called here)
  • Execute the action (Your execute() is called here)
  • Render response

When the http://example.elgg.org/action/users/register endpoint is hit, this conceptually calls:

$action = new RegisterUser();            // priority 0
$action = new CaptchaDecorator($action); // priority 500
// validate, check permissions, etc...
$action->execute($input);

Except there is actually some DI magic sprinkled in to make sure the constructors receive all the correct arguments.

@juho-jaakkola
Copy link

I'm seeing just the raw text. Are you sure the file language has been set to rst?

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