Solar 0.12.0 Released
(I know I didn't blog about the 0.11.0 release two weeks ago; sorry, been really busy here lately.)
The bad news is that, as promised, there are major changes to some core functions. However, this is the last of the expected major changes, and they promise to make added functionality and extensibility much easier.
The biggest changes are in the overarching Solar class itself and the front controller. Of those, the biggest single change is that Solar::start() no longer sets up shared objects; instead, the overarching Solar class now provides a Registry for you to set up your own shared objects. In addition, we now have read-once flashes, a TwoStepView pattern, and front controller hook scripts to set up the application environment. Finally, the content model appears to be on solid footing with a few changes to the class architecture. Read on for more about these changes (at great length).
Registry
Previously, Solar::start() would create Solar::shared('sql') and Solar::shared('user') objects. To set up shared objects of your own, you had to edit the ['Solar']['shared'] section of the config file. While useful, this holdover from Yawp was not as extensible as it should have been.
To address this, I've implemented a Registry pattern in Solar (read more about the pattern here). Now you can programmatically add and get shared objects using the registry functions. To add an object to the registry, use Solar::register('shared_name', $object_instance). Then you will be able to retrieve the $object_instance from the registry by calling Solar::registry('shared_name').
Solar::shared() and the objects managed by it via Solar::start() and Solar::stop() have been removed entirely. To mimic the previous Solar::shared() behaviors, the Solar_Controller_Front class comes with a default hook script to create Solar::registry('sql') and Solar::registry('user'). Effectively, the only thing you need to do in your apps under the front controller is change calls from Solar::shared('name') to Solar::registry('name').
As an added consequence, the Solar_Base::solar() method providing hooks for auto-shared object behaviors has also been removed, since the very concept of auto-sharing has been obviated. In practice, the only class using the solar() hook method was Solar_User, and that behavior has been replicated in the default front controller hook script.
Locale Management
As a consequence of getting Solar::start() out of the business of creating shared objects, there is no more shared Solar_Locale object. This is because Solar_Locale's only purpose was to be a shared object set up by Solar::start() for shared translation functions across the whole Solar environment.
As a result, the old Solar_Locale methods have been moved into the overarching Solar class itself. Solar::locale() (and $this->locale() for Solar_Base classes) works exactly as before, but to change the locale code, you should call Solar::setLocale($code) instead of Solar::shared('locale')->reset($code). In some ways this is a return to earlier versions of locale management.
Read-Once Flashes
Ruby on Rails has popularized the idea of read-once session values, what they call "flashes." While the name may leave a bit to be desired, the concept itself is very useful. In short, you set a value which propagates via $_SESSION, but the first time it is read, it is deleted (thus "read-once").
Initially this may sound of limited use, but in practice it's very powerful. Things like messages and short-term values can be forwarded through as many redirects as you like until you actually need them; then, when you read the value out, it disappears from the session. The values "flash forward" with the session until you need them. The nice thing is that the code to implement flashes is delightfully simple.
Solar flashes are grouped by class name and a key value under that class, which means that your different classes have their own namespace in the global flash value sets.
There are two ways of working with flashes in Solar. You can use the overarching Solar class for them, via Solar::setFlash($class, $key, $value) and Solar::addFlash($class, $key, $value). The $value gets stored in $_SESSION['Solar']['flash'][$class][$key], until you read it with Solar::getFlash($class, $key), at which time it is returned and deleted from the session.
Additionally, the Solar_Base class comes with the same methods (which in turn call the overarching Solar methods). These add the class name automatically. Outside of Solar_Base, you would have to call Solar::setFlash(get_class($this), $key, $value), but inside a Solar_Base extended class, you can just call $this->setFlash($key, $value). Voila: instant namespacing for your flashes.
Right now, I'm using flashes in Solar in two places. First, the Solar_User_Auth class sets a flash for the authentication message. The first time that message is displayed, it is no longer propagated. Similarly, the Solar_App_Bookmarks app used to propagate a "backlink" as a $_GET variable so that, after editing a bookmark, you could go directly back to your list of bookmarks, sorted and filtered exactly the same way as before. Tracking that information through the URI was ugly at the very least, and cumbersome at worst. With flashes, that information is now forwarded invisibly by the $_SESSION, and removed as soon as you actually use the backlink. Pretty nifty.
Incidentally, to support flashes, Solar::start() now calls session_start() automatically.
Front Controller Improvements
The Solar_Controller_Front class has seen some marked improvements. First, it now sports hooks for __construct() and __destruct() scripts. This means that you can have setup/teardown hooks the same way you do with Solar::start() and Solar::stop(). Solar_Controller_Front now comes with a default __construct() hook script to create shared 'user' and 'sql' objects in the Solar registry; this is where the shared-object construction that used to be in Solar::start() reappears.
The front controller also allows for a TwoStepView pattern now. Previously, the page controller was in charge of its own content, plus the site layout. When mixing and matching application components, as I expect to be the case in Solar, this makes it difficult to maintain a consistent look-and-feel between applications; you would need to change the layout in every separate application.
With a two-step view, the page controller is responsible only for its own output, and that output is then inserted into a site layout by the front controller. The page-controller can also set variables for the layout in the Solar_Controller_Page::$_layout value, so it can "hint" the front controller as to what the title value should be for the page and provide other information for the layout (all without controlling the layout directly). The front controller layout itself is just another Solar_Template script (which itself is extended from Savant3).
However, some page controllers need to explicitly defy the site layout; e.g., RSS feeds need to be presented as they are, without any sitewide theming. To facilitate this, you can set the Solar_Controller_Page::$_layout property to boolean false, and the front controller will take this to mean you need a one-step view (i.e., the page view alone).
Page Controller Improvements
The page controller changes are not as dramatic. When you extend Solar_Controller_Page for your application, you now have access to _preAction() and _postAction() methods that execute before the first action and after the last (but before the view is rendered).
Speaking of the view, the page controller now automatically picks the view for you based on the last action performed, not the first. As always, you can explictly pick the view to use by setting $this->_view.
Regarding the two-step view pattern, if you need to pass information back up the layout, you can do so by setting keys in the $this->_layout property. The front controller will read this data from the Solar_Controller_Page::getLayout() method and use it as "hinting" for the layout. Right now, the only values used by the default layout are $_layout['head']['title'] and ['body']['title'].
Finally, it is much easer to chain methods with _forward() than before. If you want to pass control temporarily to another action, call "$this->_forward('actionName')" in your action script; this will execute the action and return to the current script. If you want to forward to another action and *not* continue the current script, call "return $this->_forward('actionName')". Either way, the page controller properties will persist through all the forwards, which means you can break action scripts into smaller pieces and reuse them as needed.
Content Domain Model
And last, I think I finally have a useful and extensible class mapping for the content domain model. Previously, a Solar_Content object acted as a generic interface to all possible ways of manipulating content. In this new release, the Solar_Content object acts only as a collection point for the database table objects in the content domain, and a Solar_Content_Abstract object acts as the base from which different content node types are manipulated. You can see an example of this in Solar_Content_Bookmarks.
I don't believe I have talked about the Solar_Content domain model before. In short, it's very much like a namespaced wiki. The namespaces are called "areas" and represent logically separated areas of content, such as different sites. Each area has "nodes" of content; a node may be a bookmark, a blog entry, a wiki page, a comment, a trackback, or anything similar. Finally, each node may have a series of "tags" on it; the tags are stored as a text string in with the node itself, but searching them that way is problematic, so there's a separate table for tag-based searches.
All this means that the content domain, as it stands now, is composed of only three tables: one for areas, one for nodes, and one for tags. For areas and tags, you can see why one table each is enough. But how can it be that one table of "nodes" can possibly encompass so many different kinds of content?
It turns out that these different content types are remarkably similar. With few exceptions, they each are composed of the same kinds of elements: a subject or title, an author name and/or email address, a related link or URI pointing to a source, the body of the content itself, and perhaps a short summary of the content.
The elements may be called by different names, but they are effectively all needed no matter what the content type. Trackbacks, pingbacks, comments, wiki pages, blog entries, bookmarks, etc. are all composed of essentially the same pieces, it's just that those pieces are used differently depending on the context.
The problem with a simplified but mostly generic content domain is that you have to define the context for the node data. Previously, I had the node types as separate Solar_Model objects. However, this had a number of problems, most notably that there was no way to extend them from a common generic node type. With that in mind, I've created a Solar_Content_Abstract class that handles all the common node operations, and then I extend it for specific node types to provide context (e.g., Solar_Content_Bookmarks).
There is still the problem of hierarchies, though. Some kinds of content, such as comments and trackbacks, are probably not supposed to be addressed on their own; as such, you can specify that a node is "part_of" another node. This way you can ask for a "master" node and see that it has a number of other nodes associated with it. That's about the deepest the Solar_Content domain goes in a hierarchy; it's very flat, with tags instead of categories.
Next Tasks
The biggest problem now is that the documentation is pretty far out of sync with the codebase. Keeping the docs in sync is turning out to be a much bigger problem than I though it would (and I expected it to be a problem to begin with). So while I'm eager to start adding more classes, content types, and mini-applications, the documentation really has to take priority.
I'm evaluating possible solutions now; the best one I can think of is to start embedding more end-user documentation in the class comments themselves, and use the PHP 5 Reflection API to collect them into a wiki-like structure. That way I can edit the docs as I edit the code, instead of having to update the docs separately from the code. Once the codebase stabilizes, I can start separating the docs back out again into a user-editable wiki.