📜 ⬆️ ⬇️

Mojolicious Documentation: Lost Chapters

Update: article updated to comply with Mojolicious 6.0.

Mojolicious is a great modern Perl web framework. Among the shortcomings, I can name only two: backward compatibility policy and documentation.

This series of articles assumes that the reader is already superficially familiar with the framework, and he has a need to understand the details that are either not described in the documentation or described in insufficient detail and understandable. Official documentation is ideal for initial familiarization (in English).
')

Content


  1. disadvantages
  2. Routing: internal device
  3. Routing: setting
  4. HTTP request options
  5. Parsing
  6. Tips & Tricks
    1. Support for non-blocking CGI applications
    2. How Mojo :: UserAgent works when testing your application
    3. ojo and Mojolicious :: Lite
    4. Environment variables


Other articles in this series



disadvantages


In the official FAQ it is written: "... we will always be able to make it out." For a start, the second phrase contradicts the first. Further, here is a quote from Guides :: Contributing "It can be changed for a period of at least 3 months.". Honestly, 3 months is a ridiculous period when it comes to backward compatibility, but it seems that even this period is not always respected (support for “X-Forwarded-HTTPS” was made deprecated two months ago and deleted a month ago - yes, it was “Major release” is therefore not formally broken, but the general relation to backward compatibility is quite significant). How many developers update the framework more than once every 3 months, and even then carefully read Changes or the logs of their application for deprecated warnings? At the same time, during the last year approximately 20 functions / features were deprecated. In practice, of course, everything is not as bad as it sounds - something breaks not so often (for me personally, over the last year only the replacement of $app->secret() by $app->secrets() ) has touched me. But the fact remains - backward compatibility is broken, often broken, and without really good reasons: for example, in the case of secret() nothing prevented adding to the code
 sub secret { shift->secrets([shift]) } 
or simply add support for additional parameters to the secret() instead of adding the new function secrets() implementing the necessary feature without breaking the compatibility at all.

As for the documentation, many consider it excellent, even one of the serious advantages of Mojolicious, but not a disadvantage. The problem with documentation is that it’s all focused on examples. This is really cool when you start learning the framework. This saves a lot of time when you need to make a feature and you quickly google an example of a similar feature in the official guides. But as soon as you go beyond the standard tasks and there is a need to understand how something works ideologically or architecturally, what specific parameters this function can take and what exactly it can return in different situations - it turns out that for many Mojolicious modules such documentation is missing basically. And not because this information belongs to “undocumented possibilities” - almost all of this is mentioned briefly here and there in various examples, and therefore it is considered “documented”. Often there are several ways to access certain data (request parameters, response body, etc.), but it is not described how they differ from each other and in what situations it is more correct to use in what ways. And the last - the alphabetical order of the functions in the dock, seriously ?! No, I understand, all people are different and for sure it is convenient for someone, but it’s still an order of magnitude easier for many to understand the documentation in which functions are grouped by task. (Although in the code, especially when reading it through the browser, where it is not so convenient to use the search as in Vim, the alphabetical order of functions suddenly turned out to be quite convenient - except for new / DESTROY / AUTOLOAD - it’s still better to place them at the beginning.) As a result, to understand you have to read the code (some prefer to watch the tests instead!), which is not so easy - firstly, it is not a standard of readability: the author likes to use pearl chips, which allow you to write code compactly (and often this code works faster), but readability is worse; secondly, the active use of both the inheritance and the exchange of events between objects complicates the understanding of what is happening inside the 104 classes that make up Mojolicious-5.

With the problem of backward compatibility, we can do little (although, probably, we can make a plugin for Mojolicious, which will emulate it as far as possible). But the second problem is not difficult to solve - you can write the missing documentation yourself. As I study Mojolicious, I plan to describe some things that, in an amicable way, should be in the official documentation, hence the name of this article.

$ self


