diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..f7708e9c8934c38a4e959932dc41d5cacc404c2b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# WordPress Coding Standards
+# https://make.wordpress.org/core/handbook/coding-standards/
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = tab
+indent_size = 4
+tab_width = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..1c336fbbdf71ee15f4d8f1e995cf1f09d026f04a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.editorconfig               export-ignore
+/.gitattributes              export-ignore
+/.gitignore                  export-ignore
+/phpcs.xml.dist              export-ignore
+/phpunit.xml.dist            export-ignore
+/tests                       export-ignore
+/vendor                      export-ignore
diff --git a/.gitignore b/.gitignore
index 7579f74311d35aae05dd0f0a54537ea7a0034e89..900cbda43d4461ddda0eb1d46938ce7d0339ec55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
+.DS_Store
 vendor
 composer.lock
+.phpunit.result.cache
+.phpcs-cache
diff --git a/README.md b/README.md
index 00b9231caa3e24bfcfcb0cfcf614ea92dc26f494..2ab2a727888fdd2372ecf75e1724346340a74a9f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # WordPress plugin initializer
 
-Bootstrap for your plugins.
+Boot your plugin with superpowers.
 
 ## Installation
 
@@ -12,42 +12,17 @@ composer require wpdesk/wp-init
 
 ## Creating a Plugin
 
-A plugin is a simple object created to help bootstrap functionality by allowing you to easily 
-retrieve plugin information, reference internal files and URLs, and, most importantly, register 
-hooks.
+Preferred method of using this library exercise Object Oriented Programming and organizing your
+actions and filters in a multiple classes, although it isn't the only way you can interact (and
+benefit from this library).
 
-```php
-<?php
-/**
- * Plugin Name: Example Plugin
- */
-
-use WPDesk\Init\PluginInit;
+The plugin initialization consists of the following steps:
 
-require __DIR__ . '/vendor/autoload.php';
-
-$plugin = (new PluginInit())->init();
-```
+1. Create a regular main plugin file, following [header requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)
+1. Prepare DI container definitions for your services.
+1. Declare all classes included in hook binding.
 
-`$plugin` is an instance of `Plugin`, which provides a basic API to access information about the plugin.
-
-You can find more elaborate examples for plugin configuration in `tests/Fixtures/` directory.
-
-### Plugin configuration
-
-Although, you don't need additional configuration to begin work with your plugin in OOP manner, 
-most of the time you will need a few more steps to make the most of plugin initializer.
-
-#### Using dependency injection container
-
-Paired with `php-di/php-di`, you are able to take leverage of dependency injection in your 
-WordPress plugin. `wp-init` is ready to pick up all of your hook provider classes from special 
-container entry named `hooks`. Any class registered in this array will be called during 
-`plugins_loaded` hook.
-
-```shell
-composer require php-di/php-di
-```
+The above limits your main plugin file to a short and simple structure.
 
 ```php
 <?php
@@ -55,167 +30,58 @@ composer require php-di/php-di
  * Plugin Name: Example Plugin
  */
 
-use WPDesk\Init\PluginInit;
+use WPDesk\Init\Init;
 
 require __DIR__ . '/vendor/autoload.php';
 
-$plugin = (new PluginInit())
-    ->add_container_definitions([
-        'hooks' => [
-            \DI\autowire( \Example\PostType\BookPostType::class )
-        ]
-    ])
-    ->init();
+Init::setup('config.php')->boot();
 ```
 
-#### Target environment requirements
+### Plugin configuration
 
-`wp-init` also integrates with `wpdesk/wp-basic-requirements` to validate target environment and 
-detect, whether your plugin can actually instantiate. With this addition, if your plugin fails 
-to positively validate environment, your hooks are not triggered, and the website admin is 
-provided with actionable message.
+For plugin configuration, you may focus on succinct, declarative configuration.
 
