ErlyWeb: The Erlang Twist on Web Framworks

Copyright © Yariv Sadan 2006-2007

Authors: Yariv Sadan.

Contents

Introduction
Directory Structure
Components
Models
Controllers
Views
Containers
The App Controller
Yaws Configuration

Introduction

ErlyWeb is a component-oriented web development framework that simplifies the creation of web applications in Erlang according to the tried-and-true MVC pattern. ErlyWeb's goal is to let web developers enjoy all the benefits of Erlang/OTP while creating web applications with simplicity, productivity and fun.

ErlyWeb is designed to work with Yaws, a high-performance Erlang web server. For more information on Yaws, visit http://yaws.hyber.org.

Installation

To install ErlyWeb, obtain the latest zip file from http://erlyweb.org and unzip it in your root Erlang code path. This path varies depending on your operating system (on OS X, it's '/usr/local/lib/erlang/lib'). If you don't know your path, you can discover it by starting the Erlang shell and calling "code:lib_dir()." (note that the last directory of the code path always erlang/lib).

Directory Structure

ErlyWeb applications have the following directory structure:

[AppName]/
  src/                            contains non-component source files
    [AppName]_app_controller.erl  the app controller
    [AppName]_app_view.rt         the app view
  components/                     contains controller, view and
                                  model source files
  www/                            contains static assets
  ebin/                           contains compiled .beam files

where AppName is the name of the application. The 'src', 'components' and 'www' directories may contain additional subdirectories, whose contents are of the same type as those in the parent directory.

Components

An ErlyWeb component is made of a controller and a view, both of which are Erlang modules. Controllers may use one or more models, but this isn't required.

(Technically, a component can be implemented without a view, but most components have views.)

Controllers are implemented in Erlang source files. Views are typically implemented in ErlTL files, but views can be implemented in plain Erlang as well. The controller file is named '[ComponentName]_controller.erl' and the view file is named '[ComponentName]_view.[Extension]', where [ComponentName] is the name of the component, and [Extension] is 'erl' for Erlang files and 'et' for ErlTL files.

When ErlyWeb receives a request such as http://hostname.com/foo/bar/1/2/3, ErlyWeb interprets it as follows: 'foo' is the component name, 'bar' is the function name, and ["1", "2", "3"] are the parameters (note that parameters from browser requests are always strings).

