Modernising ZF1 applications: shared PHP configuration and error handling

by Mateusz Tymek — on PHP, Zend Framework

While the previous article opens up many refactoring strategies, it is still important to work out the details. This time, I will cover sharing application settings (including PHP options which ZF1 apps tend to set in the runtime) and sharing error handling.

This article is a part of a mini-series "Modernising ZF1 application with Zend Expressive".

  1. Introduction
  2. Wrapping Zend Framework 1 application in the middleware
  3. Sharing services between legacy and modern application
  4. Sharing configuration and error handling (this article)

PHP configuration in ZF1

It is a common practice for ZF1 applications to set the interpreter settings in the runtime, from application.ini file:

phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0
phpSettings.session.cookie_name = SID

If you want to mix the code from ZF1 and Expressive app, then you need to make sure they will share the same PHP settings - otherwise, you may get inconsistent or unpredictable behavior.

Let's start by exposing ZF1 configuration in the Expressive app. We can do it by creating a custom config provider, which will read INI config and return it under zf1_config key:

<?php

declare(strict_types=1);

namespace App\Integration;

use Zend_Config_Ini;

class Zf1ConfigProvider
{
    /** @var string */
    private $iniConfigFile;

    /** @var string */
    private $environment;

    public function __construct(string $iniConfigFile, ?string $environment = null)
    {
        $this->iniConfigFile = $iniConfigFile;
        if (null === $environment) {
            $environment = 'production';
        }
        $this->environment = $environment;
    }

    public function __invoke(): array
    {
        $config = new Zend_Config_Ini($this->iniConfigFile, $this->environment);
        return ['zf1_config' => $config->toArray()];
    }
}

You can immidiately use it in application/config.php file:

$aggregator = new ConfigAggregator([
    new \App\Integration\Zf1ConfigProvider(realpath(__DIR__) . '/../application/configs/application.ini'),
    ...
]);

This comes with another benefit: if your application.ini file configures the database or any other service, you can now pull this data in factories powering your Service Container. No need to duplicate configuration anymore.

Moving to back to PHP configuration, let's create a helper that will recursively set engine options:

<?php

namespace App\Integration;

class PhpSettingsManager
{
    public static function setPhpSettings(array $settings, $prefix = '')
    {
        foreach ($settings as $key => $value) {
            $key = empty($prefix) ? $key : $prefix . $key;
            if (is_scalar($value)) {
                ini_set($key, $value);
            } elseif (is_array($value)) {
                static::setPhpSettings($value, $key . '.');
            }
        }
    }
}

This is extracted from Zend_Application code - we can feed it directly with the original configuration.

Finally, let's connect all pieces together by creating a delegator factory for Zend\Expressive\Application class:

<?php

declare(strict_types=1);

namespace App\Integration;

use Interop\Container\ContainerInterface;
use Zend\Expressive\Application;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;

class ExpressiveApplicationDelegatorFactory implements DelegatorFactoryInterface
{
    public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null): Application
    {
        $zf1Config = $container->get('config')['zf1_config'];
        if (isset($zf1Config['phpSettings'])) {
            PhpSettingsManager::setPhpSettings($zf1Config['phpSettings']);
        }
        return $callback();
    }
}

Configure it in App`s config provider:

public function getDependencies() : array
{
    return [
        'factories'  => [
            // ...
        ],
        'delegators' => [
            Application::class => [
                ExpressiveApplicationDelegatorFactory::class
            ],
        ],
    ];
}

It took a couple of steps to finish (which may slightly differ in your case), but with this integration you can be quite comfortable with any further refactoring.

Shared error handling

Another duplication that we would like to take out before it becomes a technical debt is error handling. In the case of server error, your application should show a nice message and log it. It does not make sense to have the same logic coded twice - let's move this responsibility to the modern part.

By default, ZF1 uses ErrorController as a broker that catches exceptions and marshalls appropriate HTTP response. The logic will look like this:

switch ($errors->type) {
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
        // 404 error -- controller or action not found
        $this->getResponse()->setHttpResponseCode(404);
        $priority = Zend_Log::NOTICE;
        $this->view->message = 'Page not found';
        break;
    default:
        // application error
        $this->getResponse()->setHttpResponseCode(500);
        $priority = Zend_Log::CRIT;
        $this->view->message = 'Application error';
        break;
}

This is a perfect location to hook into. Let's make sure all exceptions (and Errors!) are propagated up to Expressive's ErrorHandler by simply rethrowing them:

 switch ($errors->type) {
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
    case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
        // 404 error -- controller or action not found
        $this->getResponse()->setHttpResponseCode(404);
        $priority = Zend_Log::NOTICE;
        $this->view->message = 'Page not found';
        break;
    default:
        // application error - pass back to Zend Expressive handler
        if (isset($errors->exception) && $errors->exception instanceof Throwable) {
            throw $errors->exception;
        }

        $this->getResponse()->setHttpResponseCode(500);
        $priority = Zend_Log::CRIT;
        $this->view->message = 'Application error';
        break;
}

This will be a good start. Next steps will be depending heavily on your ZF1 application. You may need to reconfigure logging and handling 404s. Many ZF1 apps I've seen tend to forward to ErrorController instead of relying on exceptions - you may need to address this separately.

Conclusion

We reached the end of this mini-series. At this moment your application is open for modernisation. The next steps will be likely very different depending on what your application does and how it is built.

Good luck with refactoring!

You can check out the working example on Github, by cloning this repository and checking out part4-settings-and-error-handling branch.


comments powered by Disqus