
Version 1.3.0
This is a preliminary version of the documentation. Help us to improve it by filling tickets. We are looking for native english speakers to proof read the documentation. Contact us!
When programming dynamic Web sites, you often want to personalise the behaviour and content for one user. To do this, you need to recognise the user and save and restore its data. Eliom implements several high level features to do that:
Eliom is using cookies to recognize users. One cookie is automatically set for each user when needed and destroyed when the session is closed.
Coservices, but also actions, are also means to control precisely the behaviour of the site and to implement easily very common situations that require a lot of programming work with other Web programming tools. We'll have a lot at some examples in that section.
Eliom provides a way to save session data on server side and
restore it at each request. This data is available during the whole
duration of the session.
To save session data, create a table using
Eliom_sessions.create_volatile_table
and save and get data from
this table using Eliom_sessions.set_volatile_session_data and
Eliom_sessions.get_volatile_session_data. The following example shows
a site with authentification. The name of the user is asked in the login
form and saved in a table to be displayed on the page instead of the login
form while the user is connected. Note that the session is opened
automatically when needed.
(************************************************************) (************ Connection of users, version 1 ****************) (************************************************************) (* "my_table" will be the structure used to store the session data (namely the login name): *) let my_table = Eliom_sessions.create_volatile_table () (* -------------------------------------------------------- *) (* Create services, but do not register them yet: *) let session_data_example = Eliom_services.new_service ~path:["sessdata"] ~get_params:Eliom_parameters.unit () let session_data_example_with_post_params = Eliom_services.new_post_service ~fallback:session_data_example ~post_params:(Eliom_parameters.string "login") () let session_data_example_close = Eliom_services.new_service ~path:["close"] ~get_params:Eliom_parameters.unit () (* -------------------------------------------------------- *) (* Handler for the "session_data_example" service: *) let session_data_example_handler sp _ _ = let sessdat = Eliom_sessions.get_volatile_session_data ~table:my_table ~sp () in return (html (head (title (pcdata "")) []) (body [ match sessdat with | Eliom_sessions.Data name -> p [pcdata ("Hello "^name); br (); Eliom_predefmod.Xhtml.a session_data_example_close sp [pcdata "close session"] ()] | Eliom_sessions.Data_session_expired | Eliom_sessions.No_data -> Eliom_predefmod.Xhtml.post_form session_data_example_with_post_params sp (fun login -> [p [pcdata "login: "; Eliom_predefmod.Xhtml.string_input ~input_type:`Text ~name:login ()]]) () ])) (* -------------------------------------------------------- *) (* Handler for the "session_data_example_with_post_params" *) (* service with POST params: *) let session_data_example_with_post_params_handler sp _ login = Eliom_sessions.close_session ~sp () >>= fun () -> Eliom_sessions.set_volatile_session_data ~table:my_table ~sp login; return (html (head (title (pcdata "")) []) (body [p [pcdata ("Welcome " ^ login ^ ". You are now connected."); br (); Eliom_predefmod.Xhtml.a session_data_example sp [pcdata "Try again"] () ]])) (* -------------------------------------------------------- *) (* Handler for the "session_data_example_close" service: *) let session_data_example_close_handler sp () () = let sessdat = Eliom_sessions.get_volatile_session_data ~table:my_table ~sp () in Eliom_sessions.close_session ~sp () >>= fun () -> return (html (head (title (pcdata "Disconnect")) []) (body [ (match sessdat with | Eliom_sessions.Data_session_expired -> p [pcdata "Your session has expired."] | Eliom_sessions.No_data -> p [pcdata "You were not connected."] | Eliom_sessions.Data _ -> p [pcdata "You have been disconnected."]); p [Eliom_predefmod.Xhtml.a session_data_example sp [pcdata "Retry"] () ]])) (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register session_data_example_close session_data_example_close_handler; Eliom_predefmod.Xhtml.register session_data_example session_data_example_handler; Eliom_predefmod.Xhtml.register session_data_example_with_post_params session_data_example_with_post_params_handler
To close a session, use the function
Eliom_sessions.close_session.
Session data will disappear when the session is closed (explicitely
or by timeout).
Warning: if your session data contains opened file descriptors,
they won't be closed by OCaml's garbage collector. Close it yourself!
(for example using Gc.finalise).
We will see in the following of this tutorial how to improve this example to solve the following problems:
Eliom allows to replace a public service by a service valid only for
one user.
Use this to personalise main services for one user (or to create new
coservices available only to one user, see later).
To create a "session service", register the service in
a "session service table" (valid only for one client)
instead of the public table. To do that,
use register_for_session
(for example
Eliom_predefmod.Xhtml.register_for_session).
Users are recognized automatically using a cookie.
Use this for example if you want two versions of each page,
one public, one for connected users.
To close a session, use
Eliom_sessions.close_session.
Both the session service table and the session data table for that user
will disappear when the session is closed.
Note that register_for_session and close_session take sp as parameter (because sp contains the session table).
The following example shows how to reimplement the previous one
(session_data_example),
without using Eliom_sessions.set_volatile_session_data.
Note that this version is less efficient than the other if your site
has lots of pages, because it requires to register all the new services
each time a user logs in. But in other cases, that feature is really
useful, for example with coservices (see
later).
We first define the main page, with a login form:
(************************************************************) (************ Connection of users, version 2 ****************) (************************************************************) (* -------------------------------------------------------- *) (* Create services, but do not register them yet: *) let session_services_example = Eliom_services.new_service ~path:["sessionservices"] ~get_params:Eliom_parameters.unit () let session_services_example_with_post_params = Eliom_services.new_post_service ~fallback:session_services_example ~post_params:(Eliom_parameters.string "login") () let session_services_example_close = Eliom_services.new_service ~path:["close2"] ~get_params:Eliom_parameters.unit () (* ------------------------------------------------------------- *) (* Handler for the "session_services_example" service: *) (* It displays the main page of our site, with a login form. *) let session_services_example_handler sp () () = let f = Eliom_predefmod.Xhtml.post_form session_services_example_with_post_params sp (fun login -> [p [pcdata "login: "; string_input ~input_type:`Text ~name:login ()]]) () in return (html (head (title (pcdata "")) []) (body [f])) (* ------------------------------------------------------------- *) (* Handler for the "session_services_example_close" service: *) let session_services_example_close_handler sp () () = Eliom_sessions.close_session ~sp () >>= fun () -> return (html (head (title (pcdata "Disconnect")) []) (body [p [pcdata "You have been disconnected. "; a session_services_example sp [pcdata "Retry"] () ]]))
When the page is called with login parameters, it runs the function launch_session that replaces some services already defined by new ones:
(* ------------------------------------------------------------- *) (* Handler for the "session_services_example_with_post_params" *) (* service: *) let launch_session sp () login = (* New handler for the main page: *) let new_main_page sp () () = return (html (head (title (pcdata "")) []) (body [p [pcdata "Welcome "; pcdata login; pcdata "!"; br (); a coucou sp [pcdata "coucou"] (); br (); a hello sp [pcdata "hello"] (); br (); a links sp [pcdata "links"] (); br (); a session_services_example_close sp [pcdata "close session"] ()]])) in (* If a session was opened, we close it first! *) Eliom_sessions.close_session ~sp () >>= fun () -> (* Now we register new versions of main services in the session service table: *) Eliom_predefmod.Xhtml.register_for_session ~sp ~service:session_services_example (* service is any public service already registered, here the main page of our site *) new_main_page; Eliom_predefmod.Xhtml.register_for_session ~sp ~service:coucou (fun _ () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "Coucou "; pcdata login; pcdata "!"]]))); Eliom_predefmod.Xhtml.register_for_session ~sp ~service:hello (fun _ () () -> return (html (head (title (pcdata "")) []) (body [p [pcdata "Ciao "; pcdata login; pcdata "!"]]))); new_main_page sp () () (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register ~service:session_services_example session_services_example_handler; Eliom_predefmod.Xhtml.register ~service:session_services_example_close session_services_example_close_handler; Eliom_predefmod.Xhtml.register ~service:session_services_example_with_post_params launch_session
Warning: As in the previous example, to implement such connection and disconnection forms, you get more flexibility by using actions instead of xhtml services (see below for the same example with actions).
Services registered in session tables are called session or private services. Services registered in the public table are called public.
A coservice is a service that uses the same URL as a main service, but generates another page. They are distinguished from main services only by a special parameter, called state parameter. Coservices may use GET or POST parameters.
Most of the time, GET coservices are created dynamically with respect to previous interaction with the user and are registered in the session table. They allow to give a precise semantics to the "back" button of the browser (be sure that you will go back in the past) or bookmarks, or duplication of the browser's window. (See the calc example below).
Use POST coservices if you want to particularize a link or form, but not the URL it points to. More precisely, POST coservices are mainly used in two situations:
To create a coservice, use
Eliom_services.new_coservice and
Eliom_services.new_post_coservice.
Like Eliom_services.new_post_service,
they take a public service as parameter
(labeled fallback)
to be used as fallback when the user comes back without the state
parameter (for example if it was a POST coservice and/or the coservice
has expired).
The following example shows the difference between GET coservices (bookmarkable) and POST coservices:
(************************************************************) (************** Coservices. Basic examples ******************) (************************************************************) (* -------------------------------------------------------- *) (* We create one main service and two coservices: *) let coservices_example = Eliom_services.new_service ~path:["coserv"] ~get_params:Eliom_parameters.unit () let coservices_example_post = Eliom_services.new_post_coservice ~fallback:coservices_example ~post_params:Eliom_parameters.unit () let coservices_example_get = Eliom_services.new_coservice ~fallback:coservices_example ~get_params:Eliom_parameters.unit () (* -------------------------------------------------------- *) (* The three of them display the same page, *) (* but the coservices change the counter. *) let _ = let c = ref 0 in let page sp () () = let l3 = Eliom_predefmod.Xhtml.post_form coservices_example_post sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"incr i (post)" ()]]) () in let l4 = Eliom_predefmod.Xhtml.get_form coservices_example_get sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"incr i (get)" ()]]) in return (html (head (title (pcdata "")) []) (body [p [pcdata "i is equal to "; pcdata (string_of_int !c); br (); a coservices_example sp [pcdata "reload"] (); br (); a coservices_example_get sp [pcdata "incr i"] ()]; l3; l4])) in Eliom_predefmod.Xhtml.register coservices_example page; let f sp () () = c := !c + 1; page sp () () in Eliom_predefmod.Xhtml.register coservices_example_post f; Eliom_predefmod.Xhtml.register coservices_example_get f
Try coserv.
Note that if the coservice does not exist (for example it has expired), the fallback is called.
In this example, coservices do not take any parameters (but the state parameter), but you can create coservices with parameters. Note that the fallback of a GET coservice cannot take parameters. Actually as coservices parameters have special names, it is possible to use a "pre-applied" service as fallback (see later).
Exercise: Rewrite the example of Web site with connection (session_data_example, with session data) using a POST coservice without parameter to make the disconnection link go back to the main page of the site instead of a "disconnection" page. It is better for ergonomics, but it would be even better to stay on the same page ... How to do that with POST coservices? A much better solution will be seen in the section about actions and non-attached coservices.
While designing a Web site, think carefully about the URLs you want to use. URLs are the entry points of your site. Think that they may be bookmarked. If you create a link, you want to go to another URL, and you want a page to be generated. That page may be the default page for the URL (the one you get when you go back to a bookmarked page), or another page, that depends on the precise link or form you used to go to that URL (link to a coservice, or page depending on post data). Sometimes, you want that clicking a link or submitting a form does something without changing the URL. You can do this using non-attached coservices (see below).
Eliom is using the concept of continuation. A continuation represents the future of a program (what to do after). When a user clicks on a link or a form, he chooses the future of the computation. When he uses the "back" button of the browser, he chooses to go back to an old continuation. Continuations for Web programming have been introduced by Christian Queinnec, and are a big step in the understanding of Web interaction.
Some programming languages (Scheme...) allow to manipulate continuations using control operators (like call/cc). The style of programming used by Eliom is closer to Continuation Passing Style (CPS), and has the advantage that it does not need control operators, and fits very well Web programming.
Coservices allow to create dynamically new continuations that depend on previous interactions with users (See the calc example below). Such a behaviour is difficult to simulate with traditional Web programming.
If you want continuations dedicated to a particular user register them in the session table.
Non-attached coservices are coservices that are not attached to an URL path. When you point a link or a form towards such a service, the URL does not change. The name of the service is sent as a special parameter.
As for attached coservices, there are GET and POST versions.
To create them, use
Eliom_services.new_coservice' or
Eliom_services.new_post_coservice'.
POST non-attached coservices are really useful if you want a
link or form to be present on every page but you don't want the
URL to change. Very often, non-attached POST coservices are
used with actions or redirections
(see more details and an example in the section about
actions below).
Non-attached coservices are distinguished by there names (if the optional name parameter is present), or a number (automatically generated and every times different).
You can register coservices in session tables to create dynamically new services dedicated to an user. Here is an example of pages that add two integers. Once the first number is sent by the user, a coservice is created and registered in the session table. This service takes the second number as parameter and displays the result of the sum with the first one. Try to duplicate the pages and/or to use the back button of your navigator to verify that it has the expected behaviour.
(************************************************************) (*************** calc: sum of two integers ******************) (************************************************************) (* -------------------------------------------------------- *) (* We create two main services on the same URL, *) (* one with a GET integer parameter: *) let calc = new_service ~path:["calc"] ~get_params:unit () let calc_i = new_service ~path:["calc"] ~get_params:(int "i") () (* -------------------------------------------------------- *) (* The handler for the service without parameter. *) (* It displays a form where you can write an integer value: *) let calc_handler sp () () = let create_form intname = [p [pcdata "Write a number: "; Eliom_predefmod.Xhtml.int_input ~input_type:`Text ~name:intname (); br (); Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:"Send" ()]] in let f = Eliom_predefmod.Xhtml.get_form calc_i sp create_form in return (html (head (title (pcdata "")) []) (body [f])) (* -------------------------------------------------------- *) (* The handler for the service with parameter. *) (* It creates dynamically and registers a new coservice *) (* with one GET integer parameter. *) (* This new coservice depends on the first value (i) *) (* entered by the user. *) let calc_i_handler sp i () = let create_form is = (fun entier -> [p [pcdata (is^" + "); int_input ~input_type:`Text ~name:entier (); br (); string_input ~input_type:`Submit ~value:"Sum" ()]]) in let is = string_of_int i in let calc_result = register_new_coservice_for_session ~sp ~fallback:calc ~get_params:(int "j") (fun sp j () -> let js = string_of_int j in let ijs = string_of_int (i+j) in return (html (head (title (pcdata "")) []) (body [p [pcdata (is^" + "^js^" = "^ijs)]]))) in let f = get_form calc_result sp (create_form is) in return (html (head (title (pcdata "")) []) (body [f])) (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register calc calc_handler; Eliom_predefmod.Xhtml.register calc_i calc_i_handler
Actions are services that do not generate any page.
Use them to perform an effect on the server (connection/disconnection
of a user, adding something in a shopping basket, delete a message in
a forum, etc.). The page you link to is redisplayed after the action.
For ex, when you have the same form (or link) on several pages
(for ex a connection form),
instead of making a version with post params of all these pages,
you can use only one action, registered on a non-attached coservice.
To register actions, just use the module Eliom_predefmod.Action
instead of Eliom_predefmod.Xhtml (or Eliom_duce.Xhtml, etc.).
For example
Eliom_predefmod.Action.register,
Eliom_predefmod.Action.register_new_service,
Eliom_predefmod.Action.register_for_session.
Here is one simple example. Suppose you wrote a function remove to remove one piece of data from a database (taking an identifier of the data). If you want to put a link on your page to call this function and redisplay the page, just create an action like this:
let remove_action = Eliom_predefmod.Action.register_new_post_coservice' ~post_params:(Eliom_parameters.int "id") (fun sp () id -> remove id)
Then wherever you want to add a button to do that action (on data id), create a form like:
Eliom_predefmod.Xhtml.post_form remove_action sp (fun id_name -> Eliom_predefmod.Xhtml.int_input ~input_type:`Hidden ~name:id_name ~value:id (); Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:("remove "^string_of_int id) ())
Here we rewrite the example session_data_example using actions and named non-attached coservices (note the POST coservice for disconnection, much better than the previous solution that was using another URL).
(************************************************************) (************ Connection of users, version 3 ****************) (************************************************************) (* -------------------------------------------------------- *) (* We create one main service and two (POST) actions *) (* (for connection and disconnection) *) let connect_example3 = Eliom_services.new_service ~path:["action"] ~get_params:Eliom_parameters.unit () let connect_action = Eliom_services.new_post_coservice' ~name:"connect3" ~post_params:(Eliom_parameters.string "login") () (* As the handler is very simple, we register it now: *) let disconnect_action = Eliom_predefmod.Action.register_new_post_coservice' ~name:"disconnect3" ~post_params:Eliom_parameters.unit (fun sp () () -> Eliom_sessions.close_session ~sp ()) (* -------------------------------------------------------- *) (* login ang logout boxes: *) let disconnect_box sp s = Eliom_predefmod.Xhtml.post_form disconnect_action sp (fun _ -> [p [Eliom_predefmod.Xhtml.string_input ~input_type:`Submit ~value:s ()]]) () let login_box sp = Eliom_predefmod.Xhtml.post_form connect_action sp (fun loginname -> [p (let l = [pcdata "login: "; Eliom_predefmod.Xhtml.string_input ~input_type:`Text ~name:loginname ()] in l) ]) () (* -------------------------------------------------------- *) (* Handler for the "connect_example3" service (main page): *) let connect_example3_handler sp () () = let sessdat = Eliom_sessions.get_volatile_session_data ~table:my_table ~sp () in return (html (head (title (pcdata "")) []) (body (match sessdat with | Eliom_sessions.Data name -> [p [pcdata ("Hello "^name); br ()]; disconnect_box sp "Close session"] | Eliom_sessions.Data_session_expired | Eliom_sessions.No_data -> [login_box sp] ))) (* -------------------------------------------------------- *) (* Handler for connect_action (user logs in): *) let connect_action_handler sp () login = Eliom_sessions.close_session ~sp () >>= fun () -> Eliom_sessions.set_volatile_session_data ~table:my_table ~sp login; return () (* -------------------------------------------------------- *) (* Registration of main services: *) let () = Eliom_predefmod.Xhtml.register ~service:connect_example3 connect_example3_handler; Eliom_predefmod.Action.register ~service:connect_action connect_action_handler
Note that actions return (). See later for more advanced use
That version of the site with connection solves the main problems of sessdata:
We'll see later how to display an error message if the connection goes wrong, and how to have persistent sessions (that stay opened even if the server is re-launched).
Eliom_common.Eliom_function_forbidden_outside_site_loading
most of the time,
but you may also get unexpected results (if the thread is executed
while another site is loaded).
If you use threads in the initialization phase of your module
(for example if you need information from a database),
use Lwt_unix.run to wait the end of the thread.
.