Paul M. Jones

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

Speaking at PHP Works 2007

I got stiff-armed for ZendCon 2007 (apparently they don't want presentations on competing frameworks ;-).

However, the PHP Architect folks graciously accepted two of my talk proposals for php|works 2007 in Atlanta:

In a way, the two talks complement each other, but you'll have to attend both to see why. ;-)

Hope to see you at the conference!


Solar 0.28 Alpha Released

Last Friday I released Solar 0.28 alpha. (As usual, the guys on the mailing list got notification of this on the same day.)

This is the first release in four months. The last time I delayed so long between releases I gave the change notes inline, but I won't punish readers that way again. ;-) If you really want to, you can see the very very long list of change notes here.

Over the next several days, I'll post more about individual developments in the framework, but here's a little to pique your interest:

In other news, it looks like Enygma at phpdeveloper.org gave Solar a try and liked it. Head over there and give him some comment-love! :-)


Solar Views and Layouts

Looks like the Zend Framework project doesn’t have “complex views” settled just yet. I’m sure they’ll hit on a solution soon. In the mean time, let me show you how easy it is to work with views and layouts in Solar, including automatic format discovery and inherited layouts.

Basic Directory Structure

By way of introduction, here is the directory structure for an example application. We’ll call the top-level namespace “Vendor”, and the application itself “Example”. (These would live in the PEAR directory next to the Solar installation.)

 Vendor/
     App/
         Example.php
         Example/
            View/
                hello.php
            Layout/
                default.php

Page Controller, View, and Layout

The example application is a simple “Hello World” page controller:

/* Vendor/App/Example.php */

class Vendor_App_Example extends Solar_Controller_Page {

    protected $_layout = 'default';

    public $foo;

    public $zim;

    public function actionHello()
    {
        // let's set some properties
        $this->foo = 'bar';
        $this->zim = 'gir';

        // Solar_Controller_Page automatically finds and renders the
        // 'hello.php' view, then takes that output and automatically
        // injects it into the 'default.php' layout.
    }
}

The view script in this case is dirt-simple, but you can use Solar_View helpers to jazz it up.

        <!-- Vendor/App/Example/View/hello.php -->
        <p>Hello, world!</p>
        <p>Foo is <?php echo $this->escape($this->foo) ?>.</p>

As with most 2-step view implementations, the view output is “injected” into the layout script. In this case, let’s use a bare-bones HTML layout.

<!-- Vendor/App/Example/View/default.php -->
<html>
    <head>
        <title>Example</title>
    </head>
    <body>
        <?php echo $this->layout_content ?>
    </body>
</html>

(The $layout_content property is automatically populated by the page-controller with the output of the rendered view.)

When you browse to http://example.com/example/hello, you should see this output from the application:

<!-- Vendor/App/Example/Layout/default.php -->
<html>
    <head>
        <title>Example</title>
    </head>
    <body>
        <!-- Vendor/App/Example/View/hello.php -->
        <p>Hello, world!</p>
        <p>Foo is bar.</p>
    </body>
</html>

Variable Assignment

Wait, how did $foo get into the view? The page-controller automatically assigns all public properties of the controller to the view object, so you don’t have to think about what gets set and what doesn’t. If a controller property is public, the view can use it.

Likewise, the page-controller assigns the same variables to the layout, so you have full access to them in your layouts as well. For example, we could change the layout script to use $zim as the title …

<!-- Vendor/App/Example/View/default.php -->
<html>
    <head>
        <title><?php echo $this->escape($this->zim)</title>
    </head>
    <body>
        <?php echo $this->layout_content ?>
    </body>
</html>

… and the output would become:

<!-- Vendor/App/Example/View/default.php -->
<html>
    <head>
        <title>Gir</title>
    </head>
    <body>
        <!-- Vendor/App/Example/View/hello.php -->
        <p>Hello, world!</p>
        <p>Foo is bar.</p>
    </body>
</html>

Other Layouts, or No Layout

If you want to use a layout other than the default one, just change $this->_layout to the one you want to use. First, add the layout script:

 Vendor/
     App/
         Example.php
         Example/
            View/
                hello.php
            Layout/
                default.php
                other.php

Then ask for it in your action:

/* Vendor/App/Example.php */

class Vendor_App_Example extends Solar_Controller_Page {

    protected $_layout = 'default';

    protected $_action_default = 'hello';

    public $foo;

    public $zim;

    public function actionHello()
    {
        // let's set some properties
        $this->foo = 'bar';
        $this->zim = 'gir';

        // let's use some other layout
        $this->_layout = 'other';
    }
}

If you don’t want to use a layout at all, set $this->_layout = null.

You can do the same thing for views; by default, the page controller looks for a view that matches the action name, but you can set $this->_view to the name of any view you like.

Multiple Formats

Now let’s say that we want to expose an XML version of our view. The Solar page-controller can look at the format-extension on a request and render the right view for you automatically. All you need to do it provide the view script for it – you do not have to change your controller logic at all.

Let’s add the XML view for our “hello” action (“hello.xml.php” below).

 Vendor/
     App/
         Example.php
         Example/
            View/
                hello.php
                hello.xml.php
            Layout/
                default.php
                other.php

The hello.xml.php view script looks like this:

<hello>
    <foo><?php echo $this->escape($this->foo) ?></foo>
    <zim><?php echo $this->escape($this->zim) ?></zim>
</hello>

Now when you browse to http://example.com/example/hello.xml (notice the added “.xml” at the end), you will get this output:

<hello>
    <foo>bar</foo>
    <zim>gir</zim>
</hello>

You can do this for any output format you like: .atom, .rss, and so on – and not have to change your controller logic at all.

Wait a minute, what happened to the layout? The Solar page-controller knows that if it receives a non-default format request, it should turn off the layout and use only the view.

Shared Layouts

Now, what if you have a layout or view that you want to share among multiple page controllers? This is pretty easy, too. First, define a “base” controller from which other controllers will extend, then put the shared layouts there.

 Vendor/
     App/
         Base.php
         Base/
            Layout/
                default.php
                other.php

The base controller might look like this:

/* Vendor/App/Base.php */
class Vendor_App_Base extends Solar_Controller_Page {
    // nothing really needed here, unless you want
    // shared methods and properties too
}

Now the “example” controller extends the base controller:

/* Vendor/App/Example.php */
class Vendor_App_Example extends Vendor_App_Base {
    // ...
}

And you can remove the layouts from the example controller; it will automatically look up through the class-inheritance hierarchy to find the requested layout.

 Vendor/
     App/
         Base.php
         Base/
            Layout/
                default.php
                other.php
         Example.php
         Example/
            View/
                hello.php

You can override the shared layouts with local ones if you want to. If you have Example/Layout/default.php the page-controller will use it instead of the shared one.

This works with views too. Put any views you want to share in Base/View, and the page-controller will find them if they don’t exist in the Example/View directory.

That’s All For Now

Questions? Comments? Leave a message below, or join the Solar mailing list and chime in there – we’d be happy to have you around.


Zend Devzone Podcast: Solar Overview

Cal Evans at Zend has posted my Solar Overview podcast. Thanks Cal! Click through to see the transcript ("script", really ;-) of the audio.

---

In this episode, I'm going to give a brief overview of Solar project and how it helps with the mundane aspects of building applications.

Solar is an open-source library and framework for PHP 5; you can read more about it at solarphp.com.

Some early versions of Solar formed the basis of some parts of the Zend Framework, in particular the database and view components (this was around late 2005 and early 2006). Since that time, the two projects have continued to mature along separate paths, but the structure and organization of the two projects is still very similar, with one class per file using PEAR coding-style standards and E_STRICT compliance.

Solar originated from my attempts to build a PHP 4 framework out of PEAR components, a project I called Yawp. However, I encountered a number of difficulties in doing so. One of the biggest problems was, how to have each component automatically configure itself at construction time. It turns out that even though PEAR has a great set of coding-style standards, the different packages all have different construction and configuration. This meant that for each package I wanted to include in Yawp, I would have to build a wrapper specifically for it.

As a result, Solar uses a unified construction and configuration mechanism. Solar uses a single configuration file that returns PHP array, so there's no parsing of ini, yaml, or xml files. The configuration values are all keyed on the related class name. All constructors have a single parameter, a config array, and the base constructor merges those values with the default values from the config file automatically. This means that if you write a class for Solar, it will configure itself automatically at the moment you instantiate it.

Additionally, and not to go on too long about configuration issues, but child classes inherit their parents' default configuration values. This means that if you want a set of common configurations for a particular hierarchy, you can set them once in the config file, and all the child classes will use those -- although you can override those in the child class, of course.

So that's one thing that Solar automates for you, construction and configuration.

Something else Solar has is built-in localization. Each class that needs localized text has a subfolder called "Locale", with a file for each country-and-language code. Like the config file, the locale file just returns a PHP array, with translation keys and string values. The Solar base class provides a method called locale() that automatically loads the right file for the class and returns singular or plural translations from that file. As with everything else in Solar, locale files are inherited along class hierarchies, so a child class uses its parent locale file by default, and can override any or all of the parent translations if it needs to.

The Solar base class comes with another method that helps in throwing exceptions. If you need to throw an exception in Solar, you call that method and give it a string error code. It then looks for the right exception class file for that code, gets a localized exception message from the locale file if one exists, and returns it for throwing. By now, you may have guessed that child classes inherit their parents' exceptions, so you can start with very generic exception classes, and add specific ones as you need them, all without having to change your call for throwing the exception.

As you can tell, Solar makes a lot of use of class inheritance hierarchies as an organization and automation tool. This is only one example of Solar's conceptual integrity, which is one of Solar's greatest strengths. Anywhere you look in Solar, you will see things being done almost exactly the same way every time. We even go so far as to have standard names for methods. In some projects, the words "get" and "fetch" are interchangeable; in Solar, they have well-defined separate meanings. (As an aside, "fetch" means the method reads information from some external source and returns it, while "get" reads from a property or other internal value.)

Finally, Solar is built from the ground up with name-spacing in mind. Whle PHP doesn't have real name-spaces, they are easy to emulate through a naming convention. Solar classes are fully name-spaced, and expects that developers will also name-space the code they build to work with Solar. Few if any PHP frameworks are fully name-spaced this way; some are name-spaced themselves, but do not allow for developers to extend into a different name-space if needed.

For example, a Zend Framework controller for "users" has to be called UserController; the moment you try to combine two separate projects that have a UserController, you will get name confliction issues. In Solar, you are expected to pick a top-level name-space for yourself and then extend into it; for example, you might have MyProject_App_Users. Because Solar already makes such extensive use of class hierarchies, it can tell how to address the controller automatically, even though it's in a different name-space.

This has been just a brief overview of how the Solar Framework for PHP is organized; if you want to learn more, please visit solarphp.com.


A Bit About Benchmarks

As the author of a relatively popular benchmarking article, I feel compelled to respond to this bit of misguided analysis from the Symfony camp about benchmarks.

Full disclosure: I am the lead developer on the Solar framework, and was a founding contributor to the Zend framework.

M. Zaninotto sets up a number of straw-man arguments regarding comparative benchmarks in general, although he does not link to any specific research. In doing so, he misses the point of comparative benchmarking almost entirely. Herein I will address some of M. Zaninotto’s arguments individually in reference to my previous benchmarking series.

All of the following commentary regards benchmarking and its usefulness in decision-making, and should not be construed as a general-purpose endorsement or indictment of any particular framework. Some frameworks are slower than others, and some are faster, and I think knowing “how fast is the framework?” is an important consideration when allocating scarce resources like time, money, servers, etc.

And now, on to a point-by-point response!

Symfony is not slow in the meaning of “not optimized”

But it *is* slow in the meaning of “relative to other frameworks.”

Regarding the title of M. Zaninotto’s article, I don’t know of any reputable benchmark projects that conclude Symfony is “too slow for real-world usage” in general. (Perhaps M. Zaninotto would link to such a statement?) Of course, the definition of “real-world” is subjective; the requirements of some applications are not necessarily the same as others.

