Services and providers
This chapter turns the post store into a service the whole App shares. You will learn how to:
- Create a service provider with
act make:provider - Bind the store in the App's container as a singleton
- Resolve the store when the routes register
- Tell what happens when a binding is missing
Overview
Routes creates the store today, so only the handlers can reach it. The next chapter adds a Command that lists the posts, and the chapter after it replaces the store in tests: both need the store without going through the routes.
Every App has a container, from the di package, that knows how to build each of the App's services and how long each one lives. A service provider is where a service joins the App: its Register hook binds the service in the container, and its other hooks, all optional, run at the App's boot, start, ready and shutdown. Code anywhere in the App then asks the container for the service by its type, and gets the same one.
Create the provider
act make:provider writes a provider into app/providers and adds it to the App:
act make:provider postsCREATE app/providers/posts_provider.go
UPDATE start/app.go
+ app.Provide(&providers.PostsProvider{})The generator added two lines to start/app.go: the import, and app.Provide, which hands the provider to the App before it boots:
// Package start assembles the App: its configuration, services,
// middleware and routes. main runs the App it builds, and tests build it
// the same way.
package start
import (
"context"
"github.com/mucolabs/tinka"
"github.com/mucolabs/tinka/encryption"
"github.com/mucolabs/tinka/log"
"blog/app/exceptions"
"blog/app/providers"
"blog/config"
)
// App builds the App from its configuration.
func App(cfg config.Config) (*tinka.App, error) {
enc, err := encryption.New(cfg.App.Key)
if err != nil {
return nil, err
}
cfg.HTTP.Logger = log.New(cfg.Logger)
cfg.HTTP.Encrypter = enc
app := tinka.New(cfg.HTTP)
app.Provide(&providers.PostsProvider{})
app.OnError(exceptions.Handler)
Kernel(app)
app.OnStart(func(context.Context) error { return Routes(app) })
return app, nil
}Bind the store
The generated provider has three empty hooks: Register, Boot and Shutdown. The store needs only Register, since memory needs no check at boot and nothing to release at shutdown.
Register binds *posts.Store with Singleton: the factory you pass runs the first time something asks the container for a *posts.Store, and every later request gets that same store. The factory's return type is the binding's key, so the compiler checks the wiring, and there's no name to misspell. Replace the file's contents:
package providers
import (
"github.com/mucolabs/tinka/di"
"blog/app/posts"
)
// PostsProvider adds the posts store to the App.
type PostsProvider struct{}
// Register binds the store as a singleton: the App builds one, the first
// time something asks for it, and every handler and Command shares it.
func (p *PostsProvider) Register(c *di.Container) {
c.Singleton(func(*di.Resolver) (*posts.Store, error) {
return posts.NewStore(), nil
})
}When a database replaces the store, this provider is where it grows: Register binds the connection pool, and a Boot hook checks the database answers, so a misconfigured App stops at boot instead of failing its first request.
Resolve the store in the routes
Routes runs while the App starts, after every provider registered its bindings, so it can ask the container for the store. app.Container() returns the App's container, and Make[*posts.Store]() returns the store, building it on this first call, or an error when nothing provides it. Routes returns that error, which stops the App:
postHandlers := handlers.Posts{Store: posts.NewStore()}
store, err := app.Container().Make[*posts.Store]()
if err != nil {
return err
}
postHandlers := handlers.Posts{Store: store}
app.Get("/posts", postHandlers.List)The handlers resolve nothing per request: they hold the store they were given, so a request pays nothing for the container.
Check the App
Save the files. The App behaves as before, now with its store from the container. Create a post, then list them:
curl -X POST localhost:3000/posts -d 'title=Services+and+providers&body=The+store+moves+into+the+container.'
curl localhost:3000/posts{"id":3,"title":"Services and providers","body":"The store moves into the container."}
[{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."},{"id":3,"title":"Services and providers","body":"The store moves into the container."}]act about describes the App, and now lists the provider:
act aboutEnvironment
Mode development
Debug on
Address :3000
Providers *providers.PostsProviderThe output above leaves out the first lines, which name the versions of Tinka and Go and your platform.
Recap
PostsProvider binds the store in the App's container as a singleton, and start/app.go adds the provider with app.Provide. Routes resolves the store with Make when the App starts, and a missing provider stops the App with an error before it serves anything. Next, Commands uses the same store from the terminal.
See also: Service providers, Dependency injection, Application lifecycle, Scaffolding