Paul M. Jones

Don't listen to the crowd, they say "jump."

Solar 0.10.0 Released

The good news is that I've rolled the 0.10.0 release for Solar, the simple object library and application repository for PHP5.

The bad news is that it has a ton of changes, and that the release after this will also have large number of changes. This is "bad" in the sense that none of the current users (inluding me) wants to change existing code to comply with the new Solar stuff, but it's "good" in that it prepares Solar for wider acceptance among professional developers.

Read on for a very long description of the changes, especially regarding the new MVC architecture.

Changes For v0.10.0

Code Formatting

The most obvious change is in code formatting. I'm going from tabs to 4-spaces, per the PEAR coding standard (I see their point now). Similarly, the PHP commenting docblocks are now indented one extra space; using spaces as tabs helps with this greatly. The Solar coding standards docs will be updated to reflect this.

However, I'm breaking with the PEAR convention for public/protected naming. The PEAR rule on visibility hints in naming is mostly a holdover from the PHP4 days: only private methods and properties get an underscore prefix, while both protected and public methods get no prefix (this is because protecteds may be extended and made public). This was an attempt to stay consistent with PHP4 code, but in PHP5 I just don't find it to be that great. Thus, from now on, both private and protected properties and methods in Solar will be named with an underscore prefix. For example, the protected $config property in Solar 0.9.0 and earlier is changing to $_config. (Public properties and methods still get no prefix.) Making protecteds into publics is hereby heretical. ;-)

Pattern Vocabulary

After some discussion, I have decided that using pattern-based names where possible is more appropriate than inventing a new vocabulary just for Solar; at least one person has pointed this out to me in the past (hi Travis!) and it took me a while to arrive at the same conclusions as he did.

As a result, the "Solar_Cell" class family has been renamed "Solar_Model". Likewise, the Solar::object() method has been renamed to Solar::factory().

Less Dependence On include_path

You no longer have to have the Solar base directory in your include path; Solar now "finds itself" in the filesystem and automatically prefixes loadClass() requests with the proper directory name. This makes it easier to move between installations of Solar without having to update the include_path ini settings.

However, when you require() Solar the first time, you still need to know where it is, so there is still a little bit needed from you for boostrapping. Even with that, though, I think this is easier than setting .htaccess directives, or getting your hosting provider to change include_path for you in the ini.php file.

Controller Classes

There is a new Controller family of classes: Solar_Controller_Front and Solar_Controller_Page. Their purpose should be obvious from their pattern names; they are a FrontController for routing between Solar applications, and PageController for page-based applications, respectively. Together, they form the basis of ...

New MVC Architecture

This is going to be the biggest news in Solar, even though the MVC classes are only a small part of the code base. We have talked about this on the mailing list before: the Solar_App class for MVC, while really useful, has a number of shortcomings, and needs badly to be updated.

First, Solar_App was not as automated as it could have been. You had to specify local directories, talk to the view object directly when assigning variables, etc. In the new architecture, Solar apps extend Solar_Controller_Page, and are a great deal more automatic. The controller picks the right view for the action script automatically, but you can also tell the controller explicitly what view to use. Regardless, your action scripts only manipulate the controller properties, and these are assigned by the controller to the view automatically. This dramatically improves documentation as well.

Second, and more importantly, the "model" portions did not lend themselves to distribution and auto-configuration very well at all. For example, if you had a "Blog" model class in your application, it was just named "Blog.php". The problem comes when someone else has a "Blog.php" model in a different application: the two names conflict. This makes sharing and distribution difficult, because there's no automatic deconfliction or namespacing. In the same way, it's difficult to have entries in Solar.config.php that automatically match up with the Blog class.

As a result, I have moved models entirely outside the Solar_App architecture. Your Solar-based classes should have their own top-level vendor directory anyway (this is part of the namespacing deconfliction), so any models for your Vendor_App classes should go in (e.g.) your Vendor_Model namespace. Under this architecture, VendorOne_Model_Blog and VendorTwo_Model_Blog class names don't conflict, and you can specify separate configs for them in Solar.config.php by their class names.

I know that removing models from the app-specific architecture sounds unintuitive, but after considering the problem of name deconfliction and configuration namespacing for the past couple of months, this solution seems simple and straightforward when it comes to distributed applications that may wish to draw on each other's models.