When only the component's name is present, ErlyWeb assumes the request is for the component's 'index' function and with no parameters (i.e. http://hostname.com/foo is equivalent to http://hostname.com/foo/index).

If the module 'foo_controller' exists and it exports the function 'bar/4', ErlyWeb invokes the function foo_controller:bar/4 with the parameters [A, "1", "2", "3"], where A is the Arg record that Yaws passes into the appmod's out/1 function. (For more information, visit http://yaws.hyber.org/.)

Since ErlyWeb 0.7, you can use the catch_all/2 hook to override the default behavior, i.e. treating the second token as a function name instead of as parameter. For more information, see catch_all/2.

Models

ErlyWeb treats all Erlang modules whose names don't end with '_controller' or '_view' and whose files exist under 'src/components' as models. If such modules exist, erlyweb:compile() passes their names to erlydb:code_gen() in order to generate their ErlyDB database abstraction functions.

If your application uses ErlyDB for database abstraction, you have to call erlydb:start() before erlyweb:compile() (otherwise, the call to erlydb_codegen() will fail). If you aren't using ErlyDB, don't keep any models in 'src/components' and then you won't have to call erlydb:start().

Controllers

Controllers contain most of your application's logic. They are the glue between Yaws and your applications models and views.

Controller functions always accept the Yaws Arg for the request as the first parameter, and they may return any of the values that Yaws appmods may return (please read the section below on the {response, Elems} tuple to avoid running into trouble). In addition, controller functions may return a few special values, which are listed below with their meanings (note: 'ewr' stands for 'ErlyWeb redirect' and 'ewc' stands for 'ErlyWeb Component.').

ewr
Redirect the browser to the application's root url.

{ewr, ComponentName}
Redirect the browser to the component's default ('index') function

{ewr, ComponentName, FuncName}
Redirect the browser to the URL for the given component/function combination.

{ewr, ComponentName, FuncName, Params}
Redirect the browser to the URL with the given component/function/parameters combination.

{data, Data}
Call the view function, passing into it the Data variable as a parameter.

{ewc, A}
Analyze the Yaws Arg record 'A'. If the request matches a component, render the component and send the result to the browser. Otherwise, return {page, Path}, where Path is the Arg's appmoddata field.

{ewc, ComponentName, Params}
Render the component's 'index' function with the given parameters and send the result to the view function.

{ewc, ComponentName, FuncName, Params}
Render the component's function with the given parameters, and send the result to the view function.

{ewc, ControllerModule, ViewModule, FuncName, Params}
This tuple lets you specify exacly which controller and view modules ErlyWeb uses to render the sub-component. It is considered for advanced uses only.

{replace, Ewc}
This tuple, which was introduced in ErlyWeb 0.6, tells ErlyWeb to render a different component from the requested one (the new component, Ewc, can be described using any of the 'ewc' tuples listed above). This mechanism is conceptually similar to an internal redirect. It is useful, for example, when you want multiple components to share a common error screen. Instead of having multiple view functions check if the controller result indicates an error has occurred, you can use the replace tuple to simply redirect the rendering to an error component.

Controller functions may also return (nested) lists of 'ewc' and 'data' tuples, telling ErlyWeb to render the items in their order of appearance and then send the list of their results to the view function. This feature lets you build components that are composed of a mix of dynamic data and one or more sub-components.

If a component is only supposed to be used as a subcomponent, you should implement the function "private() -> true." in its controller. This tells ErlyWeb to call exit({illegal_request, Controller}) when clients try to access the component by requesting its corresponding URL directly.

Returning Arbitrary Yaws Tuples

ErlyWeb provides the infrastructure for rendering components and redirecting to other components in the same application. This is sufficient for simple applications, but sometimes you may want to return to Yaws tuples that Yaws understands and that aren't directly supported by ErlyWeb (these are documented at http://yaws.hyber.org/yman.yaws?page=yaws_api). A common requirement, for example, is to instruct Yaws to include arbitrary HTTP headers, such as cookies, in the response Yaws sends to the browser. ErlyWeb lets you do this by returning a tuple of the form {response, Elems} from controller functions.

The second element in the {response, Elems} is a list of values that ErlyWeb should return to Yaws verbatim, with the exception of the (optional) {body, Body} for HTML or {body, MimeType, Body} tuple and any of the ewr tuples listed above (ErlyWeb translates the latter into their redirect_local equivalents). If, in addition to returning standard Yaws tuples, you want ErlyWeb to render the response's body using the component's view, you can include the {body, Body} or {body, MimeType, Body} tuple in Elems. Body may be any single ewc or data tuple, or a list thereof. ErlyWeb renders the elements of Body, sends the result to the view function, and embeds the resulting iolist in an {html, Iolist} tuple prior to returning it to Yaws.

There is currently a restriction on the usage of {response, Elems}: only the top-level component for the request may return the {response, Elems} tuple. Sub-components may return only data and/or ewc tuples. This restriction makes sense because you normally only use sub-components for rendering segments of the response's body and not for setting HTTP headers or implementing arbitrary application logic.

Optional Controller Hooks

catch_all/2
before_call/2
before_return/3
after_render/3

catch_all/2

The catch_all/2 hook was introduced in ErlyWeb 0.7. By default, ErlyWeb treats the URL token following the component name as a function name. catch_all/2 lets you override this behavior on a per-controller basis by treating all URL tokens following the component name as parameters.

For example, if a user requests the url

http://my-cool-app.com/people/bob

ErlyWeb by default would try to invoke the function 'bob/1' in people_controller. You can tell ErlyWeb to override this behavior and instead pass "bob" as a parameter to people_controller:catch_all/2 as follows:

people_controller.erl:

-module(people_controller).
-export(index/1, catch_all/2).

catch_all(A, [Name]) ->
  {data, Name}.

people_view.et:

<%@ catch_all(Name) %>
<% Name %>'s homepage
...

Note that the second parameter to catch_all/2 in the controller is a list of URL tokens (separated by "/") following the controller name. For example, when ErlyWeb receives a request with the URL

http://my-cool-app.com/people/bob/smith

ErlyWeb would call people_controller:catch_all(A, ["bob", "smith"]).

before_call/2

ErlyWeb lets you implement the before_call/2 and before_return/3 hooks in controllers to intercept function calls before they are applied and/or manipulate controller return values before ErlyWeb processes them.

The signature of before_call/2 is

before_call(FuncName::atom(), Params::[term()]) ->
  {NewFuncName::atom(), NewParams::[term()]}

You can use the before_call/2 hook to change the controller function ErlyWeb calls and/pr the parameters ErlyWeb passes to the function. This hook can be convenient, for example, for implementing authentication logic for some or all functions of a controller.

before_return/3

The signature of before_return/3 is

before_return(FuncName::atom(), Params::[term()], Response::term()) ->
  NewResponse::term()

By implementing the before_return/3 hook, you can change all return values based on the names and/or parameters of their originating controller functions. This hook can simplify the definition of response elements such HTTP headers common to some or all of a controller's functions.

after_render/3

ErlyWeb lets you implement the after_render/3 hook in controllers to get access to the rendered output.

The signature of after_render/2 is

after_render(FuncName::atom(), Params::[term()], Rendered::iolist()) ->
  Ignored::term().

You can use the after_render/3 hook to implement a granular caching system.

Views

Views are Erlang modules whose functions return iolists (nested lists of strings and/or binaries). A view function that ErlyWeb uses has the same name as its corresponding controller function, and it accepts a single parameter, which is the result of ErlyWeb's processing of the controller function's return value.

Containers

Containers are ErlyWeb components in which other components can be nested. ErlyWeb contains no special logic to enable containers; 'container' is just a word used to describe components that applications use in a special way.

What sets containers apart from standard components is that containers' controller functions accept as parameters ewc and/or data tuples representing nested sub-components. The controller functions include those parameters in their return values, telling ErlyWeb to render the nested components and pass the results to the container's corresponding view functions. The view functions embed the rendered subcomponents in the container's static HTML, and ErlyWeb returns the final result Yaws.

The code below illustrates how to implement a simple HTML container. The container's controller function tells ErlyWeb to renders its sub-component (index/2's Ewc parameter), and pass the result to the component's view, which defines a basic HTML page.

