A Non-trivial Paginated Table in Laravel Livewire

Having just gone through some trouble trying to get a decent, fully responsive, non-trivial table working in Laravel Livewire, I figured I’d share. This code is available on GitHub as Laravel Livewire Paginated Table. Feel free to use and adapt.

The goal here is to implement a responsive table that follows the style detailed by Shingo Nakayama at How to create responsive tables with pure CSS using Grid Layout Module. I’ll be adapting their solution along the way.

Setup

First set up a project with breeze. I’m assuming you’re familiar with this process. If this is new, there are lots of other resources available to help. Replace “myproject” with your project name:

composer create-project laravel/laravel myproject
cd myproject
composer require laravel/breeze --dev
php artisan breeze:install livewire-functional
npm run dev

Next, we’ll create a table to hold the data and a factory to generate test data.

php artisan make:model -mc Part
php artisan make:factory PartFactory

In practice, the data in our table would probably be the result of a join and some computation but for the purposes of this example, I’m just going to mirror the columns in the table. Our migration for creating the parts table will look like this:

    public function up(): void
    {
        Schema::create('parts', function (Blueprint $table) {
            $table->id();
            $table->string('partNumber');
            $table->text('partDescription');
            $table->string('vendorNumber');
            $table->string('vendorName');
            $table->integer('orderQuantity');
            $table->integer('receiveQuantity');
            $table->decimal('cost');
            $table->decimal('extendedCost');
            $table->decimal('dutyPct');
            $table->decimal('duty');
            $table->decimal('freightPct');
            $table->decimal('freight');
            $table->string('uom');
            $table->string('vendorPart');
        });
    }

While we’re at it, set up the factory to generate sample data.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Part>
 */
class PartFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $orderQuantity = fake()->numberBetween(5, 100);
        $receiveQuantity = fake()->numberBetween(0, $orderQuantity);
        $cost = fake()->randomFloat(2, 0.1, 150.0);
        $extendedCost = $cost * $receiveQuantity;
        $dutyPct = fake()->randomFloat(1, 5.0, 15.0);
        $freightPct = fake()->randomFloat(1, 2.0, 10.0);
        return [
            'partNumber' => fake()->numerify('####-###'),
            'partDescription' => fake()->sentence(5, true),
            'vendorNumber' => fake()->numerify('0###'),
            'vendorName' => fake()->words(3, true),
            'orderQuantity' => $orderQuantity,
            'receiveQuantity' => $receiveQuantity,
            'cost' => $cost,
            'extendedCost' => $extendedCost,
            'dutyPct' => $dutyPct,
            'duty' => round($extendedCost * $dutyPct / 100.0, 2),
            'freightPct' => $freightPct,
            'freight' => round($extendedCost * $freightPct / 100.0, 2),
            'uom' => fake()->randomElement(['EA', 'EA', 'PK', 'BX', 'CTN']),
            'vendorPart' => strtoupper(fake()->bothify('??.###.##')),
        ];
    }

}

Then create the table:

php artisan migrate

Routing

Next, we’ll make our table the home page:

<?php

use Illuminate\Support\Facades\Route;

- Route::view('/', 'welcome');
+ Route::view('/', 'parts');

Route::view('dashboard', 'dashboard')
    ->middleware(['auth', 'verified'])
    ->name('dashboard');

Route::view('profile', 'profile')
    ->middleware(['auth'])
    ->name('profile');

require __DIR__.'/auth.php';

The Livewire Component

I tried to embed pagination inside a view-component and got nowhere. What worked for me was creating a component-component. In other words, I separated the component code from the view. While we’re here, I added some methods to populate and clear the table.

<?php

namespace App\Livewire;

use App\Models\Part;
use Livewire\Component;
use Livewire\WithPagination;

class PartList extends Component
{
    use WithPagination;

    public function populate(int $count)
    {
        Part::factory($count)->create();
        return redirect()->to('/');
    }

    public function purge()
    {
        Part::truncate();
        return redirect()->to('/');
    }

    public function render()
    {
        return view('livewire.parts.list', ['parts' => Part::orderBy('partNumber')->paginate(10)]);
    }
}

Making the CSS Mobile First

