Hello David (and others), Thanks for all the hard work. Seems like there are some new cool things to play with. When I have a chance, I'll update to the latest revision and give it a try. Escecially the view/templating improvements seem to be just what I need.
Keep up the good work. Greetings, Koen > -----Oorspronkelijk bericht----- > Van: [EMAIL PROTECTED] > [mailto:[EMAIL PROTECTED] Namens David Zülke > Verzonden: donderdag 25 januari 2007 13:29 > Aan: [email protected] > CC: Agavi Users Mailing List > Onderwerp: [Agavi-Users] IMPORTANT: Breaking changes in 0.11 > branch: tons ofnew features! > > 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 > > _______________________________________________ > users mailing list > [email protected] > http://lists.agavi.org/mailman/listinfo/users > _______________________________________________ users mailing list [email protected] http://lists.agavi.org/mailman/listinfo/users
