- Notifications
You must be signed in to change notification settings - Fork252
PxtForDummies
by Bret McMillan
PXT stands for Pseudo-XML Templates. Essentially, you embed XML tagswithin HTML and have those tags replaced dynamically upon eachrequest. This is similar to, say, defining your own custom taglibrary in JSP.
PXT is the brain-child of Chip Turner, one of themembers of the Red Hat Network web team. Other members of the teamincluded Robin Norwood, Greg DeKoenigsberg, and myself.
The RHN web team uses PXT for three main reasons:
Separation of church and state: Unlike JSP, PXT does not allow youto embed actual code within the dynamic HTML documents. This forcesthe webpages to be legible, and therefore more maintainable. All theheavy lifting for dynamic code is relegated to Perl modules. Trustus--this is a !VeryGoodThing(tm.)
It's lightweight. RHN once had speed issues, largely due toApache::ASP overhead, and some decisions made in the usage ofApache::ASP. It no longer does, partly because of PXT. A simplifiedXML parser allows for fast parsing, resulting in little overhead spentaround a request.
Easy (read: near trivial) integration of XMLRPC, SOAP, and generalXML calls. This allows a page that serves HTML to also service thosetypes of calls. This means creating a command line interface forclients or other forms of automation will be much easier, as the codeto handle both the HTML display and the XML response can be shared.This simplifies interfaces and increases code reuse.
Here's a simple PXT file (foo.pxt):
#!text/html <pxt-use module="ColorSniglet" /> <html> <my_tag> <body bgcolor={bgcolor}> Hello world.<br> The background color is {bgcolor}. The posted color is <pxt-formvar>{formvar:posted_color}</pxt-formvar>. </body> </my_tag> </html>This basically is registering a perl module that, for the rest of thedocument, will be either watching for tags to replace (like<body_tag>), awaiting callbacks, or awaiting XML-RPC/SOAP calls. Forour example, let's say that the HTML output after foo.pxt processesis:
#!text/html <html> <body bgcolor=red> Hello world. The background color is red. The posted color is blue. </body> </html>The perl module (known as a "Sniglet" by the RHN Web Team) registeredthe <body_tag> tag in the following subroutine (in ColorSniglet.pm):
#!perl sub register_tags { my $class = shift; my $pxt = shift; $pxt->register_tag("my_tag" => \&my_tag); }The &my_tag is a reference to a function. The function name doesnot have to match the tag, though adopting that convention improvesmaintainability. Here is the source for the body_tag subroutine:
#!perl sub my_tag { my $pxt = shift; my %params = @_; # equivalent to $pxt->block, this returns everything between # <my_tag> and </my_tag>, in this case: # # Hello world.<br> # The background color is {bgcolor}. # The posted color is {formvar:posted_color}. # my $ret = $params{__block__}; # subtag substitution is most easily done through a simple s/// # reg-exp expression. my $ret =~ s/\{bgcolor}\}/red/gism; return $ret; }$ret is going to be the replacement value for everything between the<my_tag></my_tag> tags, inclusive. Note that as a point of style,no HTML exists in the perl module. Use subtags (things within {}'s)appropriately so that the separation between presentation and dataremains clear.
If two Perl modules register the same tag, the first one registered"wins" and determines what handles the tag. It is either a feature ora bug that PXT doesn't warn about such clashes; we haven't decidedyet. In general, do not rely on this feature. If you have two of thesame tags registered, be sure they both reference the same function.
<pxt-include/> reads the contents of a file and dumps it into thecurrent document. Any<pxt-XXXX/> tags within the included documentwill be parsed, regardless of its extension. Note that all files arerelative to the document root of the current virtual host. Also notethat parsing is re-entrant and recursive, meaning tags registered arenot necessarily registered when thepxt-include tag is expanded.
In the urlhttp://www.foo.com/index.pxt?foobar=1 {{{"{formvar:foobar}"}}} is automatically replaced with "1" byPXT.
Your basic comment. Anything begin the start and end tags will be utterlyignored by PXT, including valid PXT tags. This is different from anHTML comment which, while not affecting the rendering of a page, isstill visible in the source of the page. pxt-comments are filteredout before serving.
Typical form interaction on the web usually falls into a simplepattern: display form, process results, display results. PXTaccommodates this model through the use of tags and callbacks.
As discussed earlier, tags can create arbitrary HTML output. Thisobviously includes form elements, such as and soforth. So the first stage, displaying a form, is typically handled inthis way. Whether your PXT tag generates all of the necessary formelements, or whether most are in the HTML itself is up to thedesigner. Typically, though, it is best to let an HTML designer makethe form to their satisfaction, and then create a tag that renders theform itself and inserts appropriate values. For instance:
#!text/html <FORM METHOD="POST"> Username: <INPUT TYPE="TEXT" NAME="USERNAME" VALUE=""><BR> Password: <INPUT TYPE="PASSWORD" NAME="PASSWORD" VALUE=""><BR> </FORM>would become:
#!text/html <pxt-use> <rhn:login_form METHOD="POST"> {login:error_message} Username: <INPUT TYPE="TEXT" NAME="USERNAME" VALUE="{formvar:USERNAME}"><BR> Password: <INPUT TYPE="PASSWORD" NAME="PASSWORD" VALUE=""><BR> {login:login_hidden_formvars} </rhn:login_form>The code to render the above would essentially return the block it ishanded wrapped inside of a true
tag. Note the use of{formvar:USERNAME} to insert the value of a form variable namedUSERNAME, should it exist.Also note the use of {login:error_message} -- this is where you mightdisplay something to the effect of "Invalid username or password" sothat the user has visual feedback as to why they didn't log in. Sucha situation is where the formvar:USERNAME substitution would makesense as well, so that the user need not retype their username.(Note: there is actually core functionality for generally handlingdisplaying messages to the user like the above using a system ofmessage queues. This is necessary when you wish to display a messageoutside of a block inside of the tag handler that might generate thetag).
That's all well and good, but what happens when the user hits thesubmit button? The next phase is called a trap or callback, whichcorresponds to the "process results" stage. This is where you woulddo things like: verify username and password; update a databaserecord; load more database records to display. In other words, thisis the place you put your code that handles various input formvariables and decides what to do with them. In the above example, youwould use the USERNAME and PASSWORD form variables to either log auser in or present them with an error.
A callback is similar to a tag, in that the first parameter itreceives is the ubiquitous $pxt object. However, it receives no otherparameters, because it isn't declared as an XML tag (in other words,where a tag handler would receive tag attributes, the callback handlerreceives nothing).
Typically, a callback will verify data (though often it isn't capableof doing so meaningfully without tying unnecessary business rules atthis level). It will then either shove it in a database, update agiven record, or perhaps register a user as logged in. It could alsoperform some kind of search, or any number of things. Once it isdone, the callback can either simply return, which then tells PXT togo ahead and render the page loaded, or it can issue a redirect, sothat the user is sent to a different page (for instance, the page thatshows that you have successfully logged in, or the page that willdisplay search results). Use of redirection depends largely on theACTION= attribute of the form itself. Typically, leaving ACTION=blank and issuing a redirect to wherever a request should go is whatseems to work well, but there is no clear best practice in this case.
PXT itself decides whether or not to execute a callback based on asimple test. If there is a form variable named pxt-trap, and if a .pxtfile has registered a callback whose name is the contents of that formvariable, then it executes it. Otherwise it completely ignores it.That means IT IS VERY IMPORTANT TO HAVE A FORM'S ACTION HANDLER POINTTO A .PXT THAT REGISTERS THE CALLBACK YOU WISH TO USE. Otherwise, itwill silently ignore you. This is typically why I choose to haveACTION= point back to the same page. Also note that having oneSniglet point to a callback in another is bad practice, in that itcreates possibly tangled dependencies. It should be avoided whenpossible, and documented clearly when it is necessitated.
TODO: note how to pull out tag attributes as params in the taghandler
TODO: explain tag priorities/order. basically, you can give a taghandler function a priority (lower #'s execute 1st, negative #'s arelegal).
$pxt->register_tag('some_tag' => &some_tag_handler, 2);
Every tag, callback handler, XML-RPC handler, and SOAP handler receivesaPXT::Request object (in RHN code, it's usually the $pxt thingy). This$pxt object is the first parameter passed to the handling function.
The $pxt object is the only way a given tag handler, callback handler,XML-RPC handler, or any other kind of handler can interact with the webserver or user. The $pxt object is where you get information aboutthe user who is logged in, any session data, form variables, cookies,pnotes, etc. It is absolutely verboten to directly speak with theApache->request module in a Sniglet. All interaction MUST go throughthe $pxt object.
Since the $pxt object is the chief method of interacting with theserver or user, it has a large number of methods. However, they cantypically be grouped together into functional sets. For instance,there are the methods typically used when a PXT registration begins(register_handler, register_callback, etc). Others typically are usedto ask Apache about the user's request: what IP address are they from,what headers did they send, what cookies did they send, what formvarsthey provided. Others alter the current state: redirect, forinstance, pnotes, push_message. Some are shortcuts:prefill_form_values, message_tag_handler. However, all revolve aroundthe same axiom: if you need to know something about the HTTP request,the user, or the server, use $pxt. It is your connection toeverything that involves HTTP and the web server.
TODO: Explain this more fully.
When the httpd/mod_perl gets a request for a .pxt file, it's sent tothePXT::ApacheHandler->handler function. It sets up all utility dataobjects, registers tag handlers and callbacks, does any redirectvoodoo necessary, and also initiates the actual parsing/replacement ofthe tags. Adding any<pxt-XXX/> directives will require fullcomprehension of the flow of execution fromPXT::ApacheHandler->handler down.
(or, how to build a database website with PXT...)
Creating a website to talk to RHN using PXT involves a number of points ofunderstanding. You need to first understand how PXT interact with sniglets,and then understand how to leverage the RHN::XXXX and RHN::DB::XXXX classesfrom within your sniglets. Then add to the list session management, andauthentication issues.
TODO: Clean this piece of introduction up.
Visually:
+-------------+ | index.pxt | +-------------+ | Sniglet | +-------------+ | RHN:: | +-------------+ | RHN::DB:: | +-------------+ | DBI | +-------------+Arranged in that way, it looks like a cake. No layer of the cake cantouch (or even knows the existence of) any layer it doesn't touch.Specifically, a Sniglet handler doesn't talk to DBI; an RHN:: objectdoesn't talk to DBI; RHN:: doesn't speak HTML.
Sniglets are what the RHN Web team calls the lovely little perl modules thatregister tag handlers, callback handlers and the like. Building a webapplication primarily will involve writing .pxt files containing html andPXT tags, as well as writing the sniglets that handle said PXT tags.
For the RedHat Network, we've created a group of classes that contain thebusiness logic, as well as the necessary knowledge about the database.The RHN::XXXX modules are the hooks to build websites upon. Nearly everyRHN::XXXX module has a corresponding RHN::DB::XXXX module that is verychummy with our Oracle database.
If you're going to be making a website that ties into RHN, your sniglets willbe making copious use of the RHN::XXXX modules. Occaisionally, the RHN::XXXXfunctions will return an RHN::DB::XXXX object that you can use. More on thislater.
TODO: Talk about "the man behind the curtain" stuff. Basically, our reasonsfor organizing things this way, as well as the automagic accessor generation,and creation/update/commit stuff.
Included as a part of PXT is session handling, and is made available inthe following manner:
#!perl$session = $pxt->session();The $session object itself is defined in RHN::Session, not under PXT::*The session object in PXT is created at the initialization phase of therequest. Before we delve into more detail about the methods availble foruse in a session:
A session is a place to store permanent dataA session isnot a place to store transient data
"Why," you ask? Because RHN::Session utilizes a database, and there arebetter places than a db to store transient data.
Session data for a visitor is stored in key/value fashion, with theexception of the visitors user id. To set a new session variable duringa request:
#!perl # some request $session = $pxt->session(); $session->set('visits',1);In order to access session information during a request:
#!perl # another seperate request $session = $pxt->session(); $visits = $session->get('visits'); print "You have visited $visits pages for this session";Notice that you do not need to check for the existence of a session,because every user is given a session at the initialization phase. Also,note that a users uid is retrieved directly from the following sessionmethod (i.e.not from $session->get):
#!perl $session = $pxt->session(); $userid = $session->uid;TODO: explain how they're useful; contrast to sessions
As a convenience you can access RHN::User via an accessor method in PXT. Notethat unlike sessions, an RHN::User object is not created during initializationnor at any other time. A user of PXT retrieves the user object as follows:
#!perl$user = $pxt->user(); #object returned from RHN::Userprint "no such user exists" if(!$user);In addition, you can clear the user from pxt, if you so wish:
#!perl$pxt->clear_user();- Otherwise known as - "The kitchen sink, -- how much stuff can we cram in?"
You may be wondering "Why are you talking about XML-RPC? This is a templatingsolution." And in many ways you would be right, except, that PXT changes the'paradigm'sorry on how many XML-RPC servers work. In the CURRENT MOD_PERLWORLD, the httpd.conf controls what directories are "XML-RPC listeners".These listeners correspond to an XML-RPC handler (which is nothing more than awrapper around Frontier::RPC2) and a server file. There can only be oneserver file per directory. Listeners can branch out and obtain routines fromother files, but there must be a single point of entry, making a somewhatrigid framework to work with... (Theoretically you could create a filetype asa handler for Frontier to handle directly, but it would be rathernon-intuitive, and not exactly the "norm"..)
PXT works a little differently. Instead of registering directories or specialfiletypes, PXT uses the exact filenames that you would be talking to via a webbrowser.
Let me say that again a little differently..
http://localhost/foo/product_list.pxt and via a web browser could, saybringup a list of products on a nice html page... (maybe some buttons to some linksto start messing with the data)...
http://localhost/foo/product_list.pxt called by an XML-RPC client wouldhavethe following methods available to them.
product.list - returns an arrayRef of objectsproduct.modify - takes an arugment, returns the returned objects affected ..The PXT would inherit a few functions from PXT itself... All describingmethods about the functions that have been registered...
methods.list - returns a list of documentation on the above two functionsPXT - XML-RPC Example -- "message of the day"
#!text/html
Now here this! -> {motd} !!!
#!perl
=== Begin Sniglets::XMLTest.pm ===
use strict;
package Sniglets::XMLTest;
sub register_xmlrpc {my $class = shift;my $pxt = shift;
$pxt->register_xmlrpc("display_motd", &display_motd_xml);}
sub register_tags {my $class = shift;my $pxt = shift;
$pxt->register_tag("display_motd", &display_motd);}
sub get_motd {unless ( -f "/etc/motd") {die "motd not found!";}
open(MOTD, "/etc/motd");my $motd;while ( ) {$motd .= $_;}close MOTD;return $motd;}
sub display_motd {my $pxt = shift;my $motd;
eval {$motd = get_motd();};
if ( $@ ) {$motd = "No motd on file...";}
my $block = $pxt->block();
$block =~ s/{motd}/$motd/gism;return $block;}
sub display_motd_xml {my $pxt = shift;my $motd;eval {$motd = get_motd();};
if (
$@ ) {die['-1', "$ @"];}return $motd;}1;
=== END Sniglets::XMLTest.pm ===
Example Explained.
This routine bases itself around getting the message of the day that residesin /etc .. If the file does not exist, the http request simply fills in somedummy data to let the user know whats up... Via XML-RPC though, it is returnedvia a fault... Note how the call dies by an arrayRef, the first valuecorresponding to the fault code, the other the fault string.
Both of these functions share the same data accessor, yet, display the dataslightly differently (one by altering the block and returning it, the otherby just returning it to RPC2 for encapsulation)..
Do you want to contribute to this wiki? See pageWikiContribute for more info.