Nakayama’s CSS has a desktop first orientation. My first step was to reorganize it to be mobile-first, as a precursor to making it actually use Tailwind’s utility classes.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
    /* Mobile first styles, extra small display, single column originally up to 580px */
    /* Now small display: up to 639px */
    ol.collection {
        margin: 0;
        padding: 0;
    }

    li {
        list-style: none;
    }

    ol.collection * {
        box-sizing: border-box;
    }

    .item {
        border: 1px solid gray;
        border-radius: 2px;
        padding: 10px;
    }

    /* Don't display the first item, since it is used to display the header for tabular layouts*/
    .collection-container > li:first-child {
        display: none;
    }

    .attribute::before {
        content: attr(data-name);
    }

    /* Attribute name for first column, and attribute value for second column. */
    .attribute {
        display: grid;
        grid-template-columns: minmax(9em, 30%) 1fr;
    }

    .collection-container {
        display: grid;
        grid-template-columns: 1fr;
        grid-gap: 20px;
    }

    @media (min-width: 640px) {
        /* Mobile first styles, small display, dual column originally 581px to 736px */
        /* Now medium 640px up to 767px */
        .collection-container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            grid-gap: 20px;
        }

    }

    @media (min-width: 768px) {
        /* Mobile first styles, medium display, table originally 737px and up */
        /* Now 768 and up */

        .attribute::before {
            content: none;
        }

        /* Show the first item, since it is used to display the header for tabular layouts*/
        .collection-container > li:first-child {
            display: grid;
        }

        .item {
            border: none;
            padding: 0;
        }

        /* The maximum column width, that can wrap */
        .item-container {
            display: grid;
            grid-template-columns: 2em 2em 10fr 2fr 2fr 2fr 2fr 4em 6em;
        }

        .attribute-container {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(var(--column-width-min), 1fr));
        }

        /* Definition of wrapping column width for attribute groups. */
        .part-information {
            --column-width-min: 10em;
        }

        .part-id {
            --column-width-min: 10em;
        }

        .vendor-information {
            --column-width-min: 8em;
        }

        .quantity {
            --column-width-min: 5em;
        }

        .cost {
            --column-width-min: 5em;
        }

        .duty {
            --column-width-min: 5em;
        }

        .freight {
            --column-width-min: 5em;
        }

        .collection {
            border-top: 1px solid gray;
            border-left: 1px solid gray;
        }

        /* In order to maximize row lines, only display one line for a cell */
        .attribute {
            border-right: 1px solid gray;
            border-bottom: 1px solid gray;
            display: block;
            grid-template-columns: none;
            padding: 2px;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }

        .collection-container > .item-container:first-child {
            background-color: blanchedalmond;
        }

        .item-container:hover {
            background-color: rgb(200, 227, 252);
        }

        .collection-container {
            grid-template-columns: none;
            grid-gap: 0;
        }

        /* Center header labels */
        .collection-container > .item-container:first-child .attribute {
            display: flex;
            align-items: center;
            justify-content: center;
            text-overflow: initial;
            overflow: auto;
            white-space: normal;
        }


    }
}

To test this, I copied the <section> from Nakayama’s HTML and dropped it into the view.

<section>
  <ol class="collection collection-container">
    <!-- Copied sample table data here -->
  </ol>
</section>
<x-app-layout>
    <div class="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8">
        Parts List<br>
    </div>
    <livewire:partList />
</x-app-layout>

Everything works as expected! Good.

Making it Functional

Time to add a couple of buttons to manage the table, loop through the data and add pagination.

