Argh... "slots_var_name"
Am 25.01.2007 um 14:26 schrieb David Zülke: > 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 > _______________________________________________ Agavi Dev Mailing List [email protected] http://lists.agavi.org/mailman/listinfo/dev
