Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed Revision #18

Open
mecha opened this issue Mar 14, 2020 · 0 comments
Open

Proposed Revision #18

mecha opened this issue Mar 14, 2020 · 0 comments

Comments

@mecha
Copy link
Member

mecha commented Mar 14, 2020

1. Introduction

This proposal aims to spark discussion for a revision of the current state of modules. The main goal is to create 2 primary packages:

  • one for module-providing packages
  • one for module-consumer packages

Additionally, 2 optional utility packages are also being proposed.

2. Motivation

(a) To have reusable modules
requires:
(b) Modules to be box-able
requires:
(c) Services to expose their dependencies
requires:
(d) Ditching the service provider spec

Bonus consequences:
(e) Less module boilerplate code
(f) Module static analysis

(a) Reusable modules

Modules currently cannot be reused by applications. This is solely due to the fact that a module would conflict with itself. The solution is to namespace each module such that different instances of the same module class each exist within their own namespace, or "box".

(b) Module Boxing

The term "module boxing" refers to the process of prefixing a module's factory and extension keys in order to namespace them. The result of this process is referred to as a "boxed module".

However, if a module such as the one below ...

[
	'name' => new function() {
		return "Bob Page";
	},
	'greeting' => new function($c) {
		return "hello " . $c->get('name');
	}
]

... is prefixed as illustrated below ...

[
	'prefix/name' => new function() {
		return "Bob Page";
	},
	'prefix/greeting' => new function($c) {
		return "hello " . $c->get('name');
	}
]

... then the $c->get() calls inside each factory or extension will fail. In the above example, $c->get('name') will fail because the corresponding service was renamed to prefix/name.

Therefore, each service's dependencies must also be prefixed.

(c) Exposed Dependencies

If a service's dependencies are exposed by the service itself, a consumer algorithm could manipulate the module's factories and extensions to produce new ones. This can be achieved by giving each service a getDependencies() method and a withDependencies() method.

Consider the interface ServiceInterface that provides the aforementioned methods ...

interface ServiceInterface {
	public function getDependencies() : array;
	public function withDependencies(array $deps) : ServiceInterface;
	public function __invoke(ContainerInterface $c);
}

... and a Factory class that implements this interface:

[
	'name' => new Factory([], new function() {
		return "Bob Page";
	}),
	'greeting' => new Factory(['name'], new function($name) {
		return "hello " . $name;
	})
]

A boxing algorithm could then prefix the above services and yield:

[
	'prefix/name' => new Factory([], new function() {
		return "Bob Page";
	}),
	'prefix/greeting' => new Factory(['prefix/name'], new function($name) {
		return "hello " . $name;
	})
]

However, the current module standard is dependent on the service provider spec, which only guarantees services to be callable. This means that a boxing algorithm would need to perform instanceof ServiceInterface checks to determine if a service is capable of exposing its dependencies and being cloned, which makes module boxing be unreliable and non-standard.

(d) Ditching Service Provider

If a module is not forced to return a service provider from setup(), but instead provide factories and extensions through its own methods, then module boxing would be possible.

interface ModuleInterface {
	public function getFactories() : ServiceInterface[];
	public function getExtensions() : ServiceInterface[];
	public function run(ContainerInterface $c);
}

Furthermore, while the standard would not directly implement the service provider spec, it would still be compatible with it. Since a ServiceInterface is invocable, each service qualifies as a callable, meaning modules can be transformed into service providers via a simple decorator:

class ModuleServiceProvider implements ServiceProviderInterface {
	// ...

	public function getFactories() {
		return $this->modules->getFactories();
	}

	public function getExtensions() {
		return $this->modules->getExtensions();
	}

	// ...
}

(e) Less Module Boilerplate Code

Module boilerplate code could be drastically reduced simply thanks to the fact that commonly duplicated logic could be extracted into specialized ServiceInterface instances. The below are a few examples which I hope are fairly self-explanatory:

[
	// Call a constructor and inject services as arguments
	'cron/interval' => new Value(60),
	'cron/job' => new Constructor(CronJob::class, ['interval']),

	// Reference to a service outside this module, with a default
	'logger' => new External('app/logger', function () {
		return new NullLogger();
	}),

	// Switch between two services, based on the value of a third
	'is_dev' => new Value(false),
	'log_level' => new TernaryValue('is_dev', 'ALL', 'ERROR'),

	// Builds a string using services
	'admin' => new Value('Bob Page'),
	'greeting' => new FormattedString("Hello {name}", [
		'name' => 'users/admin',
	]),

	// Returns the function instead of calling it
	'callback' => new Invocable(['greeting'], function($greeting) {
		echo "<p>$greeting</p>";
	}),

	// Compile services into arrays
	'db/host' => new Value('localhost'),
	'db/port' => new Value(3306),
	'db' => new ArrayValue([
		'host' => 'db/host'
		'port' => 'db/port'
		'cron' => [
			'interval' => 'cron/interval'
		]
	]),
]

(f) Module static analysis

One of the benefits of exposing dependencies for each service is the ability to create service dependency graphs for one or more modules. This would allow an analysis algorithm to theoretically be able to do the following:

  • Detect references to services that do not exist
  • Detect direct references to services outside of the insepected module
  • Detect services that are not being used
  • Detect circular referencing
  • Produce service heat maps
  • Produce dependency graphs
  • Perhaps even use the dependency graph to add IDE integration

3. Proposed Packages

Module Standard

The package that provides the module standard. This can be considered the minimum requirement for packages that provide modules.

Package:
dhii/modules

Contents:

  • ModuleInterface
  • ServiceInterface
  • Factory
  • Extension

Module System

A fully working and standalone module system for packages that consume modules.

Package:
dhii/module-system

Contents:

  • CompositeModule
  • ModuleContainer
  • ModuleServiceProvider
  • BoxedModule (for decorating modules, allowing multi-boxing)
  • ModuleLoader (for storing a list of modules in static files)

Module Helpers

Optional collection of helpers for packages that provide modules.

Package:
dhii/module-helpers

Contents:
A plethora of ServiceInterface implementations.

Module CLI

Optional tool for packages that provide multiple modules or consume modules.

Package:
dhii/modules-cli

Contents:
A variety of CLI tools for module static analysis, managing module lists, generating dependency graphs, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant