Controller Overview

Windwalker Controller is a main entry of a page, after routing, the request input and IoC container will be injected into controller and execute it. Any code mentioned in this section will be executed in doExecute(), the entrance of controller.

Simple usage of controller:

use Flower\Controller\Sakura\GetController;

use Flower\View\Sakura\SakuraHtmlView;
use Windwalker\Core\Controller\AbstractController;

class GetController extends AbstractController
{
    protected function doExecute()
    {
        $view = $this->getView();

        return $view->render();
    }
}

Single Action Pattern

Windwalker Controller follows single responsibility principle, every controller has only one action (execute()). The benefit is that our controllers will be much more lighter if an action has complex logic.

Hooks

Controller provides prepareExecute() and postExecute() to allow you inject before & after process to controller.

hooks

This is an example:

class SaveController extends AbstractController
{
    // We prepare data in pre-process
    protected function prepareExecute()
    {
        $this->user   = $this->input->getArray('user');
        $this->format = $this->input->get('format', 'json');
        $this->repository  = $this->getRepository('User');
    }

    // Do real save logic
    protected function doExecute()
    {
        $this->repository
            ->validate($this->user)
            ->save($this->user);

        return true;
    }

    // Do post-process to return response.
    protected function postExecute($result = null)
    {
        if ($this->format == 'json') {
            return new JsonResponse($this->data, 200);
        }

        return new RedirectResponse($this->router->route('users'));
    }
}

Follows this way to write your logic so we can easily override the before and after process in child controller if we need extends this controller.

Get HTTP Request Input

See: Request and Input, we can use Input object to get HTTP queries:

$id = $this->input->getInput('id');

$layout = $this->input->getString('layout', 'default');

$html = $this->getVar('html');

// Get other request data
$this->input->get->get('foo');
$this->input->post->get('foo');
$this->input->put->get('foo');
$this->input->patch->get('foo');
$this->input->delete->get('foo');
$this->input->files->get('foo');
$this->input->header->get('foo');
$this->input->server->get('foo');
$this->input->env->get('foo');
$this->input->cookie->get('foo');

Use Repository and View Object

If your repository and view object located in your package, and follows Windwalker naming convention, you can get Repository and View by getter in controller.

// In doExecute()

// File: src/Flower/Repository/SakuraRepository.php
// Class: Flower\Repository\SakuraRepository
$this->getRepository('Sakura');

// File: src/Flower/View/Sakura/SakuraHtmlView
// Class: Flower\View\Sakura\SakuraHtmlView
 $this->getView('Sakura', 'html');

// Get other repositorys and views
$this->getRepository('Rose'); // RoseRepository
$this->getView('Rose', 'json'); // RoseJsonView

If you didn't tell getter the view or repository name, it will use controller name as default:

// In doExecute()

// SakuraRepository
$this->getRepository();

// SakuraHtmlView
$this->getView();

// SakuraJsonView
$this->getView(null, 'json');

See View and Repository

Delegating Tasks

Controller delegate() method supports us handle multiple tasks in one controller if we need dispatch different handlers.

protected function doExecute()
{
    return $this->delegate($this->input->get('task', 'task1'), ...$this->input->post->toArray());
}

protected function task1($arg1, $arg2, $arg3)
{
    // ...
}

protected function task2($arg1, $arg2)
{
    // ...
}

Use Container, Package & Application

Windwalker controller is a Container aware interface, we can directly use container in controller:

// In doExecute()

$session = $this->container->get('session');
$cache = $this->controller->get('cache');

$cache->call('user', function() use ($session)
{
    return $session->get('user');
});

Controller maintains package and application object, so we can get some important global services from them.

// Get services from Application
$this->app->container->get('...'); // Global container
$this->app->cache->getCache();
$this->app->session->get('user');
$this->app->browser->isMobile();
$this->app->platform->isLinux();
$this->app->database->getQuery(true);
$this->app->logger->debug('category', '...');
$this->app->mailer->createMessage('Subject');
$this->app->get('foo'); // Get global config

// Get services from package
$this->package->container->get('...'); // Package child container
$this->package->router->route('user'); // Package child router
$this->package->get('foo'); // Package config

More services please see Service Provider

Success and Failure Processor

Controller also provides an interface to help us process success and failure of executed, for example, if we throw an Exception, controller will catch it and auto run processFailure(), so we can write our failure handler buy override this method, otherwise controller will call processSuccess() to handle success logic.

