# Loading ZF1 Classes on PHP 8.4

Published 2026-05-27. Updated 2026-09-17.
Canonical URL https://log.delaa.dev/blog/2026-05-27-loading-zf1-classes-php84-shim

> Zend Framework 1 was designed for PHP 5.2. Running it on PHP 8.4 requires a shim library, an autoloader bridge between PSR-0 and PSR-4, and module autoloaders for each legacy module. Here is the infrastructure that made ZF1 work on modern PHP.

---

<Image
  src="/images/loading-zf1-classes-php84-shim/hero.png"
  alt="A legacy nested folder tree of PHP class files being processed by a gear icon into standardized indexed files"
  width={2000}
  height={1125}
  className="rounded-lg"
/>

Before you can run ZF1 inside Symfony 8, you need ZF1 to actually
run on PHP 8.4. This was not trivial. Zend Framework 1 was written
for PHP 5.2, released in 2006. Twenty years of PHP evolution separate
the framework from the runtime we needed it to work on.

Three things made it possible: a community shim, a PSR-0 autoloader,
and module autoloaders.

## The shim library (shardj/zf1-future)

The `shardj/zf1-future` package (v1.24.4 at the time of writing) is a
community-maintained fork of Zend Framework 1 that backports PHP 8
compatibility fixes. It replaces the old `zendframework/zendframework1`
package which stopped receiving updates in 2016.

The critical fixes in v1.24.4:

- **Dynamic property errors in Zend_Session.** PHP 8.4 deprecates
  dynamic properties on standard classes. Zend_Session relies heavily on
  dynamic property access for session namespaces. The shim declares
  these properties explicitly.
- **Zend_Pdf_Element** type handling regression.
- **fgetcsv escape parameter.** PHP 8.4 deprecated the default escape
  parameter behavior in `fgetcsv`; the shim passes the parameter
  explicitly.

Without this package, every ZF1 page would crash on PHP 8.4 with a
`Fatal Error: Dynamic property` message. It is the unsung hero of the
entire migration.

## The autoloader bridge (PSR-0 meets PSR-4)

ZF1 uses PSR-0 autoloading. A class called `Zend_Controller_Action`
lives in `Zend/Controller/Action.php`. Symfony uses PSR-4 autoloading
with Composer's generated classmap.

For the two frameworks to coexist, we needed both autoloading strategies
active simultaneously. The `LegacyAutoloader` service handled this:

```php
class LegacyAutoloader
{
    private static bool $initialized = false;

    public static function register(): void
    {
        if (self::$initialized) {
            return;
        }

        // 1. Set include paths for ZF1 library + shim + module libs
        set_include_path(implode(PATH_SEPARATOR, [
            realpath(APPLICATION_PATH . '/../library'),
            realpath(APPLICATION_PATH . '/../vendor/shardj/zf1-future/library'),
            realpath(APPLICATION_PATH . '/modules/License/lib'),
            realpath(APPLICATION_PATH . '/modules/Login/lib'),
        ]));

        // 2. Register Laminas StandardAutoloader (PSR-0 for ZF1 classes)
        $loader = new StandardAutoloader(['autoregister_laminas' => true]);
        $loader->register();

        // 3. Register module autoloaders for each ZF1 module
        foreach (['HR', 'Payroll', 'Base', 'License', 'Login', 'System', 'Default'] as $module) {
            $namespace = ($module === 'Default') ? '' : $module;
            new Zend_Application_Module_Autoloader([
                'namespace' => $namespace,
                'basePath' => APPLICATION_PATH . '/modules/' . $module,
            ]);
        }

        self::$initialized = true;
    }
}
```

Step 1 sets the PHP include path so that `require_once 'Zend/Application.php'`
resolves correctly. Steps 2 and 3 register two autoloading mechanisms.

**StandardAutoloader** (from Laminas, the ZF1 continuation under the
Linux Foundation) handles ZF1 core classes: `Zend_Controller_*`,
`Zend_Db_*`, `Zend_Auth_*`, `Zend_Session_*`, and the `ZendX_*`
extensions.

**Zend_Application_Module_Autoloader** handles module-specific classes.
Each ZF1 module (HR, Payroll, Base, etc.) has its own set of controllers,
models, forms, and views. The module autoloader maps the `HR_` namespace
prefix to `application/modules/HR/` directory. For example, a class
called `HR_Controller_EmployeeController` would be autoloaded from
`application/modules/HR/controllers/EmployeeController.php`.

## The initialization sequence

The autoloader was registered in `public/index.php` **before** the
Symfony kernel booted:

```php
// public/index.php
require_once APPLICATION_PATH . '/../vendor/autoload.php';

// Register ZF1 autoloading before Symfony boots
App\Domain\System\Service\LegacyAutoloader::register();

// Now Symfony can boot — its own autoloader coexists with ZF1's
$kernel = new Kernel($env, $env !== 'prod');
$response = $kernel->handle($request);
```

The order matters. If Symfony booted first and registered its PSR-4
autoloader, ZF1 classes would still be loadable (Composer's autoloader
can coexist with other autoloaders). But module autoloaders needed the
ZF1 infrastructure ready. The Laminas autoloader had to be registered
before any ZF1 class was instantiated.

## Why not Composer's autoloader for ZF1?

Composer can handle PSR-0 autoloading if you configure it in
`composer.json` with the `"psr-0"` key. We tried this. The problem was
that ZF1's module autoloaders do more than path mapping. They also
initialize module-specific configuration, translation resources, and
helper paths. Composer's autoloader only handles class resolution.
The module autoloaders were required for ZF1 modules to function
correctly.

---

The autoloader bridge is 46 lines of PHP. Without it, not a single ZF1
page renders. Every controller, every model, every module depends on
it being registered first.

---

— Delaa