html_container_controller.erl:
-module(html_container_controller).
-export([index/2, private/0]).

%% tells ErlyWeb to reject direct HTTP requests for this
%% container
private() ->
  true.

index(_A, Ewc) ->
  Ewc.
html_container_view.et:
<%@ index(Data) %>
<html>
<head>
<title>My ErlyWeb App</title>
</head>
<body>
<% Data %>
</body>
</html>

The example below shows how to pass multiple 'song' components into an 'album' container, and show the result in the 'home' component.

song_controller.erl:
index(A, Num, Title) ->
  {data, {integer_to_list(Num), Title}}.
song_view.et:
<%@ index({Num, Title}) %>
<% Num %>: <% Title %><br/>
album_controller.erl:
index(A, Title, Songs) ->
  [{data, Title}, Songs].
album_view.et:
<% index([Title, Songs]) %>
album title: <% Title %><br/>
songs:</br>
<% Songs %>

home_controller.erl:
```
index(A) ->
  Songs =
    [{ewc, song, [A, 1, <<"Back in the U.S.S.R">>]},
     {ewc, song, [A, 2, <<"Dear Prudence">>]}],
  {ewc, album, [A, <<"White Album">>, Songs]}.
home_view.et:
<% index(Album) %>
Your favorite album is:<br/>
<% Album %>

The App Controller

ErlyWeb applications have a module called [AppName]_app_controller, whose source file is in the 'src' directory by convention. The app controller provides the entry-point into ErlyWeb applications via the hook/1 function. Starting from ErlyWeb 0.6, the app controller can also have an error-trapping, error/3.

hook/1

The signature for hook/1 is

hook(A::arg()) -> hook_result() | [hook_result()]

hook_result = ewc() | yaws_term() | response() |
  {phased, Ewc::ewc() | Response::response(),
    fun(ExpandedEwc::ewc(), Data::iolist(), PhasedVars::proplist()) ->
      FinalEwc::ewc()}

ewc() = any `ewc' tuple
yaws_term() = any legal Yaws appmod return value
response() = a tuple of the form {response, [Elems::response_elem()]}
response_elem() = yaws_term() | {body, ewc()}

(Note: support for the response() return value was introduced in ErlyWeb 6.2.)

The simplest way of telling ErlyWeb to process a client request is to implement hook/1 as follows:

hook(A) -> {ewc, A}.

