0

After updating via the Composer i want to initialize the application and send it events

"scripts": {
    "post-update-cmd": [
        "Acme\\Bundle\\DemoBundle\\Composer\\ScriptHandler::notify"

handler

public static function notify(CommandEvent $event)
{
    // init app
    require __DIR__.'/../../../../../../app/autoload.php';
    require __DIR__.'/../../../../../../app/AppKernel.php';
    $kernel = new \AppKernel('dev', true);
    $kernel->boot();

    // send event
    $dispatcher = $kernel->getContainer()->get('event_dispatcher');
    $dispatcher->dispatch('acme.installed', new Event())
}

If run the update through the composer.phar then everything works fine.

But I need to run the update from the application. I add composer to requirements and call bin\composer update.

In this case there is a conflict of autoloader. Composer connects the autoloader from the application, change it, and does not connect it again.

Need to destroy the old and create a new autoloader. I found out that the old autoloader can be accessed via $GLOBALS['loader'].

I came to this decision

public static function notify(CommandEvent $event)
{
    // init loader
    require __DIR__.'/../../../../../../vendor/composer/autoload_real.php';
    $GLOBALS['loader']->unregister();
    $GLOBALS['loader'] = require __DIR__.'/../../../../../../app/autoload.php';
    // init app
    require_once __DIR__.'/../../../../../../app/AppKernel.php';
    // ...

But this option does not work because autoloading via file broadcast Composer in normal require and leads to connection conflict.

For example:

"name": "kriswallsmith/assetic",
"autoload": {
    "files": [ "src/functions.php" ]
},

translate to

require $vendorDir . '/kriswallsmith/assetic/src/functions.php';

throw error

PHP Fatal error:  Cannot redeclare assetic_init() (previously declared in /vendor/kriswallsmith/assetic/src/functions.php:20) in /vendor/kriswallsmith/assetic/src/functions.php on line 26

I create autoloader and duplicate the code of /vendor/composer/autoload_real.php and /app/autoload.php. Recommendation from Seldaek #2474

public static function notify(CommandEvent $event)
{
    if (isset($GLOBALS['loader']) && $GLOBALS['loader'] instanceof ClassLoader) {
        $GLOBALS['loader']->unregister();
    }
    $GLOBALS['loader'] = $this->getClassLoader();

    require_once __DIR__.'/../../../../../../app/AppKernel.php';
    $kernel = new \AppKernel('dev', true);
    $kernel->boot();

    // send event
    $dispatcher = $kernel->getContainer()->get('event_dispatcher');
    $dispatcher->dispatch('acme.installed', new Event())
}

protected function getClassLoader()
{
    $loader = new ClassLoader();
    $vendorDir = __DIR__.'/../../../../../../vendor';
    $baseDir = dirname($vendorDir);

    $map = require $vendorDir . '/composer/autoload_namespaces.php';
    foreach ($map as $namespace => $path) {
        $loader->set($namespace, $path);
    }

    $classMap = require $vendorDir . '/composer/autoload_classmap.php';
    if ($classMap) {
        $loader->addClassMap($classMap);
    }

    $loader->register(true);

    $includeFiles = require $vendorDir . '/composer/autoload_files.php';
    foreach ($includeFiles as $file) {
        require_once $file;
    }

    // intl
    if (!function_exists('intl_get_error_code')) {
        require_once $vendorDir.'/symfony/symfony/src/Symfony/Component/Locale/Resources/stubs/functions.php';
        $loader->add('', $vendorDir.'/symfony/symfony/src/Symfony/Component/Locale/Resources/stubs');
    }

    AnnotationRegistry::registerLoader(array($loader, 'loadClass'));

    return $loader;
}
ghost404
  • 299
  • 2
  • 16
  • What about touching a file on *"doUpdate"* and setting up a CRON which will check if the file is present then run the needed `bin/composer`? – Touki Dec 03 '13 at 10:06
  • I thought in a similar direction. All events save into the cache and after complete the installation send them in a new thread `exec('php app/console acme:send-events');`. But I'm not sure that this option is better – ghost404 Dec 03 '13 at 11:53
  • events stored a package that contains the object composer and i'm not sure that he could correct serialize – ghost404 Dec 03 '13 at 12:22

1 Answers1

0

I think I am a bit scared about what you are about to do: You want to update the components of a running application while these components are actively used. You hope that any update will run perfectly well so that after the update the application will continue to work - especially will continue to be able to do further updates.

I don't think this is a valid assumption! I have been using Composer for a while, and I have seen plenty of reasons why it did not update some parts of my dependencies, which most of the time were due to some network failure. Just think of using something from Github, and then Github is down.

What would happen then? You were probably able to download some parts, unable to download some more, and the autoloader was not updated. So the updated part now requires something new that got also downloaded, but cannot be autoloaded because some component after that failed to download. And this component is essential to repeat the update! You just broke your application, and you cannot fix it.

I can also think about very strange effects happening if the autoloader is partially loading old classes, then gets updated, and after that loads new classes that use new versions of the already loaded old classes that changed incompatible. So the assumption that you can change the components of the application during the runtime of one request seems to be very odd.

There is no way to unload a class in PHP. Once it is declared, it cannot be changed (not taking the "runkit" extension into account).

If you indeed want to get an update of your application, I think it is a better idea to duplicate everything, update the non-active copy of the application, then check if the update was successful, and after that copy it back, i.e. use symlinks to point to the current and next version and switch these.

Sven
  • 69,403
  • 10
  • 107
  • 109
  • You're absolutely right. I had not thought of that already loaded classes can be updated and the changes in current thread will not be available. Thanks for the comment. In this case really is better to cache events and send them in a new thread after the update – ghost404 Dec 04 '13 at 07:32