-```shell
-composer require wpdesk/wp-basic-requirements
-```
+[Supported configuration](docs/configuration.md):
 
 ```php
 <?php
-/**
- * Plugin Name: Example Plugin
- */
-
-use WPDesk\Init\PluginInit;
 
-require __DIR__ . '/vendor/autoload.php';
-
-$plugin = (new PluginInit())
-    ->set_requirements([
-        'wp' => '6.0',
-        'php' => '7.2'
-    ])
-    ->init();
-```
-
-## Hook Providers
-
-Related functionality can be encapsulated in a class called a "hook provider" that's registered when bootstrapping the plugin.
-
-Hook providers allow you to encapsulate related functionality, maintain state without using globals, namespace methods without prefixing functions, limit access to internal methods, and make unit testing easier.
+return [
+	'hook_resources_path' => 'config/hook_providers',
+	'services' => 'config/services.inc.php',
+	'cache_path' => 'generated',
 
-For an example, the `WPDesk\Init\Provider\I18n` class is a default hook provider that automatically 
-loads the text domain so the plugin can be translated.
+	'requirements' => [
+		'plugins' => [
+			'name' => 'woocommerce/woocommerce.php',
+			'nice_name' => 'WooCommerce',
+		]
+	],
 
-The only requirement for a hook provider is that it should implement the `HookProvider` 
-interface by defining a method called `register_hooks()`.
-
-Hook providers are registered with the main plugin instance by calling `Plugin::register_hooks()` like this:
-
-```php
-<?php
-$plugin->register_hooks(
-     new \Cedaro\WP\Plugin\Provider\I18n(),
-     new \Example\PostType\BookPostType()
-);
+	'plugin_class_name' => 'Example\Plugin',
+];
 ```
 
-The `BookPostType` provider might look something like this:
+## Usage with `wpdesk/wp-builder`
 
-```php
-<?php
-namespace Example\PostType;
-
-use WPDesk\Init\Provider\AbstractHookProvider;
-
-class BookPostType extends AbstractHookProvider {
-	const POST_TYPE = 'book';
-
-	public function register_hooks() {
-		$this->add_action( 'init', 'register_post_type' );
-		$this->add_action( 'init', 'register_meta' );
-	}
-
-	protected function register_post_type() {
-		register_post_type( static::POST_TYPE, $this->get_args() );
-	}
-
-	protected function register_meta() {
-		register_meta( 'post', 'isbn', array(
-			'type'              => 'string',
-			'single'            => true,
-			'sanitize_callback' => 'sanitize_text_field',
-			'show_in_rest'      => true,
-		) );
-	}
-
-	protected function get_args() {
-		return array(
-			'hierarchical'      => false,
-			'public'            => true,
-			'rest_base'         => 'books',
-			'show_ui'           => true,
-			'show_in_menu'      => true,
-			'show_in_nav_menus' => false,
-			'show_in_rest'      => true,
-		);
-	}
-}
-```
-<!--
-## Protected Hook Callbacks
-
-In WordPress, it's only possible to use public methods of a class as hook callbacks, but in the `BookPostType` hook provider above, the callbacks are protected methods of the class.
-
-Locking down the API like that is possible using the `HooksTrait` [developed by John P. Bloch](https://github.com/johnpbloch/wordpress-dev).
--->
+As a legacy support, it is possible to power up your existing codebase, which uses
+`wpdesk/wp-builder` with this library capabilities, as autowired services.
 
-## Plugin Awareness
+The only change, you have to do (besides configuration of services) is adding _hookables_ as class
+string, ready for handling by DI container:
 
-A hook provider may implement the `PluginAwareInterface` to automatically receive a reference to the plugin when its hooks are registered.
-
-For instance, in this class the `enqueue_assets()` method references the internal `$plugin` property to retrieve the URL to a JavaScript file in the plugin.
-
-```php
-<?php
-namespace Structure\Provider;
-
-use Cedaro\WP\Plugin\AbstractHookProvider;
-
-class Assets extends AbstractHookProvider {
-	public function register_hooks() {
-		$this->add_action( 'wp_enqueue_scripts', 'enqueue_assets' );
-	}
-
-	protected function enqueue_assets() {
-		wp_enqueue_script(
-			'structure',
-			$this->plugin->get_url( 'assets/js/structure.js' )
-		);
-	}
-}
+```diff
+- $this->add_hookable( new \WPDesk\Init\Provider\I18n() );
++ $this->add_hookable( \WPDesk\Init\Provider\I18n::class );
 ```
 
-Another example is the `I18n` provider mentioned earlier. It receives a reference to the plugin object so that it can use the plugin's base name and slug to load the text domain.
-
-Classes that extend `AbstractHookProvider` are automatically "plugin aware."
-
 ## Credits
 
-This package is heavily inspired by Cedaro's [`wp-plugin`](https://github.com/cedaro/wp-plugin/) 
+This package is heavily inspired by Cedaro's [`wp-plugin`](https://github.com/cedaro/wp-plugin/)
 and Alain Schlesser's [`basic-scaffold`](https://github.com/mwpd/basic-scaffold).
 
-## Roadmap
-
-1. Add support for path based hook providers discovery similar to Symfony's [controllers resolving](https://github.com/symfony/demo/blob/3787b9f71f6bee24f1ed0718b9a808d824008776/config/routes.yaml#L15-L17)
-1. Improve `wpdesk/wp-basic-requirements` library. This is not related directly to this project, 
-   but internals could be rewritten.
-1. Scrap plugin data from plugin comment
-1. Allow hooks to be called from private and protected methods (in PHP <8.1)
-1. Support *bundles* of hook providers. This should be easy to extend plugin capabilities with 
-   shared functions, preserving minimal init system
-
 ## License
 
-Copyright (c) 2023 WPDesk
+Copyright (c) 2024 WPDesk
 
 This library is licensed under MIT.
diff --git a/bin/wpinit b/bin/wpinit
new file mode 100755
index 0000000000000000000000000000000000000000..e23eed6e2635b1e9c844ed06214edef00eec0ef7
--- /dev/null
+++ b/bin/wpinit
@@ -0,0 +1,17 @@
+#!/usr/bin/env php
+<?php
+
+use WPDesk\Init\Util\PhpFileDumper;
+
+include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
+
+$filename = $argv[1] ?? null;
+
+$parser = new \WPDesk\Init\Plugin\DefaultHeaderParser();
+
+$data = $parser->parse( $filename );
+
+$dumper = new PhpFileDumper();
+$dumper->dump( $parser->parse( $filename ), $argv[2] );
+
+//echo "Compile static plugin resources.";
diff --git a/composer.json b/composer.json
index fb30e761a07ba9ecca8c38c6758ab66be97081d6..c00e7df202de002bc2ce616cf1bad6db0d7a6de9 100644
--- a/composer.json
+++ b/composer.json
@@ -1,12 +1,13 @@
 {
   "name": "wpdesk/wp-init",
   "description": "Bootstrap for a WordPress plugin",
-  "minimum-stability": "stable",
   "license": "MIT",
   "type": "library",
   "homepage": "https://gitlab.wpdesk.dev/wpdesk/wp-init",
   "keywords": [
-    "wordpress"
+    "wordpress",
+    "plugin",
+    "bootstrap"
   ],
   "authors": [
     {
@@ -19,6 +20,9 @@
       "role": "Developer"
     }
   ],
+  "bin": [
+    "bin/wpinit"
+  ],
   "autoload": {
     "psr-4": {
       "WPDesk\\Init\\": "src"
@@ -34,24 +38,31 @@
   },
   "require": {
     "php": ">=7.2 | ^8",
-    "psr/container": "^1 || ^2"
+    "php-di/php-di": "^6 || ^7",
+	"psr/container": "~1.0.0 || ^2"
   },
   "require-dev": {
-    "wpdesk/wp-basic-requirements": "^3",
-    "php-di/php-di": "^6 || ^7",
+    "brain/monkey": "^2.6",
     "phpunit/phpunit": "^8 || ^9",
     "symfony/filesystem": "^6.2",
-    "brain/monkey": "^2.6"
+    "wpdesk/phpstan-rules": "^1.1",
+    "wpdesk/wp-basic-requirements": "^3",
+    "wpdesk/wp-builder": "^2.0",
+	"wpdesk/wp-code-sniffer": "^1.3"
   },
   "suggest": {
-    "wpdesk/wp-basic-requirements": "Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or active plugins",
-    "php-di/php-di": "Allows a plugin to seamlessly integrate with a dependency injection container"
+    "wpdesk/wp-basic-requirements": "Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or active plugins"
   },
   "conflict": {
-    "wpdesk/wp-basic-requirements": "<3, >=4",
-    "php-di/php-di": "<6, >=8"
+    "wpdesk/wp-basic-requirements": "<3 >=4"
   },
   "scripts": {
     "test": "vendor/bin/phpunit --bootstrap tests/bootstrap.php ./tests"
+  },
+  "config": {
+    "allow-plugins": {
+			"phpstan/extension-installer": true,
+			"dealerdirect/phpcodesniffer-composer-installer": true
+		}
   }
 }
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000000000000000000000000000000000000..3ba49a8a6099fa573a625671f569b99f8d05c853
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,64 @@
+# Configuration
+
+`wp-init` relies on declarative configuration, which encapsulates process of attaching hooks to
+WordPress life cycle and provides some additional features, like filling DI container with your
+services definitions.
+
+## `hook_resources_path`
+
+Pass a path to the file/directory with your hook actions. Configuration accepts any valid path
+string, relative or absolute, either `hook_providers` or `__DIR__ . '/hook_providers/plugins_loaded.php'`
+
+Files are mapped to hooks by name, so `woocommerce_init.php` is registered inside `woocommerce_init`
+action. The exception is `index.php` file which is flushed immediately.
+
+Example of a hook resource content:
+
+```php
+<?php
+// plugins_loaded.php
+
+return [
+	MyCoolTitleChanger::class,
+	AnotherHookAction::class,
+	function ( Migarator $migrator ) {
+		// You can even use a closure, to execute simple actions.
+		// Arguments are injected by DI container.
+		$migrator->migrate();
+	}
+];
+```
+
+## `services`
+
+As you add more services with increasing complexity, you will need to provide some kind of
+definitions for a DI container to create objects. Pass a path to a file, which will hold such
+definitions. Refer to [php-di documentation](https://php-di.org/doc/definitions.html) for more
+information on such file content.
+
+> *Warning*
+>
+> If you are using _shortcut_ functions from `php-di/php-di` (e.g. `DI\autowire`, `DI\create`), you
+> must load them first. Add `require __DIR__ . '/vendor_prefixed/php-di/php-di/src/functions.php';`
+> to your plugin file.
+
+## `cache_path`
+
+Plugin header data and compiled DI container is cached in a directory specified by this
+setting. Defaults to `generated`.
+
+## `requirements`
+
+**This setting only works when `wpdesk/wp-basic-requirements` is installed.**
+
+Enables your plugin to check an environment requirement before instantiation, e.g. PHP version or
+active plugins. Refer to [wp-basic-requirements documentation](https://gitlab.wpdesk.dev/wpdesk/wp-basic-requirements)
+for more information on setting structure.
+
+## `plugin_class_name`
+
+**This setting only works when `wpdesk/wp-builder` is installed.**
+
+When a plugin is used in [legacy mode](legacy.md), `plugin_class_name` is used to create an instance
+of main plugin class. This setting is required to enable legacy mode. Despite that,
+`WPDesk\Init\Plugin\Plugin` is still accessible to your services.
diff --git a/docs/legacy.md b/docs/legacy.md
new file mode 100644
index 0000000000000000000000000000000000000000..bab5e4a1b6753596c213e82e735a2344f0540f8d
--- /dev/null
+++ b/docs/legacy.md
@@ -0,0 +1,7 @@
+# Migration from `wpdesk/wp-builder`
+
+Projects using `wpdesk/wp-builder` (or `wpdesk/wp-plugin-flow`) may easily migrate to `wp-init`
+in a few steps:
+
+1. Install `wpdesk/wp-init`.
+1.
diff --git a/docs/prefixing.md b/docs/prefixing.md
new file mode 100644
index 0000000000000000000000000000000000000000..b5e9589d7003ff92d28b4c10a6448d862663da8f
--- /dev/null
+++ b/docs/prefixing.md
@@ -0,0 +1,37 @@
+# Prefixing `wp-init` with `php-scoper`
+
+When developing plugins, it's worth to prefix all dependencies to avoid version collision between
+different plugins loaded during runtime. For this library to enable prefixing you will need to
+introduce following configuration to your `scoper.inc.php` file.
+
+- Whitelist `vendor/php-di/php-di/src/Compiler/Template.php`
+- Include `php-di` and it's dependencies in finders
+
+> *Note*
+>
+> Pay attention to actual installed `php-di/php-di` version, as it's dependencies may change,
+> requiring to update `scoper.inc.php` accordingly.
+
+## Example configuration
+
+**`php-di/php-di` up to 6.3.5**
+
+```php
+return [
+  'finder' => Finder::create()->in(['vendor/wpdesk/wp-init', 'vendor/php-di', 'vendor/opis/closure']),
+  'files-whitelist' => [
+    'vendor/php-di/php-di/src/Compiler/Template.php'
+  ],
+];
+```
+
+**`php-di/php-di` since 6.4.0**
+
+```php
+return [
+  'finder' => Finder::create()->in(['vendor/wpdesk/wp-init', 'vendor/php-di', 'vendor/laravel/serializable-closure']),
+  'files-whitelist' => [
+    'vendor/php-di/php-di/src/Compiler/Template.php'
+  ],
+];
+```
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000000000000000000000000000000000000..05dfc78138a32ed739f2e9ad714552ed440d1438
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<ruleset name="WordPress Coding Standards for WP Desk Plugin">
+
+	<!--
+	#############################################################################
+	COMMAND LINE ARGUMENTS
+	https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
+	#############################################################################
+	-->
+
+	<!-- Scan all files. -->
+	<file>./src</file>
+
+	<!-- Only check PHP files. -->
+	<arg name="extensions" value="php"/>
+
+	<!-- Show progress, show the error codes for each message (source). -->
+	<arg value="sp"/>
+
+	<!-- Check up to 8 files simultaneously. -->
+	<arg name="parallel" value="8"/>
+
+	<!-- Cache outcomes for better performance. Remember to add the file to .gitignore. -->
+	<arg name="cache" value="./.phpcs-cache"/>
+
+	<!--
+	#############################################################################
+	USE THE WPDeskCS RULESET
+	#############################################################################
+	-->
+
+	<!-- Define plugin text domain for i18n. -->
+	<config name="text_domain" value="wp-init"/>
+
+	<!-- This value should be aligned with WordPress support version declared in plugin header -->
+	<config name="minimum_supported_wp_version" value="6.2"/>
+
+	<!-- Set value aligned with supported PHP Version for PHPCompatibilityWP check. -->
+	<config name="testVersion" value="7.2-"/>
+
+	<rule ref="WPDeskPlugin"/>
+
+	<!--
+	##############################################################################
+	CUSTOM RULES
+	##############################################################################
+	-->
+	<rule ref="Squiz.ControlStructures.InlineIfDeclaration">
+		<exclude name="Squiz.ControlStructures.InlineIfDeclaration.NoBrackets"/>
+	</rule>
+</ruleset>
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000000000000000000000000000000000000..41e0048877bebd4ae3ff207f17c618ad0ee7d30e
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,4 @@
+parameters:
+    level: 6
+    paths:
+        - src/
diff --git a/src/Binding/Binder.php b/src/Binding/Binder.php
new file mode 100644
index 0000000000000000000000000000000000000000..0dc334f13f1d4aa581da04fd090154b9dc5e12ed
--- /dev/null
+++ b/src/Binding/Binder.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+interface Binder {
+
+	public function bind( Definition $def ): void;
+}
diff --git a/src/Binding/Binder/CallableBinder.php b/src/Binding/Binder/CallableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..70858003a1e18d85843bd9deb98c58037ee913f8
--- /dev/null
+++ b/src/Binding/Binder/CallableBinder.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Binder;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\ComposableBinder;
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\Binding\Definition\CallableDefinition;
+
+class CallableBinder implements ComposableBinder {
+
+	/** @var ContainerInterface */
+	private $container;
+
+	public function __construct( ContainerInterface $c ) {
+		$this->container = $c;
+	}
+
+	public function can_bind( Definition $def ): bool {
+		return $def instanceof CallableDefinition;
+	}
+
+	public function bind( Definition $def ): void {
+		$ref        = new \ReflectionFunction( $def->value() );
+		$parameters = [];
+		foreach ( $ref->getParameters() as $ref_param ) {
+		$parameters[] = $this->container->get( $ref_param->getType()->getName() );
+		}
+		$ref->invokeArgs( $parameters );
+	}
+}
diff --git a/src/Binding/Binder/CompositeBinder.php b/src/Binding/Binder/CompositeBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b88634687b2fbb7b07c99b710db520398b33c2f
--- /dev/null
+++ b/src/Binding/Binder/CompositeBinder.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Binder;
+
+use WPDesk\Init\Binding\Binder;
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\Binding\Definition\HookableDefinition;
+
+final class CompositeBinder implements Binder {
+
+	/** @var Binder[] */
+	private $binders;
+
+	public function __construct( Binder ...$binders ) {
+		$this->binders = $binders;
+	}
+
+	public function add( Binder $binder ): void {
+		$this->binders[] = $binder;
+	}
+
+	public function bind( Definition $def ): void {
+		foreach ( $this->binders as $binder ) {
+			if ( $binder->can_bind( $def ) ) {
+				$binder->bind( $def );
+				break;
+			}
+		}
+	}
+}
diff --git a/src/Binding/Binder/HookableBinder.php b/src/Binding/Binder/HookableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..f8426dacddff213d58bdd0be5e9f0d68fb9ffaee
--- /dev/null
+++ b/src/Binding/Binder/HookableBinder.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Binder;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\ComposableBinder;
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\Binding\Definition\HookableDefinition;
+
+class HookableBinder implements ComposableBinder {
+
+	/** @var ContainerInterface */
+	private $container;
+
+	public function __construct( ContainerInterface $c ) {
+		$this->container = $c;
+	}
+
+	public function can_bind( Definition $def ): bool {
+		return $def instanceof HookableDefinition;
+	}
+
+	public function bind( Definition $def ): void {
+		$this->container->get( $def->value() )->hooks();
+	}
+}
diff --git a/src/Binding/Binder/ObservableBinder.php b/src/Binding/Binder/ObservableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..8012f7ba35179c8e53ca70da1347be5cad746438
--- /dev/null
+++ b/src/Binding/Binder/ObservableBinder.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Binder;
+
+use WPDesk\Init\Binding\Binder;
+use WPDesk\Init\Binding\ComposableBinder;
+use WPDesk\Init\Binding\Definition;
+
+/**
+ * Binder decorator, specifically built for testing purposes. Can naively investigate other binders.
+ */
+final class ObservableBinder implements ComposableBinder {
+
+	/** @var Binder */
+	private $binder;
+
+	private $binds_count = 0;
+
+	public function __construct( Binder $b ) {
+		$this->binder = $b;
+	}
+
+	public function bind( Definition $def ): void {
+		$this->binder->bind( $def );
+		$this->binds_count++;
+	}
+
+	public function can_bind( Definition $def ): bool {
+		if ( $this->binder instanceof ComposableBinder ) {
+			return $this->binder->can_bind( $def );
+		}
+
+		return true;
+	}
+
+	public function binds_count(): int {
+		return $this->binds_count;
+	}
+}
diff --git a/src/Binding/Binder/StoppableBinder.php b/src/Binding/Binder/StoppableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..8917f7867e90c75ac1f20f2809cef78fb07149f9
--- /dev/null
+++ b/src/Binding/Binder/StoppableBinder.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Binder;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\ComposableBinder;
+use WPDesk\Init\Binding\StoppableBinder as Stop;
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\Binding\Binder as BinderInstance;
+use WPDesk\Init\Binding\Definition\HookableDefinition;
+
+class StoppableBinder implements ComposableBinder {
+
+	/** @var ContainerInterface */
+	private $container;
+
+	/** @var Binder */
+	private $binder;
+
+	private $should_stop = false;
+
+	public function __construct( BinderInstance $b, ContainerInterface $c ) {
+		$this->binder    = $b;
+		$this->container = $c;
+	}
+
+	public function can_bind( Definition $def ): bool {
+		return $this->binder->can_bind( $def );
+	}
+
+	public function bind( Definition $def ): void {
+		if ( $this->should_stop === true ) {
+			return;
+		}
+
+		$this->binder->bind( $def );
+
+		if ( $this->can_be_stoppable( $def ) ) {
+			$binding = $this->container->get( $def->value() );
+			if ( $binding instanceof Stop && $binding->should_stop() ) {
+				$this->should_stop = true;
+			}
+		}
+	}
+
+	private function can_be_stoppable( Definition $def ): bool {
+		return is_string( $def->value() ) && class_exists( $def->value() );
+	}
+}
diff --git a/src/Binding/ComposableBinder.php b/src/Binding/ComposableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..8498244e65ee2970b27a9e3729dc26e546d4f64c
--- /dev/null
+++ b/src/Binding/ComposableBinder.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+/**
+ * Can be composed with other binders within {@see CompositeBinder} class.
+ */
+interface ComposableBinder extends Binder {
+
+	public function can_bind( Definition $def ): bool;
+}
diff --git a/src/Binding/Definition.php b/src/Binding/Definition.php
new file mode 100644
index 0000000000000000000000000000000000000000..7184bf5f705909688e9b440e7be16abe3a8bf71c
--- /dev/null
+++ b/src/Binding/Definition.php
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+/**
+ * @template T
+ */
+interface Definition {
+
+	public function hook(): ?string;
+
+	/** @return T */
+	public function value();
+}
diff --git a/src/Binding/Definition/CallableDefinition.php b/src/Binding/Definition/CallableDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..9bf3774f733e3c1c074ce4d5614288b1ab8c1da4
--- /dev/null
+++ b/src/Binding/Definition/CallableDefinition.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Definition;
+
+use WPDesk\Init\Binding\Definition;
+
+/** @implements Definition<callable> */
+class CallableDefinition implements Definition {
+
+	/** @var ?string */
+	private $hook;
+
+	/** @var callable */
+	private $callable;
+
+	public function __construct(
+		callable $callable,
+		?string $hook = null,
+	) {
+		$this->callable = $callable;
+		$this->hook     = $hook;
+	}
+
+	public function hook(): ?string {
+		return $this->hook;
+	}
+
+	public function value() {
+		return $this->callable;
+	}
+}
diff --git a/src/Binding/Definition/HookableDefinition.php b/src/Binding/Definition/HookableDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..286d9ba5157f4a2b6fc92d7dda9b5a0eabf4f8f0
--- /dev/null
+++ b/src/Binding/Definition/HookableDefinition.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Definition;
+
+use WPDesk\Init\Binding\Definition;
+
+/** @implements Definition<class-string<Hookable>> */
+class HookableDefinition implements Definition {
+
+	/** @var ?string */
+	private $hook;
+
+	/** @var class-string<Hookable> */
+	private $hookable;
+
+	public function __construct(
+		string $hookable,
+		?string $hook = null,
+	) {
+		$this->hook     = $hook;
+		$this->hookable = $hookable;
+	}
+
+	public function hook(): ?string {
+		return $this->hook;
+	}
+
+	public function value() {
+		return $this->hookable;
+	}
+}
diff --git a/src/Binding/Definition/UnknownDefinition.php b/src/Binding/Definition/UnknownDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..87522547258f4fad569e984d85517b8fef48779f
--- /dev/null
+++ b/src/Binding/Definition/UnknownDefinition.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding\Definition;
+
+use WPDesk\Init\Binding\Definition;
+
+/** @implements Definition<mixed> */
+class UnknownDefinition implements Definition {
+
+	/** @var ?string */
+	private $hook;
+
+	/** @var mixed */
+	private $value;
+
+	public function __construct(
+		$value,
+		?string $hook = null,
+	) {
+		$this->value = $value;
+		$this->hook     = $hook;
+	}
+
+	public function hook(): ?string {
+		return $this->hook;
+	}
+
+	public function value() {
+		return $this->value;
+	}
+}
diff --git a/src/Binding/DefinitionFactory.php b/src/Binding/DefinitionFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..5b66072e90754f9850266fb160bacbab83e5919b
--- /dev/null
+++ b/src/Binding/DefinitionFactory.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+use WPDesk\Init\Binding\Definition\CallableDefinition;
+use WPDesk\Init\Binding\Definition\HookableDefinition;
+use WPDesk\Init\Binding\Definition\UnknownDefinition;
+
+class DefinitionFactory {
+
+	public function create( $value, ?string $hook ): Definition {
+		if ( is_string( $value ) && class_exists( $value ) && is_subclass_of( $value, Hookable::class, true ) ) {
+			return new HookableDefinition( $value, $hook );
+		}
+
+		if ( is_callable( $value ) ) {
+			return new CallableDefinition( $value, $hook );
+		}
+
+		return new UnknownDefinition( $value, $hook );
+	}
+}
diff --git a/src/Binding/Hookable.php b/src/Binding/Hookable.php
new file mode 100644
index 0000000000000000000000000000000000000000..3fb1e51b46ee5f6e4a0a9c410e5453bce0acb0eb
--- /dev/null
+++ b/src/Binding/Hookable.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+interface Hookable extends \WPDesk\PluginBuilder\Plugin\Hookable {
+
+	public function hooks(): void;
+}
diff --git a/src/Binding/Loader/ArrayDefinitions.php b/src/Binding/Loader/ArrayDefinitions.php
new file mode 100644
index 0000000000000000000000000000000000000000..865b0a3cd46c46c390515b66401d1add22337014
--- /dev/null
+++ b/src/Binding/Loader/ArrayDefinitions.php
@@ -0,0 +1,47 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Binding\Loader;
+
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\Binding\DefinitionFactory;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\Plugin\Plugin;
+
+class ArrayDefinitions implements BindingDefinitions {
+
+	/** @var array */
+	private $bindings;
+
+	/** @var DefinitionFactory */
+	private $factory;
+
+	public function __construct( array $bindings, ?DefinitionFactory $factory = null) {
+		$this->bindings = $bindings;
+		$this->factory  = $factory ?? new DefinitionFactory();
+	}
+
+	public function load(): iterable {
+		yield from $this->normalize( $this->bindings );
+	}
+
+	private function normalize( iterable $bindings ): iterable {
+		foreach ( $bindings as $key => $value ) {
+			if ( is_array( $value ) ) {
+				foreach ( $value as $unit ) {
+					yield $this->create( $unit, $key );
+				}
+			} else {
+				yield $this->create( $value, $key );
+			}
+		}
+	}
+
+	/**
+     * @param mixed $value
+	 * @param int|string $hook
+	 */
+	private function create( $value, $hook ): Definition {
+		return $this->factory->create( $value, is_int( $hook ) ? null : $hook );
+	}
+}
diff --git a/src/Binding/Loader/BindingDefinitions.php b/src/Binding/Loader/BindingDefinitions.php
new file mode 100644
index 0000000000000000000000000000000000000000..c253c3154b6f65a3fdce501cf510eb2c777402e0
--- /dev/null
+++ b/src/Binding/Loader/BindingDefinitions.php
@@ -0,0 +1,14 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Binding\Loader;
+
+use WPDesk\Init\Binding\Definition;
+
+interface BindingDefinitions {
+
+	/**
+	 * @return iterable<Definition>
+	 */
+	public function load(): iterable;
+}
diff --git a/src/Binding/Loader/CompositeBindingLoader.php b/src/Binding/Loader/CompositeBindingLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..81f501bdafd2c64bee62e123bdaa2db6369da37b
--- /dev/null
+++ b/src/Binding/Loader/CompositeBindingLoader.php
@@ -0,0 +1,26 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Binding\Loader;
+
+use WPDesk\Init\Plugin\Plugin;
+
+class CompositeBindingLoader implements BindingDefinitions {
+
+	/** @var BindingDefinitionLoader[] */
+	private $loaders;
+
+	public function __construct( BindingDefinitions ...$loaders ) {
+		$this->loaders = $loaders;
+	}
+
+	public function load(): iterable {
+		foreach ( $this->loaders as $loader ) {
+			yield from $loader->load();
+		}
+	}
+
+	public function add( BindingDefinitions $loader ): void {
+		$this->loaders[] = $loader;
+	}
+}
diff --git a/src/Binding/Loader/EmptyDefinitions.php b/src/Binding/Loader/EmptyDefinitions.php
new file mode 100644
index 0000000000000000000000000000000000000000..9048f2fa1bd6bd562cea83f93faf40d180b1a94e
--- /dev/null
+++ b/src/Binding/Loader/EmptyDefinitions.php
@@ -0,0 +1,11 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Binding\Loader;
+
+final class EmptyDefinitions implements BindingDefinitions {
+
+	public function load(): iterable {
+		return [];
+	}
+}
diff --git a/src/Binding/Loader/FilesystemDefinitions.php b/src/Binding/Loader/FilesystemDefinitions.php
new file mode 100644
index 0000000000000000000000000000000000000000..f86c22e5848685a0e272faa9d207a10fc09b59f5
--- /dev/null
+++ b/src/Binding/Loader/FilesystemDefinitions.php
@@ -0,0 +1,50 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Binding\Loader;
+
+use WPDesk\Init\Binding\DefinitionFactory;
+use WPDesk\Init\Util\Path;
+use WPDesk\Init\Util\PhpFileLoader;
+
+class FilesystemDefinitions implements BindingDefinitions {
+
+	/** @var Path */
+	private $path;
+
+	/** @var PhpFileLoader */
+	private $loader;
+
+	/** @var DefinitionFactory */
+	private $def_factory;
+
+	public function __construct( $path, ?PhpFileLoader $loader = null, ?DefinitionFactory $def_factory = null ) {
+		$this->path        = new Path( (string) $path );
+		$this->loader      = $loader ?? new PhpFileLoader();
+		$this->def_factory = $def_factory ?? new DefinitionFactory();
+	}
+
+	public function load(): iterable {
+		if ( $this->path->is_directory() ) {
+			foreach ( $this->path->read_directory() as $filename ) {
+				yield from $this->load_from_file( $filename );
+			}
+		} else {
+			yield from $this->load_from_file( $this->path );
+		}
+	}
+
+	private function load_from_file( Path $filename ): iterable {
+		if ( ! $filename->is_file() ) {
+			return;
+		}
+
+		$hooks = $this->loader->load( (string) $filename );
+
+		if ( $filename->get_basename() !== 'index.php' ) {
+			$hooks = [ $filename->get_filename_without_extension() => $hooks ];
+		}
+
+		yield from (new ArrayDefinitions( $hooks ) )->load();
+	}
+}
diff --git a/src/Binding/StoppableBinder.php b/src/Binding/StoppableBinder.php
new file mode 100644
index 0000000000000000000000000000000000000000..daf5852fdc2f9905705c60a0a151a96c67e627be
--- /dev/null
+++ b/src/Binding/StoppableBinder.php
@@ -0,0 +1,9 @@
+<?php
+declare(strict_types=1);
+
+namespace WPDesk\Init\Binding;
+
+interface StoppableBinder extends Hookable {
+
+	public function should_stop(): bool;
+}
diff --git a/src/Conditional.php b/src/Conditional.php
deleted file mode 100644
index 7851ce41dc48099fe8682a6444c2dc4d33ac08c2..0000000000000000000000000000000000000000
--- a/src/Conditional.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-/**
- * Something that can be instantiated conditionally.
- *
- * In hook provider context a class marked as being conditional can be asked whether its
- * hooks should be fired and integrated into WordPress system. An example would be a service that is
- * only available on the admin backend.
- *
- * This allows for a more systematic and automated optimization of how the
- * different parts of the plugin are enabled or disabled.
- *
- * @author    Alain Schlesser <alain.schlesser@gmail.com>
- */
-interface Conditional {
-
-	/**
-	 * Check whether the conditional object is currently needed.
-	 *
-	 * @return bool Whether the conditional object is needed.
-	 */
-	public function is_needed(): bool;
-}
diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php
new file mode 100644
index 0000000000000000000000000000000000000000..69c5b8cdc637cf6cf58540b3055bbdfb9d056117
--- /dev/null
+++ b/src/Configuration/Configuration.php
@@ -0,0 +1,60 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Configuration;
+
+/**
+ * @implements \ArrayAccess<string, mixed>
+ */
+class Configuration implements ReadableConfig, \ArrayAccess {
+
+	/** @var array<string, mixed> */
+	private $config;
+
+	public function __construct( array $config ) {
+		$this->config = $config;
+	}
+
+	/**
+	 * @param string $key
+	 * @param mixed $default
+	 *
+	 * @return mixed|null
+	 */
+	public function get( string $key, $default = null ) {
+		return $this->config[ $key ] ?? $default;
+	}
+
+	public function has( string $key ): bool {
+		return isset( $this->config[ $key ] );
+	}
+
+	public function set( string $key, $value ): void {
+		$this->config[ $key ] = $value;
+	}
+
+	public function remove( string $key ): void {
+		unset( $this->config[ $key ] );
+	}
+
+	public function offsetExists( $offset ): bool {
+		return $this->has( $offset );
+	}
+
+	#[\ReturnTypeWillChange]
+	public function offsetGet( $offset ) {
+		return $this->get( $offset );
+	}
+
+	public function offsetSet( $offset, $value ): void {
+		if ( $offset === null ) {
+			throw new \InvalidArgumentException( 'Cannot set value without key.' );
+		}
+
+		$this->set( $offset, $value );
+	}
+
+	public function offsetUnset( $offset ): void {
+		$this->remove( $offset );
+	}
+}
diff --git a/src/Configuration/ReadableConfig.php b/src/Configuration/ReadableConfig.php
new file mode 100644
index 0000000000000000000000000000000000000000..41491e2af09d3736191e3fdc28e928cec05481a6
--- /dev/null
+++ b/src/Configuration/ReadableConfig.php
@@ -0,0 +1,14 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Configuration;
+
+/**
+ * Allows to read configuration.
+ */
+interface ReadableConfig {
+
+	public function get( string $key, $default = null );
+
+	public function has( string $key ): bool;
+}
diff --git a/src/ContainerAwareInterface.php b/src/ContainerAwareInterface.php
deleted file mode 100644
index 95b285e1bece984f45e0830fb9cc05e27e5a41b5..0000000000000000000000000000000000000000
--- a/src/ContainerAwareInterface.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace WPDesk\Init;
-
-use Psr\Container\ContainerInterface;
-
-interface ContainerAwareInterface {
-
-  public function set_container( ContainerInterface $container ): void;
-
-}
diff --git a/src/ContainerAwareTrait.php b/src/ContainerAwareTrait.php
deleted file mode 100644
index 079701426e7f433b578fbeabe739680fdf4adbb6..0000000000000000000000000000000000000000
--- a/src/ContainerAwareTrait.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace WPDesk\Init;
-
-use Psr\Container\ContainerInterface;
-
-trait ContainerAwareTrait {
-
-  /** @var ContainerInterface */
-  private $container;
-
-  public function set_container( ContainerInterface $container ): void {
-    $this->container = $container;
-  }
-}
diff --git a/src/DependencyInjection/ContainerBuilder.php b/src/DependencyInjection/ContainerBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..c147090d3c406564dd0730a2c3f0d16c74bd5197
--- /dev/null
+++ b/src/DependencyInjection/ContainerBuilder.php
@@ -0,0 +1,37 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\DependencyInjection;
+
+use DI\Container;
+use DI\ContainerBuilder as DiBuilder;
+use DI\Definition\Source\DefinitionSource;
+
+final class ContainerBuilder {
+
+	/** @var DiBuilder */
+	private $original_builder;
+
+	public function __construct( DiBuilder $original_builder ) {
+		$this->original_builder = $original_builder;
+	}
+
+	/**
+	 * Add definitions to the container.
+	 *
+	 * @param string|array|DefinitionSource ...$definitions
+	 *  Can be an array of definitions, the name of a file containing definitions or
+	 *  a DefinitionSource object.
+	 *
+	 * @return $this
+	 */
+	public function add_definitions( ...$definitions ): self {
+		$this->original_builder->addDefinitions( ...$definitions );
+
+		return $this;
+	}
+
+	public function build(): Container {
+		return $this->original_builder->build();
+	}
+}
diff --git a/src/Extension/BuiltinExtension.php b/src/Extension/BuiltinExtension.php
new file mode 100644
index 0000000000000000000000000000000000000000..b896babff5997f5879aabbefffb4be7c78803eb1
--- /dev/null
+++ b/src/Extension/BuiltinExtension.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Extension;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+use WPDesk\Init\Binding\Loader\FilesystemDefinitions;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Plugin\Plugin;
+
+class BuiltinExtension implements Extension {
+
+	public function bindings( ContainerInterface $c ): BindingDefinitions {
+		return new FilesystemDefinitions( __DIR__ . '/../Resources/bindings' );
+	}
+
+	public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void {
+		$builder->add_definitions( __DIR__ . '/../Resources/services.inc.php' );
+	}
+}
diff --git a/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php b/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa33b90c8ef505b90c8965835ebcb7b8c8909c3a
--- /dev/null
+++ b/src/Extension/CommonBinding/CustomOrdersTableCompatibility.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace WPDesk\Init\Extension\CommonBinding;
+
+use WPDesk\Init\Binding\Hookable;
+use WPDesk\Init\Plugin\Plugin;
+
+class CustomOrdersTableCompatibility implements Hookable {
+
+	/** @var Plugin */
+	private $plugin;
+
+	public function __construct( Plugin $plugin ) {
+		$this->plugin = $plugin;
+	}
+
+	public function hooks(): void {
+		add_action('before_woocommerce_init', $this);
+	}
+
+	public function __invoke(): void {
+		$features_util_class = '\\' . 'Automattic' . '\\' . 'WooCommerce' . '\\' . 'Utilities' . '\\' . 'FeaturesUtil';
+		if ( class_exists( $features_util_class ) ) {
+			$features_util_class::declare_compatibility(
+				'custom_order_tables',
+				$this->plugin->get_basename(),
+				true
+			);
+		}
+
+	}
+}
diff --git a/src/Extension/CommonBinding/I18n.php b/src/Extension/CommonBinding/I18n.php
new file mode 100644
index 0000000000000000000000000000000000000000..a5221732e4a9c3f5f4fae4e7b76a5259898f6662
--- /dev/null
+++ b/src/Extension/CommonBinding/I18n.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace WPDesk\Init\Extension\CommonBinding;
+
+use WPDesk\Init\Binding\Hookable;
+use WPDesk\Init\Plugin\Plugin;
+
+class I18n implements Hookable {
+
+	/** @var Plugin */
+	private $plugin;
+
+	public function __construct( Plugin $plugin ) {
+		$this->plugin = $plugin;
+	}
+
+	public function hooks(): void {
+		if ( did_action( 'plugins_loaded' ) ) {
+			$this->__invoke();
+		} else {
+			add_action( 'plugins_loaded', $this );
+		}
+	}
+
+	public function __invoke(): void {
+		\load_plugin_textdomain(
+			$this->plugin->get_slug(),
+			false,
+			$this->plugin->header()->get( 'DomainPath' )
+		);
+	}
+}
diff --git a/src/Extension/CommonBinding/RequirementsCheck.php b/src/Extension/CommonBinding/RequirementsCheck.php
new file mode 100644
index 0000000000000000000000000000000000000000..37be9ec2817b4bd06230ecb8aa10add583098f83
--- /dev/null
+++ b/src/Extension/CommonBinding/RequirementsCheck.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace WPDesk\Init\Extension\CommonBinding;
+
+use WPDesk\Init\Binding\StoppableBinder;
+use WPDesk\Init\Configuration\Configuration;
+use WPDesk\Init\Plugin\Plugin;
+
+class RequirementsCheck implements StoppableBinder {
+
+	private \WPDesk_Requirement_Checker $checker;
+
+	public function __construct( Plugin $plugin, Configuration $config ) {
+		$this->checker = ( new \WPDesk_Basic_Requirement_Checker_Factory(
+		) )->create_from_requirement_array(
+			$plugin->get_basename(),
+			$plugin->get_name(),
+			$config->has( 'requirements' ) ? $config->get( 'requirements' ) : [],
+			$plugin->get_slug()
+		);
+	}
+
+	public function hooks(): void {
+		if ( $this->should_stop() ) {
+			$this->checker->render_notices();
+		}
+	}
+
+	public function should_stop(): bool {
+		return ! $this->checker->are_requirements_met();
+	}
+}
diff --git a/src/Extension/CommonBinding/WPDeskLicenseBridge.php b/src/Extension/CommonBinding/WPDeskLicenseBridge.php
new file mode 100644
index 0000000000000000000000000000000000000000..55db2c7235543e682f261db0aedea8ddd8cb8de3
--- /dev/null
+++ b/src/Extension/CommonBinding/WPDeskLicenseBridge.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace WPDesk\Init\Extension\CommonBinding;
+
+use WPDesk\Init\Binding\Hookable;
+use WPDesk\Init\Plugin\Plugin;
+use WPDesk\License\PluginRegistrator;
+
+class WPDeskLicenseBridge implements Hookable {
+
+	/** @var \WPDesk_Plugin_Info */
+	private $plugin_info;
+
+	private $registrator;
+
+	public function __construct( \WPDesk_Plugin_Info $plugin_info ) {
+		$this->plugin_info = $plugin_info;
+	}
+
+	public function hooks(): void {
+		$this->registrator = $this->register_plugin();
+		// add_action('plugins_loaded', $this);
+	}
+
+	public function __invoke(): void {
+		$is_plugin_subscription_active = $this->registrator instanceof PluginRegistrator && $this->registrator->is_active();
+		if ( $this->plugin instanceof ActivationAware && $is_plugin_subscription_active ) {
+			$this->plugin->set_active();
+		}
+
+	}
+
+	private function register_plugin() {
+		if ( apply_filters( 'wpdesk_can_register_plugin', true, $this->plugin_info ) ) {
+			$registrator = new PluginRegistrator( $this->plugin_info );
+			$registrator->initialize_license_manager();
+
+			return $registrator;
+		}
+	}
+
+}
diff --git a/src/Extension/CommonBinding/WPDeskTrackerBridge.php b/src/Extension/CommonBinding/WPDeskTrackerBridge.php
new file mode 100644
index 0000000000000000000000000000000000000000..20660f99a7eb0e236805e60ea2c29963763c52a6
--- /dev/null
+++ b/src/Extension/CommonBinding/WPDeskTrackerBridge.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace WPDesk\Init\Extension\CommonBinding;
+
+use WPDesk\Init\Binding\Hookable;
+use WPDesk\Init\Plugin\Plugin;
+
+class WPDeskTrackerBridge implements Hookable {
+
+	/** @var Plugin */
+	private $plugin;
+
+	public function __construct( Plugin $plugin ) {
+		$this->plugin = $plugin;
+	}
+
+	public function hooks(): void {
+		$tracker_factory = new \WPDesk_Tracker_Factory_Prefixed();
+		$tracker_factory->create_tracker( $this->plugin->get_basename() );
+	}
+}
diff --git a/src/Extension/ConditionalExtension.php b/src/Extension/ConditionalExtension.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ecf12ff371a70a8de3b2c2d2000dc80a859b497
--- /dev/null
+++ b/src/Extension/ConditionalExtension.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Extension;
+
+use DI\Definition\Helper\AutowireDefinitionHelper;
+use Monolog\Formatter\LineFormatter;
+use Monolog\Logger;
+use Monolog\Processor\PsrLogMessageProcessor;
+use Monolog\Processor\UidProcessor;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use WPDesk\Init\Binding\Loader\ArrayDefinitions;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Extension\CommonBinding\RequirementsCheck;
+use WPDesk\Init\Extension\CommonBinding\WPDeskLicenseBridge;
+use WPDesk\Init\Extension\CommonBinding\WPDeskTrackerBridge;
+use WPDesk\Init\Plugin\Plugin;
+use WPDesk\Logger\WC\WooCommerceHandler;
+
+class ConditionalExtension implements Extension {
+
+	public function bindings( ContainerInterface $c ): BindingDefinitions {
+		$bindings = [];
+
+		if ( class_exists( \WPDesk_Basic_Requirement_Checker::class ) ) {
+			$bindings[] = RequirementsCheck::class;
+		}
+
+		if ( class_exists( \WPDesk\License\PluginRegistrator::class ) ) {
+			$bindings[] = WPDeskLicenseBridge::class;
+		}
+
+		if ( class_exists( \WPDesk_Tracker::class ) ) {
+			$bindings[] = WPDeskTrackerBridge::class;
+		}
+
+		return new ArrayDefinitions( $bindings );
+	}
+
+	public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void {
+		$definitions = [];
+
+		if ( class_exists( \WPDesk_Basic_Requirement_Checker::class ) ) {
+			$definitions[ RequirementsCheck::class ] = new AutowireDefinitionHelper();
+		}
+
+		if ( class_exists( \WPDesk\License\PluginRegistrator::class ) ) {
+			$definitions[ WPDeskLicenseBridge::class ] = new AutowireDefinitionHelper();
+		}
+
+		if ( class_exists( \WPDesk_Tracker::class ) ) {
+			$definitions[ WPDeskTrackerBridge::class ] = new AutowireDefinitionHelper();
+		}
+
+		if ( class_exists( \WPDesk\Logger\WC\WooCommerceHandler::class ) ) {
+			$definitions[ LoggerInterface::class ] = static function () use ( $plugin ) {
+				$logger = new Logger(
+					$plugin->get_slug(),
+					[],
+					[ new PsrLogMessageProcessor( null, true ), new UidProcessor() ]
+				);
+
+				$attach_handler = function () use ( $logger, $plugin ) {
+					$handler = new WooCommerceHandler( wc_get_logger(), $plugin->get_slug() );
+					$handler->setFormatter(
+						new LineFormatter( '%channel%.%level_name% [%extra.uid%]: %message% %context% %extra%' )
+					);
+					$logger->pushHandler( $handler );
+				};
+
+				if ( \function_exists( 'wc_get_logger' ) ) {
+					$attach_handler();
+				} else {
+					\add_action( 'woocommerce_init', $attach_handler );
+				}
+
+				return $logger;
+			};
+		}
+
+		$builder->add_definitions( $definitions );
+	}
+}
diff --git a/src/Extension/ConfigExtension.php b/src/Extension/ConfigExtension.php
new file mode 100644
index 0000000000000000000000000000000000000000..303c514b2b45b664140fd11ae754d79a2e74931d
--- /dev/null
+++ b/src/Extension/ConfigExtension.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Extension;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Loader\ArrayDefinitions;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+use WPDesk\Init\Binding\Loader\FilesystemDefinitions;
+use WPDesk\Init\Configuration\Configuration;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Plugin\Plugin;
+use WPDesk\Init\Util\Path;
+
+class ConfigExtension implements Extension {
+
+	public function bindings( ContainerInterface $c ): BindingDefinitions {
+		$config = $c->get( Configuration::class );
+		if ( $config->has( 'hook_resources_path' ) ) {
+			return new FilesystemDefinitions(
+				( new Path( $config->get( 'hook_resources_path' ) ) )->absolute( $c->get( Plugin::class )->get_path() )
+			);
+		}
+
+		return new ArrayDefinitions( [] );
+	}
+
+	public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void {
+		$services = array_map(
+			function ( $service ) use ( $plugin ) {
+				return (string) ( new Path( $service ) )->absolute( $plugin->get_path() );
+			},
+			(array) $config->get( 'services', [] )
+		);
+
+		$builder->add_definitions( ...$services );
+	}
+}
diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php
new file mode 100644
index 0000000000000000000000000000000000000000..a4dace150fbf41298dd8d195e16ae7f804f7f766
--- /dev/null
+++ b/src/Extension/Extension.php
@@ -0,0 +1,17 @@
+<?php
+declare(strict_types=1);
+
+namespace WPDesk\Init\Extension;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Plugin\Plugin;
+
+interface Extension {
+
+	public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void;
+
+	public function bindings(ContainerInterface $c): BindingDefinitions;
+}
diff --git a/src/Extension/ExtensionsSet.php b/src/Extension/ExtensionsSet.php
new file mode 100644
index 0000000000000000000000000000000000000000..650560e9e1418d636b274c24f68f02e2683e1519
--- /dev/null
+++ b/src/Extension/ExtensionsSet.php
@@ -0,0 +1,29 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Extension;
+
+/**
+ * @implements \IteratorAggregate<class-string, Extension>
+ */
+class ExtensionsSet implements \IteratorAggregate {
+
+	/** @var array<class-string<Extension>, Extension> */
+	private $extensions = [];
+
+	public function __construct( Extension ...$extensions ) {
+		foreach ( $extensions as $extension ) {
+			$this->add( $extension );
+		}
+	}
+
+	public function add( Extension $extension ): void {
+		$class                      = \get_class( $extension );
+		$this->extensions[ $class ] = $extension;
+	}
+
+	public function getIterator(): \Traversable {
+		return new \ArrayIterator( $this->extensions );
+	}
+}
diff --git a/src/Extension/LegacyExtension.php b/src/Extension/LegacyExtension.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f8a0bb94b0842cff25e1144ef5ca36a2310d0db
--- /dev/null
+++ b/src/Extension/LegacyExtension.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Extension;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+use WPDesk\Init\Binding\Loader\EmptyDefinitions;
+use WPDesk\Init\Configuration\ReadableConfig;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Plugin\Plugin;
+
+class LegacyExtension implements Extension {
+
+	public function build( ContainerBuilder $builder, Plugin $plugin, ReadableConfig $config ): void {
+		if ( ! $config->has( 'plugin_class_name' ) ) {
+			throw new \LogicException( 'To use legacy driver you must set "plugin_class_name" in your config pointing to the class name of your plugin.' );
+		}
+
+		$builder->add_definitions(
+			[
+				\WPDesk_Plugin_Info::class => $this->as_plugin_info( $plugin, $config ),
+			]
+		);
+	}
+
+	private function as_plugin_info( Plugin $plugin, ReadableConfig $config ): \WPDesk_Plugin_Info {
+		$plugin_info = new \WPDesk_Plugin_Info();
+		$plugin_info->set_plugin_file_name( $plugin->get_basename() );
+		$plugin_info->set_plugin_name( $plugin->get_name() );
+		$plugin_info->set_plugin_dir( $plugin->get_path() );
+		$plugin_info->set_version( $plugin->get_version() );
+		$plugin_info->set_text_domain( $plugin->get_slug() );
+		$plugin_info->set_plugin_url( $plugin->get_url() );
+
+		$plugin_info->set_class_name( $config->get( 'plugin_class_name' ) );
+
+		$plugin_info->set_product_id( $config->get( 'product_id' ) );
+		$plugin_info->set_plugin_shops( $config->get( 'plugin_shops' ) );
+
+		return $plugin_info;
+	}
+
+	public function bindings( ContainerInterface $c ): BindingDefinitions {
+		return new EmptyDefinitions();
+	}
+}
diff --git a/src/HookDriver/CompositeDriver.php b/src/HookDriver/CompositeDriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea17e1fc452da40e0af7aec9757b62dfbef2f724
--- /dev/null
+++ b/src/HookDriver/CompositeDriver.php
@@ -0,0 +1,24 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\HookDriver;
+
+final class CompositeDriver implements HookDriver {
+
+	/** @var HookDriver[] */
+	private $drivers;
+
+	public function __construct( HookDriver ...$drivers ) {
+		$this->drivers = $drivers;
+	}
+
+	public function register_hooks(): void {
+		foreach ( $this->drivers as $driver ) {
+			$driver->register_hooks();
+		}
+	}
+
+	public function add( HookDriver $driver ): void {
+		$this->drivers[] = $driver;
+	}
+}
diff --git a/src/HookDriver/GenericDriver.php b/src/HookDriver/GenericDriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..9602623755893ac92cb5e6c0ed6f0cae64311596
--- /dev/null
+++ b/src/HookDriver/GenericDriver.php
@@ -0,0 +1,36 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\HookDriver;
+
+use WPDesk\Init\Binding\Binder;
+use WPDesk\Init\Binding\Loader\BindingDefinitions;
+
+class GenericDriver implements HookDriver {
+
+	/** @var BindingDefinitions */
+	private $definitions;
+
+	/** @var Binder */
+	private $binder;
+
+	public function __construct( BindingDefinitions $definitions, Binder $binder ) {
+		$this->definitions = $definitions;
+		$this->binder      = $binder;
+	}
+
+	public function register_hooks(): void {
+		foreach ( $this->definitions->load() as $definition ) {
+			if ( $definition->hook() ) {
+				add_action(
+					$definition->hook(),
+					function () use ( $definition ) {
+						$this->binder->bind( $definition );
+					}
+				);
+			} else {
+				$this->binder->bind( $definition );
+			}
+		}
+	}
+}
diff --git a/src/HookDriver/HookDriver.php b/src/HookDriver/HookDriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..f65d802eb4a0851a72693e3f11561db379407a9f
--- /dev/null
+++ b/src/HookDriver/HookDriver.php
@@ -0,0 +1,19 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\HookDriver;
+
+/**
+ * Hook can be attached to WordPress in different ways, and this
+ * interface decouples possible methods from our initialization system,
+ * to make it more flexible.
+ *
+ * Even though hook registration is sort of the main purpose of this
+ * library, it's better to encapsulate hook registration in a separate
+ * class, so that it can be easily replaced with a different
+ * implementation and composition.
+ */
+interface HookDriver {
+
+	public function register_hooks(): void;
+}
diff --git a/src/HookDriver/Legacy/HookableParent.php b/src/HookDriver/Legacy/HookableParent.php
new file mode 100644
index 0000000000000000000000000000000000000000..f193a49b4a177924032d8ae8c75472b24cb7de7c
--- /dev/null
+++ b/src/HookDriver/Legacy/HookableParent.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace WPDesk\Init\HookDriver\Legacy;
+
+use WPDesk\PluginBuilder\Plugin\Conditional;
+use WPDesk\PluginBuilder\Plugin\Hookable;
+use WPDesk\PluginBuilder\Plugin\HookablePluginDependant;
+
+trait HookableParent {
+
+	/** @var HooksRegistry|null */
+	private $registry;
+
+	/**
+	 * @param class-string<Hookable>|Hookable $hookable_object
+	 */
+	public function add_hookable( $hookable_object ) {
+		if ( $this->registry === null ) {
+			$this->registry = HooksRegistry::instance();
+		}
+
+		$this->registry->add( $hookable_object );
+	}
+
+	/**
+	 * @param class-string<Hookable> $class_name
+	 *
+	 * @return false|Hookable
+	 */
+	public function get_hookable_instance_by_class_name( $class_name ) {
+		if ( $this->registry === null ) {
+			return false;
+		}
+
+		foreach ( $this->registry as $hookable_object ) {
+			if ( $hookable_object instanceof $class_name ) {
+				return $hookable_object;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Run hooks method on all hookable objects.
+	 */
+	protected function hooks_on_hookable_objects() {
+		if ( $this->registry === null ) {
+			return;
+		}
+
+		foreach ( $this->registry as $hookable_object ) {
+			if (
+				$hookable_object instanceof Conditional &&
+				! $hookable_object::is_needed()
+			) {
+				continue;
+			}
+
+			if ( $hookable_object instanceof HookablePluginDependant ) {
+				$hookable_object->set_plugin( $this );
+			}
+			$hookable_object->hooks();
+		}
+	}
+}
diff --git a/src/HookDriver/Legacy/HooksRegistry.php b/src/HookDriver/Legacy/HooksRegistry.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ef8730d9370d3d82d5fd34cbf74f435b7c8db47
--- /dev/null
+++ b/src/HookDriver/Legacy/HooksRegistry.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace WPDesk\Init\HookDriver\Legacy;
+
+use Psr\Container\ContainerInterface;
+use Traversable;
+use WPDesk\PluginBuilder\Plugin\Hookable;
+
+/**
+ * @implements IteratorAggregate<int,Hookable>
+ */
+final class HooksRegistry implements \IteratorAggregate {
+
+	private static $instance;
+
+	/** @var array<class-string<Hookable>|Hookable> */
+	private $callbacks = [];
+
+	private $container;
+
+	private function __construct() {}
+
+	public function inject_container( ContainerInterface $c ) {
+		$this->container = $c;
+	}
+
+	public static function instance(): HooksRegistry {
+		if ( self::$instance === null ) {
+			self::$instance = new self();
+		}
+
+		return self::$instance;
+	}
+
+	public function getIterator(): Traversable {
+		return new \ArrayIterator(
+			array_map(
+				function ( $hookable ) {
+					if ( is_string( $hookable ) ) {
+						return $this->container->get( $hookable );
+					}
+
+					return $hookable;
+				},
+				$this->callbacks
+			)
+		);
+	}
+
+	public function add( $hookable ) {
+		$this->callbacks[] = $hookable;
+	}
+}
diff --git a/src/HookDriver/LegacyDriver.php b/src/HookDriver/LegacyDriver.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff0e7e36657bcfe16da8d6fc3329d6897d8e4570
--- /dev/null
+++ b/src/HookDriver/LegacyDriver.php
@@ -0,0 +1,29 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\HookDriver;
+
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\HookDriver\Legacy\HooksRegistry;
+
+final class LegacyDriver implements HookDriver {
+
+	/** @var ContainerInterface */
+	private $container;
+
+	public function __construct( ContainerInterface $container ) {
+		if ( ! class_exists( \WPDesk_Plugin_Info::class ) ) {
+			throw new \LogicException( 'Legacy driver cannot be used as the plugin builder component is unavailable. Try running "composer require wpdesk/wp-builder".' );
+		}
+		$this->container = $container;
+	}
+
+	public function register_hooks(): void {
+		HooksRegistry::instance()->inject_container( $this->container );
+
+		$info       = $this->container->get( \WPDesk_Plugin_Info::class );
+		$class_name = $info->get_class_name();
+		$p          = new $class_name( $info );
+		add_action( 'plugins_loaded', [ $p, 'init' ], -50 );
+	}
+}
diff --git a/src/HookProvider/AbstractHookProvider.php b/src/HookProvider/AbstractHookProvider.php
deleted file mode 100644
index 2f87292718077dc336d7297a5c14f0bad70e8211..0000000000000000000000000000000000000000
--- a/src/HookProvider/AbstractHookProvider.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\PluginAwareInterface;
-use WPDesk\Init\PluginAwareTrait;
-
-abstract class AbstractHookProvider implements HooksProvider, PluginAwareInterface {
-	use PluginAwareTrait;
-
-}
\ No newline at end of file
diff --git a/src/HookProvider/ActivationDate.php b/src/HookProvider/ActivationDate.php
deleted file mode 100644
index 49772bb9570c7968e0ac1376b33354ae7f4f88e6..0000000000000000000000000000000000000000
--- a/src/HookProvider/ActivationDate.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\PluginAwareInterface;
-use WPDesk\Init\PluginAwareTrait;
-
-class ActivationDate implements HooksProvider, PluginAwareInterface {
-	use PluginAwareTrait;
-
-	public function register_hooks(): void {
-		add_action(
-			'activated_plugin',
-			function ( $plugin_file, $network_wide = false ) {
-				if ( ! $network_wide && $this->plugin->get_basename() === $plugin_file ) {
-					$option_name     = 'plugin_activation_' . $plugin_file;
-					$activation_date = get_option( $option_name, '' );
-					if ( '' === $activation_date ) {
-						$activation_date = current_time( 'mysql' );
-						update_option( $option_name, $activation_date );
-					}
-				}
-			}
-		);
-
-	}
-
-}
diff --git a/src/HookProvider/ActivationHook.php b/src/HookProvider/ActivationHook.php
deleted file mode 100644
index 290b9808d4bb489227e4ee1f0584e98f5d2bb9c1..0000000000000000000000000000000000000000
--- a/src/HookProvider/ActivationHook.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-abstract class ActivationHook extends AbstractHookProvider {
-
-	public function register_hooks(): void {
-		register_activation_hook(
-			$this->plugin->get_basename(),
-			[$this, 'activate']
-		);
-	}
-
-	abstract public function activate(): void;
-}
\ No newline at end of file
diff --git a/src/HookProvider/ContainerHookProvider.php b/src/HookProvider/ContainerHookProvider.php
deleted file mode 100644
index 2a6681e277f6fd9feb8b41dae367f229dcbfd0e8..0000000000000000000000000000000000000000
--- a/src/HookProvider/ContainerHookProvider.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-use WPDesk\Init\ContainerAwareInterface;
-use WPDesk\Init\ContainerAwareTrait;
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\PluginAwareInterface;
-use WPDesk\Init\PluginAwareTrait;
-
-class ContainerHookProvider implements HooksProvider, PluginAwareInterface, ContainerAwareInterface {
-	use PluginAwareTrait;
-	use ContainerAwareTrait;
-
-	public function register_hooks(): void {
-		add_action(
-			'plugins_loaded',
-			function () {
-				if ( $this->container->has( 'hooks' ) ) {
-					foreach ( $this->container->get( 'hooks' ) as $hook_provider ) {
-						$this->plugin->register_hooks( $hook_provider );
-					}
-				}
-			}
-		);
-	}
-
-}
diff --git a/src/HookProvider/DeactivationHook.php b/src/HookProvider/DeactivationHook.php
deleted file mode 100644
index 7a21a84c1e06ab08bfc0de184ba782bf524c38dd..0000000000000000000000000000000000000000
--- a/src/HookProvider/DeactivationHook.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-abstract class DeactivationHook extends AbstractHookProvider {
-
-	public function register_hooks(): void {
-		register_deactivation_hook(
-			$this->plugin->get_basename(),
-			[$this, 'deactivate']
-		);
-	}
-
-	abstract public function deactivate(): void;
-}
\ No newline at end of file
diff --git a/src/HookProvider/I18n.php b/src/HookProvider/I18n.php
deleted file mode 100644
index 7781e13c16223612722f58c0f7948addb36395e2..0000000000000000000000000000000000000000
--- a/src/HookProvider/I18n.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\PluginAwareInterface;
-use WPDesk\Init\PluginAwareTrait;
-
-class I18n implements HooksProvider, PluginAwareInterface {
-	use PluginAwareTrait;
-
-	public function register_hooks(): void {
-		if ( did_action( 'plugins_loaded' ) ) {
-			$this->load_textdomain();
-		} else {
-			add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] );
-		}
-	}
-
-	public function load_textdomain(): void {
-		$plugin_rel_path = dirname( $this->plugin->get_basename() ) . '/lang';
-		\load_plugin_textdomain( $this->plugin->get_slug(), false, $plugin_rel_path );
-	}
-}
diff --git a/src/HookProvider/WooCommerceHPOSCompatibility.php b/src/HookProvider/WooCommerceHPOSCompatibility.php
deleted file mode 100644
index 33e442706214a1020e3dedeed67997949ebe7d72..0000000000000000000000000000000000000000
--- a/src/HookProvider/WooCommerceHPOSCompatibility.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-declare( strict_types=1 );
-
-namespace WPDesk\Init\HookProvider;
-
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\PluginAwareInterface;
-use WPDesk\Init\PluginAwareTrait;
-
-class WooCommerceHPOSCompatibility implements HooksProvider, PluginAwareInterface {
-	use PluginAwareTrait;
-
-	public function register_hooks(): void {
-		add_action(
-			'before_woocommerce_init',
-			function () {
-				$features_util_class = '\\' . 'Automattic' . '\\' . 'WooCommerce' . '\\' . 'Utilities' . '\\' . 'FeaturesUtil';
-				if ( class_exists( $features_util_class ) ) {
-					$features_util_class::declare_compatibility( 'custom_order_tables', $this->plugin->get_basename(), true );
-				}
-			}
-		);
-	}
-}
diff --git a/src/HooksProvider.php b/src/HooksProvider.php
deleted file mode 100644
index 2266e86fb7ad2807a21d6cd8502e473f24dfeb41..0000000000000000000000000000000000000000
--- a/src/HooksProvider.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-/**
- * Hook is a special kind of service which lives only to integrate with WordPress lifecycle
- * system. By design, hook providers should be lightweight classes which focus its main logic on
- * integration with actions and filters.
- *
- * @author Brady Vercher
- */
-interface HooksProvider {
-
-	/**
-	 * Register hooks for the plugin.
-	 */
-	public function register_hooks(): void;
-
-}
\ No newline at end of file
diff --git a/src/HooksTrait.php b/src/HooksTrait.php
deleted file mode 100644
index 2318b0bf554569479dcb147433e0ee5a08e3e6fa..0000000000000000000000000000000000000000
--- a/src/HooksTrait.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-/**
- * Hooks trait.
- *
- * Allows protected and private methods to be used as hook callbacks in PHP <8.1. Since PHP 8.1
- * you are able to take advantage of first class callable and register private methods in hooks
- * without any workarounds.
- *
- * @author  John P. Bloch
- * @link    https://github.com/johnpbloch/wordpress-dev/blob/master/src/Hooks.php
- */
-trait HooksTrait {
-
-	/**
-	 * Add a WordPress filter.
-	 *
-	 * @param callable $method
-	 *
-	 * @return true
-	 */
-	protected function add_filter(
-		string $hook,
-		$method,
-		int $priority = 10,
-		int $arg_count = 1
-	): bool {
-		return add_filter(
-			$hook,
-			$this->map_filter( $method, $arg_count ),
-			$priority,
-			$arg_count
-		);
-	}
-
-	/**
-	 * Add a WordPress action.
-	 *
-	 * This is an alias of add_filter().
-	 *
-	 * @param callable $method
-	 *
-	 * @return true
-	 */
-	protected function add_action( string $hook, $method, int $priority = 10, int $arg_count = 1 ): bool {
-		return $this->add_filter( $hook, $method, $priority, $arg_count );
-	}
-
-	/**
-	 * Remove a WordPress filter.
-	 *
-	 * @param callable $method
-	 *
-	 * @return bool Whether the function existed before it was removed.
-	 */
-	protected function remove_filter(
-		string $hook,
-		$method,
-		int $priority = 10,
-		int $arg_count = 1
-	): bool {
-		return remove_filter(
-			$hook,
-			$this->map_filter( $method, $arg_count ),
-			$priority
-		);
-	}
-
-	/**
-	 * Remove a WordPress action.
-	 *
-	 * This is an alias of remove_filter().
-	 *
-	 * @param callable $method
-	 *
-	 * @return bool Whether the function is removed.
-	 */
-	protected function remove_action(
-		string $hook,
-		$method,
-		int $priority = 10,
-		int $arg_count = 1
-	): bool {
-		return $this->remove_filter( $hook, $method, $priority, $arg_count );
-	}
-
-	/**
-	 * Map a filter to a closure that inherits the class' internal scope.
-	 *
-	 * This allows hooks to use protected and private methods.
-	 *
-	 * @param string $callable
-	 * @param int $arg_count
-	 *
-	 * @return \Closure The callable actually attached to a WP hook
-	 */
-	private function map_filter( $callable, int $arg_count ): \Closure {
-		if ( is_string( $callable ) && method_exists( $this, $callable ) ) {
-			$object = $this;
-			$method = $callable;
-		}
-
-		if ( is_array( $callable ) ) {
-			[ $object, $method ] = $callable;
-		}
-
-		return static function () use ( $object, $method, $arg_count ) {
-			return $object->{$method}( ...array_slice( func_get_args(), 0, $arg_count ) );
-		};
-	}
-}
diff --git a/src/Init.php b/src/Init.php
new file mode 100644
index 0000000000000000000000000000000000000000..7d8e37e7bdf59de3b0333988beae7df3e0fa0e97
--- /dev/null
+++ b/src/Init.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * This file have to be compatible with PHP >=7.0 to gracefully handle outdated client's websites.
+ */
+
+namespace WPDesk\Init;
+
+use WPDesk\Init\Extension\LegacyExtension;
+use WPDesk\Init\Extension\BuiltinExtension;
+use WPDesk\Init\Extension\ConfigExtension;
+use WPDesk\Init\Extension\ExtensionsSet;
+use WPDesk\Init\Util\PhpFileLoader;
+use WPDesk\Init\Configuration\Configuration;
+use WPDesk\Init\Extension\ConditionalExtension;
+
+final class Init {
+
+	private static $bootable = true;
+
+	/** @var Configuration */
+	private $config;
+
+	/**
+	 * @param string|array|Configuration $config
+	 */
+	public static function setup( $config ) {
+		$result = require __DIR__ . '/platform_check.php';
+
+		if ( $result === false ) {
+			self::$bootable = false;
+		}
+
+		return new self( $config );
+	}
+
+	/**
+	 * @param string|array|Configuration $config
+	 */
+	public function __construct( $config ) {
+		if ( $config instanceof Configuration ) {
+			$this->config = $config;
+		} elseif ( \is_array( $config ) ) {
+			$this->config = new Configuration( $config );
+		} elseif ( \is_string( $config ) ) {
+			$loader       = new PhpFileLoader();
+			$this->config = new Configuration( $loader->load( $config ) );
+		} else {
+			throw new \InvalidArgumentException( sprintf( 'Configuration must be either path to configuration file, array of configuration data or %s instance', Configuration::class ) );
+		}
+	}
+
+	/**
+	 * @param string|null $filename Filename of the booted plugin. May be null, if called from plugin's main file.
+	 */
+	public function boot( ?string $filename = null ) {
+		if ( self::$bootable === false ) {
+			return;
+		}
+
+		if ( $filename === null ) {
+			$backtrace = \debug_backtrace( \DEBUG_BACKTRACE_IGNORE_ARGS, 1 );
+			$filename  = $backtrace[0]['file'];
+		}
+
+		$extensions = new ExtensionsSet(
+			new BuiltinExtension(),
+			new ConfigExtension(),
+			new ConditionalExtension()
+		);
+
+		if ( class_exists( \WPDesk_Plugin_Info::class ) ) {
+			$extensions->add( new LegacyExtension() );
+		}
+
+		$kernel = new Kernel( $filename, $this->config, $extensions );
+		$kernel->boot();
+	}
+}
diff --git a/src/Kernel.php b/src/Kernel.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e9ed9ba405ce22d92a7c29463a38f18fba58248
--- /dev/null
+++ b/src/Kernel.php
@@ -0,0 +1,137 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init;
+
+use DI\Container;
+use DI\ContainerBuilder as DiBuilder;
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Binder\CallableBinder;
+use WPDesk\Init\Binding\Binder\CompositeBinder;
+use WPDesk\Init\Binding\Binder\HookableBinder;
+use WPDesk\Init\Binding\Binder\StoppableBinder;
+use WPDesk\Init\Binding\Loader\CompositeBindingLoader;
+use WPDesk\Init\Configuration\Configuration;
+use WPDesk\Init\DependencyInjection\ContainerBuilder;
+use WPDesk\Init\Extension\ExtensionsSet;
+use WPDesk\Init\HookDriver\CompositeDriver;
+use WPDesk\Init\HookDriver\GenericDriver;
+use WPDesk\Init\HookDriver\HookDriver;
+use WPDesk\Init\HookDriver\LegacyDriver;
+use WPDesk\Init\Util\PhpFileLoader;
+use WPDesk\Init\Plugin\Header;
+use WPDesk\Init\Util\Path;
+use WPDesk\Init\Plugin\DefaultHeaderParser;
+use WPDesk\Init\Plugin\HeaderParser;
+use WPDesk\Init\Plugin\Plugin;
+
+final class Kernel {
+
+	/** @var string|null Plugin filename. */
+	private $filename;
+
+	/** @var Configuration */
+	private $config;
+
+	/** @var PhpFileLoader */
+	private $loader;
+
+	/** @var HeaderParser */
+	private $parser;
+
+	/** @var ExtensionsSet */
+	private $extensions;
+
+	public function __construct(
+		string $filename,
+		Configuration $config,
+		ExtensionsSet $extensions
+	) {
+		$this->filename   = $filename;
+		$this->config     = $config;
+		$this->extensions = $extensions;
+		$this->loader     = new PhpFileLoader();
+		$this->parser     = new DefaultHeaderParser();
+	}
+
+	public function boot(): void {
+		$cache_path = $this->get_cache_path( 'plugin.php' );
+		try {
+			$plugin_data = $this->loader->load( $cache_path );
+		} catch ( \Exception $e ) {
+			// If cache not found, load data from memory.
+			// Avoid writing files on host environment.
+			// Generate cache with command instead.
+			$plugin_data = $this->parser->parse( $this->filename );
+		}
+
+		$plugin = new Plugin( $this->filename, new Header( $plugin_data ) );
+
+		$container = $this->initialize_container( $plugin );
+		$container->set( Plugin::class, $plugin );
+		$container->set( Configuration::class, $this->config );
+
+		$this->prepare_driver( $container )->register_hooks();
+	}
+
+	private function get_cache_path( string $path = '' ): string {
+		return (string) ( new Path( $this->config->get( 'cache_path', 'generated' ) ) )->join( $path )->absolute(
+			rtrim( plugin_dir_path( $this->filename ), '/' ) . '/'
+		);
+	}
+
+	private function get_container_name( Plugin $plugin ): string {
+		return str_replace( '-', '_', $plugin->get_slug() ) . '_container';
+	}
+
+	private function initialize_container( Plugin $plugin ): Container {
+		$original_builder = new DiBuilder();
+
+		// If there's a cache file, use it as we are in production environment.
+		// Otherwise, build the container from scratch and use live version, without compilation.
+		if ( file_exists( $this->get_cache_path( $this->get_container_name( $plugin ) . '.php' ) ) ) {
+			$original_builder->enableCompilation(
+				$this->get_cache_path(),
+				$this->get_container_name( $plugin )
+			);
+			return $original_builder->build();
+		}
+
+		$builder = new ContainerBuilder( $original_builder );
+
+		foreach ( $this->extensions as $extension ) {
+			$extension->build( $builder, $plugin, $this->config );
+		}
+
+		return $builder->build();
+	}
+
+	private function prepare_driver( ContainerInterface $container ): HookDriver {
+		$loader = new CompositeBindingLoader();
+        foreach ($this->extensions as $extension) {
+            foreach ($extension->bindings($container) as $bindings) {
+                $loader->add($bindings);
+            }
+        }
+
+		$driver = new GenericDriver(
+			$loader,
+			new StoppableBinder(
+				new CompositeBinder(
+					new HookableBinder( $container ),
+					new CallableBinder( $container )
+				),
+				$container
+			)
+		);
+
+		if ( class_exists( \WPDesk_Plugin_Info::class ) ) {
+			$driver = new CompositeDriver(
+				$driver,
+				new LegacyDriver( $container )
+			);
+		}
+
+		return $driver;
+	}
+}
diff --git a/src/Plugin.php b/src/Plugin.php
deleted file mode 100644
index 3e070ba42ee6ad982859ab7d5151b5633748caec..0000000000000000000000000000000000000000
--- a/src/Plugin.php
+++ /dev/null
@@ -1,219 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-final class Plugin implements ContainerAwareInterface {
-	use ContainerAwareTrait;
-
-	/**
-	 * Plugin basename.
-	 *
-	 * Ex: plugin-name/plugin-name.php
-	 *
-	 * @var string
-	 */
-	protected $basename;
-
-	/**
-	 * Absolute path to the main plugin directory.
-	 *
-	 * @var string
-	 */
-	protected $directory;
-
-	/**
-	 * Plugin name to display.
-	 *
-	 * @var string
-	 */
-	private $name;
-
-	/**
-	 * Absolute path to the main plugin file.
-	 *
-	 * @var string
-	 */
-	protected $file;
-
-	/**
-	 * Plugin identifier.
-	 *
-	 * @var string
-	 */
-	protected $slug;
-
-	/**
-	 * URL to the main plugin directory.
-	 *
-	 * @var string
-	 */
-	protected $url;
-
-	/**
-	 * Retrieve the absolute path for the main plugin file.
-	 *
-	 * @return string
-	 */
-	public function get_basename(): string {
-		return $this->basename;
-	}
-
-	/**
-	 * Set the plugin basename.
-	 *
-	 * @param string $basename Relative path from the main plugin directory.
-	 *
-	 * @return $this
-	 */
-	public function set_basename( string $basename ): self {
-		$this->basename = $basename;
-
-		return $this;
-	}
-
-	/**
-	 * Retrieve the plugin directory.
-	 *
-	 * @return string
-	 */
-	public function get_directory(): string {
-		return $this->directory;
-	}
-
-	/**
-	 * Set the plugin's directory.
-	 *
-	 * @param string $directory Absolute path to the main plugin directory.
-	 *
-	 * @return $this
-	 */
-	public function set_directory( string $directory ): self {
-		$this->directory = rtrim( $directory, '/' ) . '/';
-
-		return $this;
-	}
-
-	/**
-	 * Retrieve the path to a file in the plugin.
-	 *
-	 * @param string $path Optional. Path relative to the plugin root.
-	 *
-	 * @return string
-	 */
-	public function get_path( string $path = '' ): string {
-		return $this->directory . ltrim( $path, '/' );
-	}
-
-	/**
-	 * Retrieve the absolute path for the main plugin file.
-	 *
-	 * @return string
-	 */
-	public function get_file(): string {
-		return $this->file;
-	}
-
-	/**
-	 * Set the path to the main plugin file.
-	 *
-	 * @param string $file Absolute path to the main plugin file.
-	 *
-	 * @return $this
-	 */
-	public function set_file( string $file ): self {
-		$this->file = $file;
-
-		return $this;
-	}
-
-	/**
-	 * @param string $name
-	 *
-	 * @return self
-	 */
-	public function set_name( string $name ): self {
-		$this->name = $name;
-
-		return $this;
-	}
-
-	/**
-	 * @return string
-	 */
-	public function get_name(): string {
-		return $this->name ?? $this->get_slug();
-	}
-
-	/**
-	 * Retrieve the plugin identifier.
-	 *
-	 * @return string
-	 */
-	public function get_slug(): string {
-		return $this->slug;
-	}
-
-	/**
-	 * Set the plugin identifier.
-	 *
-	 * @param string $slug Plugin identifier.
-	 *
-	 * @return $this
-	 */
-	public function set_slug( string $slug ) {
-		$this->slug = $slug;
-
-		return $this;
-	}
-
-	/**
-	 * Retrieve the URL for a file in the plugin.
-	 *
-	 * @param string $path Optional. Path relative to the plugin root.
-	 *
-	 * @return string
-	 */
-	public function get_url( string $path = '' ) {
-		return $this->url . ltrim( $path, '/' );
-	}
-
-	/**
-	 * Set the URL for plugin directory root.
-	 *
-	 * @param string $url URL to the root of the plugin directory.
-	 *
-	 * @return $this
-	 */
-	public function set_url( string $url ) {
-		$this->url = rtrim( $url, '/' ) . '/';
-
-		return $this;
-	}
-
-	/**
-	 * Register hooks for the plugin.
-	 *
-	 * @param HooksProvider ...$providers
-	 *
-	 * @return void
-	 */
-	public function register_hooks( HooksProvider ...$providers ): void {
-		foreach ( $providers as $provider ) {
-			if ( $provider instanceof PluginAwareInterface ) {
-				$provider->set_plugin( $this );
-			}
-
-			if ( $provider instanceof ContainerAwareInterface ) {
-				$provider->set_container( $this->container );
-			}
-
-			if ( $provider instanceof Conditional && ! $provider->is_needed() ) {
-				continue;
-			}
-
-			$provider->register_hooks();
-		}
-	}
-
-}
diff --git a/src/Plugin/DefaultHeaderParser.php b/src/Plugin/DefaultHeaderParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..7cc48dcb15a4b11c256e2f4d4794601a75b8c708
--- /dev/null
+++ b/src/Plugin/DefaultHeaderParser.php
@@ -0,0 +1,142 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Plugin;
+
+class DefaultHeaderParser implements HeaderParser {
+
+	private const KB_IN_BYTES = 1024;
+	private const HEADERS     = [
+		'Name'            => 'Plugin Name',
+		'PluginURI'       => 'Plugin URI',
+		'Version'         => 'Version',
+		'Author'          => 'Author',
+		'AuthorURI'       => 'Author URI',
+		'TextDomain'      => 'Text Domain',
+		'DomainPath'      => 'Domain Path',
+		'Network'         => 'Network',
+		'RequiresWP'      => 'Requires at least',
+		'RequiresWC'      => 'WC requires at least',
+		'RequiresPHP'     => 'Requires PHP',
+		'TestedWP'        => 'Tested up to',
+		'TestedWC'        => 'WC tested up to',
+		'UpdateURI'       => 'Update URI',
+		'RequiresPlugins' => 'Requires Plugins',
+	];
+
+	/**
+	 * Parses the plugin contents to retrieve plugin's metadata.
+	 * All plugin headers must be on their own line. Plugin description must not have
+	 * any newlines, otherwise only parts of the description will be displayed.
+	 * The below is formatted for printing.
+	 *     /*
+	 *     Plugin Name: Name of the plugin.
+	 *     Plugin URI: The home page of the plugin.
+	 *     Author: Plugin author's name.
+	 *     Author URI: Link to the author's website.
+	 *     Version: Plugin version.
+	 *     Text Domain: Optional. Unique identifier, should be same as the one used in
+	 *          load_plugin_textdomain().
+	 *     Domain Path: Optional. Only useful if the translations are located in a
+	 *          folder above the plugin's base path. For example, if .mo files are
+	 *          located in the locale folder then Domain Path will be "/locale/" and
+	 *          must have the first slash. Defaults to the base folder the plugin is
+	 *          located in.
+	 *     Network: Optional. Specify "Network: true" to require that a plugin is activated
+	 *          across all sites in an installation. This will prevent a plugin from being
+	 *          activated on a single site when Multisite is enabled.
+	 *     Requires at least: Optional. Specify the minimum required WordPress version.
+	 *     Requires PHP: Optional. Specify the minimum required PHP version.
+	 *     * / # Remove the space to close comment.
+	 * The first 8 KB of the file will be pulled in and if the plugin data is not
+	 * within that first 8 KB, then the plugin author should correct their plugin
+	 * and move the plugin data headers to the top.
+	 * The plugin file is assumed to have permissions to allow for scripts to read
+	 * the file. This is not checked however and the file is only opened for
+	 * reading.
+	 *
+	 * @param string $plugin_file Absolute path to the main plugin file.
+	 *
+	 * @return array{
+	 *     Name: string,
+	 *     PluginURI?: string,
+	 *     Version?: string,
+	 *     Author?: string,
+	 *     AuthorURI?: string,
+	 *     TextDomain?: string,
+	 *     DomainPath?: string,
+	 *     Network?: bool,
+	 *     RequiresWP?: string,
+	 *     RequiresWC?: string,
+	 *     RequiresPHP?: string,
+	 *     TestedWP?: string,
+	 *     TestedWC?: string,
+	 *     UpdateURI?: string,
+	 * }
+	 */
+	public function parse( string $plugin_file ): array {
+
+		$plugin_data = $this->get_file_data( $plugin_file, self::HEADERS );
+
+		if ( isset( $plugin_data['Network'] ) ) {
+			$plugin_data['Network'] = filter_var( $plugin_data['Network'], \FILTER_VALIDATE_BOOLEAN );
+		}
+
+		// If no text domain is defined fall back to the plugin slug.
+		if ( empty( $plugin_data['TextDomain'] ) ) {
+			$plugin_slug = dirname( $plugin_file );
+			if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) {
+				$plugin_data['TextDomain'] = $plugin_slug;
+			}
+		}
+
+		return $plugin_data;
+	}
+
+	/**
+	 * Retrieves metadata from a file.
+	 * Searches for metadata in the first 8 KB of a file, such as a plugin or theme.
+	 * Each piece of metadata must be on its own line. Fields can not span multiple
+	 * lines, the value will get cut at the end of the first line.
+	 * If the file data is not within that first 8 KB, then the author should correct
+	 * their plugin file and move the data headers to the top.
+	 *
+	 * @link  https://codex.wordpress.org/File_Header
+	 * @since 2.9.0
+	 *
+	 * @param string $file Absolute path to the file.
+	 * @param array $default_headers List of headers, in the format `array( 'HeaderKey' => 'Header
+	 *                                Name' )`.
+	 *
+	 * @return string[] Array of file header values keyed by header name.
+	 */
+	private function get_file_data( string $file, array $default_headers ): array {
+		// Pull only the first 8 KB of the file in.
+		$file_data = file_get_contents( $file, false, null, 0, 8 * self::KB_IN_BYTES );
+
+		if ( false === $file_data ) {
+			$file_data = '';
+		}
+
+		// Make sure we catch CR-only line endings.
+		$file_data = \str_replace( "\r", "\n", $file_data );
+
+		$headers = [];
+		foreach ( $default_headers as $field => $regex ) {
+			if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
+				$headers[ $field ] = $this->_cleanup_header_comment( $match[1] );
+			}
+		}
+
+		return $headers;
+	}
+
+	/**
+	 * Strips close comment and close php tags from file headers used by WP.
+	 *
+	 * @see    https://core.trac.wordpress.org/ticket/8497
+	 */
+	private function _cleanup_header_comment( string $str ): string {
+		return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
+	}
+}
diff --git a/src/Plugin/Header.php b/src/Plugin/Header.php
new file mode 100644
index 0000000000000000000000000000000000000000..97beba37909f75e3e31baafcb886fe1bf8945a9c
--- /dev/null
+++ b/src/Plugin/Header.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Plugin;
+
+final class Header implements \ArrayAccess {
+
+	/** @var array */
+	private $header_data;
+
+	public function __construct( array $header_data ) {
+		$this->header_data = $header_data;
+	}
+
+	public function offsetExists( $offset ): bool {
+		if ( ! is_string( $offset ) ) {
+			throw new \InvalidArgumentException( 'Header key must be a string' );
+		}
+
+		return $this->has( $offset );
+	}
+
+	#[\ReturnTypeWillChange]
+	public function offsetGet( $offset ) {
+		if ( ! is_string( $offset ) ) {
+			throw new \InvalidArgumentException( 'Header key must be a string' );
+		}
+
+		return $this->get( $offset );
+	}
+
+	public function offsetSet( $offset, $value ): void {
+		throw new \BadMethodCallException( 'Header cannot be modified' );
+	}
+
+	public function offsetUnset( $offset ): void {
+		throw new \BadMethodCallException( 'Header cannot be modified' );
+	}
+
+	public function get( string $key ) {
+		return $this->header_data[ $key ];
+	}
+
+	public function has( string $key ): bool {
+		return isset( $this->header_data[ $key ] );
+	}
+}
diff --git a/src/Plugin/HeaderParser.php b/src/Plugin/HeaderParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b265046b6ab6dfb8e82d1e1b57d17ea297faa20
--- /dev/null
+++ b/src/Plugin/HeaderParser.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace WPDesk\Init\Plugin;
+
+interface HeaderParser {
+
+	public function parse( string $plugin_file ): array;
+}
diff --git a/src/Plugin/Plugin.php b/src/Plugin/Plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..5fd3c2a450b8d242c7ec154e45878427f05ba8e3
--- /dev/null
+++ b/src/Plugin/Plugin.php
@@ -0,0 +1,125 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Plugin;
+
+final class Plugin {
+
+	/**
+	 * Plugin basename.
+	 *
+	 * Ex: plugin-name/plugin-name.php
+	 *
+	 * @var string
+	 */
+	private $basename;
+
+	/**
+	 * Absolute path to the main plugin directory.
+	 *
+	 * @var string
+	 */
+	private $directory;
+
+	/**
+	 * Plugin name to display.
+	 *
+	 * @var string
+	 */
+	private $name;
+
+	/**
+	 * Absolute path to the main plugin file.
+	 *
+	 * @var string
+	 */
+	private $file;
+
+	/**
+	 * Plugin identifier.
+	 *
+	 * @var string
+	 */
+	private $slug;
+
+	/**
+	 * URL to the main plugin directory.
+	 *
+	 * @var string
+	 */
+	private $url;
+
+	/**
+	 * Plugin version string.
+	 *
+	 * @var string
+	 */
+	private $version;
+
+	/**
+	 * @var Header
+	 */
+	private $header;
+
+	public function __construct( string $file, Header $header ) {
+		$this->file      = $file;
+		$this->name      = $header['Name'];
+		$this->version   = $header['Version'] ?? '0.0.0';
+		$this->basename  = plugin_basename( $file );
+		$this->directory = rtrim( plugin_dir_path( $file ), '/' ) . '/';
+		$this->url       = rtrim( plugin_dir_url( $file ), '/' ) . '/';
+		$this->slug      = $header['TextDomain'] ?? basename( $this->directory );
+		$this->header    = $header;
+	}
+
+	/**
+	 * Retrieve the absolute path for the main plugin file.
+	 */
+	public function get_basename(): string {
+		return $this->basename;
+	}
+
+	/**
+	 * Retrieve the path to a file in the plugin.
+	 *
+	 * @param string $path Optional. Path relative to the plugin root.
+	 */
+	public function get_path( string $path = '' ): string {
+		return $this->directory . ltrim( $path, '/' );
+	}
+
+	/**
+	 * Retrieve the absolute path for the main plugin file.
+	 */
+	public function get_file(): string {
+		return $this->file;
+	}
+
+	public function get_name(): string {
+		return $this->name;
+	}
+
+	/**
+	 * Retrieve the plugin identifier.
+	 */
+	public function get_slug(): string {
+		return $this->slug;
+	}
+
+	/**
+	 * Retrieve the URL for a file in the plugin.
+	 *
+	 * @param string $path Optional. Path relative to the plugin root.
+	 */
+	public function get_url( string $path = '' ): string {
+		return $this->url . ltrim( $path, '/' );
+	}
+
+	public function get_version(): string {
+		return $this->version;
+	}
+
+	public function header(): Header {
+		return $this->header;
+	}
+}
diff --git a/src/PluginAwareInterface.php b/src/PluginAwareInterface.php
deleted file mode 100644
index 4b6c9895cd05ea41191a119bbc6a5c5431424b0b..0000000000000000000000000000000000000000
--- a/src/PluginAwareInterface.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-/**
- * Plugin aware interface.
- *
- * @author Brady Vercher
- */
-interface PluginAwareInterface {
-	/**
-	 * Set the main plugin instance.
-	 */
-	public function set_plugin( Plugin $plugin ): void;
-}
\ No newline at end of file
diff --git a/src/PluginAwareTrait.php b/src/PluginAwareTrait.php
deleted file mode 100644
index 558688066e2bdc13ca8aa07b0b848613346d459d..0000000000000000000000000000000000000000
--- a/src/PluginAwareTrait.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-/**
- * @author Brady Vercher
- */
-trait PluginAwareTrait {
-	/** @var Plugin */
-	protected $plugin;
-
-	/**
-	 * Set the main plugin instance.
-	 */
-	public function set_plugin( Plugin $plugin ): void {
-		$this->plugin = $plugin;
-	}
-}
\ No newline at end of file
diff --git a/src/PluginInit.php b/src/PluginInit.php
deleted file mode 100644
index ed6f2fa6d5e18b23d5a54d43be7b6ac190130f3e..0000000000000000000000000000000000000000
--- a/src/PluginInit.php
+++ /dev/null
@@ -1,173 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init;
-
-use DI\ContainerBuilder;
-use DI\Definition\Source\DefinitionSource;
-use Psr\Container\ContainerInterface;
-
-/**
- * Plugin builder class responsible for our initialization system.
- */
-final class PluginInit {
-
-	/** @var array */
-	private $requirements = [];
-
-	/** @var array Dependency injection container definitions */
-	private $definitions = [];
-
-	/** @var string|null Plugin filename. */
-	private $filename;
-
-	/** @var string|null Plugin identifier. */
-	private $slug;
-
-	/**
-	 * Define environment constraints required by a plugin. If requirements are not fulfilled,
-	 * the plugin will not be able to instantiate.
-	 *
-	 * @param array $requirements Validation rules are defined according to `wp-basic-requirements`
-	 * documentation.
-	 *
-	 * @see https://gitlab.wpdesk.dev/wpdesk/wp-basic-requirements
-	 */
-	public function set_requirements( array $requirements ): self {
-		$this->requirements = $requirements;
-
-		return $this;
-	}
-
-	/**
-	 * Define plugin slug. This value will be used as main plugin identifier and as translation
-	 * text domain. By default, plugin slug is set to plugin directory name.
-	 */
-	public function set_slug( string $slug ): self {
-		$this->slug = $slug;
-
-		return $this;
-	}
-
-	/**
-	 * Explicitly set name of main plugin file used for reference. This value should be set with
-	 * caution, as filename is the most important identifier, which allows us to retreive further
-	 * plugin data, such as basename, plugin dir and url. By default, this value is set to
-	 * original caller's file name.
-	 */
-	public function set_filename( string $filename ): self {
-		$this->filename = $filename;
-
-		return $this;
-	}
-
-	/**
-	 * Add definition sources to use in dependency injection container.
-	 * If plugin doesn't receive any definitions, then container build is skipped.
-	 *
-	 * @see https://php-di.org/doc/container-configuration.html
-	 *
-	 * @param array|DefinitionSource|string $definitions
-	 */
-	public function add_container_definitions( ...$definitions ): self {
-		$this->definitions = array_merge( $this->definitions, $definitions );
-
-		return $this;
-	}
-
-	/**
-	 * Build and return a plugin.
-	 *
-	 * @return Plugin|null If plugin failed to build (e.g. requirements are not fulfilled),
-	 * initialization process returns null. There are no exceptions thrown on foreseeable issues
-	 * as those cases should be handled gracefully, by displaying admin notice if possible and
-	 * preventing to initialize plugin functions without disrupting a website.
-	 */
-	public function init(): ?Plugin {
-		if ( empty( $this->filename ) ) {
-			$backtrace      = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, 1 );
-			$this->filename = $backtrace[0]['file'];
-		}
-
-		$plugin = $this->create_plugin();
-
-		if (
-			! empty( $this->requirements ) &&
-			class_exists( \WPDesk_Basic_Requirement_Checker_Factory::class ) &&
-			! $this->requirements_met( $plugin )
-		) {
-			return null;
-		}
-
-		if ( ! empty( $this->definitions ) && class_exists( ContainerBuilder::class ) ) {
-			$plugin->set_container( $this->build_container( $plugin ) );
-		}
-
-		return $this->register_default_providers( $plugin );
-	}
-
-	private function build_container( Plugin $plugin ): ContainerInterface {
-		$builder = new ContainerBuilder();
-
-		if ( ! getenv( 'WPDESK_DEVELOPMENT' ) ) {
-			$builder->enableCompilation(
-				$plugin->get_path( '/cache' ),
-				str_replace( '-', '_', $plugin->get_slug() ) . '_container'
-			);
-		}
-
-		$builder->addDefinitions( ...$this->definitions );
-
-		return $builder->build();
-	}
-
-	private function register_default_providers( Plugin $plugin ): Plugin {
-		$plugin->register_hooks(
-			new HookProvider\I18n(),
-			new HookProvider\WooCommerceHPOSCompatibility(),
-			new HookProvider\ActivationDate()
-		);
-
-		if ( ! empty( $this->definitions ) ) {
-			$plugin->register_hooks( new HookProvider\ContainerHookProvider() );
-		}
-
-		return $plugin;
-	}
-
-
-	private function create_plugin(): Plugin {
-		$plugin = new Plugin();
-		$plugin->set_file( $this->filename )
-		       ->set_basename( plugin_basename( $this->filename ) )
-		       ->set_directory( plugin_dir_path( $this->filename ) )
-		       ->set_url( plugin_dir_url( $this->filename ) )
-		       ->set_slug( $this->slug ?? basename( $plugin->get_directory() ) );
-
-		return $plugin;
-	}
-
-	/**
-	 * @param Plugin $plugin
-	 *
-	 * @return bool
-	 */
-	private function requirements_met( Plugin $plugin ): bool {
-		$requirements_factory = new \WPDesk_Basic_Requirement_Checker_Factory();
-		$requirements         = $requirements_factory->create_from_requirement_array(
-			$plugin->get_basename(),
-			$plugin->get_name(),
-			$this->requirements,
-			$plugin->get_slug()
-		);
-
-		if ( ! $requirements->are_requirements_met() ) {
-			$requirements->render_notices();
-
-			return false;
-		}
-
-		return true;
-	}
-
-}
diff --git a/src/Resources/bindings/index.php b/src/Resources/bindings/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..511616180dc2e65c555f55efd70441aa6c036ad3
--- /dev/null
+++ b/src/Resources/bindings/index.php
@@ -0,0 +1,9 @@
+<?php
+
+use WPDesk\Init\Extension\CommonBinding\I18n;
+use WPDesk\Init\Extension\CommonBinding\CustomOrderTableCompatibility;
+
+return [
+	I18n::class,
+	CustomOrderTableCompatibility::class,
+];
diff --git a/src/Resources/services.inc.php b/src/Resources/services.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..276657c7c550d1ac829ffb9cebc78604a2604b55
--- /dev/null
+++ b/src/Resources/services.inc.php
@@ -0,0 +1,16 @@
+<?php
+
+use DI\Definition\Helper\AutowireDefinitionHelper;
+use WPDesk\Init\Extension\CommonBinding\CustomOrdersTableCompatibility;
+use WPDesk\Init\Extension\CommonBinding\I18n;
+
+return [
+	wpdb::class                           => static function () {
+		global $wpdb;
+
+		return $wpdb;
+	},
+
+	I18n::class                           => new AutowireDefinitionHelper(),
+	CustomOrdersTableCompatibility::class => new AutowireDefinitionHelper(),
+];
diff --git a/src/Util/Path.php b/src/Util/Path.php
new file mode 100644
index 0000000000000000000000000000000000000000..a7cf66fde392accc8bc0ec0cdd45eaf12e6b1859
--- /dev/null
+++ b/src/Util/Path.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WPDesk\Init\Util;
+
+final class Path {
+
+	/** @var string */
+	private $path;
+
+	public function __construct( string $path ) {
+		$this->path = $path;
+	}
+
+	public function canonical(): self {
+		$root = str_starts_with( $this->path, '/' ) ? '/' : '';
+		return new self( $root . implode( '/', $this->find_canonical_parts() ) );
+	}
+
+	public function absolute( ?string $base_path = null ): self {
+		$base_path = $base_path ?? getcwd();
+		return ( new self( rtrim( $base_path, '/\\' ) . '/' . $this->path ) )->canonical();
+	}
+
+	private function find_canonical_parts(): array {
+		$parts = explode( '/', $this->path );
+		$root  = str_starts_with( $this->path, '/' ) ? '/' : '';
+
+		$canonical_parts = [];
+
+		// Collapse "." and "..", if possible
+		foreach ( $parts as $part ) {
+			if ( '.' === $part || '' === $part ) {
+				continue;
+			}
+
+			// Collapse ".." with the previous part, if one exists
+			// Don't collapse ".." if the previous part is also ".."
+			if ( '..' === $part && \count( $canonical_parts ) > 0 && '..' !== $canonical_parts[ \count( $canonical_parts ) - 1 ] ) {
+				array_pop( $canonical_parts );
+
+				continue;
+			}
+
+			// Only add ".." prefixes for relative paths
+			if ( '..' !== $part || '' === $root ) {
+				$canonical_parts[] = $part;
+			}
+		}
+
+		return $canonical_parts;
+	}
+
+	public function is_directory(): bool {
+		return is_dir( $this->path );
+	}
+
+	public function is_file(): bool {
+		return is_file( $this->path );
+	}
+
+	public function get_basename(): string {
+		return basename( $this->path );
+	}
+
+	public function get_filename_without_extension(): string {
+		return pathinfo( $this->path, \PATHINFO_FILENAME );
+	}
+
+	public function join( string ...$parts ): self {
+		return new self( $this->path . '/' . implode( '/', $parts ) );
+	}
+
+	/** @return self[] */
+	public function read_directory(): array {
+		if ( ! $this->is_directory() ) {
+			throw new \InvalidArgumentException( sprintf( 'Path "%s" is not a directory', $this->path ) );
+		}
+
+		return array_map(
+			function ( $file ) {
+				return ( new self( $file ) )->absolute( $this->path );
+			},
+			array_values(
+				array_diff(
+					scandir( $this->path ),
+					[ '.', '..' ]
+				)
+			)
+		);
+	}
+
+	public function __toString(): string {
+		return $this->path;
+	}
+}
diff --git a/src/Util/PhpFileDumper.php b/src/Util/PhpFileDumper.php
new file mode 100644
index 0000000000000000000000000000000000000000..8bc45f9644b4a3bebca72143d0dae353395972b7
--- /dev/null
+++ b/src/Util/PhpFileDumper.php
@@ -0,0 +1,51 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Util;
+
+class PhpFileDumper {
+
+	public function dump( array $config, string $filename ): void {
+		$directory = dirname( $filename );
+		$this->createCompilationDirectory( $directory );
+
+		$content  = '<?php' . PHP_EOL . PHP_EOL;
+		$content .= 'declare(strict_types=1);' . PHP_EOL . PHP_EOL;
+		$content .= 'return ' . var_export( $config, true ) . ';' . PHP_EOL;
+
+		$this->writeFileAtomic( $filename, $content );
+	}
+
+	private function createCompilationDirectory( string $directory ): void {
+		if ( ! is_dir( $directory ) && ! @mkdir( $directory, 0777, true ) && ! is_dir( $directory ) ) {
+			throw new \InvalidArgumentException( sprintf( 'Compilation directory does not exist and cannot be created: %s.', $directory ) );
+		}
+		if ( ! is_writable( $directory ) ) {
+			throw new \InvalidArgumentException( sprintf( 'Compilation directory is not writable: %s.', $directory ) );
+		}
+	}
+
+	private function writeFileAtomic( string $fileName, string $content ): void {
+		$tmpFile = @tempnam( dirname( $fileName ), 'swap-compile' );
+		if ( $tmpFile === false ) {
+			throw new \InvalidArgumentException(
+				sprintf( 'Error while creating temporary file in %s', dirname( $fileName ) )
+			);
+		}
+		@chmod( $tmpFile, 0666 );
+
+		$written = file_put_contents( $tmpFile, $content );
+		if ( $written === false ) {
+			@unlink( $tmpFile );
+
+			throw new \InvalidArgumentException( sprintf( 'Error while writing to %s', $tmpFile ) );
+		}
+
+		@chmod( $tmpFile, 0666 );
+		$renamed = @rename( $tmpFile, $fileName );
+		if ( ! $renamed ) {
+			@unlink( $tmpFile );
+			throw new \InvalidArgumentException( sprintf( 'Error while renaming %s to %s', $tmpFile, $fileName ) );
+		}
+	}
+}
diff --git a/src/Util/PhpFileLoader.php b/src/Util/PhpFileLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..81209c46c0eff17b968e557eff3dfeeb0c79de03
--- /dev/null
+++ b/src/Util/PhpFileLoader.php
@@ -0,0 +1,27 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Util;
+
+class PhpFileLoader {
+
+	/**
+	 * @param string|Path $resource
+	 *
+	 * @return mixed
+	 */
+	public function load( $resource ) {
+		return ( static function () use ( $resource ) {
+			if ( ! is_readable( (string) $resource ) ) {
+				throw new \RuntimeException( "Could not load $resource" );
+			}
+
+			$data = include (string) $resource;
+			if ( $data === false ) {
+				throw new \RuntimeException( "Could not load $resource" );
+			}
+
+			return $data;
+		} )();
+	}
+}
diff --git a/src/platform_check.php b/src/platform_check.php
new file mode 100644
index 0000000000000000000000000000000000000000..c90ebb7b3fec272bba3d7b947c215216f527cf09
--- /dev/null
+++ b/src/platform_check.php
@@ -0,0 +1,16 @@
+<?php
+if ( ! ( PHP_VERSION_ID >= 70200 ) ) {
+	add_action(
+		'admin_notices',
+		function () {
+			printf(
+				'<div class="notice notice-error"><p>%s</p></div>',
+				__( 'The plugin cannot run on PHP versions older than 7.2. Please, contact your host and ask them to upgrade.', 'wp-init' )
+			);
+		}
+	);
+
+	return false;
+}
+
+return true;
diff --git a/tests/Binding/ArrayDefinitionsTest.php b/tests/Binding/ArrayDefinitionsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c6f965d17e432691a0e6405f015f165dd18a9fbe
--- /dev/null
+++ b/tests/Binding/ArrayDefinitionsTest.php
@@ -0,0 +1,67 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\Binding;
+
+use WPDesk\Init\Binding\Definition\UnknownDefinition;
+use WPDesk\Init\Binding\Loader\ArrayDefinitions;
+use WPDesk\Init\Tests\TestCase;
+
+class ArrayDefinitionsTest extends TestCase {
+
+	public function test_loading_empty_bindings(): void {
+		$a = new ArrayDefinitions([]);
+		$this->assertEquals(0, iterator_count($a->load()));
+	}
+
+	public function test_loading_structured_bindings(): void {
+		$a = new ArrayDefinitions([
+			'hook' => [
+				'bind1',
+				'bind2',
+			],
+			'hook2' => [
+				'bind3',
+			]
+		]);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', 'hook'),
+				new UnknownDefinition('bind2', 'hook'),
+				new UnknownDefinition('bind3', 'hook2'),
+			],
+			iterator_to_array($a->load())
+		);
+	}
+
+	public function test_loading_unstructured_bindings(): void {
+		$a = new ArrayDefinitions([
+			'bind1',
+			'bind2',
+			'hook' => 'bind3',
+		]);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', null),
+				new UnknownDefinition('bind2', null),
+				new UnknownDefinition('bind3', 'hook'),
+			],
+			iterator_to_array($a->load())
+		);
+
+		$a = new ArrayDefinitions([
+			'bind1',
+			'not_a_hook' => 'bind2',
+			'hook' => ['bind3'],
+		]);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', null),
+				new UnknownDefinition('bind2', 'not_a_hook'),
+				new UnknownDefinition('bind3', 'hook'),
+			],
+			iterator_to_array($a->load())
+		);
+	}
+
+}
diff --git a/tests/Binding/CompositeBindingLoaderTest.php b/tests/Binding/CompositeBindingLoaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b8e2a992fd3e0f786c6f6f27eb5caed3f877d7dc
--- /dev/null
+++ b/tests/Binding/CompositeBindingLoaderTest.php
@@ -0,0 +1,87 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\Binding;
+
+use WPDesk\Init\Binding\Definition\UnknownDefinition;
+use WPDesk\Init\Binding\Loader\ArrayDefinitions;
+use WPDesk\Init\Binding\Loader\CompositeBindingLoader;
+use WPDesk\Init\Tests\TestCase;
+
+class CompositeBindingLoaderTest extends TestCase {
+
+	public function test_loading_empty_bindings(): void {
+		$a = new CompositeBindingLoader(new ArrayDefinitions([]));
+		$this->assertEquals(0, iterator_count($a->load()));
+	}
+
+	public function test_loading_structured_bindings(): void {
+		$a = new CompositeBindingLoader(
+			new ArrayDefinitions(
+				[
+					'hook' => [
+						'bind1',
+						'bind2',
+					],
+				]
+			),
+			new ArrayDefinitions(
+				[
+					'hook2' => [
+						'bind3',
+					]
+				]
+			)
+		);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', 'hook'),
+				new UnknownDefinition('bind2', 'hook'),
+				new UnknownDefinition('bind3', 'hook2'),
+			],
+			iterator_to_array($a->load(), false)
+		);
+	}
+
+	public function test_loading_unstructured_bindings(): void {
+		$a = new CompositeBindingLoader(
+			new ArrayDefinitions([
+				'bind1',
+			]),
+			new ArrayDefinitions([
+				'bind2',
+			]),
+			new ArrayDefinitions([
+				'hook' => 'bind3',
+			])
+		);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', null),
+				new UnknownDefinition('bind2', null),
+				new UnknownDefinition('bind3', 'hook'),
+			],
+			iterator_to_array($a->load(), false)
+		);
+
+		$a = new CompositeBindingLoader(
+			new ArrayDefinitions([
+				'bind1',
+			]),
+			new ArrayDefinitions([
+			'not_a_hook' => 'bind2',
+			]),
+			new ArrayDefinitions([
+			'hook' => ['bind3'],
+			]),
+		);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('bind1', null),
+				new UnknownDefinition('bind2', 'not_a_hook'),
+				new UnknownDefinition('bind3', 'hook'),
+			],
+			iterator_to_array($a->load(), false)
+		);
+	}
+}
diff --git a/tests/Binding/DirectoryBasedLoaderTest.php b/tests/Binding/DirectoryBasedLoaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..68cacc30567e70ee3d9ca90770e442dc4eefcc4c
--- /dev/null
+++ b/tests/Binding/DirectoryBasedLoaderTest.php
@@ -0,0 +1,48 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\Binding;
+
+use WPDesk\Init\Binding\Definition\UnknownDefinition;
+use WPDesk\Init\Binding\Loader\FilesystemDefinitions;
+use WPDesk\Init\Configuration\Configuration;
+use WPDesk\Init\Tests\TestCase;
+
+class DirectoryBasedLoaderTest extends TestCase {
+
+	public function xtest_throws_when_configuration_entry_is_missing(): void {
+		$this->expectException(\InvalidArgumentException::class);
+		$a = new FilesystemDefinitions(new Configuration([]));
+		$a->load();
+	}
+
+	public function test_loading_empty_bindings(): void {
+		$this->initTempPlugin('hook-bindings');
+		$a = new FilesystemDefinitions('./');
+		$actual = iterator_to_array($a->load(), false);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('binding', 'hook1'),
+				new UnknownDefinition('binding1', 'plugins_loaded'),
+				new UnknownDefinition('binding2', 'plugins_loaded'),
+			],
+			$actual
+		);
+	}
+
+	public function test_load_illogical_bindings(): void {
+		$this->initTempPlugin('borked-bindings');
+		$a = new FilesystemDefinitions('./');
+
+		$actual = iterator_to_array($a->load(), false);
+		$this->assertEquals(
+			[
+				new UnknownDefinition('binding', 'hook1'),
+				new UnknownDefinition('binding1', 'plugins_loaded'),
+				new UnknownDefinition('binding2', 'plugins_loaded'),
+			],
+			$actual
+		);
+	}
+
+}
diff --git a/tests/DefaultHeaderParserTest.php b/tests/DefaultHeaderParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f3f2a40ff7c2679cb88788a8664858d1c5eebe80
--- /dev/null
+++ b/tests/DefaultHeaderParserTest.php
@@ -0,0 +1,67 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests;
+
+use WPDesk\Init\Plugin\DefaultHeaderParser;
+
+class DefaultHeaderParserTest extends TestCase {
+
+	/** @dataProvider provider */
+	public function test_should_parse_plugin_data_from_file( $name,  string $content, array $expected ): void {
+		$file = $this->createTempFile($name, $content);
+
+		$data = new DefaultHeaderParser();
+		$this->assertEquals( $expected, $data->parse( $file ) );
+	}
+
+	public function provider(): iterable {
+		yield [
+			'first.php',
+<<<PHP
+<?php
+/**
+ * Plugin Name: Example plugin
+ */
+PHP,
+			[ 'Name' => 'Example plugin' ],
+		];
+
+		yield [
+			'second.php',
+<<<PHP
+<?php
+/**
+ * Plugin Name: ShopMagic for WooCommerce
+ * Plugin URI: https://shopmagic.app/
+ * Description: Marketing Automation and Custom Email Designer for WooCommerce
+ * Version: 3.0.9-beta.1
+ * Author: WP Desk
+ * Author URI: https://shopmagic.app/
+ * Text Domain: shopmagic-for-woocommerce
+ * Domain Path: /lang/
+ * Requires at least: 5.0
+ * Tested up to: 6.1
+ * WC requires at least: 4.8
+ * WC tested up to: 7.2
+ * Requires PHP: 7.2
+ */
+PHP,
+			[
+				'Name'        => 'ShopMagic for WooCommerce',
+				'PluginURI'   => 'https://shopmagic.app/',
+				'Version'     => '3.0.9-beta.1',
+				'Author'      => 'WP Desk',
+				'AuthorURI'   => 'https://shopmagic.app/',
+				'TextDomain'  => 'shopmagic-for-woocommerce',
+				'DomainPath'  => '/lang/',
+				'RequiresWP'  => '5.0',
+				'RequiresWC'  => '4.8',
+				'RequiresPHP' => '7.2',
+				'TestedWP'    => '6.1',
+				'TestedWC'    => '7.2',
+],
+		];
+	}
+
+}
diff --git a/tests/Dumper/PhpFileDumperTest.php b/tests/Dumper/PhpFileDumperTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..84460038121f0a2b3a734cdb440529b2e54f7454
--- /dev/null
+++ b/tests/Dumper/PhpFileDumperTest.php
@@ -0,0 +1,20 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\Dumper;
+
+use WPDesk\Init\Util\PhpFileDumper;
+
+class PhpFileDumperTest extends \WPDesk\Init\Tests\TestCase {
+
+	public function test_dump_php_file() {
+		$dir    = $this->initTempPlugin();
+		$dumper = new PhpFileDumper();
+		$dumper->dump( [ 'foo' => 'bar' ], $dir . '/dump.php' );
+		$this->assertFileExists( $dir . '/dump.php' );
+		$content = include $dir . '/dump.php';
+
+		$this->assertEquals( [ 'foo' => 'bar' ], $content );
+	}
+
+}
diff --git a/tests/Fixtures/advanced-plugin/advanced-plugin.php b/tests/Fixtures/advanced-plugin/advanced-plugin.php
index fb4b39f5fbc2b92c17678c9e313fc52d0f561de5..15e95871193b73fb258f7cc2b43563a5b27c1ae8 100644
--- a/tests/Fixtures/advanced-plugin/advanced-plugin.php
+++ b/tests/Fixtures/advanced-plugin/advanced-plugin.php
@@ -1,21 +1,29 @@
 <?php
 
