Monday, August 21, 2006

Local adapters in Zope 2.10

Further to Hanno's excitement about local components, I thought I'd add something of my own. As Hanno points out, systems like Plone where more than one site (with possibly different configurations) can be in the same Zope instance, and where much of the configuration needs to be done through-the-web, there is often a need for persistent, local components. In CMF, we have tools, which are persistent (in-ZODB) singletons (per site). Their analogue in the Zope 3 world are persistent (local) utilities.

In Zope 2.10 (and 3.3), Jim has refactored the component architecture to make it much, much easier to work with local components. Components are registered with a site manager, of which there is a global one (the deafult one use when you register components in ZCML) and any number of nested local ones attached to one or more sites. Hanno has made the Plone site root a site on the 3.0 bundle, which means that we can attach local components to it.

Imagine you have an object 'obj' that you want to register as a local utility providing an interface 'iface' with the given name 'name'. Here's how you'd register that as a utility on a particular site:
sm = getSiteManager(site)
sm.registerUtility(obj, iface, name)
Hanno has bundled this functionality into a GenericSetup export/import handler here, which means that setting up local utilities should be as simple as an XML file.

After this registration has been made, you can do this:
util = getUtility(iface, name=name)
and you should get back 'obj'.

That is - if you are in the right context. Upon traversal, Zope (and Five, in our case) will know the context and look up the containment hierarchy to find out where the nearest site manager is. If a site manager does not contain a given component registration, it will fall through to its parent, all the way up to the global site manager. Again, Hanno has taken care of this in Plone. In a test, you may need to do something like:
setHooks()
setSite(site)
To simulate what Zope may otherwise do during acquistion.

Now, in plone.portlets I have been doing something similar with local adapters, with no small amount of help from Philipp von Weitershausen. Now, to register a local adapter, you only need to call registerAdapter() on a site manager:
sm.registerAdapter(required=(iface1, iface2,),
provided=iface, name=name, factory=callable)
Here, 'callable' could be a class or a function that produces the adapter providing 'iface'. It will take two parameters, being passed the objects providing iface1 and iface2.

In plone.portlets, however, the pattern is a bit more interesting. The reason for needing a local adapter in the first place is to satisfy zope.contentprovider. This comes with a TALES expression type handler for the provider: expression type. If you write a page template:
tal:replace="structure provider:plone.leftcolumn"
zope.contentprovider will look up a multi-adapter providing IContentProvider from (context, request, view) and call its update() and render() methods.

In plone.portlets, we want to be able to register a content provider to render the left portlet column in place of this expression, which means that an appropriate adapter lookup must be found. Furthermore, the assignment of portlets to a context is a persistent, site-local concept. This implies that not only must the correct IContentProvider local adapter be located, it must also know which "portlet manager" to query for the list of portlets to render.

The solution to this problem is to let the adapter factory (the callable that produces the actual registration) be a persistent object. The implementation of PortletManager has a __call__() method which can produce the appropriate implementation of IContentProvider in the form of a PortletManagerRenderer. When instantiated, this is told which portlet manager it is rendering.

Thus, a portlet manager in /.portlets/left, for example, is registered (using a GenericSetup handler) as the adapter factory for the plone.leftcolumn adapter:
sm.registerAdapter(
required=(Interface, IBrowserRequest, IBrowserView,),
provided=IContentProvider, name='plone.leftcolumn',
factory=site['.portlets']['left'])
In effect, this achieves a link between the (persistent) adapter registration for the plone.leftcolumn content provider adapter and the (persistent) portlet manager storage.

Of course, users of the library shouldn't have to do any more than this in a GenericSetup profile:


2 comments:

Hanno Schlichting said...

Finally I can read optilude-esk blog posts in addition to your mails, I've been waiting for it for soooo long :)

Just a short update, the exportimport handler is now part of its own package for easier re-use and can be found here:

http://dev.plone.org/collective/browser/GSLocalAddons/trunk/exportimport/sitemanager.py

Martin Aspeli said...

Great, Hanno :)

But why such a Zope2-ish name? Upper-mixed-case? brrrrr :)

Martin