This is an sample code to show how to implement a DB transaction in controller.

class SaveController extends AbstractController
{
    protected function doExecute()
    {
        if (!$data = $this->input->getArray('data')) {
            throw new \InvalidArgumentException('No data');
        }

        /** @var DatabaseRepositoryRepository $this->repository */
        $this->repository->transactionStart(true);

        if (!$this->repository->save($data)) {
            throw new \RuntimeException('Save fail');
        }

        return true;
    }

    public function processSuccess()
    {
        $this->repository->transactionCommit(true);

        return true;
    }

    public function processFailure(\Exception $e = null)
    {
        $this->repository->transactionRollback(true);

        $this->addMessage($e->getMessage(), 'warning');
        $this->setRedirect($this->router->route('user'));

        return false;
    }
}

Json Response

Use JsonResponse to return json content-type in doExecute()

use Windwalker\Http\Response\JsonResponse;

// in doExecute()

$this->response = new JsonResponse();

// The returned data will auto convert to json
return ['foo' => 'bar'];

Use trait to auto prepare json response.

use Windwalker\Core\Controller\Traits\JsonResponseTrait;

class GetController extends AbstractController
{
    use JsonResponseTrait;

Use JsonApiTrait to return a standard api format:

use Windwalker\Core\Controller\Traits\JsonApiTrait;

class GetController extends AbstractController
{
    use JsonApiTrait;

    public function doExecute()
    {
        $this->addMessage('Hello');

        return return ['foo' => 'bar'];
    }

Output data:

{
    "success": true,
    "code": 200,
    "message": "Hello"
    "data": {
        "foo": "bar"
    }
}

Validate Middleware

Add ValidateErrorHandlingMiddleware so controller can catch ValidateFailException to handle multiple invalid data messages.

use Windwalker\Core\Controller\Middleware\ValidateErrorHandlingMiddleware;
use Windwalker\Core\Repository\Exception\ValidateFailException;

// ...

protected $middlewares = [
    ValidateErrorHandlingMiddleware::class
];

protected function doExecute()
{
    $data = $this->input->getArray('data');

    $this->validate($data);

    return $this->repository->save($data);
}

protected function validate($data)
{
    // Set one message
    if (empty($data['name'])) {
        throw new ValidateFailException('Please enter user name.');
    }

    // Set multiple messages
    if (...) {
        throw new ValidateFailException([
            'Message1',
            'Message2',
            'Message3',
        ]);
    }
}

Controller Config

You can set some config in $this->config and pass it into View and Repository, then they will know everything about this controller.

// In doExecute()

$this->config->set('package.name', 'other_package_name');

$view->setConfig($this->config);
$repository->setConfig($this->config);

// Then View will find template from other package

If you use getter to get View and Repository, config will auto set into them.

Config is a Structure object, see: Windwalker Structure

CSRF Token

Windwalker provides a simple CSRF token generator, please add this line to your HTML form:

<form action="..." method="post">

    <!-- ... -->

    <?php echo \Windwalker\Core\Security\CsrfProtection::input(); ?>
</form>

In Edge of Blade

@formToken()

This will generate a token input to your form. You can also add token to an URL.

<a href="flower/sakura?<?php echo \Windwalker\Core\Security\CsrfProtection::getFormToken(); ?>"></a>

Then just check token in your Controller.

use Windwalker\Core\Security\CsrfProtection;

// Validate or throw exception
CsrfProtection::validate();

// Validate or die
CsrfProtection::validate(true);

// Validate with message
CsrfProtection::validate([bool], 'Sorry your token is invalid');

// Only check and return boolean
if (! CsrfProtection::checkToken()) {
    throw new \RuntimeException('Invalid Token');
}

Use trait to auto check CSRF token:

use Windwalker\Core\Controller\Traits\CsrfProtectionTrait;

class SaveController extends AbstractController
{
    use CsrfProtectionTrait;