What is not subjective is the responsiveness of Symfony when compared to other frameworks in a controlled scenario: for a target of 500 req/sec, you are likely to need more servers to balance across with Symfony than with Cake, Solar or Zend. This is implied by my earlier benchmarking article.

If some benchmarks show that symfony is slower, jumping to the conclusion that symfony is not optimized is a big mistake.

I don’t know of any comparative benchmark research that concludes “Symfony is not optimized.” M. Zaninotto is arguing against a point that no benchmark project seems to be making. (Note that the benchmarks I generated explicitly attempt use each framework in its most-optimized dynamic state, including opcode caching. You can even download the source of the benchmarking code to see what the optimizations are.)

I'd say that people who take this shortcut are either way smarter than us, or they don't know what profiling is, they didn't look at the symfony code, and they can't make the difference between efficient code and a bottle of beer.

Profiling to optimize a subset of code lines is not the same as benchmarking the responsiveness of a dynamic controller/action/view dispatch sequence. (The speed of the code blocks together are taken into account by the nature of the benchmark.)

So for instance, you will not find this code in symfony:

for ($i = 0; $i<count($my_array); $i++)

instead, we try to always do:

for ($i = 0, $count = count($my_array); $i<$count; $i++)

This is because we know it makes a difference.

How do we know it? Because we measured. We do use profiling tools a lot, on our own applications as well as on symfony itself. In fact, if you look at the symfony code, you can see that there are numerous optimizations already in place.

I agree that the second code-block is much better than the first speedwise. (N.b.: the first one calls count() on each cycle of the loop, whereas the second one calls count() only once.)

But if that faster piece is called only once or twice, and another much-slower piece is called 2 or three times, the overall effect is to slow down the system as a whole. Optimizing individual blocks of code does not necessarily result in a fast system as a whole.

And if you use a profiling tool yourself on a symfony application, you will probably see that there is no way to significantly optimize symfony without cutting through its features.

… at least, not without rewriting the system as a whole using a different and more-responsive architecture.

Of course, there might still be a lot of small optimizations possible here and there.

I think one would need a lot of “small optimizations” to make the 41 percentage-point gain necessary to equal the next faster dispatch cycle of Cake (per my benchmarking article; your mileage may vary).

Symfony results from a vision of what would the perfect tool for developers, based on our experience. For instance, we decided that output escaping should be included by default, and that configuration should be written preferably in YAML. This is because output escaping protects applications from cross-site scripting (XSS) attacks, and because YAML files are much easier to read and write than XML. I could name similar arguments about security, validation, multiple environments, and all the other features of symfony. We didn't add them to symfony because it was fun. We added them because you need them in almost every web application.

I don’t see how this is different from how Cake, Solar, or Zend approached their development process. Each of those frameworks has output escaping, configuration (either by YAML or by much-faster native PHP arrays), security, validation, multiple environment support, etc. (Those frameworks still perform a dynamic controller/action/view dispatch faster than Symfony does.)

It is very easy to add a new server to boost the performance of a website, it is very hard to add a new developer to an existing project to make it complete faster. Benchmarking the speed of a “Hello, world” script makes little sense

M. Zaninotto completely misses the point here.

At least for my own benchmarking series, the purpose is not to merely to say “this one is faster!” but to say “you can only get so much responsiveness from any particular framework, I wonder how each compares to other frameworks?”

A “hello world” application is the simplest possible thing you can do with a dynamic controller/action/view dispatch, and so it marks the most-responsive point of the framework. Your application code cannot get faster than the framework it’s based on, and the “hello world” app tells you how fast the framework is.

Based on that information, you can get an idea how many servers you will need to handle a particular requests-per-second load. Based on my benchmarking, you are likely to need more servers with a Symfony-based app than with a comparable application in Cake, Solar, or Zend. This is about resource usage prediction, not speed for its own sake.

Using plain PHP will make your application way faster than using a framework. Nevertheless, none of the framework benchmarks actually compare frameworks to a naked language.

Incorrect; my benchmarking series specifically compares all the frameworks to a plain PHP “echo ‘hello world’” so you can see what the responsiveness limits are for PHP itself. I also compare the responsiveness of serving a plain-text ‘hello world’ file without PHP, to see what the limits are for the web server. These numbers become important for caching static and semi-static pages.

... none of the framework benchmarks actually compare frameworks to a naked language. This is because it doesn't make sense.

Incorrect again. It does make sense to do so, because you can use a framework to cache a static or semi-static page. Caching lets you avoid the dynamic controller/action/view dispatch cycle and improve responsiveness dramatically. However, if your requests-per-second requirements are higher even than that provided by caching, you’ll still need more servers to handle the load. Again, this is about resource usage, not speed per se.

If frameworks exist, it is not for the purpose of speed, it is for ease of programming, and to decrease the cost of a line of code. This cost not only consists of the time to write it, but also the time to test it, to refactor it, to deploy it, to host it, and to maintain it over several years.

Ease of programming is a valid concern … and so is resource usage. If you can get comparable ease-of-use in a different framework, and it’s also more responsive, it would seem to make sense to use the less resource-intensive one. (Of course, measuring ease-of-use and programmer productivity is much harder than measuring responsiveness – the plural of “anecdote” is not “data”. ;-)

It doesn't make much more sense to compare frameworks that don't propose the same number of features. The next time you see symfony compared with another framework on a “hello, world”, try to check if the other framework has i18n, output escaping, Ajax helpers, validation, and ORM turned on. It will probably not be the case, so it's like comparing pears and apples.

I completely agree: one must compare like with like. And my benchmarking series attempts exactly that: all features that can be turned off are turned off: no ORM, no helpers, no validation, etc. Only the speed of the controller/action/view dispatch cycle is benchmarked, and Symfony still came out as the least-responsive with all those fetaures turned off.

Also, how often do you see pages displaying “hello, world” in real life web applications? I never saw one. Web applications nowadays rely on very dynamic pages, with a large amount of code dedicated to the hip features dealing with communities, mashups, Ajax and rich UI. Surprisingly, such pages are never tested in framework benchmarks. And besides, even if you had a rough idea of the difference in performance between two frameworks on a complex page, you would have to balance this result with the time necessary to develop this very page with each framework.