<section>
    <x-secondary-button wire:click="populate(10)">Add records</x-secondary-button>
    <x-secondary-button wire:click="purge">Purge All Records</x-secondary-button>
    @if(count($parts))
        <ol class="collection collection-container">
            <!-- The first list item is the header of the table -->
            <li class="item item-container">
                <div class="attribute"></div>
                <div class="attribute" data-name="#">#</div>
                <!-- Enclose semantically similar attributes as a div hierarchy -->
                <div class="attribute-container part-information">
                    <div class="attribute-container part-id">
                        <div class="attribute" data-name="Part Number">Part Number</div>
                        <div class="attribute" data-name="Part Description">Part Description</div>
                    </div>
                    <div class="attribute-container vendor-information">
                        <div class="attribute">Vendor Number</div>
                        <div class="attribute">Vendor Name</div>
                    </div>
                </div>
                <div class="attribute-container quantity">
                    <div class="attribute">Order Qty.</div>
                    <div class="attribute">Receive Qty.</div>
                </div>
                <div class="attribute-container cost">
                    <div class="attribute">Cost</div>
                    <div class="attribute">Extended Cost</div>
                </div>
                <div class="attribute-container duty">
                    <div class="attribute">Duty %</div>
                    <div class="attribute">Duty</div>
                </div>
                <div class="attribute-container freight">
                    <div class="attribute">Freight %</div>
                    <div class="attribute">Freight</div>
                </div>
                <div class="attribute">UOM</div>
                <div class="attribute">Vendor Part Number</div>
            </li>
            <!-- The rest of the items in the list are the actual data -->
            @foreach ($parts as $part)
                <li class="item item-container" wire:key="{{ $part->id }}">
                    <div class="attribute" data-name="Select"><input type="checkbox" name="" id="{{ $part->id }}"></div>
                    <div class="attribute" data-name="#">{{ $loop->index + 1 }}</div>
                    <div class="attribute-container part-information">
                        <div class="attribute-container part-id">
                            <div class="attribute" data-name="Part Number">{{ $part->partNumber }}</div>
                            <div class="attribute" data-name="Part Description">{{ $part->partDescription }}</div>
                        </div>
                        <div class="attribute-container vendor-information">
                            <div class="attribute" data-name="Vendor Number">{{ $part->vendorNumber }}</div>
                            <div class="attribute" data-name="Vendor Name">{{ $part->vendorName }}</div>
                        </div>
                    </div>
                    <div class="attribute-container quantity">
                        <div class="attribute" data-name="Order Qty">{{ $part->orderQuantity }}</div>
                        <div class="attribute" data-name="Receive Qty">{{ $part->receiveQuantity }}</div>
                    </div>
                    <div class="attribute-container cost">
                        <div class="attribute" data-name="Cost">${{ number_format($part->cost, 2) }}</div>
                        <div class="attribute" data-name="Extended Cost">
                            ${{ number_format($part->extendedCost, 2) }}</div>
                    </div>
                    <div class="attribute-container duty">
                        <div class="attribute" data-name="Duty %">{{ number_format($part->dutyPct, 1) }}%</div>
                        <div class="attribute" data-name="Duty">${{ number_format($part->duty, 2) }}</div>
                    </div>
                    <div class="attribute-container freight">
                        <div class="attribute" data-name="Freight %">{{ number_format($part->freightPct, 1) }}%</div>
                        <div class="attribute" data-name="Freight">${{ number_format($part->freight, 2) }}</div>
                    </div>
                    <div class="attribute" data-name="UOM">{{ $part->uom }}</div>
                    <div class="attribute" data-name="Vendor Part Number">{{ $part->vendorPart }}</div>
                </li>
            @endforeach
        </ol>
    @else
        <p>There are no parts in the table.</p>
    @endif
    {{ $parts->links() }}
</section>

“Tailwindifying” the CSS

Right now we have a working solution, and we could call it done. But merely dumping our custom CSS into utilities isn’t exactly Tailwind native, and these styles also belong in the components layer, so let’s fix all that. Note that some of the colours changed slightly so we can use those provided by Tailwind. There are also a few tweaks for alignment, which will show up soon.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
    /* Mobile first styles, extra small display, single column originally up to 580px */
    /* Now small display: up to 639px */
    ol.collection {
        @apply m-0 p-0;
    }

    li {
        @apply list-none;
    }

    ol.collection * {
        @apply box-border;
    }

    .item {
        @apply border border-gray-500 p-3 rounded-sm;
    }

    /* Don't display the first item, since it is used to display the header for tabular layouts*/
    .collection-container > li:first-child {
        @apply hidden;
    }

    .attribute::before {
        @apply content-[attr(data-name)] text-end pr-3;
    }

    /* Attribute name for first column, and attribute value for second column. */
    .attribute {
        @apply grid grid-cols-[minmax(9em,30%)_1fr];
    }

    .collection-container {
        @apply grid gap-5 grid-cols-1;
    }

    @media (min-width: 640px) {
        /* Mobile first styles, small display, dual column originally 581px to 736px */
        /* Now medium 640px up to 767px */
        .collection-container {
            @apply grid gap-5 grid-cols-2;
        }

    }

    @media (min-width: 768px) {
        /* Mobile first styles, medium display, table originally 737px and up */
        /* Now 768 and up */

        .attribute::before {
            @apply content-none;
        }

        /* Show the first item, since it is used to display the header for tabular layouts*/
        .collection-container > li:first-child {
            @apply grid;
        }

        .item {
            @apply border-0 p-0;
        }

        /* The maximum column width, that can wrap */
        .item-container {
            @apply grid grid-cols-[2em_2em_10fr_2fr_2fr_2fr_2fr_4em_6em];
        }

        .attribute-container {
            @apply grid grid-cols-[repeat(auto-fit,minmax(var(--column-width-min),1fr))];
        }

        /* Definition of wrapping column width for attribute groups. */
        .part-information {
            --column-width-min: 10em;
        }

        .part-id {
            --column-width-min: 10em;
        }

        .vendor-information {
            --column-width-min: 8em;
        }

        .quantity {
            --column-width-min: 5em;
        }

        .cost {
            --column-width-min: 5em;
        }

        .duty {
            --column-width-min: 5em;
        }

        .freight {
            --column-width-min: 5em;
        }

        .collection {
            @apply border-t border-l border-gray-500;
        }

        /* In order to maximize row lines, only display one line for a cell */
        .attribute {
            @apply block border-r border-b border-gray-500 grid-cols-none overflow-hidden p-0.5 text-ellipsis whitespace-nowrap;

        }

        .collection-container > .item-container:first-child {
            @apply bg-orange-200;
        }

        .item-container:hover {
            @apply bg-blue-100;
        }

        .collection-container {
            @apply grid-cols-none gap-0;
        }

        /* Center header labels */
        .collection-container > .item-container:first-child .attribute {
            @apply flex items-center justify-center overflow-auto whitespace-normal;
            text-overflow: initial;
        }
    }
}