    // ...

Utilities Methods

Redirect

Use setRedirect($url) to tell controller redirect to other url after executed.

$this->setRedirect('pages.html');

// Then after executed, we can call `redirect()` to do redirect action.
$this->redirect();

Set Message when redirect:

$this->setRedirect('pages.html', 'Save success', 'info');

We can override redirect target anywhere when executing.

// In doExecute()

$repository->save($data);

$this->setRedirect('pages.html', 'Save success', 'success');

try
{
    $repository->saveRelations($data);
}
catch (Exception $e)
{
    // Override pervious url
    $this->setRedirect('edit.html', 'Save fail', 'error');

    return false;
}

return true;

We can also redirect instantly by set URL as first argument to redirect()

$this->redirect('http://foo.com/page.html');

Add Flash Messages

Flash message is a disposable message in session, if we show it, these messages will be purged.

$this->addMessage('Message', 'type');

// Set multiple messages by array
$this->addMessage(['Message', 'Message2'], 'type');

// OR

$this->setRedirect('url.html', 'Message', 'type');

Mute

If we set controller to mute, this controller will not add any messages:

$this->mute(true);

$this->addMessage('Message'); // This action no use

HMVC

The Hierarchical-Repository-View-Controller (HMVC) pattern is a direct extension to the MVC pattern that manages to solve many of the scalability issues already mentioned. HMVC was first described in a blog post entitled HMVC: The layered pattern for developing strong client tiers on the JavaWorld web site in July 2000.

mvc-hmvc

In Windwalker, using HMVC is very easy, look this example:

class ParentController extends Controller
{
    protected function doExecute()
    {
        // A standard way to push all data of current controller into sub controller
        $child = new ChildController($this->input, $this->package, $this->container);

        $hmvcResult = $child->execute();

        // Use this result to do something
        $this->getView()->set('some_widget', $hmvcResult);
    }
}

Actually, all params of constructor can be ignored because the global IoC container will handle this dependency.

// Every dependency will be provided by IoC
$child = new ChildController();

But you can force push some params you needed:

$newInput = new Input(array('foo' => 'bar'));

$child = new ChildController($newInput);

// OR
$subContainer = Ioc::factory('sub.container');

$child = new ChildController(null, $subContainer->get('package'), $subContainer);

Use hmvc() Method

Windwalker provides a hmvc() method to make this step more quickly:

$result = $this->hmvc('Flower\Controller\Rose\SaveController', array('data' => $data));

// OR

$result = $this->hmvc(new ChildController(), $this->input);

Middlewares

Windwalker Controller supports middleware pattern, you can add middlewares in middleware property when class declaring:

class GetController extends AbstractController
{
    protected $middlewares = [
        // The key: 800 is priority number
        800 => \Windwalker\Core\Controller\Middleware\JsonResponseMiddleware::class
    ];

    // ...

Or add it in init() or boot() method before executed:

// ...

protected function init()
{
    parent::init();

    $this->addMiddleware(ValidateErrorHandlingMiddleware::class, PriorityQueue::HIGH + 10);
}

// ...

Controller interface is different from application and package, it is not Psr7 invokable, so we must use another interface to execute middlewares.

Create Custom Middleware

Use callback as middleware:

protected $middlewares = [
    500 => function ($data, $next)
    {
        // pre-process

        $result = $next->execute($data);

        // post-process
        // Controller will be binded as $this
        $this->response->getBody()->write('Hello');

        return $result;
    }
];

Or use class as middleware:

use Windwalker\Debugger\Helper\DebuggerHelper;
use Windwalker\Http\Response\JsonResponse;

class MyMiddleware extends AbstractControllerMiddleware
{
    public function execute($data = null)
    {

        $result = $this->next->execute($data);

        $this->controller->response->getBody()->write('Hello');

        return $result;
    }
}

Then register it to controller:

protected $middlewares = [
    800 => MyMiddleware::class
];

Traits

Controller is an instance of BootableTrait, which can auto boot used traits.

Create a trait with a method named boot{TraitName}():

trait MyTestTrait
{
    public function bootMyTestTrait()
    {
        $this->adMiddleware(...);
    }
}

Now use it in controller and it will be auto booted.

class GetController extends AbstractController
{
    use MyTestTrait;
}

Built-in Traits

  • Windwalker\Core\Controller\Traits\JsonResponseTrait
  • Windwalker\Core\Controller\Traits\JsonApiTrait
  • Windwalker\Core\Controller\Traits\HtmlResponseTrait
  • Windwalker\Core\Controller\Traits\CsrfProtectionTrait
  • Windwalker\Core\Controller\Traits\CorsTrait

If you found a typo or error, please help us improve this document.