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