M. Zaninotto is again missing the point; the idea is not to generate “hello world” but to see what the fastest response time for the framework is. You can’t do much less than “hello world”, so generating that kind of page measures the responseiveness of the framework itself, not the application built on top of the framework.

In a way, the above is M. Zaninotto’s strongest point. Any Ajax, rich UI, and other features you add after “hello world” will only reduce responsiveness, but it is difficult to measure how much they reduce responsiveness in a controlled manner (especially when comparing frameworks). It may be that some frameworks will degrade at a faster rate than others as these features are added. Having said that, Symfony starts at a much lower point on the responsiveness scale than other frameworks, so it doesn’t have as much leeway as other frameworks do.

The speed of a framework is not the most important argument

While not the most important argument, it is *an* important argument. And it is one we can reliably measure if we are careful – at least in comparison to other frameworks. Ignoring it is to ignore one of many important considerations.

And between two framework alternatives with comparable speed, a company will look at other factors to make a good decision.

Agreed – when the speeds are comparable, other factors will have stronger weight. This was the point of benchmarking a “hello world” implementation: to compare speed/responsiveness in a controlled fashion.

And if you need a second opinion, because you can't believe what the creator of a framework says about his own framework, perhaps you could listen to other companies who did choose symfony. Yahoo! picked symfony for a 20 Million users application, and I bet they measured the speed of a “hello, world” and other factors before making that decision. Many other large companies picked the symfony framework for applications that are crucial to their business, and they continue to trust us.

M. Zaninotto “bets” they measured it, but does not say “they did” measure it. I would be interested to hear what Yahoo themselves have to say about that experience. All public references to this seem to be from Symfony developers and user sites, not the Yahoo project team. (Yahoo folks, please feel free to contact me directly, pmjones88 -at- gmail -dot- com, or to leave a comment on this page.)

This page from the Symfony developers says that documentation, not speed, was Yahoo’s “first reason” to choose Symfony. It also says that Yahoo “extended and modified symfony to fit their needs,” which is plenty possible with Cake, Solar, and Zend.

Perhaps this is an example of a developer at Yahoo who used Symfony not because he compared it to other frameworks, but because he was already familiar with it or liked the way it looked. That would be perfectly fair, I think; we all pick what we like and then try to popularize it. But did Yahoo actually do a cost-benefit study (or even a simple “hello world” implementation comparison) ?

While we’re at it, how much hardware does it take for Yahoo to serve up the bookmarks application? Yahoo can afford to throw more servers at an application than most of us – a framework with better responsiveness (and thus needing fewer servers to balance across) is sure to become an important factor.


Solar 0.27.0 and 0.27.1 Released

Yesterday, I released Solar 0.27.0, then quick-fixed two minor bugs and released 0.27.1 an hour later. It feels so good to be back doing releases on a monthly basis. :)

There are a few highlights in this release:

  1. We're using spl_autoload now to auto-load Solar classes as requested. One nice thing about this is that SPL uses a stack of autoloading functions, so Solar doesn't override any autoload you already have set up.

  2. The locale translation functions have been split out to their own class, Solar_Locale, and you can now configure your own replacement localization class if you need custom behaviors.

  3. It appears we now have the fastest and most-compliant JSON encoder/decoder in the PHP universe, thanks to Clay Loveless. It uses ext/json but does a little pre-checking to make sure the strings to be decoded are actually JSON payloads.

  4. Our SQL adapter adds a bit of convenience to get around stricter binding behaviors in the PHP 5.2.1 version of PDO.

  5. The Solar_Uri class now determines the '.ext' filename extension in a URI automatically; this bit of magic helps when determining what format is being requested from a page-controller, and helps when constructing alternative links for a single page that supports multiple formats.


New PDO Behavior In PHP 5.2.1

UPDATE (2016-05-30): Rasmus Schultz comments that "this does work – it was fixed after this article was published." So apparently the issue described herein has been fixed. I'll leave the article in place for archival purposes.


Prior to PHP 5.2.1, you could do this with PDO ...

<?php
// assume $pdo is PDO connection
$sql = "SELECT * FROM some_table
        WHERE col1 = :foo
        OR col2 = :foo
        OR col3 = :foo";

$sth = $pdo->prepare($sql);

$sth->bindValue('foo', 'bar');

$sth->execute();
?>

... and PDO would happily bind the value 'bar' to every ':foo' placeholder in the statement.

Sadly, this is no longer the case in PHP 5.2.1. For valid reasons of security and stability in memory handling, as noted to me by Wez Furlong, the above behavior is no longer supported. That is, you cannot bind a single parameter or value to multiple identical placeholders in a statement. If you try it, PDO will throw an exception or raise an error, and will not execute the query. In short, you now need to match exactly the number of bound parameters or values with the number of placeholders.

In most cases, I'm sure that's not a problem. However, in Solar, we can build queries piecemeal, so we can't necessarily know in advance how many placeholders there are going to be in the final query.

Also, it's often convenient to throw an array of data against a statement with placeholders, and only bind to the placeholders that have elements in the data array. Alas, this too is no longer allowed in PDO under PHP 5.2.1, because the number of bound values might not match the number of placeholders.

As a result, the newest Solar_Sql_Adapter::query() method includes some code to examine the statement and try to extract the named placeholders that PDO expects to see. Given the above example statement, PDO will expect placeholders for :foo, :foo2, and :foo3 (PDO auto-numbers repeated placeholder names). While a bit brain-dead, it does seem to do its job tolerably well ... at least well enough to get around this newly-implemented (but apparently always-planned) behavior.

The code in the query() method looks something like this; note that we call it by sending along an array of $data to bind as values into the statement.

// prepare the SQL command and get a statement handle
$sth = $this->_pdo->prepare($sql);

// find all :placeholder matches.  note that this will
// find placeholders in literal text, which will cause
// errors later.  so in general, you should *either*
// bind at query time *or* bind as you go, not both.
preg_match_all(
    "/W:([a-zA-Z_][a-zA-Z0-9_]+?)W/m",
    $sql . "n",
    $matches
);