In Mojolicious documentation, $self often used, which does not add readability — there are too many classes in the framework, and not always looking at $self it is easy to figure out which class this object is in this example. Therefore, I will use instead of $self in the examples:
 $app # YourApp → Mojolicious $r # Mojolicious::Routes ($app->routes) $c # YourApp::SomeController → Mojolicious::Controller $ua # Mojo::UserAgent 

Routing: internal device


The first thing you need to understand about the device routing in Mojolicious is that it is implemented as a tree of nodes, and the structure of this tree is (almost) in no way connected with the hierarchy of the path in the url. Consider an example:
 $r->get("/a/b/c/d") ->to(text=>"1"); $ab = $r->route("/a")->route("/b"); $ab->get("/c") ->to(text=>"2-1"); $ab->get("/c/d") ->to(text=>"2-2"); $r->get("/a/b/c/d/e") ->to(text=>"3"); 

As a result, the following tree will be built:
 $ r {}
  ├─ / a / b / c / d {text => "1"}
  ├─ / a─── / b─┬─ / c {text => "2-1"}
  │ └─ / c / d {text => "2-2"}
  └─ / a / b / c / d / e {text => "3"}

And this is how it will work:
 GET / a / b => 404 Not Found
 GET / a / b / c => "2-1"
 GET / a / b / c / d => "1"
 GET / a / b / c / d / e => "3"
 GET / a / b / c / d / e / f => 404 Not Found

As you can guess, the tree is scanned sequentially (inwards), until the first successful match with the current query - so if you have nodes in the routing definition that match the same queries, then watch carefully where they are in the order, in which they coincide with queries, it may not coincide with the order in which they are written in code.

The second thing you need to understand is that only leaves (terminal nodes) of the tree will process incoming requests, all intermediate (internal) nodes will not process requests, regardless of how they were created (via route() , normal get() , etc.) and whether the request handler is set for them ( "controller#action" or {cb=>\&handler} , etc.).

For example, create a tree using get() :
 $r->get("a", {text=>"A"})->get("b", {text=>"B"}); 

 GET / a => 404 Not Found
 GET / a / b => "B"

Or we will not create any nodes at all, but instead simply configure an already existing root node:
 $app->routes->to(text=>"wow"); 

 GET / => "wow"

The only case where the handler specified by the intermediate node is used is if this node is under. Such nodes are created via under() , or an existing node can be made under by calling inline(1) . After determining the terminal node that should process the current request, handlers of all the under-nodes will be sequentially called from the root of the tree to the terminal. These handlers must return true or false (you can even asynchronously) - if they return false, subsequent handlers, including the terminal node handler, will not be called.

Further, any tree node may contain the following information:
  1. HTTP method (s)
  2. path pattern (pattern), may include placeholders
     "/something/:name/:id" 
  3. constraints for allowed values ​​of placeholders
     name => ["alex","nick"], id => qr/^\d+$/ 
    • special limitation: is it possible to add extensions to the path template and which ones
       format => 0  format => ["json","xml"] 

  4. conditions - any functions that will be called after matching the path with the template to perform any additional checks (eg http-headers) and return true / false to allow or prohibit the use of this node to process the current request
     agent => qr/Firefox/ 
  5. default parameters (defaults) - here you can set control parameters (controller / action / cb / ...), and default values ​​for placeholders (which make these placeholders optional) and any other values ​​that should be in $c->stash() when processing the request
  6. the explicit name of this node - for example. for use in url_for
    • if you do not specify it, it will be generated automatically.

All this data (except for the node name) is “inherited” by nested nodes (unless they override them explicitly), which allows you to create and use intermediate nodes solely to set all of this default data for nested nodes. By the way, nodes can be created without giving them any parameters at all, even a path template — in this case, it will be the same as that of the parent node.
 #  defaults     . $r->to("users#"); #    /resource   . $b = $r->route("/resource", format => 0); #   $b     $b     #         /resource. #    get()    ,    #     ,    , ..  /resource. $b->get()->over(agent=>qr/Firefox/)->to("#ff_only"); $b->get()->to("#not_ff"); 