-$plugin = ( new \WPDesk\Init\PluginInit() )
-	->set_requirements( [
-		'wp'  => '5.6',
-		'php' => '7.2'
-	] )
-	->add_container_definitions( [
-		'hello' => 'world',
-		'hooks' => [
-			'anonymous' => static function () {
-				return new class implements \WPDesk\Init\HooksProvider {
+/**
+ * Plugin Name: ShopMagic for WooCommerce
+ * Plugin URI: https://shopmagic.app/
+ * Description: Marketing Automation and Custom Email Designer for WooCommerce
+ * Version: 3.0.9-beta.1
+ * Author: WP Desk
+ * Author URI: https://shopmagic.app/
+ * Text Domain: shopmagic-for-woocommerce
+ * Domain Path: /lang/
+ * Requires at least: 5.0
+ * Tested up to: 6.1
+ * WC requires at least: 4.8
+ * WC tested up to: 7.2
+ * Requires PHP: 7.2
+ */
 
-					public function register_hooks(): void {
-						// TODO: Implement register_hooks() method.
-					}
-				};
-			}
-		],
-	] )
-	->init();
+$plugin = ( new \WPDesk\Init\Kernel( [
+	'bundles'               => [
+		\WPDesk\Init\Bundle\ContainerBundle::class
+	],
+	'cache_path'            => 'generated',
+	'require'               => [],
+	'container_definitions' => [],
+	'hook_subscribers'      => [
+		\WPDesk\Init\Bundle\ContainerBundle::class
+	],
+] ) )->boot();
diff --git a/tests/Fixtures/borked-bindings/index.php b/tests/Fixtures/borked-bindings/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f43e3641b1077874f255149c27237746f012755
--- /dev/null
+++ b/tests/Fixtures/borked-bindings/index.php
@@ -0,0 +1,5 @@
+<?php
+
+return [
+	'hook1' => 'binding',
+];
diff --git a/tests/Fixtures/borked-bindings/plugins_loaded.php b/tests/Fixtures/borked-bindings/plugins_loaded.php
new file mode 100644
index 0000000000000000000000000000000000000000..67fa7b2ff4c37c32beebefd8eb1a986a07e913fc
--- /dev/null
+++ b/tests/Fixtures/borked-bindings/plugins_loaded.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+	'hook1' => 'binding1',
+	'binding2',
+];
diff --git a/tests/Fixtures/hook-bindings/index.php b/tests/Fixtures/hook-bindings/index.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f43e3641b1077874f255149c27237746f012755
--- /dev/null
+++ b/tests/Fixtures/hook-bindings/index.php
@@ -0,0 +1,5 @@
+<?php
+
+return [
+	'hook1' => 'binding',
+];
diff --git a/tests/Fixtures/hook-bindings/plugins_loaded.php b/tests/Fixtures/hook-bindings/plugins_loaded.php
new file mode 100644
index 0000000000000000000000000000000000000000..fc53751f7825ecd25afd538e9b11917db736f03c
--- /dev/null
+++ b/tests/Fixtures/hook-bindings/plugins_loaded.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+	'binding1',
+	'binding2',
+];
diff --git a/tests/Fixtures/load.php b/tests/Fixtures/load.php
new file mode 100644
index 0000000000000000000000000000000000000000..1733709e027a5f5be9a429672e3be627ee9938f8
--- /dev/null
+++ b/tests/Fixtures/load.php
@@ -0,0 +1,5 @@
+<?php
+
+return [
+	'foo' => 'bar'
+];
diff --git a/tests/Fixtures/simple-plugin/config.php b/tests/Fixtures/simple-plugin/config.php
new file mode 100644
index 0000000000000000000000000000000000000000..63da9d1507e1849521f2401cf3f9b745182096ea
--- /dev/null
+++ b/tests/Fixtures/simple-plugin/config.php
@@ -0,0 +1,20 @@
+<?php
+
+return [
+	'bundles'   => [
+		new class {
+			public static function subscribers(): array {
+				return [
+					'WPDesk\Init\Tests\Fixtures\SimplePlugin\SimplePluginSubscriber',
+				];
+			}
+		}
+	],
+	'hookables' => [
+		new class implements \WPDesk\Init\HookProvider\HookProvider {
+			public function hooks(): void {
+				// TODO: Implement hooks() method.
+			}
+		}
+	]
+];
\ No newline at end of file
diff --git a/tests/Fixtures/simple-plugin/simple-plugin.php b/tests/Fixtures/simple-plugin/simple-plugin.php
index 5a22cd05e86bfe3767943183cc20df1659b5bf74..7be21fd683e4597fa075ea42bfb980eafd4a59e5 100644
--- a/tests/Fixtures/simple-plugin/simple-plugin.php
+++ b/tests/Fixtures/simple-plugin/simple-plugin.php
@@ -1,3 +1,6 @@
 <?php