// bind values to placeholders, adding numbers as needed
// in the way that PDO renames repeated placeholders.
$repeat = array();
foreach ($matches[1] as $key) {

    // only attempt to bind if the data key exists.
    // this allows for nulls and empty strings.
    if (! array_key_exists($key, $data)) {
        // skip it
        continue;
    }

    // what does PDO expect as the placeholder name?
    if (empty($repeat[$key])) {
        // first time is ":foo"
        $repeat[$key] = 1;
        $name = $key;
    } else {
        // repeated times of ":foo" are treated by PDO as
        // ":foo2", ":foo3", etc.
        $repeat[$key] ++;
        $name = $key . $repeat[$key];
    }

    // bind the $data value to the placeholder name
    $sth->bindValue($name, $data[$key]);
}

// now we can execute, even if we had multiple identical
// placeholders in the statement.
$sth->execute();

With this code in place, we can now bind one 'foo' value to many identical ':foo' placeholders.

NB: Do not try doing this with bound parameters, or you are likely to run into memory problems.


UPDATE (2016-05-30): Rasmus Schultz comments that "this does work – it was fixed after this article was published." So apparently the issue described herein has been fixed. I'll leave the article in place for archival purposes.


TypeKey and Big-Number Math: Yay Wez!

Wez Furlong gives us good news about implementing the math functions needed to support TypeKey and OpenID more directly within PHP.

Solar users have had integrated TypeKey support via Solar_Auth_Adapter_Typekey for almost 6 months now. This is
in addition to all our other auth adapters (SQL database, LDAP, .htpasswd, even .ini file).

The internals of our TypeKey adapter use big-number math functions implemented in userland by Daiji Hirata. These are very useful and serve a purpose unfulfilled by anything else in PHP, but simply cannot compare in speed or simplicity to the big-number functions presented by Wez.

I for one welcome our new big-number overlord, and I hope it arrives sooner rather than later. It will make our TypeKey adapter that much faster, and open the way for an easier-to-implement OpenID adapter for Solar.


Solar 0.26.0 Released, and New Website

The first new release of Solar in three months, version 0.26.0 alpha, has arrived! There are over 150 separate changes and improvements noted in the change log.

In conjunction with the new release, we have a brand new website. The site design is from Matt Brett, the CSS and hosting are courtesy of Clay Loveless, and the logo was designed by Ben Carter.

The single biggest change is a move from the Facade pattern to the Factory pattern for classes using adapters, such as Access, Auth, Cache, Log, Sql, and Role. If you've been using Solar::factory() to create your adapter instances, you should have no problems at all with this change, because the mechanics of instantiation are encapsulated for you.

The front-controller and page-controller now support automatic discovery of alternative output formats from the URI. For example, if the URI ends in ".rss" and that page-action allows the ".rss" format, the controller will automatically load up the ".php.rss" view and turn off the layout (instead of just the ".php" view with the default layout). This means you can use one action method to provide data for multiple output formats automatically.

Solar_Sql has a lot of little improvements: built-in profiling, emulated prepare-statment using PDO, new fetch*() methods to eventually replace the select(*) method, table-column definition retrieval via fetchTableCols(), and much more.

There's a new data filter class, although it has not been incorporated to the rest of Solar yet (look for that in a future release).

Finally, with a lot of work from Travis Swicegood, we have moved to PHPUnit3 for unit testing. Much as I love Solar_Test, there are some good arguments against using the testing library embedded in a framework to test the framework itself.

The full log of change notes follows, but it is really long, so consider yourself warned. ;-)

Solar 0.26.0 Release Notes

  • [BRK] Naming standards change: "submit" is now "process".

    Previously, we had "controller", "action", "submit", and submission buttons were named 'submit'. Rodrigo Moraes pointed out, and Clay Loveless verified, that having buttons named 'submit' makes it difficult to work in JavaScript (such as calling a submit() method on a button named 'submit', etc).

    To fix this, we need a standards change. Henceforth, we will use "process" in place of "submit". So the progression is now "controller", "action", "process". Also, locale keys are changed from SUBMIT_* to PROCESS_*.

    This release effects the new standard across the entire Solar code base, and is essentially a global search-and-replace from 'submit' to 'process'. Notable exceptions are Solar_Form, Solar_View_Helper_Form, Solar_View_Helper_FormSubmit.

  • [CHG] Global removal of func_num_args() and func_get_arg().

    In the interest of speeding things up when possible, this change introduces a new global constant: SOLAR_IGNORE_PARAM. This constant is used when we need an optional method param that may or may not be present.

    Previously, we would check for added optional params using func_num_args(), and then get the values of those params using func_get_arg(). The func_*() functions are known to be quite slow.

    Now, we set the param default value to SOLAR_IGNORE_PARAM. If the param is that value, we know it was not passed.

    Why do this instead of using null or some other empty value? Because sometimes you want to pass a null or empty value; using the SOLAR_IGNORE_PARAM value lets you state that normally no parameter is to be passed at all.

    Hope this makes sense to everyone.

  • [CHG] Removed ending ?> from all scripts to avoid "headers cannot be sent, output started at line X" error due to accidental trailing newlines in scripts.

  • [CHG] Updated example .htaccess file with better rewrite rules. Also added some php flag & value defaults.

  • [CHG] Changed "e.g." to "for example". This helps the documentation generator not get stuck on the periods (which indicate the summary is complete).

  • [NEW] Class: Solar_View_Helper_FormXhtml.

  • [NEW] Class: Solar_DataFilter. Using PHP's "filter" extension, combines methods from Solar_Valid and Solar_Filter into a single class. Will eventually replace Solar_Valid and Solar_Filter ... but not just yet.

Solar

  • [CHG] Method locale() now accepts an object as the first param

  • [CHG] Method exception() now accepts an object as the first param

  • [CHG] Per discussions w/Travis Swicegood about testing, move cleaning of globals (and config fetching) to independently-testable locations. Effectively, this means two new methods: cleanGlobals() and fetchConfig().

  • [CHG] The factory() method now checks to see if the new object itself has a solarFactory() method; if so, it returns the result from that factory. This allows for factory classes to return specific adapters.

  • [CHG] The dependency() method no longer checks that the dependency object is an instance of the $class parameter value; this is because factory classes will return an adapter instance, not an instance of the factory class itself.

  • [CHG] Method registry() doesn't check if the string $spec is registered; lets the registry itself do that. We want it to fail if the string $spec is not registered, not pass on to creating a new object.

  • [FIX] Method config() now allows empty values as valid config values (changed from an empty() check to a ! isset() check; thanks, Travis Swicegood).

  • [CHG] The fileExists() method now returns the full-path file-location it found instead of boolean true. Still returns boolean false when the file is not found. Thanks, Clay, for the suggestion.

  • [CHG] Method run() is more friendly to opcode caches (takes the include() out of a conditional).

  • [BRK] Now uses 'solarFactory' as the Solar::factory() auto-factory method name (vice 'factory').

  • [CHG] Solar::dump() no longer takes values by reference.

Solar_Access

  • [BRK] Converted from "facade" pattern to "factory" pattern. Is now a factory class, not a facade, and returns adapter instances instead of an instance of itself. Although I note this as a BC-break, you should be able to continue using it almost exactly the way you have; i.e., through Solar::factory(). The only difference should be that you get back an adapter instance instead of the facade instance.

Solar_Access_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the Facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

  • [BRK] All 'submit' keys in access lists are now 'process' keys.

Solar_Access_Adapter_File

  • [CHG] The fetch() method now checks to see if $this->_config['file'] exists and throws an exception if it does not.

Solar_App_Bookmarks

  • [BRK] All actions and views use a 'process' name and ID for all submit buttons.

  • [BRK] All locale strings for SUBMIT_* are now PROCESS_*.

  • [BRK] All calls to _isSubmit() are now _isProcess().

  • [DEL] Removed methods actionUserFeed() and actionTagFeed(); replaced by new extension-aware formatting (.rss)

  • [ADD] New browse.rss.php view to generate RSS feeds when the .rss format is specified

  • [DEL] Removed "feed" view, supplanted by "browse.rss".

  • [CHG] Bookmark actions now assume layout is turned off for RSS format.

  • [CHG] Added $_action_format map to say which actions support which formats.

Solar_Auth

  • [BRK] Is now a factory class, not a facade, and returns adapter instances instead of an instance of itself. Although I note this as a BC-break, you should be able to continue using it almost exactly the way you have; i.e., through Solar::factory(). The only difference should be that you get back an adapter instance instead of the facade instance.

  • [BRK] Moved locale files to Solar/Auth/Adapter/Locale.

Solar_Auth_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

  • [BRK] No more use of the 'common' config key; since adapters are factoried (not behind a facade) those elements can become part of the regular config array.

  • [CHG] Streamlined start() method internals.

  • [CHG] Removed _setup() method entirely, now using _loadSession() intelligently.

  • [BRK] Renamed isLoginValid() to processLogin().

  • [BRK] Renamed _verify() to _processLogin(). Instead of returning true/false, it should return an array of user info on success; or, on failure, return a string error code or an empty value.

  • [NEW] Added processLogout() and _processLogout() methods.

  • [CHG] Removed _setInfo() method, modified reset() to take a second param for user information.

  • [CHG] Now forces a temporary 'ANON' state when attempting to log in.

  • [CHG] Method isValid() now loads the session before checking

  • [ADD] Added config key 'session_class'. This allows you to pick what name to use as the session segment; e.g., different adapters can refer to the same session values.

  • [CHG] Config key 'session_class' now defaults to 'Solar_Auth_Adapter', not the actual adapter class name. This mimics the previous facade-based behavior.

  • [BRK] Config keys for 'source_submit', 'submit_login', and 'submit_logout' are now 'source_process', 'process_login', and 'process_logout' respectively.

  • [NEW] Adds support for automated redirection to a specified URI on valid login.

Solar_Auth_Adapter_*

  • [CHG] Each adapter now sets public values for $this->handle, email, uri, and moniker (as needed) instead of the protected versions.

Solar_Auth_Adapter_Htpasswd

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

  • [BRK] Method _processLogin() now returns user info on success.

Solar_Auth_Adapter_Ini

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

  • [BRK] Method _processLogin() now returns user info on success.

Solar_Auth_Adapter_Ldap

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

  • [BRK] Method _processLogin() now returns user info on success.

  • [BRK] On failure, no longer throws an exception; instead, returns a string error code and text from the LDAP server.

Solar_Auth_Adapter_Mail

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

Solar_Auth_Adapter_Post

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

  • [BRK] Method _processLogin() now returns user info on success.

Solar_Auth_Adapter_Sql

  • [CHG] Now uses $this->_handle and $this->_passwd directly.

  • [BRK] Method _processLogin() now returns user info on success.

Solar_Auth_Adapter_Typekey

  • [CHG] All adapter-specific logic is now in _processLogin(), not isLoginValid()

  • [BRK] Method _processLogin() now returns user info on success.

  • [CHG] Method _processLogin() now returns 'ERR_TIME_WINDOW' when the verification window is expired

Solar_Cache

  • [BRK] Converted Solar_Cache and all adapters from facade pattern to factory. Although I note this as a BC-break, you should be able to continue using Solar_Cache exactly the way you have; i.e., through Solar::factory(). The only difference is that you get back a Solar_Cache_Adapter instance instead of a Solar_Cache instance.

Solar_Cache_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the Facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

Solar_Cache_Adapter_*

  • [CHG] The save()/fetch()/delete()/deleteAll() methods now check internally if $this->_active is true instead of depending on the facade to check on it, because we're now using factories instead of facades.

  • [BRK] When not active, adapters return null instead of boolean false when save/fetch/delete/deleteAll methods are called.

Solar_Cache_Adapter

  • [CHG] When you call fetch() and the cache is not active, returns null (used to be boolean false)

Solar_Cache_Adapter_File

  • [CHG] Now serializes all non-scalar values. (Previously, the adapter serialized only objects and arrays).

Solar_Cache_Adapter_Memcache

  • [CHG] Now throws a CONNECTION_FAILED exception at construction time if it cannot connect to the memcache service.

Solar_Content

  • [ADD] Added config keys for 'areas', 'nodes', and 'tags' for dependency injection. Thanks, Rodrigo Moraes.

  • [CHG] Now uses Solar::dependency() instead of Solar::factory() internally for areas, noted, and tags models. Thanks, Rodrigo Moraes.

Solar_Content_Abstract

  • [BRK] Renamed fetchWhere() to fetchRow().

Solar_Content_Bookmarks

  • [BRK] Renamed fetchWhere() to fetchRow().

Solar_Controller_Front

  • [ADD] Added _notFound() method to customize behavior when a page-controller is not found.

  • [CHG] Now properly falls back to the default page-controller when the requested controller is not found.

  • [CHG] Moved inflection of page name from fetch() to _getPageName()

  • [CHG] Moved handling of empty page-name from fetch() to _getPageName()

  • [FIX] When falling back to the default controller page, now places the original page-name request on top of the URI stack to preserve it for the page-controller as the first param for the default action.

Solar_Controller_Page

  • [CHG] Setting $this->_view to an empty value turns off the view-template processing, for "null" output. (Note that the null output will still be inserted into a layout unless you set $this->_layout to an empty value.) Thanks to Travis for the discussion that led to this implementation.

  • [BRK] Changed property $_submit_key to $_process_key.

  • [BRK] Changed method _isSubmit() to _isProcess().

  • [BRK] All locale string comparisons on SUBMIT_* are now on PROCESS_*.

  • [NEW] Supports rendering of multiple formats based on the last path-info element having a dot extension. For example, "foo/bar/baz.xml" will cause the _render() method to look for a "baz.xml.php" view script (instead of just "baz.php"). This applies to layouts as well as views. To override the format, set $this->_format as you wish (default is empty).

  • [CHG] Now throws an exception when the layout template is not found.

  • [CHG] Using a format extension now turns off layout to begin with, rather than using the same layout name with a format extension. Per talk w/Clay Loveless.

  • [CHG] Now "ignores" .php format requests, resets $this->_format to null in such cases.

  • [CHG] Now honors only formats listed in the $_action_format array, on a per-action basis. This is to fix problems where a param is a valid value, but has a dot in it (such as filename.doc or example.com).

  • [FIX] Removes blank elements from end of info array.

Solar_Debug_Var

  • [CHG] Method dump() no longer takes values by reference

Solar_Docs_Apiref

  • [ADD] Now collects constants from the class, albeit without their docblocks (the Reflection API does not yet support that).

  • [NEW] Now collects the classes with @package and @subpackage tags into $package and $subpackage properties, respectively. Also warns when no @package tag is present.

  • [FIX] Pulls package name from proper location now.

Solar_Docs_Phpdoc

  • [ADD] Added @ignore support. Thanks for the patch, Clay Loveless.

  • [FIX] Narrative portions no long strip the first character. Thanks again, Clay.

  • [CHG] Improved summary-extraction logic.

  • [NEW] Added more tag parsers: @author, @copyright, @deprec[ated], @license, @link, @since, @version, @example, @staticvar.

