Introducing: DecodeLabs Archetype

Introducing: DecodeLabs Archetype

Simple but powerful class and file resolver

Archetype aims to solve a common but surprisingly complex problem when building libraries and frameworks: resolving classes from simple name strings.

A fundamental aspect of object oriented programming revolves around the concept of implementing interfaces: an interface defines a contract and potentially unlimited numbers of implementations abide by those rules.

Lets take translations as a very simple example:

// Interface file
namespace Interactions;

interface Welcomer {
    public function sayHello(): string;
}
// Implementations
namespace Interactions\Welcomer;

use Interactions\Welcomer;

class En implements Welcomer {
    public function sayHello(): string {
        return 'Hello';
    }
}

class Fr implements Welcomer {
    public function sayHello(): string {
        return 'Bonjour';
    }
}
// Consumer code
$language = 'en'; // get from locale
$class = 'Interactions\\Welcomer\\'.ucfirst($language);
$welcomer = new $class();

echo $welcomer->sayHello();

Here we have a contract requiring implementations to say hello in their respective language - the class is resolved by taking the language string from the environment and resolving it to an existing implementation class, in this case `Interactions\Welcomer\En'.

This is fine in this simple example, however there's no error handling (it will fail if the locale language is German) and no extensibility - all implementation classes must reside in the Interactions\Welcomer\* namespace to be resolvable.

Resolvers

Archetype provides the concept of a Resolver - it has one job: take a string name and interface, and return a class name that should implement that interface.

By default, the package will always look for the provided name within the resolved namespace of the interface:

use Interactions\Welcomer;
use DecodeLabs\Archetype;

// Looks for 'En' in Interactions\Welcomer\*
$class = Archetype::resolve(Welcomer::class, 'En');

If the class is not found or doesn't implement the interface, an exception is thrown.

Archetype will also check to see if the passed string is a class in its own right -

use Interactions\Welcomer;
use Interactions\Welcomer\En;
use DecodeLabs\Archetype;

$class = Archetype::resolve(Welcomer::class, En::class);

This can come in very useful in code that defines configuration such as your bootstrap or kernel.

No extra work is needed for this basic functionality, however creating your own Resolver classes affords you a little more flexibility:

namespace Utils;

use Interactions\Welcomer;
use DecodeLabs\Archetype;
use DecodeLabs\Archetype\Resolver;

class LanguageResolver implements Resolver 
{
    public function getInterface(): string
    {
        return Welcomer::class;
    }

    public function getPriority(): int
    {
        return 10;
    }

    public function resolve(string $locale): ?string
    {
        $language = explode('_', $locale, 2)[0];
        return Welcomer::class.'\\'.ucfirst($language);   
    }
}

Archetype::register(new LanguageResolver());

Notice here that the resolve method can accept a full Locale string and split the language from it.

use Interactions\Welcomer;
use DecodeLabs\Archetype;
use Locale;

// Resolves 'En' from en_GB
$class = Archetype::resolve(Welcomer::class, Locale::getDefault());

Also notice that this new resolver defines a priority - you can register as many resolvers as you like, called in order of priority where the first to provide a class name that exists and implements the interface wins.

In effect, this allows any level of code to define its own entry point into your library architecture and provide plugin functionality without any extra boilerplate at all. A different library could provide alternative language translations with its own Resolver in its own namespace, leaving all of the existing code untouched.

A number of DecodeLabs libraries make use of this architecture to allow for flexible class loading at runtime - Metamorph being a great example where various string transformation libraries (Idiom and Chirp for example) provide their own implementations in their own packages without compromising code structure.

File finder

A useful extension to the Resolver is to implement the DecodeLabs\Archetype\Finder interface:

use DecodeLabs\Archetype\Finder;

class ThingArchetype implements Finder
{

    public function getInterface(): string
    {
        return Thing::class;
    }

    public function getPriority(): int
    {
        return 10;
    }

    public function resolve(string $name): ?string
    {
        return 'Some\\Other\\Namespace\\'.$name;
    }

    public function findFile(string $name): ?string
    {
        return './some/other/namespace/'.$name.'.jpg';   
    }
}

The findFile() method can then resolve string names in the same fashion as class resolution, but with a view to locate arbitrary files within that namespace.

use DecodeLabs\Archetype;
use My\Library\Thing;

$boxImagePath = Archetype::findFile(Thing::class, 'box');

How a file name should be resolved is entirely down to the implementation of the Finder (Archetype doesn't come with any implementations of Finder out of the box), however it allows for consuming libraries to define their own asset collections alongside classes natively with Archetype, with all of the same flexibility and extensibility present in the core architecture.

Roadmap

As of the time of writing, Archetype is at pre-release v0.2.5 - it is relatively stable but has not yet reached a full v1.0 public release.

However the library is essentially feature complete and any remaining work needed for a v1.0 release is essentially completing test coverage, documentation, etc, and should be ready for prime time in the near future.

Any suggestions for how to improve Archetype? Leave a comment below!