diff options
Diffstat (limited to 'docs/beep-beep/plugin.md')
-rw-r--r-- | docs/beep-beep/plugin.md | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/docs/beep-beep/plugin.md b/docs/beep-beep/plugin.md new file mode 100644 index 00000000..7d5adf69 --- /dev/null +++ b/docs/beep-beep/plugin.md @@ -0,0 +1,246 @@ +# Writing Plugins + +RoadRunner uses Endure container to manage dependencies. This approach is similar to the PHP Container implementation +with automatic method injection. You can create your own plugins, event listeners, middlewares, etc. + +To define your plugin, create a struct with public `Init` method with error return value (you can use `spiral/errors` as +the `error` package): + +```golang +package custom + +const PluginName = "custom" + +type Plugin struct{} + +func (s *Plugin) Init() error { + return nil +} +``` + +You can register your plugin by creating a custom version of `main.go` file and [building it](/beep-beep/build.md). + +### Dependencies + +You can access other RoadRunner plugins by requesting dependencies in your `Init` method: + +```golang +package custom + +import ( + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/rpc" +) + +type Service struct {} + +func (s *Service) Init(r *rpc.Plugin, rr *http.Plugin) error { + return nil +} +``` + +> Make sure to request dependency as pointer. + +### Configuration + +In most of the cases, your services would require a set of configuration values. RoadRunner can automatically populate +and validate your configuration structure using `config` plugin (via an interface): + +Config sample: + +```yaml +custom: + address: tcp://localhost:8888 +``` + +Plugin: + +```golang +package custom + +import ( + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/rpc" + + "github.com/spiral/errors" +) + +const PluginName = "custom" + +type Config struct{ + Address string `mapstructure:"address"` +} + +type Plugin struct { + cfg *Config +} + +// You can also initialize some defaults values for config keys +func (cfg *Config) InitDefaults() { + if cfg.Address == "" { + cfg.Address = "tcp://localhost:8088" + } +} + +func (s *Plugin) Init(r *rpc.Plugin, h *http.Plugin, cfg config.Configurer) error { + const op = errors.Op("custom_plugin_init") // error operation name + if !cfg.Has(PluginName) { + return errors.E(op, errors.Disabled) + } + + // unmarshall + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + // Error will stop execution + return errors.E(op, err) + } + + // Init defaults + s.cfg.InitDefaults() + + return nil +} + +``` + +`errors.Disabled` is the special kind of error which indicated Endure to disable this plugin and all dependencies of +this root. The RR2 will continue to work after this error type if at least plugin stay alive. + +### Serving + +Create `Serve` and `Stop` method in your structure to let RoadRunner start and stop your service. + +```golang +type Plugin struct {} + +func (s *Plugin) Serve() chan error { + const op = errors.Op("custom_plugin_serve") + errCh := make(chan error, 1) + + err := s.DoSomeWork() + err != nil { + errCh <- errors.E(op, err) + return errCh + } + + return nil +} + +func (s *Plugin) Stop() error { + return s.stopServing() +} + +func (s *Plugin) DoSomeWork() error { + return nil +} +``` + +`Serve` method is thread-safe. It runs in the separate goroutine which managed by the `Endure` container. The one note, is that you should unblock it when call `Stop` on the container. Otherwise, service will be killed after timeout (can be set in Endure). + +### Collecting dependencies in runtime + +RR2 provide a way to collect dependencies in runtime via `Collects` interface. This is very useful for the middlewares or extending plugins with additional functionality w/o changing it. +Let's create an HTTP middleware: + +Steps (sample based on the actual `http` plugin and `Middleware` interface): + +1. Declare a required interface + +```go +// Middleware interface +type Middleware interface { + Middleware(f http.Handler) http.HandlerFunc +} +``` + +2. Implement method, which should have as an arguments name (`endure.Named` interface) and `Middleware` (step 1). + +```go +// Collects collecting http middlewares +func (s *Plugin) AddMiddleware(name endure.Named, m Middleware) { + s.mdwr[name.Name()] = m +} +``` + +3. Implement `Collects` endure interface for the required structure and return implemented on the step 2 method. + +```golang +// Collects collecting http middlewares +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.AddMiddleware, + } +} +``` + +Endure will automatically check that registered structure implement all the arguments for the `AddMiddleware` method (or will find a structure if argument is structure). In our case, a structure should implement `endure.Named` interface (which returns user friendly name for the plugin) and `Middleware` interface. + +### RPC Methods + +You can expose a set of RPC methods for your PHP workers also by using Endure `Collects` interface. Endure will automatically get the structure and expose RPC method under the `PluginName` name. + +To extend your plugin with RPC methods, plugin will not be changed at all. Only 1 thing to do is to create a file with RPC methods (let's call it `rpc.go`) and expose here all RPC methods for the plugin w/o changing plugin itself: +Sample based on the `informer` plugin: + +I assume we created a file `rpc.go`. The next step is to create a structure: + +1. Create a structure: (logger is optional) + +```golang +package custom + +type rpc struct { + srv *Plugin + log logger.Logger +} +``` + +2. Create a method, which you want to expose: + +```go +func (s *rpc) Hello(input string, output *string) error { + *output = input + return nil +} +``` + +3. Use `Collects` interface to expose the RPC service to Endure: + +```go +// CollectTarget resettable service. +func (p *Plugin) CollectTarget(name endure.Named, r Informer) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.CollectTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} +``` + +Let's take a look at these methods: + +1. `CollectTarget`: tells Endure, that we want to collect all plugins which implement `endure.Named` and `Informer` interfaces. +2. `Collects`: Endure interface implementation. +3. `Name`: `endure.Named` interface implementation which return a user-friendly plugin name. +4. `RPC`: RPC plugin Collects all plugins which implement `RPC` interface and `endure.Named`. RPC interface accepts no arguments, but returns interface (plugin). + +To use it within PHP using `RPC` [instance](/beep-beep/rpc.md): + +```php +var_dump($rpc->call('custom.Hello', 'world')); +``` |