Solar/Locale/*

  • [BRK] All SUBMIT_* keys are now PROCESS_* keys.

Solar_Log

  • [BRK] Converted Solar_Log and all adapters from facade pattern to factory. Although I note this as a BC-break, you should be able to continue using Solar_Log almost exactly the way you have; i.e., through Solar::factory(). The only difference should be that you get back a Solar_Log_Adapter instance instead of a Solar_Log instance.

Solar_Log_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the Facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

Solar_Markdown

  • [CHG] Disabling "tidy" is somewhat more loose now; any empty value for the 'tidy' config key now turns it off, vice only a boolean false.

Solar_Markdown_Wiki_Header

  • [CHG] Added support for {#id-attrib} markup, like with Solar_Markdown_Extra_Header.

Solar_Markdown_Wiki_MethodSynopsis

  • [FIX] Now shows default parameters of integer 0.

Solar_Request

  • [BRK] Renaming "isXml()" to "isXhr()" (Xml Http Request) for clarity. Per suggestion from Rodrigo Moraes.

Solar_Role

  • [BRK] Converted from "facade" pattern to "factory" pattern. Is now a factory class, not a facade, and returns adapter instances instead of an instance of itself. Although I note this as a BC-break, you should be able to continue using it almost exactly the way you have; i.e., through Solar::factory(). The only difference should be that you get back an adapter instance instead of the facade instance.

Solar_Role_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the Facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

  • [FIX] Added config key for 'refresh'.

  • [CHG] The load() method now takes a second param, $refresh, to force or ignore a refresh regardless of the default setting.

  • [CHG] In line with similar issue found with Solar_Auth_Adapter, the session class segment now defaults to 'Solar_Role_Adapter' (because factory returns different class names).

Solar_Role_Adapter_Sql

  • [ADD] New 'where' config key like Solar_Auth_Adapter_Sql to pass in additional multiWhere() conditions.

  • [BRK] Default table is now 'roles' (vice 'member_roles').

  • [FIX] Default role_col is now 'name' (vice 'role', which is a reserved word in many databases).

  • [FIX] Now properly performs the database fetch call (thanks, Jeff Surgeson, for the bug report).

Solar_Sql

  • [CHG] Moved sql exceptions down to adapter level (i.e., from Solar_Sql_Exception_* to Solar_Sql_Adapter_Exception_*).

  • [BRK] Converted Solar_Sql and all adapters from facade pattern to factory. Although I note this as a BC-break, you should be able to continue using Solar_Sql almost exactly the way you have; i.e., through Solar::factory(). The only difference should be that you get back a Solar_Sql_Adapter instance instead of a Solar_Sql instance.

Solar_Sql_Adapter

  • [BRK] Now contains all the methods and properties previously contained in the facade wrapper. I note this as a BC break, but if you have been using Solar::factory() all along, you should experience few if any problems on upgrade.

  • [BRK] The public method buildSelect() is now a protected _buildSelect().

  • [BRK] The exec() method is replaced by query().

  • [NEW] Added protected methods [_create|_drop|_next]Sequence(), _dropIndex(), etc. for factory adapters to support the related public methods.

  • [ADD] Added quick and dirty profiling mechanism. Includes new 'profiling' config key and $profile property, new method getProfile() to get the underlying adapter query profile, and new method setProfiling() turns profiling off and on.

  • [CHG] Now uses PDO prepared-statement emulation; this should be a speed boost. No longer checks the query statement for "direct" non-prepared execution.

  • [ADD] Added new fetch*() methods to eventually replace the select() method.

  • [CHG] Method select() now uses the fetch*() methods internally, but should be maintaining backwards-compatibility.

  • [CHG] Methods fetchRow() and fetchOne() now automatically set LIMIT 1, thus selecting a single row before returning results. Thanks to Clay Loveless and Travis Swicegood for pointing out the need for this.

  • [CHG] Method fetchAssoc() now allows returning of a Solar_Sql_Rowset, per request from Clay.

  • [CHG] The query() method always returns a PDOStatement now.

  • [BRK] Renamed method listTables() to fetchTableList().

  • [NEW] Added method fetchTableCols() to get the schema for a table.

  • [CHG] The query() method always returns a PDOStatement object.

  • [CHG] Now uses "emulated prepares", which speeds things up a great deal in some cases, but requires PHP 5.1.3 or later for it to be useful.

  • [CHG] Now uses $this->_native instead of $native.

  • [CHG] On query failure, now throws Solar_Sql_Adapter_Exception_QueryFailed instead of PDOException. Carries more info about the failure than PDOException does. Suggested by Travis Swicegood.

Solar_Sql_Adapter_Mysql

  • [BRK] changed the native type of 'bool' from DECIMAL(1,0) to TINYINT(1)

Solar_Sql_Adapter_Pgsql

  • [FIX] The _nextSequence() method now properly quotes the sequence name.

Solar_Sql_Adapter_Sqlite

  • [DEL] Removed the 'mode' config key, as it is never used.

Solar_Sql_Select

  • [FIX] Method quoteInto() now returns the quoted value (thanks Antti Holvikari).

Solar_Sql_Table

  • [ADD] New 'create' config key turns auto-creation off and on; useful in production environments to reduce number of queries.

  • [CHG] Now lazy-connects to the database only on first query attempt, not at construction time. This helps delay the database connection until actually needed. See the new _connect() method and the new $_connected property. Methods save(), insert(), update(), delete(), and select() honor this.

  • [NEW] Added _newSelect() method to create a Solar_Sql_Select tool for you, injecting the same SQL connection as the table itself is using. Previously, we used Solar::factory() to create Solar_Sql_Select objects on the fly, but that would not inject the exact same SQL connection object. This way, you don't have to remember. Thanks to Clay for pointing this out.

  • [CHG] Deprecated method fetchWhere(); use the new method name fetchRow() instead.

  • [FIX] Method _autoSetup() now sets $this->_name properly from the class name.

  • [ADD] Added method getColName() to get fully-qualified name ("tablename.colname") for a column.

  • [ADD] Added setter methods (and __get() support) for the fetchAll() and fetchRow() class values. Thanks for the suggestion, Rodrigo Moraes.

Solar_Uri

  • [FIX] The setQuery() method now works when magic_quotes_gpc is turned on (the parse_str() function honors that setting). Thanks to Travis for noting this.

Solar_Session

  • [CHG] Now the regenerateId() method only regenerates if headers have not been sent.

Solar_Valid

  • [FIX] Use chr(255) as regex delimiter instead of English "pounds" character. Thanks for noting this problem, Antti Holvikari.

Solar_View_Helper_Js

  • Updated to Prototype 1.5.0 final.

  • Updated to Script.aculo.us 1.7.0 final.

Solar_View_Helper_TypekeyLink

  • Adds a config key 'process_key' with a default value of 'process'. This helps avoid double-logins by checking the current process value.

Sanitation with PHP filter_var()

I'm adding a combined validate-and-sanitize class to Solar, Solar_DataFilter. It uses some of the new filter extension functions internally.

However, I found a problem with the "float" sanitizing function in the 5.2.0 release, and thought others might want to be aware of it. In short, if you allow decimal places, the sanitizer allows any number of decimal points, not just one, and it returns an un-sanitary float.

I entered a bug on it, the text of which follows:

Description:
------------
When using FILTER_SANITIZE_NUMBER_FLOAT with FILTER_FLAG_ALLOW_FRACTION,
it seems to allow any number of decimal points, not just a single
decimal point.  This results in an invalid value being reported as
sanitized.

Reproduce code:
---------------
<?php
$val = 'abc ... 123.45 ,.../';
$san = filter_var($val, FILTER_SANITIZE_NUMBER_FLOAT,
    FILTER_FLAG_ALLOW_FRACTION);
var_dump($san);
?>

Expected result:
----------------
float 123.45

Actual result:
--------------
string(12) "...123.45..."

The bug has been marked as bogus, with various reasons and explanations that all make sense to the developers. "You misunderstand its use" and "it behaves the way we intended it to" seem to be the summary responses.

However, I would argue that intended behavior is at best naive and of only minimal value. If I'm sanitizing a value to be a float, I expect to get back a float, or a notification that the value cannot be sanitized to become a float ... but maybe that's just me.

Regardless, I'm not going to belabor the point any further; I'll just avoid that particular sanitizing filter.

Update: Pierre responds with, essentially, "RTFM." I agree that the manual describes exactly what FILTER_SANITIZE_NUMBER_FLOAT does. My point is that what it does is not very useful. I think it's reasonable to expect that a value, once sanitized, should pass its related validation, and the situation described in the above bug report indicates that it will not. My opinion is that the filter should either (1) attempt to extract a float value, or (2) indicate in some way that the value cannot be reasonably sanitized (in the sense that the returned value is not "sane"). Since it does not, and since the developers seem unwilling to accept that approach, I'll just avoid using that filter and write my own.

Update 2: Something just occurred to me. Pierre says in the comments that accepting "abc "¦ 123.45 ,"¦/" to create a float is a bad idea. Yet the PHP float sanitizer will happily accept "123.abc,/45"³ and return a float that will validate. Is *that* a good idea? If so, why?