While we’re at it, let’s make the card view a little nicer and align the columns in the table view:

<section>
    <x-secondary-button wire:click="populate(10)">Add records</x-secondary-button>
    <x-secondary-button wire:click="purge">Purge All Records</x-secondary-button>
    @if(count($parts))
        <ol class="collection collection-container">
            <!-- The first list item is the header of the table -->
            <li class="item item-container">
                <div class="attribute"></div>
                <div class="attribute" data-name="#">#</div>
                <!-- Enclose semantically similar attributes as a div hierarchy -->
                <div class="attribute-container part-information">
                    <div class="attribute-container part-id">
                        <div class="attribute" data-name="Part Number">Part Number</div>
                        <div class="attribute" data-name="Part Description">Part Description</div>
                    </div>
                    <div class="attribute-container vendor-information">
                        <div class="attribute">Vendor Number</div>
                        <div class="attribute">Vendor Name</div>
                    </div>
                </div>
                <div class="attribute-container quantity">
                    <div class="attribute">Order Qty.</div>
                    <div class="attribute">Receive Qty.</div>
                </div>
                <div class="attribute-container cost">
                    <div class="attribute">Cost</div>
                    <div class="attribute">Extended Cost</div>
                </div>
                <div class="attribute-container duty">
                    <div class="attribute">Duty %</div>
                    <div class="attribute">Duty</div>
                </div>
                <div class="attribute-container freight">
                    <div class="attribute">Freight %</div>
                    <div class="attribute">Freight</div>
                </div>
                <div class="attribute">UOM</div>
                <div class="attribute">Vendor Part#</div>
            </li>
            <!-- The rest of the items in the list are the actual data -->
            @foreach ($parts as $part)
                <li class="item item-container" wire:key="{{ $part->id }}">
                    <div class="attribute md:text-center" data-name="Select"><input type="checkbox" name="" id="{{ $part->id }}"></div>
                    <div class="attribute md:text-center" data-name="#">{{ $loop->index + 1 }}</div>
                    <div class="attribute-container part-information">
                        <div class="attribute-container part-id">
                            <div class="attribute" data-name="Part Number:">{{ $part->partNumber }}</div>
                            <div class="attribute" data-name="Part Description:">{{ $part->partDescription }}</div>
                        </div>
                        <div class="attribute-container vendor-information">
                            <div class="attribute" data-name="Vendor Number:">{{ $part->vendorNumber }}</div>
                            <div class="attribute" data-name="Vendor Name:">{{ $part->vendorName }}</div>
                        </div>
                    </div>
                    <div class="attribute-container quantity">
                        <div class="attribute md:text-end" data-name="Order Qty.:">{{ $part->orderQuantity }}</div>
                        <div class="attribute md:text-end" data-name="Receive Qty.:">{{ $part->receiveQuantity }}</div>
                    </div>
                    <div class="attribute-container cost">
                        <div class="attribute md:text-end" data-name="Cost:">${{ number_format($part->cost, 2) }}</div>
                        <div class="attribute md:text-end" data-name="Extended Cost:">
                            ${{ number_format($part->extendedCost, 2) }}</div>
                    </div>
                    <div class="attribute-container duty">
                        <div class="attribute md:text-end" data-name="Duty %:">{{ number_format($part->dutyPct, 1) }}%</div>
                        <div class="attribute md:text-end" data-name="Duty Amount:">${{ number_format($part->duty, 2) }}</div>
                    </div>
                    <div class="attribute-container freight">
                        <div class="attribute md:text-end" data-name="Freight %:">{{ number_format($part->freightPct, 1) }}%</div>
                        <div class="attribute md:text-end" data-name="Freight Amount:">${{ number_format($part->freight, 2) }}</div>
                    </div>
                    <div class="attribute md:text-center" data-name="UOM:">{{ $part->uom }}</div>
                    <div class="attribute" data-name="Vendor Part#:">{{ $part->vendorPart }}</div>
                </li>
            @endforeach
        </ol>
    @else
        <p>There are no parts in the table.</p>
    @endif
    {{ $parts->links() }}