As far as I understand, there is most likely no difference between setting default values ​​via $app->defaults and through the root node $app->routes->to (maybe some hooks work completely before routing, and then the values ​​from $app->defaults will be available and may not be available from the root node).

There are several other nuances, for example: the under-node does not process requests even if it is a terminal node, the root node handles the format value a little differently than all other nodes ... but I don’t think this is important for a general understanding, so I won’t go into details.

I have not yet figured out how to connect a separate Mojolicious application to the current one through $r->any("/path")->to(app=>$otherapp) , maybe there are additional nuances.

Routing: setting


There is a difference between Mojolicious and Mojolicious :: Lite - in Mojolicious :: Lite, under (together with a group) works a little differently than under Mojolicious. Here I will describe the functions of Mojolicious (more precisely, Mojolicious :: Routes :: Route).

All parameters of all functions are optional (except over() , to() and via() - they return the current value when called without parameters).


 #      Mojolicious $r->get("/users/:id", [ format => 0 ], agent => qr/Firefox/, { id => -1, controller => "users" }, [ id => qr/^\d+$/ ], headers => { "X-Secret" => "letmeit" }, \&cb, { action => "list" }, "my_cool_route", ); #        get() $r->route("/users/:id", id => qr/^\d+$/, format => 0) ->via("GET") ->over(agent => qr/Firefox/, headers => { "X-Secret" => "letmeit" }) ->to(id => -1, controller => "users", action => "list", cb => \&cb) ->name("my_cool_route"); 


HTTP request options


There are not just a lot of ways to get to the request parameters, but a lot . At the same time, not all of them are worth using them - in some cases, the parameters obtained from different places are mixed together, and it’s impossible to understand where it comes from.

Mojolicious has 4 types of parameters:
  1. GET - obtained from query string in url, and the HTTP request method can be any - GET, POST, etc.
  2. POST — received from the body of a POST request of the type application/x-www-form-urlencoded or type multipart/form-data — but in this case only ordinary parameters are taken, except for files
  3. UPLOAD - files received from the body of a POST request of the type multipart/form-data
  4. ROUTE - values ​​cut from the url path using placeholders in routing, excluding those reserved for stash

Further, it should be noted that the same parameter can be passed several times, moreover, it can be transmitted several times in each of the ways - GET, POST, UPLOAD, and when defining routing we can mention the same placeholder several times . For GET, POST and UPLOAD, all transferred values ​​of one parameter are saved, but for ROUTE only one is used, the last value if one placeholder is specified several times.

Most often in examples $c->param is mentioned - let's see where the values ​​returned by this function come from (in Mojolicious up to 5.47) :
Personally, I prefer to write the most clear code, and I do not like such "magic" functions, which will return the parameter, but it is completely unknown where it comes from. Of course, in some cases you need to ignore the fact that the parameters are transferred by GET or POST, but what $c->param does is already beyond good and evil (for example, if you were expecting a GET / POST parameter , and get UPLOAD, then instead of the string value , get the object Mojo :: Upload). Everything is good in moderation, even the magic sloth functions that create the wow factor that Mojolicious so much like to implement.

Here is a list of functions that are recommended to restrict access to the parameters of the HTTP request:
This approach will ensure clarity and consistency of the code. But, for completeness, here’s a list of remaining features:

Parsing


This is not entirely from my dictionary, but I cannot find another description: in terms of parsing the downloaded pages of mojo, it’s just nice! The documentation for this part of Mojo is much better. However, there is something to add.

The fact is that Mojo is not just a server-side web framework, but a web framework in general - for both the server and the client. Therefore, modules that implement the format of HTTP messages are used by both the server and the client — and they have very different needs for processing these messages. As a result, when you need to do something and climb into the documentation, at this moment you are interested in either server functionality or client functionality - and you see not only that, both, and so carefully sorted in alphabetical order. As a result, it is quite difficult to find the necessary function, and there is a need for a visual cheat sheet, where there will be functions only for the client or only for the server, preferably grouped by some adequate criterion.

