Hi guys,

revision 1568 (http://trac.agavi.org/changeset/1568) saw the merge of  
the david-execution_flow branch to the 0.11 branch. This means some  
things will break, and it means many things have improved.

Before I begin, let me apologize for promising that there wouldn't be  
any more breaking changes after 0.11RC1. It was found out that the  
way things were, we couldn't implement caching (http://trac.agavi.org/ 
ticket/78, coming to SVN today!), so I figured I should just throw  
out rendering filters (http://trac.agavi.org/ticket/377) which were  
the primary reason caching wouldn't be possible, and while doing  
that, it became clear that I could remodel the execution flow  
entirely, with minimal to no BC breaks, away from the hacky Action  
Stack implementation to a system where we have a container that fully  
encapsulates the execution of an action, without any possibility of  
the "outer world" breaking things inside this container (http:// 
trac.agavi.org/ticket/373, and http://trac.agavi.org/ticket/290 which  
was originally scheduled for 2.0).

That all went very well, and it even became possible to use  
decorators within slots, and slots inside the content template,  
without decorators. We now had a very modern, very advanced, and were  
forward compatible execution flow model, but the templating system  
all of a sudden seemed horribly archaic in comparison. After some  
pondering, I finally had the idea to re-do the templating system so  
it works just like the decorators, but with an infinite number of  
layers, like an onion or a russian doll (http://trac.agavi.org/ticket/ 
287, also originally scheduled for 2.0). As a result, existing  
templates would work without modification, only the API in the views  
would change.

We also discovered that while our validation system worked very well  
for protecting code from malicious or non-validating request  
parameters, files, cookies, http headers etc didn't get filtered that  
way. Thus, we introduced request data holders that are now passed to  
the actions and hold all request data: parameters, files, cookies,  
headers etc (http://trac.agavi.org/ticket/389). While doing that, it  
became clear how awfully crappy the current file handling was. getFile 
() will now return an object on which you can call getSize() to get  
the size, or move() to move the uploaded file (http://trac.agavi.org/ 
ticket/391). These two changes should result in only minimal breaking  
changes and can mostly be adapted to by some search and replace work.

Also, let me stress that that's it for now. No more planned features,  
everyone's happy. There are some issues and minor bugs to clean up,  
but besides that, I'm sure we won't see any more breakage from now  
on. Also because neither I nor anyone else wants that. Really. Again,  
my apologies.


Now let's move on to the actual changes.


First, the execution flow.

Until now, execution was done using Controller::forward(). A forward  
put the information about the action to run on the global action  
stack and then started running through the filters etc. Each time  
some code wanted to get, say, the action name, it had to pull the  
last item from the stack and read the information from that. As you  
can probably imagine, that was very prone to errors and totally  
messed things up when forwarding, for instance, since the information  
about what was going on (the action name, module name, action  
instance, request data etc) was on the stack, not encapsulated within  
the execution. This decoupling and indirect way of accessing the  
current state of execution was rather ugly.

Enter execution containers.
Controller::forward() is gone now, and each container encapsulates  
all information necessary to run an action: module name, action name,  
request information, the output type (yes, the output type is _not_  
global anymore!), the response, information about the view etc. When  
execute() is called, the entire execution happens fully isolated,  
like a black box. This also brings a slight performance improvement  
for the general execution process.

As a consequence, you obviously cannot use $actionStack->getLastEntry 
()->getActionName() etc anymore. Instead, Views and Actions are given  
the container instance as the first argument to the initialize()  
method. It is then available via $this->getContainer(). Note that in  
Views, $this->getResponse() is a shortcut for $this->getContainer()- 
 >getResponse(). So for example to grab the micro timestamp at which  
the current action started execution, do $this->getContainer()- 
 >getMicrotime().

Also, filters have changed due to this. Filter execute() methods now  
get the filter chain as the first argument, and the container as the  
second. To proceed to the next filter in the chain, you must call  
$filterChain->execute($container), i.e. hand on the container you got  
as the second argument. Again, the response for the container is  
available from $container->getResponse().

Also, for forwarding and for setting slots, you use an execution  
container instance now, so to forward to EggAction in module Spam,  
you do return $this->container->createExecutionContainer('Spam',  
'Egg'); Likewise, to set a slot, you do setSlot('name', $container);  
(I'll get to slots later)
An important note here. As I mentioned, each execution container also  
has it's own output type. This is rather useful since you can run a  
slot with a different output type and have it produce, say, JSON data  
you use in a script tag in the page header. Unfortunately, the  
standard way to create an execution container is the  
createExecutionContainer method in the Controller, which will use the  
default output type. That's why each container has such a method too,  
which calls the controller's method, but with the same output type as  
that controller - basically, the container creates a "brother" of  
itself.
createExecutionContainer() optionally accepts a RequestDataHolder  
object as the third argument to pass additional information such as  
parameters, files, cookies to the forwarded action or the slot, and  
the name of an output type as the fourth argument.

When you define a forward, this information is stored in the current  
execution container. Once that finished running, it will execute this  
next container, and return THAT CONTAINER's RESPONSE. That means if  
you set a cookie or a redirect in the container and then forward,  
that information gets lost. This is usually the desired behavior, but  
there might be situations where you don't want that to happen.  
Because of that, there's also a global response in $controller- 
 >getGlobalResponse(). There are circumstances when you should use  
that one, like when setting a cookie from a routing callback or so.

Also, redirection is now done via the response by calling $response- 
 >redirect('targeturl'); Since you don't want & there, remember  
to pass array('separator' => '&') as the third argument to  
Routing::gen(). WebResponse::redirect also accepts a second parameter  
for an HTTP status code other than the default 302.

Again, you have to make a wise decision between local container's  
response and the global response. Remember, only a local container's  
response will be included in caches. Use it whenever possible.

Other than that, this change shouldn't affect actions and/or views.  
Yes, rendering filters were removed, but I don't think anyone ever  
uesed these anyway. Likely since there was absolutely no use case for  
them, all they did was cause unnecessary overhead.


Next: the new template architecture.

This is a rather big change.
Until now, we had a content template, and a decorator template. A  
decorator template could also have slots, but the content template  
couldn't, and a slot couldn't use a decorator template itself. Per- 
locale templates were only possible for the content template, not for  
the decorator. You could only have these two layers.  Both content  
templates any layers had to use the same renderer with the same  
configuration. Templates couldn't come from a database. And so on.  
Clearly, that was poorly implemented.

Now, all of the above works. And it's not even poorly implemented ;)

First, the layering.
Instead of setTemplate and setDecoratorTemplate, layers are now used.  
To mimic the old behavior, a view would have a layer called "content"  
with the content template (e.g. "IndexSuccess"), and another layer  
who's name doesn't really matter (let's call it "decorator") with the  
template "Master".
Now what happens is that the first layer is rendered, and it's result  
is available to the next layer as a slot. The name of the slot is the  
name of that layer. Here, we used "content" because it doesn't only  
make sense, but actually produces results similar to the old system,  
where the content template was always available as "content" in the  
decorator.
As you probably have guessed, layers are executed in the order they  
are defined. Of course, you can re-arrange them at runtime, remove  
them, add new ones at arbitrary locations and so on. So let's have a  
look at the code:

     $l1 = $this->createLayer('AgaviFileTemplateLayer', 'content');
     $l1->setTemplate('IndexSuccess');
     $this->appendLayer($l1);
     $l2 = $this->createLayer('AgaviFileTemplateLayer', 'decorator');
     $l2->setTemplate('Master');
     $this->appendLayer($l2);

Yes, you are absolutely right. This code is HIDEOUS. You don't want  
to do that in every view, and even with a base view (more on that  
later), it's awfully ugly.

BUT!

You can configure these layouts very easily, and as a result, you  
don't have to write that code anymore, instead you just do

     $this->loadLayout();

to load a layout (no name passed here, so it will use the default  
one). I'll explain this in more detail in a minute.

But first, let's look at slots and renderers.

First, slots. You can now set slots on _any_ of these layers:
     $l2->setSlot('latestproductbox', $this->container- 
 >createExecutionContainer('ZeModule', 'LatestProductWidget'));
will set the slot on the decorator layer, and is then available there  
via $slots['latestproductbox'] (yes, $slots, not $template, more on  
that later).

Second, renderers. Let's say you wanted to use Smarty instead of the  
default renderer for the decorator layer:
     $l2 = $this->createLayer('AgaviFileTemplateLayer', 'decorator',  
'nameofthesmartyrendererasconfiguredinoutput_types.xml');

Of course, you can also pass in a renderer instance:
     $r = new AgaviSmartyRenderer();
     $r->initialize($this->context);
     $l2 = $this->createLayer('AgaviFileTemplateLayer', 'decorator',  
$r);

!!! This means you can easily use renderers everywhere now, let's say  
in a model to render an email. What's more, you can even use layers,  
slots etc, by just creating them and rendering each of them. This is  
a bit of an advanced task, and there might be a helper to assist you  
with that, but even without it, it's only a couple of lines of code.  
Let me know if you need any assistance.

You probably noticed AgaviFileTemplateLayer. That's the name of the  
layer implementation we want to use. AgaviFileTemplateLayer is a  
special implementation of AgaviStreamTemplateLayer that allows the  
use of a directory etc, and is designed for the file system.  
AgaviStreamTemplateLayer is a generic implementation for PHP streams  
that allows you to load a template via HTTP, for example, or any  
other (built-in or userspace) stream wrapper registered with PHP:
     $l = $this->createLayer('AgaviStreamTemplateLayer', 'asdf');
     $l->setTemplate('www.myhost.com/thetemplate.php');
     $l->setScheme('http');
to load http://www.myhost.com/thetemplate.php, or, even cooler, using  
an RFC 2397 data stream (http://www.faqs.org/rfcs/rfc2397, http:// 
de.php.net/manual/en/wrappers.data.php):
     $l = $this->createLayer('AgaviStreamTemplateLayer', 'blah');
     $l->setTemplate('text/plain;base64,SGVsbG8gVGVzdA==');
     $l->setScheme('data');
That will result in "Hello Test".

Note: you can also use setParameter('name', 'value') or setParameters 
(array(...)) instead of setWhatever('value').

Back to our original example:
     $l1 = $this->createLayer('AgaviFileTemplateLayer', 'content');
     $l1->setTemplate('IndexSuccess');
     $this->appendLayer($l1);

setTemplate() isn't necessary here, because createLayer() sets the  
following parameters on a layer by default:
- "template" => the name of the current view
- "module" => the name of the current view's module
- "output_type" => the name of the output type
- "name" => the name of the layer ("content", "decorator" and so on).

Now it's getting interesting: there's a parameter called "targets",  
which holds a list of formatting patterns used for looking up the  
template resource name. For AgaviFileTemplateLayer, the default  
target pattern is:
     ${directory}/${template}${extension}
Where ${extension} is the extension you have set, or, if none was  
set, the default extension of the renderer (e.g. ".php").
${directory} is a pattern itself:
     %core.module_dir%/${module}/templates
It's a bit complicated, but I hope you get the idea - the effect is  
that the default directory is app/modues/${module}/templates, and $ 
{module} is resolved to the current module, because that is set by  
createLayer(). Now you can easily set a different directory for the  
template of a layer:
     $l->setDirectory('/the/template/dir'); (and yes, you guessed it,  
you could also do $l->setParameter('directory', '/the/template/dir');)

Of course, you can again use variables that will be expanded. This  
variable can be _any_ parameter set on the layer:
     $l->setDirectory(AgaviConfig::get('core.template_dir') . '/$ 
{module}/${output_type});
would look for the template in app/templates/Spam/html provided that  
the current module is "Spam" and the output type is "html".

IMPORTANT: Variables are expanded just in time, not as you set them,  
which means you can set a string containing a variable, and change  
that variable after that. Also, as an alternative to ${foo} you can  
also use {$foo} and $foo, the syntax works just like with PHP vars.

NOTE: if i18n is enabled, there are two more default targets for  
FileTemplateLayer:
     ${directory}/${locale}/${template}${extension}
     ${directory}/${template}.${locale}${extension}

You can now easily implement your own template layer class that loads  
the template from a database, and then caches it into a file and  
returns that file as the resource name. Or, alternatively, you can of  
course write a PHP stream wrapper that interfaces with your database  
and simply use the StreamTemplateLayer. Or you read from memcached.  
The possibilities are endless.

But we already established that doing all this in code is rather  
annoying. That's why you can define layers into layouts, and then  
load a layout in your view. This is done in output_types.xml, per  
<output_type>:

     <layouts default="standard">
       <layout name="standard">
         <layers>
           <layer name="content" class="AgaviFileTemplateLayer" />
           <layer name="decorator" class="AgaviFileTemplateLayer">
             <parameters>
               <parameter name="template">Master</parameter>
             </parameters>
           </layer>
         </layers>
       </layout>
     </layouts>

$this->loadLayout() in a view will then load the default layout  
"standard" unless you give it the name of a layout to load as the  
first argument. The method returns an array of parameters set for the  
layout, that might come in handy if you need to carry any further  
information from the definition into the view.

Each <layer> accepts the following attributes:
- "name": name of the layer
- "class": the name of the AgaviTemplateLayer implementation to use
- "renderer": the optional name of a renderer to use if you don't  
want to use the default renderer

IMPORTANT: you can now define multiple renderers per output type.  
Each renderer must now have a name, and the <renderers> element must  
define a default renderer.

Of course, you can do $this->getLayer('zename'); to grab a layer  
after calling loadLayout()!

Also, you can configure slots for a layer by putting them into a  
<slots> element which (obviously) goes into the <layer> element:

     <slot name="loginbox" module="Default" action="LoginBox" />

Each <slot> accepts the following attributes:
- "name": the name of the slot
- "module": the module of the action
- "action": the name of the action
- "output_type": an optional output type to use for the slot, else  
the current container's output type will be used.

You can also place <parameters> into a <slot> to pass these  
parameters to the action as additional request information.

To manually set a slot in the code, use $layer->setSlot(). Of course,  
again, you can modify slots, for instance:

     $this->loadLayout();
     $this->getLayer('decorator')->getSlot('loginbox')->getRequestData 
()->setParameter('foo', 'bar');

and so on.


A general word on views, base views and base actions

It is strongly recommended that you _always_ use a CustomBaseAction  
that all your actions extend, even if it is empty. Same goes for the  
view, use a CustomBaseView. You can have these autoloaded per-module  
using a module autoload.xml, that keeps things tidy. The sample app  
does this, look at it for an example.

The main reason why you should do this is because you can easily  
inject code into all actions and all views at a later time. Do it.  
Even if the classes are empty. Do it. Always. The manual has  
instructions on how to use custom build templates so "agavi action"  
always generates actions and views that extend from your base classes.

Also, you should NEVER use execute() in views! Always, always, always  
use executeHtml(), executeRss() and so on. Then, in your base view,  
implement a "public final function execute()" that forwards to the  
404 action or so. There is a very simple reason for this: if the  
output type that is set isn't handled by your view, nothing breaks!  
For instance, you might have this route at the top of your routing.xml:

<route pattern="text/javascript" source="_SERVER[HTTP_ACCEPT]"  
output_type="json" />
that would set the output type to "json" when an ajax request comes  
in (prototype sends this Accept: header, for example), or
<route name="rss" pattern="/rss$" cut="true" stop="false"  
output_type="rss" />
to set the output type to "rss" when a URL ends on /rss.

But imagine what happens if the view doesn't implement the "rss"  
output type! Things break horribly. Therefor, always use a specific  
execute method, and in your execute(), handle the situation. Instead  
of forwarding to 404, you could also check the output type and set a  
response accordingly, like the HTTP 407 Not Implemented code. Or  
throw an exception and handle it in the exception template (remember,  
these can be per output type, too). But do it right!


And finally: request data holders.

You already know the parameter holder passed to action and view  
execute() methods. This is now an AgaviRequestDataHolder (typically  
AgaviWebRequestDataHolder) object instead which also holds  
information other than parameters, like cookies, files and headers.  
they all pass validation too inthe exactly same manner as parameters:

$rd->getHeader('User-Agent'); // or 'USER_AGENT' or 'user-agent' etc,  
doesn't matter

$rd->getCookie('cookiename');

Also, there's no getFileName, getFileError etc anymore. Instead,  
getFile() returns an AgaviUploadedFile object, which holds all this  
information:

$file = $rd->getFile('foo[bar]');
$file->getSize();
$file->hasError();
$file->move('/path/to/destination');

The global request data holder is copied into each execution  
container when that container is executed. You can access it via  
$context->getRequest->getRequestData(). As before, this cannot be  
done while inside an action's or view's execute() method to encourage  
people to use validated request data only.


Hope that helps. Let me know if there are any questions. Comments and  
feedback welcome :)

Yours,

David

_______________________________________________
Agavi Dev Mailing List
[email protected]
http://lists.agavi.org/mailman/listinfo/dev

Reply via email to