</section>

All Done

Let’s look at the results! First the card view.

And the table view:

There’s more that could be done here. For example, some of these constructs could be generalized to make it easier to support multiple tables. If your application has a lot of different tables, that would be the best approach.

Docker, Explained for “Old School” Developers

Docker, Explained for “Old School” Developers

Anyone writing code has probably seen a bunch of references to Docker by now. It’s like this new toy that’s all the rage, but for people like me — where picking up new things takes a crap-load more work than it used to — the general reaction is “I’ll learn that when it’s unavoidable.” Alas, a few weeks back it became unavoidable, and I’m here to report back.

If you’re even mildly inquisitive, a quick scan of Docker says a lot about containers and it’s pretty obvious that a container is some kind of virtual environment, but there’s not much that tells you why you should give a damn. I’m going to try to fix that. I’m not going to describe how to use Docker here, just what it’s for. There’s lots of great resources on making use of it elsewhere.

If you got into development in a time when your workplace had a nice air-conditioned room with raised floors and racks of servers and keyboard/console switches, then this post is for you.

The TL:DR on this is the learning curve here is a lot less than it seems at first, and the flexibility it gives you to set up and change configurations is truly powerful. Invest a few days in learning the basics and you can build, rebuild, and reconfigure virtual server rooms that significantly reduce the amount of time needed to maintain your own local environment. As a common example, if you’ve ever sworn at LAMP/ XAMP / MAMP configurations only to start from scratch or if you’ve tried to get two versions of just about anything running on the same system, then Docker definitely is for you.

(more…)

Laravel 7/8, Behat, Mink, BDD, Netbeans 12, and subdomain testing!

UPDATES:

  • Verified no changes with Laravel 8.
  • Added some details on running with Chromium.

There’s a mouthful. There’s two parts to this post. The first part is the story of how I arrived at using Behat, a tool that facilitates Behavioural Driven Design (BDD). The last part is the TL;DR configuration I used to get it working. If you just want to get this configuration going and don’t care about the background, skip down to it. I won’t be hurt.

First an admission: I’m really, really late to the BDD camp, and it kind of pisses me off. If I’d been using this approach for the past 15 years, there’s no question I would have gotten more done in less time.

(more…)

Fix: Single File Tests Break in Netbeans 11.0 / PHPUnit 8.2

UPDATED 2019-07-22: Netbeans 11.1, released today, incorporates a robust fix for this problem that should survive future changes in PHPUnit.

TL/DR: There’s a one line patch to PHPUnit below that will kludge the kludge and get you running again.

Anyone who has worked with PHPUnit for some time knows that backwards compatibility isn’t exactly a prime consideration. Meanwhile, although Netbeans currently has very good support for PHP, you have to figure that the intersection set between the Java developers working on the project and the people who figure that PHP is anything but a toy language for building simple projects is, well… small. [We’ll just ignore the fact that Facebook, the largest application on the planet, is written in PHP].

So when Netbeans says it offers support for PHPUnit 3.4.0 or higher, it’s okay to expect the integration to be out of date. Rather surprisingly, it’s actually worked right up to PHPUnit 8.1.

But now we have 8.2. Command line parsing has been made more robust, and the extra parameter Netbeans passes in to a kludged custom test when running a single file doesn’t work anymore. [BTW I don’t blame the Netbeans developers for this kludge, it was probably the only solution that worked back in 3.4.0.]