The result was the following tablet. This is the first alpha version :) so if something is not clear or there are ideas for improvement - write, we will finish it with joint efforts. A few notes:

 $tx = $ua->get($url); # Mojo::Transaction::HTTP → Mojo::Transaction $tx->error # undef  {message=>'…',…} $tx->success # undef  $tx->res $tx->req # Mojo::Message::Request → Mojo::Message $tx->res # Mojo::Message::Response → Mojo::Message $tx->redirects # [ Mojo::Transaction::HTTP, … ] $res = $tx->res; # Mojo::Message::Response → Mojo::Message $res->error # undef  {message=>'Parse error',…} $res->to_string # "…" (headers+content) $res->is_status_class(200); # bool $res->code # 404 $res->message # "Not Found" $res->headers # Mojo::Headers $res->cookies # [ Mojo::Cookie::Response, … ] $res->cookie('name') # Mojo::Cookie::Response → Mojo::Cookie $res->body # "…" $res->text # "…" (decoded body using charset) $res->dom # Mojo::DOM $res->json # Mojo::JSON $headers = $res->headers; # Mojo::Headers $headers->names # [ "Content-Type", "Server", … ] $headers->to_hash # { "Content-Type" => "…", … } $headers->header('Server') # "…" $headers->$standard_header_name # "…" (shortcuts for useful headers) $dom = $res->dom; # Mojo::DOM $dom->to_string # "…" ( ,  ) $dom->content # "…" (  ) $dom->type # "…" ( : root,tag,text,comment,…) $dom->tag # "…"  "" ( ) $dom->attr # {name=>"val",…} $dom->attr('name') # "val" $dom->{name} #  $dom->attr("name") $dom->all_text # "…" (  ) $dom->all_text(0) # "…" (  ,   ) $dom->text # "…" (  ) $dom->text(0) # "…" (  ,   ) $dom->root # Mojo::DOM ( ) $dom->parent # Mojo::DOM  undef (-) $dom->next # Mojo::DOM  undef ( -) $dom->next_node # Mojo::DOM  undef ( -) $dom->previous # Mojo::DOM  undef ( -) $dom->previous_node # Mojo::DOM  undef ( -) $dom->matches('*') # true/false (    ) $dom->at('*') # Mojo::DOM  undef (  ) $dom->find('*') # Mojo::Collection ( ) $dom->ancestors # Mojo::Collection (-) $dom->ancestors('*') # Mojo::Collection ( -) $dom->following # Mojo::Collection ( -) $dom->following("*") # Mojo::Collection (  -) $dom->following_nodes # Mojo::Collection ( -) $dom->preceding # Mojo::Collection ( -) $dom->preceding("*") # Mojo::Collection (  -) $dom->preceding_nodes # Mojo::Collection ( -) $dom->children # Mojo::Collection (-) $dom->children('*') # Mojo::Collection ( -) $dom->descendant_nodes # Mojo::Collection ( ) $dom->child_nodes # Mojo::Collection ( -) $dom->[0] #  $dom->child_nodes->[0] $res->dom('*') #  $dom->find('*') 


Tips & Tricks


CGI

Mojolicious- cgi: https://gist.github.com/powerman/5456484

Mojo::UserAgent

, $ua , TCP- - — : $app , Mojo::UserAgent, , , url, , $app .

« » , Mojolicious::Lite. , - Mojo::UserAgent — « » . :
 Mojo::UserAgent::Server->app($app); 

ojo Mojolicious::Lite

ojo , ojo Mojolicious::Lite, Mojolicious:
 $ perl -Mojo -E 'get "/", {text=>"wow\n"}; app->start' get / wow 

Environment variables

Mojolicious MOJO_ — , .. Mojolicious, :

Source: https://habr.com/ru/post/227493/


All Articles