Therefore, with some renaming based on the new pattern vocbulary standards, we now have the following directory structure; basically, the old "controllers/" directory is now "Actions/", the "models/" directory is replaced with a Models directory higher in the hierarchy, and the "helpers/" directory is removed completely (you can keep action helpers in an Actions/Helpers/ directory itself if you wish).

Solar/
  App/
    Bookmarks.php        -- the page-based application controller
    Bookmarks/
      Actions/           -- application actions
        edit.action.php
        tag.action.php
        user.action.php
      Locale/            -- locale strings
        fr_FR.php
        en_US.php
      Views/             -- views related to actions
        auth.php
        edit.view.php
        error.view.php
        footer.php
        header.php
        list.view.php
        rss.view.php

In future, I'll add directories for view helpers (likely in Views/Helpers/) and a directory for public assets such as stylesheets and javascript code (likely in Public/).

Front-Controller Unification

Finally, Solar_App applications now expect to be unified under a front controller. (This is the "Solar_Site" class we've talked about in the past.)

Thus, instead of having (e.g.) bookmarks.php and blog.php in your web directory, you will have only (e.g.) front.php, which will run the Solar_App_Bookmarks app for you when you browse to "front.php/bookmarks". The front-controller code looks similar to the old app-based code:

    require_once 'Solar.php';
    Solar::start();
    $front = Solar::factory('Solar_Controller_Front');
    $front->display();
    Solar::stop();

Under this, and with the new Solar_Controller_Page based applications, instead of browsing to ...

    http://example.com/bookmarks.php/edit/?id=0

... you can browse to this instead:

    http://example.com/front.php/bookmarks/edit/0

This way, your applications can all route between another using the front.php front controller.

Upcoming Changes

These are changes I expect to make after the 0.10.0 release.

Exceptions

I'm going to start using exceptions more liberally throughout Solar. I've been using exceptions in other work, and I am finally convinced of their usefulness. Frankly, my use of Solar_Error has been mostly to generate script-stopping E_USER_ERROR trace dumps, and that's exactly what exceptions do. I don't think I'll get rid of Solar_Error, as it's useful for internal messaging if nothing else.

Two-Step View

I'm also going to start using the TwoStepView pattern, where the page-based application controller only generates its specific output, and the front controller will generate the surrounding site layout. (Right now the application controller does both, using a header.php and footer.php for the surrounding site layout.) This should help with site-wide theming and application output sharing.

Move Some Solar::start() Code to Solar_Controller_Front

Right now, Solar::start() builds a number of shared objects for applications: an SQL connection, a User object (with automated authentication processing), and so on. Now that Solar has a real front controller, I'm starting to think those processes are more appropriate for starting up the application space, not the general Solar environment. As such, I'm going to experiment with moving them into a _setup() method in Solar_Controller_Front.

Move Shared Objects To A Registry

This one I'm not so sure about. Under the new pattern-based vocabulary, the Solar shared objects should probably be called a "registry". This may entail an actual Solar_Registry class; at the very least, it will mean renaming Solar::shared() to Solar::registry() to retrieved shared objects, and perhaps adding new methods like register() and unregister() to manipulate the registry.



A Spate of Savant News

Savant is an object-oriented template system for PHP; it provides plugins, output filters, path management, stream support, and lots more. Savant uses PHP itself as the template language, so it is both fast and lightweight. Savant3 is for PHP5 E_STRICT compliance, while Savant2 is E_ALL compliant under both PHP4 and PHP5.

Savant3

Savant3 is marked "stable" as of yesterday, bringing to a close the very long beta period. You can read the change log and download it here. Woohoo!

Cyberlot notes his success using Savant3 and gettext; he wrote up an output filter that translates special tags, then caches the result. Nice work! (Update, 10am: see also his brief writeup via the Savant mailing list archive.)

Antti Holvikari noted some bugs (fixed in yesterday's release of Savant3) and wrote a proof-of-concept app using Savant3 inside a PHP_Archive .phar file. Pretty cool, Antti!

Savant2

Also as of yesterday, Savant2 series has a 2.4.2 release. You can read the full change log, but the main points are:

  • Fixed a strpos() param order bug in the code for stream support
  • Use of __autoload() under PHP5 is now configurable; it's off by default, but you can turn it on with setAutoload(true)

Header Image: Jupiter and Io

So I finally got around to putting up a custom header image for this blog; now it won't look exactly like every other default installation of Wordpress. It's a picture of Io in orbit above Jupiter, taken by the Cassini space probe on its way to Saturn. The original is here.

Although I didn't intend it, there is a minor visual pun here. The Wordpress theme is Kubrick, and the image subject is Jupiter, and given my fascination with the movie 2001 ... well, you see what I mean. What a geek.


Fluent Interfaces Require Fluent Situations

My friend and coworker Mike Naberezny wrote recently of fluent interfaces (a.k.a. object chaining). You should read more about it there, but in the mean time, here's an example usage provided by Mike for a new customer order with multiple lines and a shipping priority:

$customer->newOrder()
         ->with(6, "˜TAL')
         ->with(5, "˜HPK')->skippable()
         ->with(3, "˜LGV')
         ->priorityRush();

The idea is that the fluent interface makes it very easy to read the resulting code in a way that flows naturally. This is cool, but I want to add a caveat to his examples.

I think, for a fluent interface to be effective, you need situations where you actually have all that information at one time so that you can chain the methods in a fluid way. In the above case, we actually know what the whole order is, what the shipping priority is, etc. But very often (I would say "almost always") you *don't* know what the whole customer order is. Instead, you are more likely to iterate through the order lines as the customer adds them, or as you retrieve them from some data source. You might need something more like Mike's initial contra-example:

$order = $customer->newOrder();
foreach ($line as $item) {
    $order->with($item['number'], $item['type']);
}

That's not to say that a fluent interface cannot perform in such a way (it can if written to allow for it). My point is that writing class methods in a fluent interface style may not be worth the effort when you don't often expect to have all the necessary information at once; however, if you *do* expect to have that information, it can be very nice. For example, I could see where a fluent interface could be quite effective in something like Solar_Sql_Select. Currently, you need to do this:

$select = Solar::object('Solar_Sql_Select');
$select->cols('*');
$select->from('table_name');
$select->where('colname = ?', $value);
$select->order('colname DESC');
$result = $select->fetch('all');

But with a fluent interface, one could do it this way (which seems very clean to me at this point):

$select = Solar::object('Solar_Sql_Select');
$select->cols('*')
       ->from('table_name')
       ->where('colname = ?', $value)
       ->order('colname DESC');
$result = $select->fetch('all');

Anybody have thoughts on this particular style of expression?


A Dearth of Blogging Lately

A number of people have emailed me to note that my blog has died since I started at Zend. Very sorry to disappoint those of you who look forward to my missives. :-(

The main reason for my lack of authorship has been that this blog (with some notable exceptions) has mostly been about whatever PHP project I'm currently working on. Since I'm now working on Zend-internal stuff, none of which can be public right now, there's not much I am at liberty to write about, especially given that my other public projects are mostly stable in one sense or another. That, and my free time has been in short supply given the holidays, family commitments, etc.

Having said that, look for more in this space over the next few days and weeks; in the mean time; thanks for your patience. :-)


Hired by Zend

I've recently been hired by Zend as a PHP developer for them. Many thanks to Andi, Daniel, Mike, and especially Matthew for their consideration and support throughout the process.

I start on Tuesday, and will be spending three weeks out in California as part of my initial time with them. After that, I'll be telecommuting from home (in Memphis TN). I'm very excited about this change of employment; Zend are big guns in PHP world, and it's a big deal to me to be working for them. :-)

I'm not sure yet as to my specific duties, although I get the impression it will be for internal development (not client work). I don't know if I'll be part of the framework development process (although I sure hope so!). Obviously there are non-disclosure issues, so I may not be able to talk about my work there much (although again I hope I will be able to do so, as I think Zend suffers from a little closed-mouthedness in regard to the wider PHP community, and could do with some regular blogging).

What does this mean for Solar, Savant, Yawp, and the rest of my public projects? In a philosophical sense, I don't know; but in a practical sense, I see no reason why my work on these will suffer or drag in any way, once the first few weeks in California are complete.

So if I'm less responsive to emails than normal for the next 3-4 weeks, that's why. :-)


Solar 0.9.0 Released

Solar is a simple object library and application repository for PHP 5. You can view the rather extensive change log here.

This release is a major break to backwards compatibility, mostly because we now use PDO for the SQL database API abstraction (as opposed to the homegrown solution I was using in preparation for PDO). The distribution includes a number of migration documents to help users of prior versions move to the new system.

I want to specifically mention one aspect of the Solar SQL system, the Solar_Sql_Select object. This is probably not new in the PHP world, but it's the first time I've done anything like this, so I want to talk about it.

One of the problems with database portability is that you can't depend on a LIMIT to work the same way across database backends; indeed, it may not even exist as such. Different DB abstraction packages use different methods to support LIMIT emulation, either by rewriting the SELECT statement, or by supporting only the portions of a LIMIT clause available to the particular backend.

What Solar_Sql_Select does is let you programmatically build a SELECT statement, and it keeps the clause portions separated internally. It then combines those portions in a manner specific to the database backend driver, putting the appropriate LIMIT clauses in the right place. Here's a quick example; let's build this SELECT statement.

SELECT id, date, type, name
FROM example
WHERE
    date >= '2005-01-01' AND
    date <= '2005-01-31' AND
    type IN('a','b','c')
ORDER BY id
LIMIT 10,50

(The LIMIT in this case is to grab 10 rows starting at row 50.)

The equivalent Solar_Sql_Select code is:



<?php
$select = Solar::object("Solar_Sql_Select");

// the basic columns
$table = 'example';
$cols = array('id', 'date', 'type', 'name');

$select->from($table, $cols);

// WHERE clauses for the date
$select->where('date >= ?', '2005-01-01');

$select->where('date <= ?', '2005-01-31');

// WHERE clause for the type
$types = array('a', 'b', 'c');

$select->where('type IN(?)', $types);

// ORDER and LIMIT
$select->order('id');
$select->limit(10, 50);


$statement = $select->fetch('statement');
?>

Some notes:

  • You can use $select->fetch() to retrieve the 'statement' as built, or actual results using 'all', 'col', 'row', 'PDOStatement', etc.
  • In this example, quoting happens on-the-fly, and quoting an array returns a comma-separated string of the individually-quoted array values. However, you can also use named placeholders (:start_date, :end_date, :type_list) and then use $select->bind() to bind data to those placeholders all at once. All hail the glory of PDO. :-)

In the above Solar-based PHP code, when MySQL is the driver, the $statement contents will look something like the initial example. However, when using the Microsoft SQL driver (which does not support LIMIT, only TOP), the resulting SELECT looks something like this:

SELECT *
FROM (
    SELECT TOP 10 *
    FROM (
        SELECT TOP 60 id, date, type, name
        FROM example
        WHERE
            date >= '2005-01-01' AND
            date <= '2005-01-31' AND
            type IN('a','b','c')
        ) AS solar_limit_rev
        ORDER BY id DESC
    ) AS solar_limit
ORDER BY id ASC

Which looks like a mess, but these guys seem to think it works. The point is that by keeping the SELECT clauses separate until you build the statement, you can manipulate the individual pieces with great precision for better portability.

Another thing about Solar_Sql_Select is that paging is built in. If you wanted to grab page 5, where pages are 10 rows each, you can do this:


<?php
// instead of $select->limit(10,50) ...
$select->paging(10); // 10 rows per page

$select->limitPage(5); // limit to page 5
?>

Also, row-and-page counting is built in.


<?php

// instead of $select->fetch() ...
$total = $select->countPages();

/*
$total = array(
    'count' => number_of_rows,
    'pages' => number_of_pages
);
*/
?>

You can see more extensive Solar_Sql_Select docs here (although they are not "real" docs, just migration examples).


Yawp 1.2.0 Released

Yawp is "yet another web programming" foundation for PHP4 (it works in PHP5, too, but is not E_STRICT compliant). It composes a number of PEAR classes into a single encapsulating class so that you can concentrate on writing business logic, not instantiating your support objects. Yawp is a single PHP file, and uses a single .ini file for its configuration.

Among other things, Yawp lets you define "hook" scripts to execute at certain times. For example, a "start" hook runs every time you call Yawp::start(), a "stop" hook executes every time you call Yawp::stop(), and so on.

One of the issues with a "start" hook is that it runs only after the internal Yawp objects have been instantiated (DB, Auth, Log, etc). Sometimes, however, a developer will want to run scripts **before** the objects are created, perhaps in order to replace the standard Yawp objects with objects of his own. That kind of functionality was not available in Yawp 1.x and earlier without a lot of extra work.

The guys at Babel.com.au got tired of this, and one of their developers (Justin Randell) sent me a major patch to add a new kind of hook, the "prep" hook, which is now available in the new Yawp 1.2.0 release.

"Prep" hooks run almost immediately in the Yawp::start() process: right after the configuration file text is loaded, and before the first internal Yawp object is created. Thereafter, each internal Yawp object creation routine checks to see if it has already been created by a prep hook, and skips creation if so. This allows a great deal of control over the Yawp internal objects. (One note of caution: if you override a default object like Auth, make sure your overriding API matches the API expected by the Yawp static convenience methods, otherwise those convenience methods will break.)

In other news, the Yawp::getObject() method had a bug in it where, if a requested object did not exist, there was no return statement. This bug was not an issue in PHP 4.3.x and earlier, but in PHP 4.4.x (when the internals of the engine were changed somewhat) that behavior causes an error stating "only variables may be returned by reference." The fix was simple: if the object doesn't exist, return null (instead of having no return statement at all).


Solar 0.8.0 Released

(UPDATE: Fixed the broken link to Solar. Thanks, Justin Patrin.)

After *entirely* too long, I've packaged and released Solar version 0.8.0 (devel). I had wanted to release it two weeks ago, but other events demanded priority.

There are lots of changes, so be sure to read the change notes before upgrading. One thing to pay attention to in particular is that I've updated the bundled Savant3 installation to beta 2, and updated the Savant3_Plugin_Form installation to 0.2.1 (dev). You can read the various Savant3 change notes at the Savant site.

You'll find the 0.8.0 release notes for Solar below; be warned, it's quite long.

* WARNING: This is a public development release, and is not yet stable.

* Translations: Jean-Eric Laurent has contributed fr_FR locale files for
all existing classes.  Thanks, JEL!  :-)

* Documentation: added HelloWorld app to the docs/ folder per note from
James Kilbride

* Solar:

    * The environment() setup now uses an improved routine to unset
    variables set from global sources when register_globals is on (per
    Stefan Esser and Richard Heyes)

    * The environment() setup no longer unsets $_REQUEST (although you
    still shouldn't use it if you can reasonably avoid it).

* Solar_Base:

    * If a 'locale' config key is not set, auto-generate one based on
    the class name. For example, 'Solar_Example_Class' maps to
    'Solar/Example/Class/Locale/'.

* Solar_Form:

    * Made addValidate() and addFilter() public.

    * In validate(), the 'validate' key is forced to an array; this
    allows you to use just the Solar_Valid method name, and the default
    VALID_* message will be used as the feedback text.

    * The populate() method is now free of the eval() calls copied from
    HTML_QuickForm; we now recursively populate the $submitted property
    with the _populate() method.

    * The values() method is now free of the eval() calls copied from
    HTML_QuickForm; we now recursively build the returned values using
    the _values() method.

    * In the addValidate() method, the $message parameter is now
    optional; if not present, the method uses the generic VALID_* locale
    string for the validation.

* Solar_Form_Load_Xml:

    * Applied patch from Matther Weier O'Phinney to add XML format doc
    comments inline; also fixes a bug where $this was addressed instead
    of $filter.

* Solar_Sql:

    * WARNING: This may be the last release of Solar with the current
    RDBMS API abstraction; a future release should incorporate PDO as
    the underlying abstraction layer.

    * The quote() method now recursively quotes all values in arrays.

    * Added a new select() method in anticipation of Solar_Sql_Select
    class.

* Solar_Sql_Driver_*:

    * Added new select() and limitSelect() methods in anticipation of
    new Solar_Sql_Select class. The new limitSelect() method will
    provide much better offset emulation in, e.g., MS-SQL.

* Solar_Sql_Driver_Mssql:

    * Fixed bug in createSequence() method where $start was in
    single-quotes (changed to double-quotes to interpolate the variable
    properly)

* Solar_Sql_Locale:

    * Added new OP_CREATE, LABEL_OP, and VALID_WORD translation keys

    * Swapped VALID_BLANK and VALID_NOTBLANK translation strings (they
    were backwards)

* Solar_Valid:

    * Added ipv4() method to validate IPv4 addresses; also added test
    file.

    * Added locale() method to validate locale code formats; also added
    test file.

    * Added word() method to validate against word characters (regex
    "w" type); also added test file.

    * The nonZero() method now honors the $blank parameter.

* Solar_Template:

    * Updated to use most-recent releases of Savant3 (beta 2) and
    Savant3_Plugin_Form (devel 0.2.1)

    * Updated all Solar/App/*/views/* to use the new Savant3 code

* Solar_User_Auth:

    * Moved driver instantiation to start() instead of __construct();
    this should allow custom drivers better integration into the start()
    process (e.g., single sign-on systems and HTTP-based auth).  This
    eliminates the __construct() method as well.

* Solar_User_Role_Sql:

    * Fixed double-concat bug when generating the WHERE clause