The Best Container

Last modified 2023/10/05 11:15

There are few things that I really like about Phpactor, but its DI Container [link] is one of them.

In seven years it has hardly changed at all. It’s modular, supports tags, parameters (with schemas) and the has only 233 lines of code including comments and whitespace[1]

There is no YAML, XML or compilation. No auto wiring, no property injection, factory modifiers, weird ways to extend services. All services are singletons. Singletons. And if you want a factory, well, you make a factory. That’s OOP right? Phpactor does not allow you to have $container->get('current_user') or $container->get('current_request') .

It’s PHP! No fancy PHPStorm extensions required to jump to or rename your classes, and since it has a conditional return type your static analysis tool automatically understands that $foo = $container->get(Foo::class) provides a Foo instance.

Want to know how your object is instantiated? There’s no magic. No mystery. Want to do some weird shit because why not? Go for it! It’s PHP code. You don’t need compiler passes to do weird shit here.

[1] ok that excludes the schema thing in another package.

SIMPLE

The Phpactor Container is simple:

$container->register(MyService::class, function (Container $container) {
    return new MyService($container->get(DependencyOne::class));
});

You need parameters?

It’s got you:

$container->register(MyService::class, function (Container $container) {
    return new MyService($container->getParameter('foo.bar'));
});

But wait Dan, are you saying I just pass in an unstructured array to the container? Where’s the safety and power?

Well, I’m not showing you the whole thing here:

class MyExtension implements Extension {
    public function configure(Resolver $resolver): void {
        $resolver->setDefault([
            'foo.bar' => 'Hello World',
        ]);
    }

    public function build(ContainerBuilder $container): void {
        $container->register(Foo::class, function (Container $container) {
            return new MyService($container->getParameter('foo.bar'));
        });
    }
}

Wait, that looks alot like the Symfony Options Resolver - why didn’t you use that?

I didn’t want to have a Symfony dependency because

  • I can’t modify it.
  • I don’t want to bump the dependencies every year even if they don’t change.

Was that a good choice?

Probably not. The Symfony one works better but if I did it again I’d probably not use that pattern at all.

What about tags?

You need to aggregate services by tag? Why not:

class MyExtension implements Extension {
    public const TAG_MY_SERVICE = 'service';

    public function configure(Resolver $resolver): void {
        $resolver->setDefault([
            'foo.bar' => 'Hello World',
        ]);
    }

    public function build(ContainerBuilder $container): void {
        $container->register(Foo::class, function (Container $container) {
            return new MyService(array_map(
                fn (string $serviceId) => $container->get($serviceId),
                array_keys($container->getServiceIdsForTag(self::TAG_MY_SERVICE))
            ));
        });

        $container->register('my_tagged_service', fn () => new MyTaggedService(), [
            self::TAG_MY_SERVICE => [],
        ]);
    }
}

That tag concept looks alot like Symfony

Yes it does doesn’t it.

Environment variables?

Sure!! Use getenv however you like.

Is it web scale?

That’s a weird question, it’s a container. It doesn’t even need to be used on the web.

But yes, it certainly is web scale! I didn’t show you how the container is created yet:

$container = new PhpactorContainer([
    LangaugeServerExtension::class,
    CompletionExtension::class,
    PathFinderExtension::class,
], [
   'the' => 'configuration',
   'you' => 'loaded',
   'from' => 'the',
   'user' => '_',
]);

You can easily structure your application. No bundles!

Wait! This isn’t simple at all! look at all the code you need to write!

Well, you do need to write an extension and create the container and bootstrap it. But hey, you only need to do this once. It’s totally worth it.

Limitations

It does have some limitations though - it registers at runtime - which, if you have eager services, could add overhead that may make it unsuitable for large projects that have the short-lived request lifecycle that PHP is famous for (although it would be good to get some actual benchmarks on that). But i’s great for small projects or long running processes like a language server.

Cache?

No cache! That’s right, no compilation no cache. You can deploy this in a lambda or on a read-only filesystem.

Should I use this great container?

Probably not, but I do sometimes use it at work. I’ve tried to use other light-weight containers like Pimple and League but often I just don’t need the extra, complicating, features they provide (and nobody really wants use array sytax to define services right?? right?).

For example with League:

$container->add(Acme\Foo::class)->addArgument(Acme\Bar::class);
$container->add(Acme\Bar::class);

$foo = $container->get(Acme\Foo::class);

So far so good. But (don’t ask me why) what if I wanted to delegate to another container? Use environment variables? Do weird shit? Maybe it’s possible, but is it simple? The answer is no. No it’s not.

The previous example can be written like this with the Phpactor container:

$container->register(
    Acme\Foo::class, 
    fn(Container $c) => new Acme\Foo($c->get(Acme\Bar::class))
);

That’s the definition I want. I can do anything I like there - by default - because there is ONLY ONE WAY TO CREATE A SERVICE. I don’t need to spend hour becoming an expert and writing enterprise code to fulfil a simple requirement:

$container->register(
    Acme\Foo::class, 
    fn(Container $c) => new Acme\Foo(\Drupal::service('acme_bar'))
);

What?

But Dan that League library is awesome and it compiles the container so it’s really fast

Yeah that’s true… or - wait, oh no, it doesn’t compile. It’s much slower than Phpactor Container then because it does more stuff. But who cares about microseconds anyway.

But is it PSR safe?

Yes! It’s 100% safe for PSR and Agile. It implements the PSR-11 interface:

Summary

The Phpactor Container is really simple and that is it’s greatest strength.

Can you get simpler? Sure, it’s called a factory:

class MyFactory {
    public function myService(): MyService {
        return new MyService($this->dependencyOne());
    }

    private function dependencyOne(): DependencyOne {
        return new DependencyOne();
    }
}

Sometimes this is all you need. Use this. But at other times you need shared services and to scale your application and this method begins to get cumbersome.

This isn’t a promotion of the Phpactor container, more that everything else is far more complicated than I like. I really enjoy working on Phpactor because I spend my time being frustrated by Phpactor 🥳 and not its DI container 😥