A case for PEAR and PHP4 (Or, why BC is important!)

Tuesday, September 22. 2009

Every once in someone likes to argue that PEAR is all fugly PHP4 code and why you should not use it, and instead go and use another framework or component library. Most of those people also say that they looked at or used PEAR x years ago and then act all surprised when someone else disagrees.

In related (BC) news, most people probably read my blog because of Zend Framework, and I remember that one of the reasons I sold my clients on Zend Framework was a supposedly backward compatibility and clean API. Well, a couple years later one knows it's not all that and since another BC break was argued today on one of the mailing lists and the project lead said I spread fud, I felt l needed to write something on the topic.

Facts first.

A small history of PEAR.

I don't know how old exactly PEAR is, but the manual is copyrighted since 2001 and none of the other current frameworks have been around eight — almost nine — years.

Because PEAR has been around much longer, we also have more older code than any of those PHP5-only frameworks. In comparison, Zend Framework's first stable was release in June, 2007, almost six years later.

Major versus minor releases.

A package' version consists of x.y.z.

The rules are as follow:

  • A BC break (see below) — increment x, and set y and z to 0.
  • Adding a new feature — keep x, increment y and set z to 0.
  • Fixing a bug — keep x and y, but increment z.

When someone refers to a major release in PEAR's context (and a lot of other projects such as Zend Framework, Solar and ezComponents follow this), it's one with a change in x. :-)

What is backward compatibility?

Backward compatibility, or BC, describes that a component, package or library preserves compatibility with an older versions.

Because programming itself and developers tend to evolve, PEAR tries to keep BC in all minor versions, but allows you to break compatibility to an earlier version with a new major release.

The exception to the rule is that you may break BC during alpha and beta releases before the package reaches a stable 1.0.0. Once a 1.0.0 is reached, BC may not be broken — for whatever reason.

PHP4?

Because PEAR aims to provide BC all the way, BC includes the PHP version when the package was first released. Which in turn means that of course you may make the code compatible to a later PHP release, but not without breaking compatibility to the initial release.

If you followed the above, you understand the reason why for example there is a Mail_Queue and (a soon to be) Mail_Queue2, or more importantly: why the Mail_Queue release in 2009 is still compatible to PHP4. Even though PHP4 EOL'd a while ago.

The first Mail_Queue package was released in September of 2002, the 1.0.0 stable release followed in December of the same year. Because its 1.0.0 was compatible with PHP4, we keep it backwards compatible with PHP4 until we release Mail_Queue2-0.1.0.

A principal

A lot of people argue that with the official end of life of PHP4, one should break BC anyway. But here is why you should not.

  • Even though we all love to use PHP5, there is still a lot of PHP4 in the enterprise. And like it or not, many of those apps use PEAR, and not your funky PHP5 framework.

  • How do you keep so-called necessary and unnecessary BC breaks apart? From another point of you (which is not your own), there is always a necessary BC break to fix implement something else.

  • Because there is no such thing as small or acceptable BC breaks. There are BC breaks or there are none, it's one of those things that is black and white.

BC in other frameworks

I know for a fact that ezComponents is very strict on BC. I cannot comment on Solar or Symfony, but at least in Solar's case, I'm assuming that adhere to BC as well since a some former PEAR developers are active and they also follow the PEAR Coding Standard in many respects.

Zend Framework?

A friend of mine said that if Zend Framework really kept BC, we would at 10.0.0 and not on 1.9.3.

Reasons why Zend Framework likes to break BC, even though it advertises full BC:

  • No versioning per components but per framework.

  • Missing peer review and QA leads to unstable code in a so-called stable release. (Which in turn also fakes the stability of the entire framework since it suggests that a component that was added a couple weeks ago is as stable as a component added in 2007.)

  • Because it fixes an "issue". (Biggest WTF of them all.)

The issue in question, I'm not even sure what they were trying to fix. Supposedly some developers found it too hard or did not understand how to write an adapter for Zend_Db and someone committed a fix in the 1.9 tree/branch and apparently it was OK to break BC because it was the easier way out.

I haven't updated some projects since late 1.8.x because of these BC breaks because no one can tell me what the issue is and I don't have a day to debug my application to figure out where and how it breaks. This is annoying as hell, especially since they are supposed to be tailored to the business.

On a side-note, I know of a couple components (e.g. Zend_Session) which really deserve (!) a BC break and don't get one until 2.0. And I totally get why, but why is it OK in some cases? All BC breaks fix issues!

(Btw, as I finish this post, I see an email to zf-contributor@ in my inbox where someone considers pulling the 1.9.3 release (because it obviously breaks BC). Guess I didn't spread that much FUD after all.)

Defined tags for this entry: , , , , ,

BEPHPUG: Debugging mit Eclipse und Xdebug

Wednesday, July 29. 2009

Mostly German content ahead!

Die Berliner PHP Usergroup trifft sich am 5. August, ab 20:30 Uhr, in der Z-Bar.

Thema: Debugging mit Eclipse und Xdebug (Vortragender: Marthin Rothenberger)

Adresse:


View Larger Map

Teilnahme: kostenlos

( In English: The next meetup of the Berlin PHP Usergroup will take place on the 5th August, 2009. We'll meet at Z-Bar, at around 8:30 PM. This meetup's topic is Debugging with Eclipse and Xdebug. Feel free to join. The admission is free! (Free, as in free Beer, or something)! )

Defined tags for this entry: , , , , ,

Great success!

Monday, May 11. 2009

Even though I didn't like the Borat movie very much, I still like to use his line, Great success!. But on to more important things! ;-)

Last weekend, we — the Berlin PHP UserGroup (and Christian Weiske from Leipzig) — took part in what some people labeled the European PHP TestFest 2009. In a nutshell, we were writing tests to cover different PHP extensions.

Here's the outcome (numbers courtesy of Falko Menge):

Extension  Code Coverage    Line Coverage            New Tests
gettext    62.8 -> 98.8 %    54 ->  85 /  86 lines   19
intl/idn   11.6 -> 81.4 %     5 ->  35 /  43 lines    3
sockets    32.9 -> 70.5 %   282 -> 604 / 857 lines   23
posix      47.9 -> 70.0 %   147 -> 215 / 307 lines   16
xsl        67.3 -> 77.1 %   350 -> 401 / 520 lines   19

Our tests are available on a github repository and synced to the offical TestFest subversion repository.

I think we had a lot of fun, even recruited a couple newbies :-) to the whole testing thing. I want to extend my gratitude once more to everyone (Tim, Moritz, Robin, Falko, Christian, Jan and Florian) who showed up and helped us to improve PHP.

Great effort!

Defined tags for this entry: , , , , ,

MySQL: Using indices correctly

Tuesday, May 5. 2009

The objective was to select sessions from a table, that are older than two days.

Table setup

This is the definition:

CREATE TABLE `session` (
  `id` varchar(32) NOT NULL DEFAULT '',
  `data` text NOT NULL,
  `user` int(11) DEFAULT NULL,
  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  KEY `user_id` (`user`),
  KEY `rec_datemod` (`updated`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Right and wrong

Wrong query:

SELECT * FROM session
WHERE DATE_ADD(`updated`, INTERVAL 2 DAY)< NOW()

Correct query:

SELECT * FROM session
WHERE `updated` < DATE_SUB(NOW(), INTERVAL 2 DAY)

You wonder why?

Executing the first query, MySQL will scan the entire table and calculate the date from each row. Then it will continue and compare the value to NOW() and return the row if it matches. This is somewhat (Not really!) OK until a certain amount of traffic on the table. In my case, I have 500,000 (five-hundred-thousand) active sessions (aka rows) in the table, which makes it slower and slower and slower.

Because of the full table scan, this will also effectively lock the table (even though it's INNODB) and block it from further updates.

The second query (obviously) works around that and uses the KEY on updated.

Conclusion

The first lesson is to always use EXPLAIN!

Further more, I know some of you will shiver but phpMyAdmin is actually a pretty useful tool for these circumstances. The website stalled, you log into phpMyAdmin and figure out what's running ("Processes" tab, when you're logged in as a privileged account). If you're a shell-ninja, just execute SHOW PROCESSLIST (in mysql) and push whatever runs the longest to EXPLAIN.

The slow query-log is also something you should read up on.

Defined tags for this entry: , ,

Avoiding common pitfalls with Zend_Test

Saturday, March 28. 2009

Sometimes I think I'm particularly stupid when it comes to learning new things. Well, that, or maybe I'm just the only one complaining enough. ;-)

I feel like I've wasted a great deal of time last week with basics that were nowhere to be found or required debugging to no end. The following is the outcome, a rather random list of things to watch out for when you're starting on Zend_Test.

A general understanding of testing and PHPUnit is more than helpful.

How do you debug?

Did your test not redirect, or did your query assertions go wrong?

Generally, there are two ways:

<?php
// (...)
function testIfTheSunCameUp()
{
    var_dump($this->response, $this->request);
}

Not so pretty, right?

In general I don't understand why it will say, "failed asserting status code 404", and will not tell you what it got instead. It offers you a backtrace so you can go into the class and add var_dump(), but that's hardly useful.

One of the things I love about PHPT is that a failed test case is so descriptive. There's really no debugging a failed test because the process of debugging generally involves gathering information where PHPT hands it over to you right away. But maybe that's a PHPUnit thing.

And of course:

phpunit --verbose AllTests.php

My grudge here is that I'm a total PHPUnit newbie. Before Zend_Test I avoided PHPUnit where I could because I felt that it is a beast — one that's especially hard to tame.

Update: Someone else noticed too, please vote on ZF-6013.

Control your environment

We offer a REST-API to partners and that API is basically MVC and lots of context switching depending on GET, POST, PUT, HEAD, DELETE and a couple parameters. I like to think of it as true REST. ;-)

