PHP Programming Practice Web

My PHP framework has been through nine versions, across most of the history of PHP itself, and it's been used commercially all the way through. Each version was a chance to keep what had worked, throw away what hadn't, and take advantage of whatever PHP had learned to do since the last one.

Origins

It started when I ran a web design company. There was a lot in common between what different customers wanted, and a system slowly evolved where we could reuse a lot of the code. At first that meant copying and pasting it from one site to the next. It's not a big step from copy-pasting code to trying to make one common set of libraries for every site, and that's where the challenges begin. Versions 1 to 3 of the framework came from this time.

Versatile, but not too versatile

The first challenge is pulling out everything that's common, while leaving enough room for each project to have what it actually needs. A great deal was common, and could be customised with a few simple switches, along with templates specific to each website. These are some of the things we built that way:

  • A content management system, with an admin system where customers could update the content on their pages, and in some cases make new pages.
  • Focused pages with their own forms, such as news pages.
  • Shopping sites, with a product catalogue and categories.

A product catalogue can generally be shoehorned into a fairly common format. But two parts of a shop needed custom code nearly every time. Postage was very specific to each client: what they were delivering, whether they delivered internationally, and the weight of the products. And every customer seemed to want a different discount system. Both were hard to fit into a common system, because they reach into the product catalogue and the payment processing in ways that don't lend themselves to a clean abstraction or interface.

A hundred and fifty websites

We made about a hundred and fifty websites for just over a hundred customers with that system. Which brought the next challenge: as the number of customers grew, each upgrade to the framework meant upgrading all of the existing customers' sites as well.

So some decisions turned out to be very expensive to change. And to a certain degree that led to analysis paralysis when it came to adding new features, trying to think of every case that could come up, because getting one wrong would be paid for across every site.

Moving forward

After the web design company I carried on using the framework on a variety of projects, and it went through more versions, 4 and 5, shaped by my own needs on each one. The next big commercial use of it was for MyVoucherCodes.

MyVoucherCodes

MyVoucherCodes is the UK's largest voucher codes website, with eight million members in the UK, and it operates in twelve different territories. Their site was built on the Zend Framework and was very complex. It had also been copied and pasted for each territory, so there were twelve different versions, all out of sync with each other.

I was brought in as part of a consultancy team to fix up the website, acting as head of development, and it very quickly became apparent that the whole thing needed replacing. It had accumulated a lot of cruft over a long period: half-finished features, experiments, and other things that may have mattered at the time, for SEO or for other demands of the market, but didn't any more. What it needed was to become something streamlined, fast to load and reliable.

So we rebuilt it on the PHP framework, and that was an opportunity to rebuild the framework from the ground up, as version 6, based on everything the earlier versions had taught me. These were the main ideas:

  • Every component is a PHP class. PHP was just about ready to use object-oriented programming sensibly, so a page was built as a tree of component objects first, and rendered afterwards.
  • Each component brings its own CSS and JavaScript, with one function to render its CSS, one for its JavaScript, and one for its HTML.
  • Children render in the middle. Because the page is a tree, a component renders the HTML that comes before its children, then its children in a loop, then the HTML that comes after them.
  • No whitespace in the output, for pages as small as they could be.

Components as classes

Every component extended one base class. Rendering a component rendered its top, then its middle, which by default is its children, and then its bottom:

class Layout extends Base
{
    public final function Render()
    {
        $this->RenderTop();
        $this->RenderMiddle();
        $this->RenderBottom();
    }

    public function RenderMiddle()
    {
        foreach( $this->GetChildren() as $child )
            $child->Render();

        return $this;
    }

    public static function RenderCSS() {}
    public static function RenderJS() {}
    ...
}
The base of every component in version 6 (trimmed)

A component added its children in its constructor, so building the page and rendering it were two separate steps:

class LayoutAdminCacheSideMenu extends AdminSideMenuLayout
{
    public final function __construct()
    {
        parent::__construct();

        $items = CacheAdminModel::GetItemCount();
        $bytes = CacheAdminModel::GetItemTotalSize();

        $this->Add( new AdminSideMenuItemLayout( 'Cache', '/admin/Cache', $bytes ) );
        $this->Add( new AdminSideMenuItemLayout( 'Items', '/admin/Cache/items', $items ) );
    }
}
A component adding its children

And a component's CSS sat right next to its HTML, in a static function, so the framework could gather the CSS of every component class into one stylesheet:

class PopupItem_LabelLayout extends PopupItemLayout
{
    public function __construct( $label )
    {
        Assert::Str( $label );
        $this->label = $label;
        parent::__construct();
    }

    public function RenderMiddle()
    {
        $h_label = Encode::Html( $this->label );
        echo '<div class="PopupItem_LabelLayout">'.$h_label.'</div>';
    }

    public static function RenderCSS()
    {
        ?><style>
            .PopupItem_LabelLayout { padding: 8px; font-size: 11px; font-weight: bold; color: #606366; }
        </style><?
    }

    private $label;
}
A component with its own CSS

This was before it became fashionable to keep a component's HTML, CSS and JavaScript together, and before reactive frameworks in JavaScript, so it seemed an unusual approach at the time. But it paid off really well, especially here, because many of the components were designed to be embedded in various other sites, and they had to keep the corporate branding wherever they went. That's one place where CSS that belongs to a component pays off. It was already becoming clear, though, that it made the CSS very repetitive, with the same padding and the same colours written out again in every component.

No whitespace

To keep the pages small, the output had no whitespace at all. Indented HTML costs more than you'd expect: even with compression, the indentation takes up about as much of the compressed page as it does of the uncompressed one. Leaving it out saved between 40 and 60 percent on every page.

The usual way of writing HTML in PHP outputs all the indentation along with the tags:

<div class="offer">
    <h2><?= h($offer->title) ?></h2>
    <?php foreach ($offer->codes as $code) { ?>
        <p class="code"><?= h($code) ?></p>
    <?php } ?>
</div>
HTML embedded in PHP, indentation and all

So we went through and changed every piece of embedded HTML into echo statements, indented in the same way as the HTML would have been. That kept the structure easy to follow, so we could still see where each element started and ended, while nothing but the tags themselves was output:

echo '<div class="offer">';
    echo '<h2>'.h($offer->title).'</h2>';
    foreach ($offer->codes as $code) {
        echo '<p class="code">'.h($code).'</p>';
    }
echo '</div>';
The same, as echo statements with the same indentation

The framework still writes its HTML this way today.

Functions instead of classes

Version 7 was used for Synthesizing and for FractalToy, and it made the biggest change since version 6. PHP now supported closures properly, so components became functions instead of classes. A component is a static function that takes closures for its content, and calls them where its content goes:

final class MemberLoginUi {
    static function Page(): void {
        UiContent::Area(function() {
            StatusUi::Message();
            UiContent::Title('Log In');
            UiContent::Text('Select a login method and enter your details.');
            MemberLoginUi::Forms();
        });
    }

    static function Forms(): void {
        $allMethods = MemberAuthMethodConfig::GetMethods();
        UiContent::Section(function() use($allMethods) {
            UiLayout::Flex(UiLayout::SIZE_400, UiLayout::SIZE_600, function() use($allMethods) {
                foreach ($allMethods as $method) {
                    UiContent::Panel(function() use($method) {
                        UiContent::Title($method->methodName);
                        call_user_func($method->loginFormCallback);
                    });
                }
            });
        });
    }
}
A login page, as functions, from version 9 (trimmed)

There's no tree to build first, and no objects to keep: the nesting of the code is the nesting of the page, and it renders as it goes. It's a cleaner and more declarative way of writing templates, and with the user interface in one shared module, the pages have no HTML and no CSS of their own, which also put an end to the repetitive CSS. Version 8 was used for Frenzi, and versions 8 and 9, the latest, carry on with the same approach.

What PHP made possible

Each big change in the framework was waiting on something in PHP itself:

PHPWhat it made possible
Object orientation that was officially supported and worked properly, in PHP 5Components as classes, built into a tree and rendered afterwards (version 6)
Closures in PHP 5.3, and $this inside them in 5.4Components as functions that take their content as closures (version 7 onwards)
Scalar type declarations and return types in PHP 7Typed signatures on every function, checked by PHP itself
Constructor promotion in PHP 8.0, and readonly properties in 8.1Immutable data objects in a few lines
The PHP features behind each change

How the modules grew up

Running through all of these versions is the idea of a module. In the early versions it was a shared library that every site included. By version 6 the framework was divided into modules, each with its own components, but what a module should contain, and how modules should talk to each other, was still being worked out.

Today the modules are very much decoupled. Every module has the same shape, owns its own database tables, never joins its tables with another module's, and lets other modules extend it through hooks instead of reaching into it. That was hard to come by, and it took a lot of experience to get there. How it works now is in Modular monoliths in PHP.

See also