+/**
+ * Plugin Name: Example plugin
+ */
 
-$plugin = ( new \WPDesk\Init\PluginInit() )->init();
\ No newline at end of file
+$plugin = ( new \WPDesk\Init\Kernel( 'config.php' ) )->boot();
diff --git a/tests/HookDriver/GenericDriverTest.php b/tests/HookDriver/GenericDriverTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ce92478304e5937f25ca752d80a7191be955d31d
--- /dev/null
+++ b/tests/HookDriver/GenericDriverTest.php
@@ -0,0 +1,107 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\HookDriver;
+
+use WPDesk\Init\Binding\Binder;
+use WPDesk\Init\Binding\Binder\HookableBinder;
+use WPDesk\Init\Binding\Binder\ObservableBinder;
+use WPDesk\Init\Binding\Definition;
+use WPDesk\Init\HookDriver\GenericDriver;
+use WPDesk\Init\Configuration\Configuration;
+use Psr\Container\ContainerInterface;
+use WPDesk\Init\Binding\Loader\ArrayDefinitions;
+use WPDesk\Init\Tests\TestCase;
+
+class GenericDriverTest extends TestCase {
+
+	public function provider(): iterable {
+		yield [
+				'fake_binder' => new ObservableBinder(new class implements Binder {
+
+					public function bind( Definition $def ): void {
+					}
+				}),
+			function ( $binder ) {
+				$this->assertTrue( $binder->is_bound() );
+			}
+		];
+
+// 		yield 'interrupted with stoppable binder' => [
+// 			[
+// 				'stoppable_binder' => new class implements StoppableBinder {
+//
+// 					public function should_stop(): bool {
+// 						return true;
+// 					}
+//
+// 					private $is_bound = false;
+// 					public function bind(): void {
+// 						$this->is_bound = true;
+// 					}
+//
+// 					public function is_bound(): bool {
+// 						return $this->is_bound;
+// 					}
+// 				},
+// 				'fake_binder' => new ObservableBinder(new class implements Binder {
+//
+// 					public function bind(): void {
+// 					}
+// 				}),
+// 			],
+// 			function ( $binder ) {
+// 				$this->assertTrue( $binder->is_bound() );
+// 			}
+// 		];
+	}
+
+	public function test_register_no_hooks(): void {
+		$binder = new ObservableBinder($this->getBinder());
+		$driver = new GenericDriver(
+			new ArrayDefinitions([]),
+			$binder
+		);
+
+		$driver->register_hooks();
+
+		$this->assertEquals(0, $binder->binds_count());
+	}
+
+	public function test_register_hooks(): void {
+		$binder = new ObservableBinder($this->getBinder());
+		$driver = new GenericDriver(
+			new ArrayDefinitions(['' => ['hook1', 'hook2']]),
+			$binder
+		);
+
+		$driver->register_hooks();
+
+		$this->assertEquals(2, $binder->binds_count());
+	}
+
+	private function getContainer( array $services ): ContainerInterface {
+		return new class($services) implements ContainerInterface {
+			private $services;
+			public function __construct( $services ) {
+				$this->services = $services;
+			}
+
+			public function get( $id ) {
+				return $this->services[$id];
+			}
+
+			public function has(string $id ): bool {
+				return isset( $this->services[$id] );
+			}
+		};
+	}
+
+	private function getBinder(): Binder {
+		return new class implements Binder {
+
+			public function bind( Definition $def ): void {
+			}
+		};
+	}
+}
diff --git a/tests/HooksTraitTest.php b/tests/HooksTraitTest.php
deleted file mode 100644
index b9ba4d053249a9aefb791a32cdd180baf8cd8573..0000000000000000000000000000000000000000
--- a/tests/HooksTraitTest.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\Tests;
-
-class HooksTraitTest extends \PHPUnit\Framework\TestCase {
-
-	/**
-	 * How you can call a hook:
-	 * 1. callable string: '__return_true'
-	 * 2. closure: function () { echo "hello"; }
-	 * 3. static call: [stdClass::class, 'hello']
-	 * 4. class instance call: [ $this, 'hello' ]
-	 * 5. invokable object: $this
-	 * ! Since PHP 8.1
-	 * 6. first-class callable: __return_true(...), $this->hello(...)
-	 */
-
-}
\ No newline at end of file
diff --git a/tests/Loader/PhpFileLoaderTest.php b/tests/Loader/PhpFileLoaderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b996b7e9d634d4e460b582ed44d3b72d17d2a2fc
--- /dev/null
+++ b/tests/Loader/PhpFileLoaderTest.php
@@ -0,0 +1,17 @@
+<?php
+declare( strict_types=1 );
+
+namespace WPDesk\Init\Tests\Loader;
+
+use WPDesk\Init\Util\PhpFileLoader;
+
+class PhpFileLoaderTest extends \WPDesk\Init\Tests\TestCase {
+
+	public function test_load_php_file() {
+		$loader   = new PhpFileLoader();
+		$resource = __DIR__ . '/../Fixtures/load.php';
+		$data     = $loader->load( $resource );
+		$this->assertEquals( [ 'foo' => 'bar' ], $data );
+	}
+
+}
diff --git a/tests/PluginInitTest.php b/tests/PluginInitTest.php
index fe5d97d7b98089a8eb25989767d4a202accf79d7..888748f9da6056d3f69d8548f5c88724ed58d554 100644
--- a/tests/PluginInitTest.php
+++ b/tests/PluginInitTest.php
@@ -3,8 +3,8 @@ declare( strict_types=1 );
 
 namespace WPDesk\Init\Tests;
 