This makes the current Netbeans approach outdated and unworkable. Like many open source projects, this means someone who cares has to go in and do some significant re-work. Don’t hold your breath. I’d give it a go but my Java foo is about 20 years old now. I probably don’t know enough any more to even be dangerous. I think I know a good architectural approach but attempting to implement it would be a recipe for failure.

So what to do? Patch PHPUnit! This is a pain since the patch will have to be reapplied every time PHPUnit is updated, which is frequently. But at least it works.

So here’s the kludge: in the file TextUI/Command.php in the handleArguments array, just change the line that reads

if (isset($this->options[1][1])) {

to

if (isset($this->options[1][1]) && substr($this->options[1][1], 0, 6) != '--run=') {

This ignores the Netbeans-generated argument and everything works as before. Not pretty but it works.

For more information (and a possible fix), follow the Netbeans Issue.

When Should You Upgrade Your Joomla 1.5 Site

[Ed. Note: this was originally published on a now-defunct site in 2013. Republished (and back-dated) here because seven years later people are still running old versions of Joomla 1.5! Also, Joomla is still a far better CMS than WP. WordPress is like the Microsoft of CMS systems… everyone is using it, but not because it’s the best solution.]

According to W3Techs, as of the beginning of July 2013, 63% of all Joomla sites are running version 1.x. Of these, some 92% are running version 1.5. That works out to a rather large 58% of all Joomla sites running 1.5! The other 5% are mostly version 1.6 and 1.7. [Aside: if your site is one of those 5% please just upgrade now. It’s not going to be that painful and you are a sitting duck for hackers. By “now” I mean stop reading this and go upgrade. Seriously.]

So why is the number so high? There are usually a long list of factors, and most of them are valid. Here are the ones I hear regularly:

  • Simply porting the site is going to be a lot of work.
  • We just did our site a few years ago and don’t have the budget for it.
  • Things we rely upon didn’t make it to 2.5.
  • We hate change.
  • The site is outdated; if we’re going to update it we want to redesign it and that’s a big job.
  • There’s no reason to upgrade (AKA “if it ain’t broke, don’t fix it”).
  • You’re only telling me I need to upgrade because you want more business.

Every web site is different, so each of the reasons above can be more or less relevant depending on circumstances. At one end of the spectrum is the hobby site that generates no revenue, and doesn’t have much traffic. A site that could be off line for a few weeks or months and not suffer. I’m going to exclude them from this discussion.

For everyone else, the question to ask is “what’s the cost of having my site suddenly go to an ‘under construction’ page?” What’s the monetary value? What’s the value of lost reputation? Take a serious look at your situation and try to come up with a reasonable number. Compare this with the cost of upgrading your site. If the numbers are close, it’s probably a good idea to start budgeting. If the cost is significantly higher than upgrading, find the budget now because it’s time to start planning!

Here’s the key issue: the technologies that Joomla uses, most significantly PHP, are also changing over time. This chart illustrates the problem for Joomla 1.5:

PHP VersionRuns Joomla 1.5?Status
5.2YesUnsupported, past end-of-life, no updates.
5.3YesEnd-of-Life cycle started March 2013, critical updates only.
5.4NOSupported.
5.5NOSupported (available as of June 2013).


To put it clearly: there is no currently supported version of PHP that will run Joomla 1.5! While that shouldn’t be panic-inducing, it is not something to be ignored. There’s a lot of code out there (not just Joomla) that will have problems running under PHP 5.4, and lots of hosting companies will continue to support it, including Abivia. But – and this is a big one – sooner or later your host is going to send out a notice saying that they’re moving to PHP 5.4 or 5.5. Depending on the host, you’re likely to get anywhere between a week to 90 days notice. Even at 90 days, that’s a pretty tight timeline for a mid-range site, particularly if you want to throw in a redesign at the same time.

This problem is made particularly challenging by the fact that the PHP folks chose to stick with the same major version number, even though they made some major changes to the language. There are some hosts who are just now retiring PHP 4. This was made possible because hosts could run PHP 4 in parallel with PHP 5. By not making recent versions PHP 6 and PHP 7, this mechanism is no longer available. If a host wants to support 5.4, they have to drop support for 5.3 at the same time.

So put your finger on the calendar a month from today, whatever day you happen to be reading this. Imagine that at the same moment you’re doing that, you get a notice from your host saying “PHP 5.3 will not longer supported after…” and substitute the date under your finger. If that makes you uncomfortable, then it’s time to start planning your upgrade!

Mastodon