I just released Di v0.1.0, a new dependency injection shard.
Di is macro first and type safe. It supports auto wire from constructor types, named services, transient services, fiber local scopes, health checks, and graceful shutdown.
require "di"
Di.provide { Database.new(ENV["DATABASE_URL"]) }
Di.provide UserRepo
Di.provide UserService
svc = Di.invoke(UserService)
# or
svc = Di[UserService]
Di.scope(:request) do
Di.provide { CurrentUser.from_token(token) }
current = Di.invoke(CurrentUser)
# or
current = Di[CurrentUser]
end
Though I’m curious to the thinking behind naming the getter invoke? I think provide is a nice alternative to the common register, but I think the mental model is “getting” something from the container not “invoking” the service (you do that afterwards when calling methods on the returned object). Di[UserService] would work well too, if that’s feasible.
You are right that the user mental model is usually “get from container” rather than “invoke service”.
The invoke name came from two ideas:
It matches naming from samber/do influence.
The container is doing more than a simple read. It may run a factory, resolve dependencies, detect circular chains, and cache a singleton. So semantically it is invoking provider logic, not invoking service methods.
That said, I agree Di[UserService] feels like more idiomatic Crystal, and I like how it reads. I am going to work on implementing it tonight
Coming from C#, I’m used to working against interfaces in the DI system. Now, Crystal doesn’t have interfaces, but I usually do the abstract module trick to get around this.
require "di"
module Printable
abstract def get_print_data() : String
end
class Square
include Printable
def get_print_data() : String
"Square"
end
end
class Circle
include Printable
def get_print_data() : String
"Circle"
end
end
Using this library, it was easy to get this to work when using the block form
Di.provide { Square.new.as(Printable) }
p Di[Printable] # prints #<Square:0x1e3be056fb0>
but I’m wondering if it is possible to get this kind of behavior with the non-block form. ie
Di.provide Printable, Square # Would return Square when asking for Printable
In my implementation I handled this by giving each registered service an ID. E.g. "square", or "my_app_square" if in a MyApp namespace. From here, there is an annotation that can be applied to a service that includes interface module(s) that’ll denote that service as the “default”. Within the autowiring logic, you can look at the constructor argument name/type to deduce which you should use. So by default if you have like @printable : Printable you get the default. If you have like @circle : Printable you get Circle since the parameter name would match its service ID. In these cases I found it’s also good to not use the @circle syntax and assign it like a normal variable in the constructor. This way you can change the service that gets injected w/o having to change all the internal usages of the ivar.
Even though it’s very nice to have a more flexible DI registration, in this case I’m quite happy to just use the type as a key, I merely want to be able to use another (module/interface) type as the key instead of the implementing class.
It’s possible to access the modules a type includes, so could possibly implicitly add them as aliases behind the scenes. But that might have some side-effects vs being explicit .
It’s almost as if we’re taking about Symfonys contra Laravels container implementation in PHP.
Symfonys container started out, AFAICT, with named services (though classed services are showing up now), while Laravels used classes (we’ll skip over the fact that named classes is basically just a string, not important here).
In Symfony land they’ve also been fan of using attributes on parameters to tell the container what to inject. Laravel on the other hand has a concept of telling the container that when This class needs That interface, it should use ThatOtherImplementation.
I find the latter concept much more compelling. It’s not the concern of any random controller class which Logger implementation it should use. That’s the job of the container, which knows that in general it should be the FileLogger, but that class over there needs to get a NullLogger instead.
Yea for sure, it’s a pretty slick feature being able to define a default implementation of an interface, but still provide a way to use others. Symfony handles this via AsAlias, and does something similar with constructor parameter names as i mentioned, or can use AutoWire attribute as you mentioned, to get a specific implementation and not just the default.
Is interesting there being two new DI shards fairly recently after years of not many others :S. But always interesting to see what other people do.
I’m not sure if that’s quite the same. I’m sure you can make the Symfony container use a default service and then pass another implementation for some specific service. You can always use a factory. Or perhaps arguments will do. But then we’re back at explicitly specifying constructor parameters, which is a tighter coupling than “when this needs a logger, it should get this implementation”.
I’ll spare you my “what’s wrong with annotation based wiring” rant unless someone asks for it.
Maybe they needed to grow unhappy with the existing offerings and figure out how to (try) and do better? It’s one thing to port an existing concept to a new language, it’s something else to make it feel like it was born there.
Then I’m confused. Isn’t the idea that you have multiple implementations of LoggerInterface, but SysLogger is the “default” one. You then have like many services injecting LoggerInterface and it just knows to provide it SysLogger without any other/extra configuration? Or? Because that’s what i was trying to convey.
If they did I haven’t heard any complains/feedback otherwise could have worked to improve things . It’s a pretty awesome implementation these days. Having new use cases to solve for is how things improve. Happy to chat more about that if you’d like .
For the immediate use case, I think the most Crystal feeling path is explicit interface/module aliasing in non-block form, e.g.:
Di.provide Printable, Square
With a compile-time check that Square actually includes Printable.
I like the implicit-alias idea in theory (auto register under included abstract modules), but I’m leaning against making that the default because of ambiguity/side effects once multiple implementations exist.
On the Symfony point: I think AsAlias maps closer to global alias/default wiring, while “when this specific service needs interface X, use Y” is contextual binding. Those feel like two separate concepts worth modeling separately.
So current direction I’m considering:
explicit global alias/default (Printable -> Square).
optional contextual overrides as a separate API (something like Di.bind SomeService, Printable, Circle), instead of overloading provide too much.
The ADI shard is fantastic I peaked at it for some inspiration, but decided to go a different route eventually. I had not heard about the other new shard until you mentioned it.
I am building di as part of a family of shards:
di for Dependency Injection
re for ReactiveX paradigms
mo for Monads
These are meant to be the core building blocks for a TUI framework on top of Termisu.
I also want control over the implementations so I can shape them to the framework’s needs.
Keeping everything minimal and staying within stdlib constraints is a top priority for me.
Yeah, but with the added wrinkle that I can say “but when that class asks for a LoggerInterface it shouldn’t get a SysLogger, but this FileLogger instead”.
Which the Symfony container can do, of course, but when I see it happen, I usually see them using an annotation on the constructor parameter of the class asking for a LoggerInterface. But then it’s the class deciding what logger to use and not the container.
Well, every piece of code can’t be everything for everybody, so rather than complain it’s more productive to see if one can come up with something more to one’s taste. PHP has at least three widely used DI implementations. It’s also nice with different options for the rest of us. I’ve always been a fan of “there’s more than one way to do it”, I don’t think I’d have formed as many opinions about what I like in a container if I hadn’t been exposed to a selection.
You mean instead of provide?
nvm I’m stupid lol! resolve does sound nice! I wanna avoid having many aliases so probably will drop invoke in favor of resolve and keep [] as well