This tells ErlyWeb to check if the component that's mapped to the request's URL is a top-level component (i.e., it doesn't implement private() -> true.). If it is top-level, ErlyWeb renders the component and returns the to Yaws. Otherwise, ErlyWeb calls exit({illegal_request, ControllerName}).

Warning: ErlyWeb only checks if tuples of the form {ewc, A} represent top-level components. If you return a different ewc tuple (e.g. one returned by calling erlyweb:get_initial_ewc/1), ErlyWeb expects you to ensure the safety of your components manually.

Phased Rendering

In ErlyWeb 0.5, the phased return type for hook/1 was introduced to let you embed components in containers after those components are rendered but before the result of the rendering is sent to the browser (before ErlyWeb 0.5, a similar but weaker functionality was provided by app views, which ErlyWeb no longer supports). The most common use for this feature is to make ErlyWeb render different components with the same outer layout (header, footer, and/or sidebars).

The reason this is feature called 'phased rendering' is that it makes ErlyWeb render the response in two phases: first, the requested component, and second, the container in which the component should be embedded.

The benefits of phased rendering are:

To use phased rendering, you should return a tuple of the form {phased, Ewc, Fun} from hook/1. This return value instructs ErlyWeb to first render the requested component, and if the result includes a rendered iolist (i.e., the requested component returned a body and not just headers) then pass the iolist to the function Fun.

Fun takes 3 parameters: the fully expanded 'ewc' tuple that ErlyWeb has rendered (this is usually the result of calling erlyweb:get_initial_ewc({ewc, A})), the rendered iolist, and an opaque parameter called 'PhasedVars' (PhasedVars was introduced in ErlyWeb 0.7). Through its return value, Fun can tell ErlyWeb to pass the iolist as a parameter to the container component function that will be used in the second rendering phase.

PhasedVars is a list of values returned from the rendered controller function, in a special tuple of the type {phased_vars, Vars} (an example is shown below). This tuple should be included in the Elems list of the {response, Elems} tuple that controller functions may return (see the section Returning Arbitrary YAWS Tuples for more info).

The purpose of 'PhasedVars' is to let the controller function parameterize the component that is rendered in the second phase. It is useful, for example, for letting the controller function set the title of a page when the page's header is rendered by the container in the second rendering phase.

Fun may return any 'ewc' or 'data' tuple. ErlyWeb takes the result of Fun, renders it, and send the result back to the browser.

Consider this example return value from hook/1:

{phased, {ewc, A}, fun(_Ewc, Data, _PhasedVars) -> {data, Data} end}

It is equivalent to returning

{ewc, A}

Both return values tells ErlyWeb to render the component that corresponds to the arg's appmoddata field, and then return the rendered iolist to the browser verbatim.

Below is a more advanced example.

{phased, {ewc, A},
  fun(_Ewc, Data, PhasedVars) ->
    {html_container,
      [A, {data, Data}, proplists:get_value(title, PhasedVars)]}
  end}

This return value tells ErlyWeb to first render the component that corresponds to the arg's appmoddata field, and then pass the rendered iolist (the 'Data' variable) as the second parameter to html_container_controller:index/3. The third parameter is the 'title' property that may exist in the PhasedVars property list, or 'undefined' if the 'title' property doesn't exist.

How do you set the value of PhasedVars? Below is an example controller function for an 'album' component that fetches from the database an album and its related songs, passes the song names to the view function, and sets the page's title according to the album name.

album_controller.erl:

show(A, Id) ->
  Album = album:find_id(Id),
  AlbumName = album:name(Album),
  Songs = album:songs(Album),
  {response,
    [{phased_vars, [{title, [<<"song list for ">>, AlbumName]}]},
     {body, {data, [{song:name(S) || S <- Songs}]}}]}.

This code snippet exemplifies how a controller function can tell ErlyWeb both what data to pass to the view function as well as the value of the PhasedVars parameter that will be passed to Fun in the second rendering phase.

Finally, below is an example html_container component that takes the rendered iolist and the 'title' parameter and inserts them into the proper positions in the HTML page.

html_container_controller.erl:

index(A, Ewc, Title) ->
  [Ewc, {data, Title}].

html_container_view.et:

<%@ index([Data, Title]) %>
<html>
<head>
<title><% Title %></title>
</head>
<body>
<% Data %>
</body>
</html>

error/3

Starting from ErlyWeb 0.6, the app controller may implement an error-trapping function whose signature is

error(A, Ewc, Err) -> result()

When ErlyWeb catches an exit signal when rendering a component, ErlyWeb tries to call the app controller's error/3 function. error/3's parameters are the Yaws arg, the Ewc tuple representing the original request, and the second element of the {'EXIT', Err} tuple that ErlyWeb has caught. error/3 may return any of the values that hook/1 may return, allowing you to use ErlyWeb components to render an error page.

App Controller Compilation Hooks

App controllers may implement two additional functions that ErlyWeb uses: before_compile/1 and after_compile/1. These functions let you hook into the compilation process and extend it in arbitrary ways. Both functions take a single parameter indicating the last compilation time (as returned from calendar:local_time()), or 'undefined' if the last compilation time is unknown.

Yaws Configuration

To use ErlyWeb, you need to configure Yaws to use the erlyweb module as an appmod (for more information, visit http://yaws.hyber.org) for your application. You can do this by adding the the following lines to your yaws.conf file:
docroot = /path/to/app/www
appmods = <[UrlPrefix], erlyweb>
<opaque>
  appname = [AppName]
</opaque>

where AppName is the name of your application and UrlPrefix is the URL prefix that Yaws uses to identify requests for this application (common values are '/[AppName]', for deploying multiple applications on the same server, or '/', for deploying a single application).

Note: It is recommended to use ErlyWeb with Yaws v1.66 and above.