Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loading multiple versions of the same class

Let's say I release a code library as a standalone PHP class. Someone then uses version 1.0 of that library in their application. Later, I release version 2.0 of the library and that same someone, for any reason, needs to use both 1.0 and 2.0 side-by-side in their application because either he or I broke backwards compatibility with the new release.

If the class names are different, it's easy enough to include and instantiate both because there's no naming conflict. But if the class names are kept the same, we run into problems:

include /lib/api-1.0/library.php;
$oldlibary = new Library();

include /lib/api-2.0/library.php;
$newlibrary = new Library();

This just won't work because we can't load two classes both with the name Library. One alternative another developer suggested was to use namespaces. The following should work:

namespace old {
    include /lib/api-1.0/library.php;
}
namespace new {
    include /lib/api-2.0/library.php;
}

$oldlibary = new old\Library();
$newlibrary = new new\Library();

Unfortunately, this isn't very scalable. It would work with a 2-instance situation (which, hopefully, I wouldn't have to use in the first place), but to scale it to 3, 4, 5, or more instances you'd need to have additional namespaces defined and set up, If you're not using those namespaces in the first place, that's a bunch of unnecessary code.

So is there a way to dynamically create a namespace, include a file, and instantiate the class contained within that file in a uniquely-named variable?


Let me add some more clarification ...

I'm building a set of libraries to be used by other developers who build plugins/modules for a couple of CMS platforms. Ideally, everyone would always use the latest version of my library, but I can't guarantee that and I can't guarantee the end user will always upgrade their modules when new versions become available.

The use case I'm trying to work with is one where the end user installs two different modules by two different developers: call them Apple and Orange. Both modules are using version 1.0 of my library, which is great. We can instantiate it once and both sets of code can benefit from the functionality.

Later, I release a minor patch to this library. It's versioned 1.1 because it doesn't break backwards compatibility with the 1.x branch. The developer of Apple immediately updates his local version and pushes a new edition of his system. The developer of Orange is on vacation and doesn't bother.

When the end user updates Apple she gets the latest maintenance release of my library. Because it's a maintenance release, it's assumed to be safe to completely replace version 1.0. So the code only instantiates 1.1 and Orange benefits from a maintenance patch even though the developer never bothered to update their release.

Even later, I decide to update my API to add some hooks to Facebook for some reason. The new features and API extensions change a lot about the library, so I up the version to 2.0 to flag it as potentially not backwards-compatible in all situations. Once again, Apple goes in and updates his code. Nothing broke, he just replaced my library in his /lib folder with the latest version. Orange decided to go back to school to become a clown and has stopped maintaining his module, though, so it doesn't get any updates.

When the end user updates Apple with the new release, she automatically gets version 2.0 of my library. But Orange had code in his system that added Facebook hooks already, so there would be a conflict if 2.0 was rolled in to his library by default. So instead of replacing it entirely, I instantiate 2.0 once for Apple and, side-by-side, instantiate the 1.0 version that shipped with Orange so it can use the right code.

The entire point of this project is to allow third party developers to build systems based on my code without depending on them to be reliable and update their code when they're supposed to. Nothing should break for the end user, and updating my library when used inside someone else's system should be a simple file replacement, not going through and changing all of the class references.

like image 512
EAMann Avatar asked Apr 26 '11 15:04

EAMann


2 Answers

I decided on a slightly alternate route. The namespace method works, but you need a different namespace for each version of the class. So it's not really scalable, because you have to pre-define the number of available namespaces.

Instead, I've settled on a specific naming schema for the classes and a version loader/instantiater.

Each class will take the following format:

<?php
if( ! class_exists( 'My_Library' ) ) { class My_Library { } }

if( ! class_exists( 'My_Library_1_0' ) ) :
class My_Library_1_0 extends My_Library {
    ... class stuff ...
}
endif;

The parent My_Library class will actually end up containing a few identifiers specific to the library - purpose, compatibility statements, etc. That way I can perform other logical checks to make sure the right My_Library exists before moving forward and claiming that My_Library_1_0 is really version 1.0 of the library I want.

Next, I have a loader class that I'll be using in my main project:

<?php
class Loader {
    static function load( $file, $class, $version ) {
        include( $file );
        $versionparts = explode('.', $version);
        foreach($versionparts as $part) $class .= '_' . $part;
        return new $class();
    }
}

Once this is done, you can use Loader to load both instances of the class or simple references if you want to use static methods:

$reference = Loader::load( 'library.php', 'My_Library', '1.0' );

$loader = new Loader();
$instance = $loader->load( 'library.php', 'My_Library', '1.0' );

Not quite the same as the namespace version I was shooting for, but it works and alleviates my concerns about breaking things for the end user. I am assuming that two different versions of My_Library_1_0 would be the same, though ... so there's still a dependence on third party developers knowing what they're doing.

like image 126
EAMann Avatar answered Oct 16 '22 23:10

EAMann


So is there a way to dynamically create a namespace, include a file, and instantiate the class contained within that file in a uniquely-named variable?

Yes, such method exists. You can do anything you want with eval and stream handlers. But it is bad practice and wrong approach - you can try to use factory method (the code is not tested - it only shows example):

<?php

if (!class_exists('Library')) {

    class Library
    {
        public static function create($version)
        {
            if (class_exists($c = 'Library' . $version))
                return new $c();
            return null;
        }
    }

}

class Library1
{

}

class Library2
{

}

...
like image 40
Alex Netkachov Avatar answered Oct 17 '22 00:10

Alex Netkachov