It is Response::setRedirect(), not redirect(), sorry. Also, I forgot to mention that you cannot extract slots anymore, and that slots and template attributes cannot share a variable. That means slots won't be available in $template anymore, the default variable name is now $slots. All still configurable using "var_name", "slot_var_name" and "extract_vars" parameters of the renderer
Am 25.01.2007 um 13:28 schrieb David Zülke: > 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 > _______________________________________________ Agavi Dev Mailing List [email protected] http://lists.agavi.org/mailman/listinfo/dev