Since the unit tests are run on the cli, not even GET is set. So make sure to add this to your test:

<?php
// (...)
$this->request->setMethod('GET');

Keep in mind that $_SERVER is not set and if you really happen to rely on anything from that ($this->getServer()), you need to explicitly set it or figure out another smart way to deal with it. In my case, I'm overriding some of these $_SERVER variables in my bootstrap with a config setting when I'm using the "testing" environment.

Update: I've opend ZF-6162, please vote.

The basic AllTests.php

AllTests.php is used to tell phpunit what to do, a simple setup would look like this:

tests/AllTests.php

tests/controllers/AllTests.php

tests/controllers/IndexControllerTest.php

tests/controllers/AllTests.php:

<?php
require_once dirname(__FILE__) . '/IndexControllerTest.php';

class ControllersAllTests
{
    public static function main()
    {
        PHPUnit_TextUI_TestRunner::run(self::suite());
    }

    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite('Controllers');
        $suite->addSuiteCase('IndexControllerTestCase');
        return $suite;
    }
}

tests/AllTests.php:

<?php
(...)
class AllTests
{
    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite('AllTests');
        $suite->addSuite(ControllersAllTests::suite());
        return $suite;
    }
}

Zend_Session_Namespace

Avoid locking session namespaces, they will stab you in the eye. See ZF-6072.

Zend_Dom_Query

I'd suggest you remove all @-operators from Zend_Dom_Query since it doesn't really tell you what is wrong with the response otherwise. I've opened ZF-6142 so this gets fixed.

Slightly related to Zend_Dom_Query is a bug I noticed when you're response is empty. It's a rather trivial issue where null is not the same as an empty string. I've opened ZF-6143 and included a patch.

Bootstrapping the correct way

The most simple way to bootstrap is to create a small class to initialize your application. The class takes an environment parameter to __construct() which allows you to load a different config section or other nifty stuff. I called mine Lagged_App() and the code came originally from Andries Seuten's example ZF application but it evolved over the past years.

I wouldn't recommend his tutorial today (because things changed since 2007), but at the time it was pretty easy and straight forward. Today's bar is the quickstart section in the official manual. But since the quickstart suggests includes, I spent some time cleaning up my by going through said quickstart guide and I posted a quick and dirty example (which I labeled Lagged_Application)) in my subversion repository on Google Code.

A test case is a test case

I'm using the following "base" test case for all my controller tests:

<?php
class Lagged_PHPUnit_ControllerTestCase
    extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        Zend_Session::$_unitTestEnabled = true;

        $bootstrap       = new Bootstrap('testing');
        $this->bootstrap = array($bootstrap, 'start');
        parent::setUp();
    }
}

Clean up

One of the most fundamental things your mother tried to teach you.

This is in most of my tearDown()'s:

public function tearDown()
{
    $this->resetRequest()->resetResponse();
    $this->setQuery(array());
    $this->setPost(array());
}

Avoid direct PHP calls

I'd avoid the following:

  1. Skipping views with echo in the controller (bad, bad, bad ;-)).
  2. Use the Zend_Action_Helper redirector instead of header().
  3. Use Zend_Session etc. vs. session_*() (Don't forget Zend_Session::$_unitTestEnabled = true;).
  4. Don't dispatch always. ;-)
Defined tags for this entry: , , , , ,