-use WPDesk\Init\Plugin;
-use WPDesk\Init\PluginInit;
+use WPDesk\Init\Plugin\Plugin;
+use WPDesk\Init\Kernel;
 use Brain\Monkey;
 
 class PluginInitTest extends TestCase {
@@ -20,36 +20,10 @@ class PluginInitTest extends TestCase {
 		parent::tearDown();
 	}
 
-	public function test_minimal_init(): void {
-		Monkey\Functions\stubs([
-			'plugin_basename',
-			'plugin_dir_url'
-		]);
+	public function xtest_initialization(): void {
+		$this->initTempPlugin('simple-plugin');
 
-		$slug   = 'simple-plugin';
-		$dir    = $this->initTempPlugin( $slug );
-		$plugin = $this->load_plugin_file( $dir, $slug );
-
-		$this->assertFileDoesNotExist( $dir . '/cache' );
-		$this->assertEquals( 'simple-plugin', $plugin->get_slug() );
-	}
-
-	public function test_advanced_init(): void {
-		Monkey\Functions\stubs([
-			'plugin_basename',
-			'plugin_dir_url'
-		]);
-		Monkey\Functions\expect('get_bloginfo')
-			->with('version')
-			->andReturn('5.6');
-
-		$slug = 'advanced-plugin';
-		$dir  = $this->initTempPlugin( $slug );
-
-		$plugin = $this->load_plugin_file( $dir, $slug );
-
-		$this->assertNotNull($plugin);
-		$this->assertFileExists( $dir . '/cache' );
+		(new Kernel([]))->boot();
 	}
 
 	private function load_plugin_file( $dir, $slug ): ?Plugin {
@@ -61,4 +35,4 @@ class PluginInitTest extends TestCase {
 
 		return $load();
 	}
-}
\ No newline at end of file
+}
diff --git a/tests/PluginTest.php b/tests/PluginTest.php
deleted file mode 100644
index 54fc2a7a1a4da46ef2384980345d5e316fc1e3a8..0000000000000000000000000000000000000000
--- a/tests/PluginTest.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace WPDesk\Init\Tests;
-
-use Psr\Container\ContainerInterface;
-use WPDesk\Init\Conditional;
-use WPDesk\Init\ContainerAwareInterface;
-use WPDesk\Init\HooksProvider;
-use WPDesk\Init\Plugin;
-use WPDesk\Init\PluginAwareInterface;
-
-class PluginTest extends \PHPUnit\Framework\TestCase {
-
-	public function test_should_register_hook_provider(): void {
-		$plugin = new Plugin();
-
-		$provider = new class implements HooksProvider {
-			public $called = false;
-
-			public function register_hooks(): void {
-				$this->called = true;
-			}
-		};
-
-		$plugin->register_hooks( $provider );
-
-		$this->assertTrue( $provider->called );
-	}
-
-	public function test_should_not_register_failing_conditional_hook_provider(): void {
-		$plugin = new Plugin();
-
-		$provider = new class implements HooksProvider, Conditional {
-			public $called = false;
-
-			public function is_needed(): bool {
-				return false;
-			}
-
-			public function register_hooks(): void {
-				$this->called = true;
-			}
-		};
-
-		$plugin->register_hooks( $provider );
-
-		$this->assertFalse( $provider->called );
-	}
-
-	public function test_should_inject_plugin_on_provider(): void {
-		$plugin = new Plugin();
-
-		$provider = new class implements HooksProvider, PluginAwareInterface {
-			public $called = false;
-			public $plugin;
-
-			public function set_plugin( Plugin $plugin ): void {
-				$this->plugin = $plugin;
-			}
-
-			public function register_hooks(): void {
-				$this->called = true;
-			}
-		};
-
-		$plugin->register_hooks( $provider );
-
-		$this->assertSame( $plugin, $provider->plugin );
-	}
-
-	public function test_should_inject_container_on_provider(): void {
-		$plugin = new Plugin();
-		$container = new class implements ContainerInterface {
-
-			public function get( string $id ) {
-				// TODO: Implement get() method.
-			}
-
-			public function has( string $id ): bool {
-				return true;
-			}
-		};
-		$plugin->set_container( $container );
-
-		$provider = new class implements HooksProvider, ContainerAwareInterface {
-			public $called = false;
-			public $container;
-
-			public function set_container( ContainerInterface $container ): void {
-				$this->container = $container;
-			}
-
-			public function register_hooks(): void {
-				$this->called = true;
-			}
-		};
-
-		$plugin->register_hooks( $provider );
-
-		$this->assertSame( $container, $provider->container );
-	}
-
-}
\ No newline at end of file
diff --git a/tests/TestCase.php b/tests/TestCase.php
index d3462cbd98a0c58eaf0eb3fc93f430add44b17b1..ba3a16d9d8c85d6c8e3bc83cb71f05a9caf291df 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -6,11 +6,9 @@ namespace WPDesk\Init\Tests;
 use Symfony\Component\Filesystem\Filesystem;
 
 class TestCase extends \PHPUnit\Framework\TestCase {
-	/** @var string|null */
 	private ?string $prevCwd = null;
 
-	/** @var array */
-	private array $tempPluginDirs = [];
+	protected array $tempPluginDirs = [];
 
 	public static function getUniqueTmpDirectory( string $suffix = '' ): string {
 		$attempts = 5;
@@ -42,6 +40,13 @@ class TestCase extends \PHPUnit\Framework\TestCase {
 		return $dir;
 	}
 
+	public function createTempFile( string $name, string $content ): string {
+		$dir = self::getUniqueTmpDirectory();
+		$fs  = new Filesystem();
+		$fs->dumpFile( $dir . '/' . $name, $content );
+		return $dir . '/' . $name;
+	}
+
 	protected function tearDown(): void {
 		parent::tearDown();
 		if ( null !== $this->prevCwd ) {
@@ -73,4 +78,4 @@ class TestCase extends \PHPUnit\Framework\TestCase {
 	}
 
 
-}
\ No newline at end of file
+}
diff --git a/tests/Util/PathTest.php b/tests/Util/PathTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e21fcd5b3e9db404c82f88986c568c020ef683f9
--- /dev/null
+++ b/tests/Util/PathTest.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace WPDesk\Init\Tests\Util;
+
+use WPDesk\Init\Tests\TestCase;
+use WPDesk\Init\Util\Path;
+
+class PathTest extends TestCase {
+
+	public function test_canonical_path(): void
+    {
+        $path = new Path('src/unit/../etc');
+        $this->assertEquals('src/etc', (string) $path->canonical());
+    }
+
+    public function test_absolute_path(): void
+    {
+        $path = new Path('src');
+        $this->assertEquals(getcwd().'/src', (string) $path->absolute());
+    }
+
+    public function test_join(): void
+    {
+        $path = new Path('/var/www');
+        $joinedPath = $path->join('public', 'html', 'index.php');
+        $this->assertEquals('/var/www/public/html/index.php', (string) $joinedPath);
+    }
+
+    public function test_get_basename(): void
+    {
+        $path = new Path('/var/www/public/html/index.php');
+        $this->assertEquals('index.php', $path->get_basename());
+    }
+
+    public function test_get_filename_without_extension(): void
+    {
+        $path = new Path('/var/www/public/html/index.php');
+        $this->assertEquals('index', $path->get_filename_without_extension());
+    }
+
+    public function test_read_directory(): void
+    {
+        $path = new Path(__DIR__ . '/../Fixtures/hook-bindings/');
+        $dirContent = $path->read_directory();
+        $this->assertEquals(getcwd() . '/tests/Fixtures/hook-bindings/index.php', (string) $dirContent[0]);
+    }
+
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 733441b514f558ab78146d4adf64669d1d0c6496..0ecc16b872be88c251106b193cafde9d1a96174b 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,5 +1,3 @@
 <?php
 
-require __DIR__ . '/../vendor/autoload.php';
-
-WP_Mock::bootstrap();
\ No newline at end of file
+require __DIR__ . '/../vendor/autoload.php';
\ No newline at end of file
diff --git a/tests/generated/plugin.php b/tests/generated/plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..a0057af93b0e7bd2ded056e4f3a88bf3ae5703f0
--- /dev/null
+++ b/tests/generated/plugin.php
@@ -0,0 +1,6 @@
+<?php
+
+declare(strict_types=1);
+
+return array (
+);