From 3be8563870963442b0455a353e02c459d51ebd19 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 23 Nov 2023 17:43:56 +0000
Subject: [PATCH 1/4] Refactor app container to modularity.
---
bootstrap.php | 74 +-
composer.json | 6 +-
composer.lock | 70 +-
.../Container/ContainerConfigurator.php | 153 ++++
.../Container/PackageProxyContainer.php | 102 +++
.../Container/ReadOnlyContainer.php | 138 ++++
.../Modularity/Module/ExecutableModule.php | 21 +
.../Modularity/Module/ExtendingModule.php | 24 +
.../Modularity/Module/FactoryModule.php | 18 +
.../Inpsyde/Modularity/Module/Module.php | 20 +
.../Module/ModuleClassNameIdTrait.php | 23 +
.../Modularity/Module/ServiceModule.php | 21 +
lib/packages/Inpsyde/Modularity/Package.php | 727 ++++++++++++++++++
.../Modularity/Properties/BaseProperties.php | 217 ++++++
.../Properties/LibraryProperties.php | 209 +++++
.../Properties/PluginProperties.php | 176 +++++
.../Modularity/Properties/Properties.php | 139 ++++
.../Modularity/Properties/ThemeProperties.php | 131 ++++
.../Container/ContainerExceptionInterface.php | 3 -
.../Psr/Container/ContainerInterface.php | 9 +-
.../Container/NotFoundExceptionInterface.php | 3 -
src/DhiiToModularityModule.php | 128 +++
22 files changed, 2367 insertions(+), 45 deletions(-)
create mode 100644 lib/packages/Inpsyde/Modularity/Container/ContainerConfigurator.php
create mode 100644 lib/packages/Inpsyde/Modularity/Container/PackageProxyContainer.php
create mode 100644 lib/packages/Inpsyde/Modularity/Container/ReadOnlyContainer.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/ExecutableModule.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/ExtendingModule.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/FactoryModule.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/Module.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/ModuleClassNameIdTrait.php
create mode 100644 lib/packages/Inpsyde/Modularity/Module/ServiceModule.php
create mode 100644 lib/packages/Inpsyde/Modularity/Package.php
create mode 100644 lib/packages/Inpsyde/Modularity/Properties/BaseProperties.php
create mode 100644 lib/packages/Inpsyde/Modularity/Properties/LibraryProperties.php
create mode 100644 lib/packages/Inpsyde/Modularity/Properties/PluginProperties.php
create mode 100644 lib/packages/Inpsyde/Modularity/Properties/Properties.php
create mode 100644 lib/packages/Inpsyde/Modularity/Properties/ThemeProperties.php
create mode 100644 src/DhiiToModularityModule.php
diff --git a/bootstrap.php b/bootstrap.php
index fe4552110..fe9a0799b 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -11,8 +11,11 @@
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\DelegatingContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ProxyContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
+use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Package;
+use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties\PluginProperties;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
+use WooCommerce\PayPalCommerce\DhiiToModularityModule;
return function (
string $root_dir,
@@ -34,38 +37,47 @@
*/
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
- $providers = array_map(
- function ( ModuleInterface $module ): ServiceProviderInterface {
- return $module->setup();
- },
- $modules
- );
+ // Initialize plugin.
+ $properties = PluginProperties::new(__FILE__);
+ $bootstrap = Package::new($properties);
- $provider = new CompositeCachingServiceProvider( $providers );
- $proxy_container = new ProxyContainer();
- // TODO: caching does not work currently,
- // may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
- // for now not fixed since we were using this behavior for long time and fixing it now may break things.
- $container = new DelegatingContainer( $provider );
- /**
- * Skip iterable vs array check.
- *
- * @psalm-suppress PossiblyInvalidArgument
- */
- $app_container = new CachingContainer(
- new CompositeContainer(
- array_merge(
- $additional_containers,
- array( $container )
- )
- )
- );
- $proxy_container->setInnerContainer( $app_container );
+ $bootstrap->addModule( new DhiiToModularityModule( $modules ) );
+ $bootstrap->boot();
+
+ return $bootstrap->container();
- foreach ( $modules as $module ) {
- /* @var $module ModuleInterface module */
- $module->run( $app_container );
- }
+// $providers = array_map(
+// function ( ModuleInterface $module ): ServiceProviderInterface {
+// return $module->setup();
+// },
+// $modules
+// );
- return $app_container;
+// $provider = new CompositeCachingServiceProvider( $providers );
+// $proxy_container = new ProxyContainer();
+// // TODO: caching does not work currently,
+// // may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
+// // for now not fixed since we were using this behavior for long time and fixing it now may break things.
+// $container = new DelegatingContainer( $provider );
+// /**
+// * Skip iterable vs array check.
+// *
+// * @psalm-suppress PossiblyInvalidArgument
+// */
+// $app_container = new CachingContainer(
+// new CompositeContainer(
+// array_merge(
+// $additional_containers,
+// array( $container )
+// )
+// )
+// );
+// $proxy_container->setInnerContainer( $app_container );
+//
+// foreach ( $modules as $module ) {
+// /* @var $module ModuleInterface module */
+// $module->run( $app_container );
+// }
+//
+// return $app_container;
};
diff --git a/composer.json b/composer.json
index bb2ccbf7c..96c9459ef 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
"wikimedia/composer-merge-plugin": "^2.0",
"wp-oop/wordpress-interface": "^0.1.0-alpha1",
"dhii/versions": "^0.1.0-alpha1",
- "symfony/polyfill-php80": "^1.19"
+ "symfony/polyfill-php80": "^1.19",
+ "inpsyde/modularity": "^1.7"
},
"require-dev": {
"psr/container": "^1.0",
@@ -77,7 +78,8 @@
"packages": [
"psr/container",
"dhii/containers",
- "dhii/module-interface"
+ "dhii/module-interface",
+ "inpsyde/modularity"
],
"delete_vendor_directories": true
}
diff --git a/composer.lock b/composer.lock
index 450c96e96..36ce94f4e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "97559eb2b9d8b6f1529d44abd2604f99",
+ "content-hash": "214504278540f7c3d05f10d1e67920b1",
"packages": [
{
"name": "container-interop/service-provider",
@@ -294,6 +294,74 @@
},
"time": "2021-12-08T16:54:50+00:00"
},
+ {
+ "name": "inpsyde/modularity",
+ "version": "1.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/inpsyde/modularity.git",
+ "reference": "43f4b3a7c8fbb585a44fb7a619e2546b338a8934"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/inpsyde/modularity/zipball/43f4b3a7c8fbb585a44fb7a619e2546b338a8934",
+ "reference": "43f4b3a7c8fbb585a44fb7a619e2546b338a8934",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=7.2",
+ "psr/container": "^1.1.0 || ^2"
+ },
+ "require-dev": {
+ "brain/monkey": "^2.6.1",
+ "inpsyde/php-coding-standards": "^1",
+ "johnpbloch/wordpress-core": ">=5.8",
+ "mikey179/vfsstream": "^v1.6.10",
+ "php-stubs/wordpress-stubs": ">=5.8@stable",
+ "phpunit/phpunit": "^8.5.21 || ^9.6.7",
+ "vimeo/psalm": "^4.13.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Inpsyde\\Modularity\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Inpsyde GmbH",
+ "email": "hello@inpsyde.com",
+ "homepage": "https://inpsyde.com/",
+ "role": "Company"
+ },
+ {
+ "name": "Christian Leucht",
+ "email": "c.leucht@inpsyde.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Giuseppe Mazzapica",
+ "email": "g.mazzapica@inpsyde.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Modular PSR-11 implementation for WordPress plugins, themes or libraries.",
+ "support": {
+ "issues": "https://github.com/inpsyde/modularity/issues",
+ "source": "https://github.com/inpsyde/modularity/tree/1.7.2"
+ },
+ "time": "2023-11-16T11:19:47+00:00"
+ },
{
"name": "psr/container",
"version": "1.1.1",
diff --git a/lib/packages/Inpsyde/Modularity/Container/ContainerConfigurator.php b/lib/packages/Inpsyde/Modularity/Container/ContainerConfigurator.php
new file mode 100644
index 000000000..429869912
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Container/ContainerConfigurator.php
@@ -0,0 +1,153 @@
+
+ */
+ private $services = [];
+
+ /**
+ * @var array
+ */
+ private $factoryIds = [];
+
+ /**
+ * @var array>
+ */
+ private $extensions = [];
+
+ /**
+ * @var ContainerInterface[]
+ */
+ private $containers = [];
+
+ /**
+ * @var null|ContainerInterface
+ */
+ private $compiledContainer;
+
+ /**
+ * ContainerConfigurator constructor.
+ *
+ * @param ContainerInterface[] $containers
+ */
+ public function __construct(array $containers = [])
+ {
+ array_map([$this, 'addContainer'], $containers);
+ }
+
+ /**
+ * Allowing to add child containers.
+ *
+ * @param ContainerInterface $container
+ */
+ public function addContainer(ContainerInterface $container): void
+ {
+ $this->containers[] = $container;
+ }
+
+ /**
+ * @param string $id
+ * @param callable(ContainerInterface $container):mixed $factory
+ */
+ public function addFactory(string $id, callable $factory): void
+ {
+ $this->addService($id, $factory);
+ // We're using a hash table to detect later
+ // via isset() if a Service as a Factory.
+ $this->factoryIds[$id] = true;
+ }
+
+ /**
+ * @param string $id
+ * @param callable(ContainerInterface $container):mixed $service
+ *
+ * @return void
+ */
+ public function addService(string $id, callable $service): void
+ {
+ /*
+ * We are being intentionally permissive here,
+ * allowing a simple workflow for *intentional* overrides
+ * while accepting the (small?) risk of *accidental* overrides
+ * that could be hard to notice and debug.
+ *
+ * Clear a factory flag in case it was a factory.
+ * If needs be, it will get re-added after this function completes.
+ */
+ unset($this->factoryIds[$id]);
+
+ $this->services[$id] = $service;
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function hasService(string $id): bool
+ {
+ if (array_key_exists($id, $this->services)) {
+ return true;
+ }
+
+ foreach ($this->containers as $container) {
+ if ($container->has($id)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $id
+ * @param callable(mixed $service, ContainerInterface $container):mixed $extender
+ *
+ * @return void
+ */
+ public function addExtension(string $id, callable $extender): void
+ {
+ if (!isset($this->extensions[$id])) {
+ $this->extensions[$id] = [];
+ }
+
+ $this->extensions[$id][] = $extender;
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function hasExtension(string $id): bool
+ {
+ return isset($this->extensions[$id]);
+ }
+
+ /**
+ * Returns a read only version of this Container.
+ *
+ * @return ContainerInterface
+ */
+ public function createReadOnlyContainer(): ContainerInterface
+ {
+ if (!$this->compiledContainer) {
+ $this->compiledContainer = new ReadOnlyContainer(
+ $this->services,
+ $this->factoryIds,
+ $this->extensions,
+ $this->containers
+ );
+ }
+
+ return $this->compiledContainer;
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Container/PackageProxyContainer.php b/lib/packages/Inpsyde/Modularity/Container/PackageProxyContainer.php
new file mode 100644
index 000000000..2c8d79d5d
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Container/PackageProxyContainer.php
@@ -0,0 +1,102 @@
+package = $package;
+ }
+
+ /**
+ * @param string $id
+ * @return mixed
+ *
+ * @throws \Exception
+ */
+ public function get(string $id)
+ {
+ $this->assertPackageBooted($id);
+
+ return $this->container->get($id);
+ }
+
+ /**
+ * @param string $id
+ * @return bool
+ *
+ * @throws \Exception
+ */
+ public function has(string $id): bool
+ {
+ return $this->tryContainer() && $this->container->has($id);
+ }
+
+ /**
+ * @return bool
+ *
+ * @throws \Exception
+ * @psalm-assert-if-true ContainerInterface $this->container
+ */
+ private function tryContainer(): bool
+ {
+ if ($this->container) {
+ return true;
+ }
+
+ /** TODO: We need a better way to deal with status checking besides equality */
+ if (
+ $this->package->statusIs(Package::STATUS_READY)
+ || $this->package->statusIs(Package::STATUS_BOOTED)
+ ) {
+ $this->container = $this->package->container();
+ }
+
+ return (bool)$this->container;
+ }
+
+ /**
+ * @param string $id
+ * @return void
+ *
+ * @throws \Exception
+ *
+ * @psalm-assert ContainerInterface $this->container
+ */
+ private function assertPackageBooted(string $id): void
+ {
+ if ($this->tryContainer()) {
+ return;
+ }
+
+ $name = $this->package->name();
+ $status = $this->package->statusIs(Package::STATUS_FAILED)
+ ? 'is errored'
+ : 'is not ready yet';
+
+ throw new class ("Error retrieving service {$id} because package {$name} {$status}.")
+ extends \Exception
+ implements ContainerExceptionInterface {
+ };
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Container/ReadOnlyContainer.php b/lib/packages/Inpsyde/Modularity/Container/ReadOnlyContainer.php
new file mode 100644
index 000000000..763ae23ac
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Container/ReadOnlyContainer.php
@@ -0,0 +1,138 @@
+
+ */
+ private $services;
+
+ /**
+ * @var array
+ */
+ private $factoryIds;
+
+ /**
+ * @var array>
+ */
+ private $extensions;
+
+ /**
+ * Resolved factories.
+ *
+ * @var array
+ */
+ private $resolvedServices = [];
+
+ /**
+ * @var ContainerInterface[]
+ */
+ private $containers;
+
+ /**
+ * ReadOnlyContainer constructor.
+ *
+ * @param array $services
+ * @param array $factoryIds
+ * @param array> $extensions
+ * @param ContainerInterface[] $containers
+ */
+ public function __construct(
+ array $services,
+ array $factoryIds,
+ array $extensions,
+ array $containers
+ ) {
+ $this->services = $services;
+ $this->factoryIds = $factoryIds;
+ $this->extensions = $extensions;
+ $this->containers = $containers;
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return mixed
+ */
+ public function get(string $id)
+ {
+ if (array_key_exists($id, $this->resolvedServices)) {
+ return $this->resolvedServices[$id];
+ }
+
+ if (array_key_exists($id, $this->services)) {
+ $service = $this->services[$id]($this);
+ $resolved = $this->resolveExtensions($id, $service);
+
+ if (!isset($this->factoryIds[$id])) {
+ $this->resolvedServices[$id] = $resolved;
+ unset($this->services[$id]);
+ }
+
+ return $resolved;
+ }
+
+ foreach ($this->containers as $container) {
+ if ($container->has($id)) {
+ $service = $container->get($id);
+
+ return $this->resolveExtensions($id, $service);
+ }
+ }
+
+ throw new class ("Service with ID {$id} not found.")
+ extends \Exception
+ implements NotFoundExceptionInterface {
+ };
+ }
+
+ /**
+ * @param string $id
+ *
+ * @return bool
+ */
+ public function has(string $id): bool
+ {
+ if (array_key_exists($id, $this->services)) {
+ return true;
+ }
+
+ if (array_key_exists($id, $this->resolvedServices)) {
+ return true;
+ }
+
+ foreach ($this->containers as $container) {
+ if ($container->has($id)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $id
+ * @param mixed $service
+ *
+ * @return mixed
+ */
+ private function resolveExtensions(string $id, $service)
+ {
+ if (!isset($this->extensions[$id])) {
+ return $service;
+ }
+
+ foreach ($this->extensions[$id] as $extender) {
+ $service = $extender($service, $this);
+ }
+
+ return $service;
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Module/ExecutableModule.php b/lib/packages/Inpsyde/Modularity/Module/ExecutableModule.php
new file mode 100644
index 000000000..2f7771fb5
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Module/ExecutableModule.php
@@ -0,0 +1,21 @@
+
+ */
+ public function extensions(): array;
+}
diff --git a/lib/packages/Inpsyde/Modularity/Module/FactoryModule.php b/lib/packages/Inpsyde/Modularity/Module/FactoryModule.php
new file mode 100644
index 000000000..6e686c0c2
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Module/FactoryModule.php
@@ -0,0 +1,18 @@
+
+ */
+ public function factories(): array;
+}
diff --git a/lib/packages/Inpsyde/Modularity/Module/Module.php b/lib/packages/Inpsyde/Modularity/Module/Module.php
new file mode 100644
index 000000000..49dd0e5e3
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Module/Module.php
@@ -0,0 +1,20 @@
+
+ */
+ public function services(): array;
+}
diff --git a/lib/packages/Inpsyde/Modularity/Package.php b/lib/packages/Inpsyde/Modularity/Package.php
new file mode 100644
index 000000000..f55eaeccb
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Package.php
@@ -0,0 +1,727 @@
+
+ * $package = Package::new();
+ * $package->boot();
+ *
+ * $container = $package->container();
+ * $container->has(Package::PROPERTIES);
+ * $container->get(Package::PROPERTIES);
+ *
+ *
+ * @var string
+ */
+ public const PROPERTIES = 'properties';
+
+ /**
+ * Custom action to be used to add Modules to the package.
+ * It might also be used to access package properties.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ *
+ * add_action(
+ * $package->hookName(Package::ACTION_INIT),
+ * $callback
+ * );
+ *
+ */
+ public const ACTION_INIT = 'init';
+
+ /**
+ * Custom action which is triggered after the application
+ * is booted to access container and properties.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ *
+ * add_action(
+ * $package->hookName(Package::ACTION_READY),
+ * $callback
+ * );
+ *
+ */
+ public const ACTION_READY = 'ready';
+
+ /**
+ * Custom action which is triggered when a failure happens during the building stage.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ *
+ * add_action(
+ * $package->hookName(Package::ACTION_FAILED_BUILD),
+ * $callback
+ * );
+ *
+ */
+ public const ACTION_FAILED_BUILD = 'failed-build';
+
+ /**
+ * Custom action which is triggered when a failure happens during the booting stage.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ *
+ * add_action(
+ * $package->hookName(Package::ACTION_FAILED_BOOT),
+ * $callback
+ * );
+ *
+ */
+ public const ACTION_FAILED_BOOT = 'failed-boot';
+
+ /**
+ * Custom action which is triggered when a package is connected.
+ */
+ public const ACTION_PACKAGE_CONNECTED = 'package-connected';
+
+ /**
+ * Custom action which is triggered when a package cannot be connected.
+ */
+ public const ACTION_FAILED_CONNECTION = 'failed-connection';
+
+ /**
+ * Module states can be used to get information about your module.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ * $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // false
+ * $package->boot(new SomeModule());
+ * $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // true
+ *
+ */
+ public const MODULE_ADDED = 'added';
+ public const MODULE_NOT_ADDED = 'not-added';
+ public const MODULE_REGISTERED = 'registered';
+ public const MODULE_REGISTERED_FACTORIES = 'registered-factories';
+ public const MODULE_EXTENDED = 'extended';
+ public const MODULE_EXECUTED = 'executed';
+ public const MODULE_EXECUTION_FAILED = 'executed-failed';
+ public const MODULES_ALL = '*';
+
+ /**
+ * Custom states for the class.
+ *
+ * @example
+ *
+ * $package = Package::new();
+ * $package->statusIs(Package::IDLE); // true
+ * $package->boot();
+ * $package->statusIs(Package::BOOTED); // true
+ *
+ */
+ public const STATUS_IDLE = 2;
+ public const STATUS_INITIALIZED = 4;
+ public const STATUS_MODULES_ADDED = 5;
+ public const STATUS_READY = 7;
+ public const STATUS_BOOTED = 8;
+ public const STATUS_FAILED = -8;
+
+ /**
+ * Current state of the application.
+ *
+ * @see Package::STATUS_*
+ *
+ * @var int
+ */
+ private $status = self::STATUS_IDLE;
+
+ /**
+ * Contains the progress of all modules.
+ *
+ * @see Package::moduleProgress()
+ *
+ * @var array>
+ */
+ private $moduleStatus = [self::MODULES_ALL => []];
+
+ /**
+ * Hashmap of where keys are names of connected packages, and values are boolean, true
+ * if connection was successful.
+ *
+ * @see Package::connect()
+ *
+ * @var array
+ */
+ private $connectedPackages = [];
+
+ /**
+ * @var list
+ */
+ private $executables = [];
+
+ /**
+ * @var Properties
+ */
+ private $properties;
+
+ /**
+ * @var ContainerConfigurator
+ */
+ private $containerConfigurator;
+
+ /**
+ * @var bool
+ */
+ private $built = false;
+
+ /**
+ * @var bool
+ */
+ private $hasContainer = false;
+
+ /**
+ * @var \Throwable|null
+ */
+ private $lastError = null;
+
+ /**
+ * @param Properties $properties
+ * @param ContainerInterface[] $containers
+ *
+ * @return Package
+ */
+ public static function new(Properties $properties, ContainerInterface ...$containers): Package
+ {
+ return new self($properties, ...$containers);
+ }
+
+ /**
+ * @param Properties $properties
+ * @param ContainerInterface[] $containers
+ */
+ private function __construct(Properties $properties, ContainerInterface ...$containers)
+ {
+ $this->properties = $properties;
+
+ $this->containerConfigurator = new ContainerConfigurator($containers);
+ $this->containerConfigurator->addService(
+ self::PROPERTIES,
+ static function () use ($properties) {
+ return $properties;
+ }
+ );
+ }
+
+ /**
+ * @param Module $module
+ *
+ * @return static
+ * @throws \Exception
+ */
+ public function addModule(Module $module): Package
+ {
+ try {
+ $this->assertStatus(self::STATUS_IDLE, sprintf('add module %s', $module->id()));
+
+ $registeredServices = $this->addModuleServices(
+ $module,
+ self::MODULE_REGISTERED
+ );
+ $registeredFactories = $this->addModuleServices(
+ $module,
+ self::MODULE_REGISTERED_FACTORIES
+ );
+ $extended = $this->addModuleServices(
+ $module,
+ self::MODULE_EXTENDED
+ );
+ $isExecutable = $module instanceof ExecutableModule;
+
+ // ExecutableModules are collected and executed on Package::boot()
+ // when the Container is being compiled.
+ if ($isExecutable) {
+ /** @var ExecutableModule $module */
+ $this->executables[] = $module;
+ }
+
+ $added = $registeredServices || $registeredFactories || $extended || $isExecutable;
+ $status = $added ? self::MODULE_ADDED : self::MODULE_NOT_ADDED;
+ $this->moduleProgress($module->id(), $status);
+ } catch (\Throwable $throwable) {
+ $this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param Package $package
+ * @return bool
+ * @throws \Exception
+ */
+ public function connect(Package $package): bool
+ {
+ try {
+ if ($package === $this) {
+ return false;
+ }
+
+ $packageName = $package->name();
+ $errorData = ['package' => $packageName, 'status' => $this->status];
+ $errorMessage = "Failed connecting package {$packageName}";
+
+ // Don't connect, if already connected
+ if (array_key_exists($packageName, $this->connectedPackages)) {
+ $error = "{$errorMessage} because it was already connected.";
+ do_action(
+ $this->hookName(self::ACTION_FAILED_CONNECTION),
+ $packageName,
+ new \WP_Error('already_connected', $error, $errorData)
+ );
+
+ throw new \Exception($error, 0, $this->lastError);
+ }
+
+ // Don't connect, if already booted or boot failed
+ $failed = $this->statusIs(self::STATUS_FAILED);
+ if ($failed || $this->statusIs(self::STATUS_BOOTED)) {
+ $status = $failed ? 'errored' : 'booted';
+ $error = "{$errorMessage} to a {$status} package.";
+ do_action(
+ $this->hookName(self::ACTION_FAILED_CONNECTION),
+ $packageName,
+ new \WP_Error("no_connect_on_{$status}", $error, $errorData)
+ );
+
+ throw new \Exception($error, 0, $this->lastError);
+ }
+
+ $this->connectedPackages[$packageName] = true;
+
+ // We put connected package's properties in this package's container, so that in modules
+ // "run" method we can access them if we need to.
+ $this->containerConfigurator->addService(
+ sprintf('%s.%s', $package->name(), self::PROPERTIES),
+ static function () use ($package): Properties {
+ return $package->properties();
+ }
+ );
+
+ // If the other package is booted, we can obtain a container, otherwise
+ // we build a proxy container
+ $container = $package->statusIs(self::STATUS_BOOTED)
+ ? $package->container()
+ : new PackageProxyContainer($package);
+
+ $this->containerConfigurator->addContainer($container);
+
+ do_action(
+ $this->hookName(self::ACTION_PACKAGE_CONNECTED),
+ $packageName,
+ $this->status,
+ $container instanceof PackageProxyContainer
+ );
+
+ return true;
+ } catch (\Throwable $throwable) {
+ if (isset($packageName)) {
+ $this->connectedPackages[$packageName] = false;
+ }
+ $this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
+
+ return false;
+ }
+ }
+
+ /**
+ * @return static
+ */
+ public function build(): Package
+ {
+ try {
+ // Don't allow building the application multiple times.
+ $this->assertStatus(self::STATUS_IDLE, 'build package');
+
+ do_action(
+ $this->hookName(self::ACTION_INIT),
+ $this
+ );
+ // Changing the status here ensures we can not call this method again, and also we can not
+ // add new modules, because both this and `addModule()` methods check for idle status.
+ // For backward compatibility, adding new modules via `boot()` will still be possible, even
+ // if deprecated, at the condition that the container was not yet accessed at that point.
+ $this->progress(self::STATUS_INITIALIZED);
+ } catch (\Throwable $throwable) {
+ $this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
+ } finally {
+ $this->built = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param Module ...$defaultModules Deprecated, use `addModule()` to add default modules.
+ * @return bool
+ *
+ * @throws \Throwable
+ */
+ public function boot(Module ...$defaultModules): bool
+ {
+ try {
+ // Call build() if not called yet, and ensure any new module passed here is added
+ // as well, throwing if the container was already built.
+ $this->doBuild(...$defaultModules);
+
+ // Don't allow booting the application multiple times.
+ $this->assertStatus(self::STATUS_MODULES_ADDED, 'boot application', '<');
+ $this->assertStatus(self::STATUS_FAILED, 'boot application', '!=');
+
+ $this->progress(self::STATUS_MODULES_ADDED);
+
+ $this->doExecute();
+
+ $this->progress(self::STATUS_READY);
+
+ do_action(
+ $this->hookName(self::ACTION_READY),
+ $this
+ );
+ } catch (\Throwable $throwable) {
+ $this->handleFailure($throwable, self::ACTION_FAILED_BOOT);
+
+ return false;
+ }
+
+ $this->progress(self::STATUS_BOOTED);
+
+ return true;
+ }
+
+ /**
+ * @param Module ...$defaultModules
+ * @return void
+ */
+ private function doBuild(Module ...$defaultModules): void
+ {
+ if ($defaultModules) {
+ $this->deprecatedArgument(
+ sprintf(
+ 'Passing default modules to %1$s::boot() is deprecated since version 1.7.0.'
+ . ' Please add modules via %1$s::addModule().',
+ __CLASS__
+ ),
+ __METHOD__,
+ '1.7.0'
+ );
+ }
+
+ if (!$this->built) {
+ array_map([$this, 'addModule'], $defaultModules);
+ $this->build();
+
+ return;
+ }
+
+ if (
+ !$defaultModules
+ || ($this->status >= self::STATUS_MODULES_ADDED)
+ || ($this->statusIs(self::STATUS_FAILED))
+ ) {
+ // if we don't have default modules, there's nothing to do, and if the status is beyond
+ // "modules added" or is failed, we do nothing as well and let `boot()` throw.
+ return;
+ }
+
+ $backup = $this->status;
+
+ try {
+ // simulate idle status to prevent `addModule()` from throwing
+ // only if we don't have a container yet
+ $this->hasContainer or $this->status = self::STATUS_IDLE;
+
+ foreach ($defaultModules as $defaultModule) {
+ // If a module was added by `build()` or `addModule()` we can skip it, a
+ // deprecation was trigger to make it noticeable without breakage
+ if (!$this->moduleIs($defaultModule->id(), self::MODULE_ADDED)) {
+ $this->addModule($defaultModule);
+ }
+ }
+ } finally {
+ $this->status = $backup;
+ }
+ }
+
+ /**
+ * @param Module $module
+ * @param string $status
+ * @return bool
+ */
+ private function addModuleServices(Module $module, string $status): bool
+ {
+ $services = null;
+ $addCallback = null;
+ switch ($status) {
+ case self::MODULE_REGISTERED:
+ $services = $module instanceof ServiceModule ? $module->services() : null;
+ $addCallback = [$this->containerConfigurator, 'addService'];
+ break;
+ case self::MODULE_REGISTERED_FACTORIES:
+ $services = $module instanceof FactoryModule ? $module->factories() : null;
+ $addCallback = [$this->containerConfigurator, 'addFactory'];
+ break;
+ case self::MODULE_EXTENDED:
+ $services = $module instanceof ExtendingModule ? $module->extensions() : null;
+ $addCallback = [$this->containerConfigurator, 'addExtension'];
+ break;
+ }
+
+ if (!$services) {
+ return false;
+ }
+
+ $ids = [];
+ array_walk(
+ $services,
+ static function (callable $service, string $id) use ($addCallback, &$ids) {
+ /** @var callable(string, callable) $addCallback */
+ $addCallback($id, $service);
+ /** @var list $ids */
+ $ids[] = $id;
+ }
+ );
+ /** @var list $ids */
+ $this->moduleProgress($module->id(), $status, $ids);
+
+ return true;
+ }
+
+ /**
+ * @return void
+ *
+ * @throws \Throwable
+ */
+ private function doExecute(): void
+ {
+ foreach ($this->executables as $executable) {
+ $success = $executable->run($this->container());
+ $this->moduleProgress(
+ $executable->id(),
+ $success
+ ? self::MODULE_EXECUTED
+ : self::MODULE_EXECUTION_FAILED
+ );
+ }
+ }
+
+ /**
+ * @param string $moduleId
+ * @param string $status
+ * @param list|null $serviceIds
+ *
+ * @return void
+ */
+ private function moduleProgress(string $moduleId, string $status, ?array $serviceIds = null)
+ {
+ isset($this->moduleStatus[$status]) or $this->moduleStatus[$status] = [];
+ $this->moduleStatus[$status][] = $moduleId;
+
+ if (!$serviceIds || !$this->properties->isDebug()) {
+ $this->moduleStatus[self::MODULES_ALL][] = "{$moduleId} {$status}";
+
+ return;
+ }
+
+ $description = sprintf('%s %s (%s)', $moduleId, $status, implode(', ', $serviceIds));
+ $this->moduleStatus[self::MODULES_ALL][] = $description;
+ }
+
+ /**
+ * @return array>
+ */
+ public function modulesStatus(): array
+ {
+ return $this->moduleStatus;
+ }
+
+ /**
+ * @return array
+ */
+ public function connectedPackages(): array
+ {
+ return $this->connectedPackages;
+ }
+
+ /**
+ * @param string $packageName
+ * @return bool
+ */
+ public function isPackageConnected(string $packageName): bool
+ {
+ return $this->connectedPackages[$packageName] ?? false;
+ }
+
+ /**
+ * @param string $moduleId
+ * @param string $status
+ *
+ * @return bool
+ */
+ public function moduleIs(string $moduleId, string $status): bool
+ {
+ return in_array($moduleId, $this->moduleStatus[$status] ?? [], true);
+ }
+
+ /**
+ * Return the filter name to be used to extend modules of the plugin.
+ *
+ * If the plugin is single file `my-plugin.php` in plugins folder the filter name will be:
+ * `inpsyde.modularity.my-plugin`.
+ *
+ * If the plugin is in a sub-folder e.g. `my-plugin/index.php` the filter name will be:
+ * `inpsyde.modularity.my-plugin` anyway, so the file name is not relevant.
+ *
+ * @param string $suffix
+ *
+ * @return string
+ * @see Package::name()
+ *
+ */
+ public function hookName(string $suffix = ''): string
+ {
+ $filter = self::HOOK_PREFIX . $this->properties->baseName();
+
+ if ($suffix) {
+ $filter .= '.' . $suffix;
+ }
+
+ return $filter;
+ }
+
+ /**
+ * @return Properties
+ */
+ public function properties(): Properties
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @return ContainerInterface
+ *
+ * @throws \Exception
+ */
+ public function container(): ContainerInterface
+ {
+ $this->assertStatus(self::STATUS_INITIALIZED, 'obtain the container instance', '>=');
+ $this->hasContainer = true;
+
+ return $this->containerConfigurator->createReadOnlyContainer();
+ }
+
+ /**
+ * @return string
+ */
+ public function name(): string
+ {
+ return $this->properties->baseName();
+ }
+
+ /**
+ * @param int $status
+ */
+ private function progress(int $status): void
+ {
+ $this->status = $status;
+ }
+
+ /**
+ * @param int $status
+ *
+ * @return bool
+ */
+ public function statusIs(int $status): bool
+ {
+ return $this->status === $status;
+ }
+
+ /**
+ * @param \Throwable $throwable
+ * @param Package::ACTION_FAILED_* $action
+ * @return void
+ * @throws \Throwable
+ */
+ private function handleFailure(\Throwable $throwable, string $action): void
+ {
+ $this->progress(self::STATUS_FAILED);
+ $hook = $this->hookName($action);
+ did_action($hook) or do_action($hook, $throwable);
+
+ if ($this->properties->isDebug()) {
+ throw $throwable;
+ }
+
+ $this->lastError = $throwable;
+ }
+
+ /**
+ * @param int $status
+ * @param string $action
+ * @param string $operator
+ *
+ * @throws \Exception
+ * @psalm-suppress ArgumentTypeCoercion
+ */
+ private function assertStatus(int $status, string $action, string $operator = '=='): void
+ {
+ if (!version_compare((string) $this->status, (string) $status, $operator)) {
+ throw new \Exception(
+ sprintf("Can't %s at this point of application.", $action),
+ 0,
+ $this->lastError
+ );
+ }
+ }
+
+ /**
+ * Similar to WP's `_deprecated_argument()`, but executes regardless of WP_DEBUG and without
+ * translated message (so without attempting loading translation files).
+ *
+ * @param string $message
+ * @param string $function
+ * @param string $version
+ *
+ * @return void
+ */
+ private function deprecatedArgument(string $message, string $function, string $version): void
+ {
+ do_action('deprecated_argument_run', $function, $message, $version);
+
+ if (apply_filters('deprecated_argument_trigger_error', true)) {
+ trigger_error($message, \E_USER_DEPRECATED);
+ }
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Properties/BaseProperties.php b/lib/packages/Inpsyde/Modularity/Properties/BaseProperties.php
new file mode 100644
index 000000000..47bb4db74
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Properties/BaseProperties.php
@@ -0,0 +1,217 @@
+sanitizeBaseName($baseName);
+ $basePath = (string) trailingslashit($basePath);
+ if ($baseUrl) {
+ $baseUrl = (string) trailingslashit($baseUrl);
+ }
+
+ $this->baseName = $baseName;
+ $this->basePath = $basePath;
+ $this->baseUrl = $baseUrl;
+ $this->properties = array_replace(Properties::DEFAULT_PROPERTIES, $properties);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function sanitizeBaseName(string $name): string
+ {
+ substr_count($name, '/') and $name = dirname($name);
+
+ return strtolower(pathinfo($name, PATHINFO_FILENAME));
+ }
+
+ /**
+ * @return string
+ */
+ public function baseName(): string
+ {
+ return $this->baseName;
+ }
+
+ /**
+ * @return string
+ */
+ public function basePath(): string
+ {
+ return $this->basePath;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function baseUrl(): ?string
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * @return string
+ */
+ public function author(): string
+ {
+ return (string) $this->get(self::PROP_AUTHOR);
+ }
+
+ /**
+ * @return string
+ */
+ public function authorUri(): string
+ {
+ return (string) $this->get(self::PROP_AUTHOR_URI);
+ }
+
+ /**
+ * @return string
+ */
+ public function description(): string
+ {
+ return (string) $this->get(self::PROP_DESCRIPTION);
+ }
+
+ /**
+ * @return string
+ */
+ public function textDomain(): string
+ {
+ return (string) $this->get(self::PROP_TEXTDOMAIN);
+ }
+
+ /**
+ * @return string
+ */
+ public function domainPath(): string
+ {
+ return (string) $this->get(self::PROP_DOMAIN_PATH);
+ }
+
+ /**
+ * @return string
+ */
+ public function name(): string
+ {
+ return (string) $this->get(self::PROP_NAME);
+ }
+
+ /**
+ * @return string
+ */
+ public function uri(): string
+ {
+ return (string) $this->get(self::PROP_URI);
+ }
+
+ /**
+ * @return string
+ */
+ public function version(): string
+ {
+ return (string) $this->get(self::PROP_VERSION);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function requiresWp(): ?string
+ {
+ $value = $this->get(self::PROP_REQUIRES_WP);
+
+ return $value && is_string($value) ? $value : null;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function requiresPhp(): ?string
+ {
+ $value = $this->get(self::PROP_REQUIRES_PHP);
+
+ return $value && is_string($value) ? $value : null;
+ }
+
+ /**
+ * @return array
+ */
+ public function tags(): array
+ {
+ return (array) $this->get(self::PROP_TAGS);
+ }
+
+ /**
+ * @param string $key
+ * @param null $default
+ * @return mixed
+ */
+ public function get(string $key, $default = null)
+ {
+ return $this->properties[$key] ?? $default;
+ }
+
+ /**
+ * @param string $key
+ * @return bool
+ */
+ public function has(string $key): bool
+ {
+ return isset($this->properties[$key]);
+ }
+
+ /**
+ * @return bool
+ * @see Properties::isDebug()
+ */
+ public function isDebug(): bool
+ {
+ if ($this->isDebug === null) {
+ $this->isDebug = defined('WP_DEBUG') && WP_DEBUG;
+ }
+
+ return $this->isDebug;
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Properties/LibraryProperties.php b/lib/packages/Inpsyde/Modularity/Properties/LibraryProperties.php
new file mode 100644
index 000000000..5c06577e1
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Properties/LibraryProperties.php
@@ -0,0 +1,209 @@
+ 0) {
+ $properties[self::PROP_AUTHOR] = implode(', ', $names);
+ }
+
+ // Custom settings which can be stored in composer.json "extra.modularity"
+ $extra = $composerJsonData['extra']['modularity'] ?? [];
+ foreach (self::EXTRA_KEYS as $key) {
+ $properties[$key] = $extra[$key] ?? '';
+ }
+
+ // PHP requirement in composer.json "require" or "require-dev"
+ $properties[self::PROP_REQUIRES_PHP] = self::extractPhpVersion($composerJsonData);
+
+ // composer.json might have "version" in root
+ $version = $composerJsonData['version'] ?? null;
+ if ($version && is_string($version)) {
+ $properties[self::PROP_VERSION] = $version;
+ }
+
+ [$baseName, $name] = static::buildNames($composerJsonData);
+ $basePath = dirname($composerJsonFile);
+ if (empty($properties[self::PROP_NAME])) {
+ $properties[self::PROP_NAME] = $name;
+ }
+
+ return new self(
+ $baseName,
+ $basePath,
+ $baseUrl,
+ $properties
+ );
+ }
+
+ /**
+ * @param array $composerJsonData
+ *
+ * @return array{string, string}
+ */
+ private static function buildNames(array $composerJsonData): array
+ {
+ $composerName = (string) ($composerJsonData['name'] ?? '');
+ $packageNamePieces = explode('/', $composerName, 2);
+ $basename = implode('-', $packageNamePieces);
+ // "inpsyde/foo-bar-baz" => "Inpsyde Foo Bar Baz"
+ $name = mb_convert_case(
+ str_replace(['-', '_', '.'], ' ', implode(' ', $packageNamePieces)),
+ MB_CASE_TITLE
+ );
+
+ return [$basename, $name];
+ }
+
+ /**
+ * Check PHP version in require, require-dev.
+ *
+ * Attempt to parse requirements to find the _minimum_ accepted version (consistent with WP).
+ * Composer requirements are parsed in a way that, for example:
+ * `>=7.2` returns `7.2`
+ * `^7.3` returns `7.3`
+ * `5.6 || >= 7.1` returns `5.6`
+ * `>= 7.1 < 8` returns `7.1`
+ *
+ * @param array $composerData
+ * @param string $key
+ *
+ * @return string|null
+ */
+ private static function extractPhpVersion(array $composerData, string $key = 'require'): ?string
+ {
+ $nextKey = ($key === 'require')
+ ? 'require-dev'
+ : null;
+ $base = (array) ($composerData[$key] ?? []);
+ $requirement = $base['php'] ?? null;
+ $version = ($requirement && is_string($requirement))
+ ? trim($requirement)
+ : null;
+ if (!$version) {
+ return $nextKey
+ ? static::extractPhpVersion($composerData, $nextKey)
+ : null;
+ }
+
+ static $matcher;
+ $matcher or $matcher = static function (string $version): ?string {
+ $version = trim($version);
+ if (!$version) {
+ return null;
+ }
+
+ // versions range like `>= 7.2.4 < 8`
+ if (preg_match('{>=?([\s0-9\.]+)<}', $version, $matches)) {
+ return trim($matches[1], " \t\n\r\0\x0B.");
+ }
+
+ // aliases like `dev-src#abcde as 7.4`
+ if (preg_match('{as\s*([\s0-9\.]+)}', $version, $matches)) {
+ return trim($matches[1], " \t\n\r\0\x0B.");
+ }
+
+ // Basic requirements like 7.2, >=7.2, ^7.2, ~7.2
+ if (preg_match('{^(?:[>=\s~\^]+)?([0-9\.]+)}', $version, $matches)) {
+ return trim($matches[1], " \t\n\r\0\x0B.");
+ }
+
+ return null;
+ };
+
+ // support for simpler requirements like `7.3`, `>=7.4` or alternative like `5.6 || >=7`
+
+ $alternatives = explode('||', $version);
+ $found = null;
+ foreach ($alternatives as $alternative) {
+ /** @var callable(string):?string $matcher */
+ $itemFound = $matcher($alternative);
+ if ($itemFound && (!$found || version_compare($itemFound, $found, '<'))) {
+ $found = $itemFound;
+ }
+ }
+
+ if ($found) {
+ return $found;
+ }
+
+ return $nextKey
+ ? static::extractPhpVersion($composerData, $nextKey)
+ : null;
+ }
+
+ /**
+ * @param string $url
+ *
+ * @return static
+ *
+ * @throws \Exception
+ */
+ public function withBaseUrl(string $url): LibraryProperties
+ {
+ if ($this->baseUrl !== null) {
+ throw new \Exception(sprintf('%s::$baseUrl property is not overridable.', __CLASS__));
+ }
+
+ $this->baseUrl = trailingslashit($url);
+
+ return $this;
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Properties/PluginProperties.php b/lib/packages/Inpsyde/Modularity/Properties/PluginProperties.php
new file mode 100644
index 000000000..f3d3f5485
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Properties/PluginProperties.php
@@ -0,0 +1,176 @@
+ 'Author',
+ self::PROP_AUTHOR_URI => 'AuthorURI',
+ self::PROP_DESCRIPTION => 'Description',
+ self::PROP_DOMAIN_PATH => 'DomainPath',
+ self::PROP_NAME => 'Name',
+ self::PROP_TEXTDOMAIN => 'TextDomain',
+ self::PROP_URI => 'PluginURI',
+ self::PROP_VERSION => 'Version',
+ self::PROP_REQUIRES_WP => 'RequiresWP',
+ self::PROP_REQUIRES_PHP => 'RequiresPHP',
+
+ // additional headers
+ self::PROP_NETWORK => 'Network',
+ ];
+
+ /**
+ * @var string
+ */
+ private $pluginMainFile;
+
+ /**
+ * @var string
+ */
+ private $pluginBaseName;
+
+ /**
+ * @var bool|null
+ */
+ protected $isMu;
+
+ /**
+ * @var bool|null
+ */
+ protected $isActive;
+
+ /**
+ * @var bool|null
+ */
+ protected $isNetworkActive;
+
+ /**
+ * @param string $pluginMainFile
+ *
+ * @return PluginProperties
+ */
+ public static function new(string $pluginMainFile): PluginProperties
+ {
+ return new self($pluginMainFile);
+ }
+
+ /**
+ * PluginProperties constructor.
+ *
+ * @param string $pluginMainFile
+ */
+ protected function __construct(string $pluginMainFile)
+ {
+ if (!function_exists('get_plugin_data')) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ $pluginData = get_plugin_data($pluginMainFile);
+ $properties = Properties::DEFAULT_PROPERTIES;
+
+ // Map pluginData to internal structure.
+ foreach (self::HEADERS as $key => $pluginDataKey) {
+ $properties[$key] = $pluginData[$pluginDataKey] ?? '';
+ unset($pluginData[$pluginDataKey]);
+ }
+ $properties = array_merge($properties, $pluginData);
+
+ $this->pluginMainFile = wp_normalize_path($pluginMainFile);
+
+ $this->pluginBaseName = plugin_basename($pluginMainFile);
+ $basePath = plugin_dir_path($pluginMainFile);
+ $baseUrl = plugins_url('/', $pluginMainFile);
+
+ parent::__construct(
+ $this->pluginBaseName,
+ $basePath,
+ $baseUrl,
+ $properties
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function pluginMainFile(): string
+ {
+ return $this->pluginMainFile;
+ }
+
+ /**
+ * @return bool
+ *
+ * @psalm-suppress PossiblyFalseArgument
+ */
+ public function network(): bool
+ {
+ return (bool) $this->get(self::PROP_NETWORK, false);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isActive(): bool
+ {
+ if ($this->isActive === null) {
+ if (!function_exists('is_plugin_active')) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+ $this->isActive = is_plugin_active($this->pluginBaseName);
+ }
+
+ return $this->isActive;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isNetworkActive(): bool
+ {
+ if ($this->isNetworkActive === null) {
+ if (!function_exists('is_plugin_active_for_network')) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+ $this->isNetworkActive = is_plugin_active_for_network($this->pluginBaseName);
+ }
+
+ return $this->isNetworkActive;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isMuPlugin(): bool
+ {
+ if ($this->isMu === null) {
+ /**
+ * @psalm-suppress UndefinedConstant
+ * @psalm-suppress MixedArgument
+ */
+ $muPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR);
+ $this->isMu = strpos($this->pluginMainFile, $muPluginDir) === 0;
+ }
+
+ return $this->isMu;
+ }
+}
diff --git a/lib/packages/Inpsyde/Modularity/Properties/Properties.php b/lib/packages/Inpsyde/Modularity/Properties/Properties.php
new file mode 100644
index 000000000..8663d6883
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Properties/Properties.php
@@ -0,0 +1,139 @@
+ '',
+ self::PROP_AUTHOR_URI => '',
+ self::PROP_DESCRIPTION => '',
+ self::PROP_DOMAIN_PATH => '',
+ self::PROP_NAME => '',
+ self::PROP_TEXTDOMAIN => '',
+ self::PROP_URI => '',
+ self::PROP_VERSION => '',
+ self::PROP_REQUIRES_WP => null,
+ self::PROP_REQUIRES_PHP => null,
+ self::PROP_TAGS => [],
+ ];
+
+ /**
+ * @param string $key
+ * @param null $default
+ *
+ * @return mixed
+ */
+ public function get(string $key, $default = null);
+
+ /**
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function has(string $key): bool;
+
+ /**
+ * @return bool
+ */
+ public function isDebug(): bool;
+
+ /**
+ * @return string
+ */
+ public function baseName(): string;
+
+ /**
+ * @return string
+ */
+ public function basePath(): string;
+
+ /**
+ * @return string|null
+ */
+ public function baseUrl(): ?string;
+
+ /**
+ * @return string
+ */
+ public function author(): string;
+
+ /**
+ * @return string
+ */
+ public function authorUri(): string;
+
+ /**
+ * @return string
+ */
+ public function description(): string;
+
+ /**
+ * @return string
+ */
+ public function textDomain(): string;
+
+ /**
+ * @return string
+ */
+ public function domainPath(): string;
+
+ /**
+ * The name of the plugin, theme or library.
+ *
+ * @return string
+ */
+ public function name(): string;
+
+ /**
+ * The home page of the plugin, theme or library.
+ * @return string
+ */
+ public function uri(): string;
+
+ /**
+ * @return string
+ */
+ public function version(): string;
+
+ /**
+ * Optional. Specify the minimum required WordPress version.
+ *
+ * @return string|null
+ */
+ public function requiresWp(): ?string;
+
+ /**
+ * Optional. Specify the minimum required PHP version.
+ *
+ * @return string
+ */
+ public function requiresPhp(): ?string;
+
+ /**
+ * Optional. Currently, only available for Theme and Library.
+ * Plugins do not have support for "tags"/"keywords" in header.
+ *
+ * @link https://developer.wordpress.org/reference/classes/wp_theme/#properties
+ * @link https://getcomposer.org/doc/04-schema.md#keywords
+ *
+ * @return array
+ */
+ public function tags(): array;
+}
diff --git a/lib/packages/Inpsyde/Modularity/Properties/ThemeProperties.php b/lib/packages/Inpsyde/Modularity/Properties/ThemeProperties.php
new file mode 100644
index 000000000..24a464caa
--- /dev/null
+++ b/lib/packages/Inpsyde/Modularity/Properties/ThemeProperties.php
@@ -0,0 +1,131 @@
+ 'Author',
+ self::PROP_AUTHOR_URI => 'AuthorURI',
+ self::PROP_DESCRIPTION => 'Description',
+ self::PROP_DOMAIN_PATH => 'DomainPath',
+ self::PROP_NAME => 'Name',
+ self::PROP_TEXTDOMAIN => 'TextDomain',
+ self::PROP_URI => 'ThemeURI',
+ self::PROP_VERSION => 'Version',
+ self::PROP_REQUIRES_WP => 'RequiresWP',
+ self::PROP_REQUIRES_PHP => 'RequiresPHP',
+
+ // additional headers
+ self::PROP_STATUS => 'Status',
+ self::PROP_TAGS => 'Tags',
+ self::PROP_TEMPLATE => 'Template',
+ ];
+
+ /**
+ * @param string $themeDirectory
+ *
+ * @return ThemeProperties
+ */
+ public static function new(string $themeDirectory): ThemeProperties
+ {
+ return new self($themeDirectory);
+ }
+
+ /**
+ * ThemeProperties constructor.
+ *
+ * @param string $themeDirectory
+ */
+ protected function __construct(string $themeDirectory)
+ {
+ if (!function_exists('wp_get_theme')) {
+ require_once ABSPATH . 'wp-includes/theme.php';
+ }
+
+ $theme = wp_get_theme($themeDirectory);
+ $properties = Properties::DEFAULT_PROPERTIES;
+
+ foreach (self::HEADERS as $key => $themeKey) {
+ /** @psalm-suppress DocblockTypeContradiction */
+ $properties[$key] = $theme->get($themeKey) ?? '';
+ }
+
+ $baseName = $theme->get_stylesheet();
+ $basePath = $theme->get_stylesheet_directory();
+ $baseUrl = (string) trailingslashit($theme->get_stylesheet_directory_uri());
+
+ parent::__construct(
+ $baseName,
+ $basePath,
+ $baseUrl,
+ $properties
+ );
+ }
+
+ /**
+ * If the theme is published.
+ *
+ * @return string
+ */
+ public function status(): string
+ {
+ return (string) $this->get(self::PROP_STATUS);
+ }
+
+ public function template(): string
+ {
+ return (string) $this->get(self::PROP_TEMPLATE);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isChildTheme(): bool
+ {
+ return (bool) $this->template();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isCurrentTheme(): bool
+ {
+ return get_stylesheet() === $this->baseName();
+ }
+
+ /**
+ * @return ThemeProperties|null
+ */
+ public function parentThemeProperties(): ?ThemeProperties
+ {
+ $template = $this->template();
+ if (!$template) {
+ return null;
+ }
+
+ $parent = wp_get_theme($template, get_theme_root($template));
+
+ return static::new($parent->get_template_directory());
+ }
+}
diff --git a/lib/packages/Psr/Container/ContainerExceptionInterface.php b/lib/packages/Psr/Container/ContainerExceptionInterface.php
index e36fa7885..78ee05e4a 100644
--- a/lib/packages/Psr/Container/ContainerExceptionInterface.php
+++ b/lib/packages/Psr/Container/ContainerExceptionInterface.php
@@ -1,7 +1,4 @@
modules = $modules;
+ }
+
+ private function setup(): void {
+ if ( $this->is_initialized ) {
+ return;
+ }
+
+ $this->services = array();
+ $this->extensions = array();
+
+ foreach ( $this->modules as $module ) {
+ $service_provider = $module->setup();
+
+ $this->services = array_merge(
+ $this->services,
+ $service_provider->getFactories()
+ );
+
+ foreach ( $service_provider->getExtensions() as $key => $extension ) {
+ if ( ! isset( $this->extensions[ $key ] ) ) {
+ $this->extensions[ $key ] = array();
+ }
+ $this->extensions[ $key ][] = $extension;
+ }
+ }
+
+ $this->is_initialized = true;
+ }
+
+ /**
+ * Returns the services.
+ *
+ * @return array|callable[]
+ */
+ public function services(): array {
+ $this->setup();
+ return $this->services;
+ }
+
+ /**
+ * Returns the extensions.
+ *
+ * @return array|callable[]
+ */
+ public function extensions(): array {
+ $this->setup();
+
+ $map = array_map( function ( $extension_group ) {
+ return function ( $previous, ContainerInterface $container ) use ( $extension_group ) {
+ $value = $previous;
+ foreach ( $extension_group as $extension ) {
+ $value = $extension( $container, $value );
+ }
+ return $value;
+ };
+ }, $this->extensions );
+
+ return $map;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function run( ContainerInterface $container ): bool {
+ foreach ( $this->modules as $module ) {
+ $module->run( $container );
+ }
+ return true;
+ }
+
+}
From 63690fa696fc034b1993a2e5202cef1a4d658d87 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 24 Nov 2023 18:07:56 +0000
Subject: [PATCH 2/4] Fix lint
---
bootstrap.php | 46 ++--------------------------
src/DhiiToModularityModule.php | 56 +++++++++++++++++++++-------------
2 files changed, 37 insertions(+), 65 deletions(-)
diff --git a/bootstrap.php b/bootstrap.php
index fe9a0799b..4ab5049aa 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -5,15 +5,8 @@
* @package WooCommerce\PayPalCommerce
*/
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CachingContainer;
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CompositeCachingServiceProvider;
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CompositeContainer;
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\DelegatingContainer;
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ProxyContainer;
-use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Package;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties\PluginProperties;
-use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\DhiiToModularityModule;
@@ -38,46 +31,11 @@
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
// Initialize plugin.
- $properties = PluginProperties::new(__FILE__);
- $bootstrap = Package::new($properties);
+ $properties = PluginProperties::new( __FILE__ );
+ $bootstrap = Package::new( $properties );
$bootstrap->addModule( new DhiiToModularityModule( $modules ) );
$bootstrap->boot();
return $bootstrap->container();
-
-// $providers = array_map(
-// function ( ModuleInterface $module ): ServiceProviderInterface {
-// return $module->setup();
-// },
-// $modules
-// );
-
-// $provider = new CompositeCachingServiceProvider( $providers );
-// $proxy_container = new ProxyContainer();
-// // TODO: caching does not work currently,
-// // may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
-// // for now not fixed since we were using this behavior for long time and fixing it now may break things.
-// $container = new DelegatingContainer( $provider );
-// /**
-// * Skip iterable vs array check.
-// *
-// * @psalm-suppress PossiblyInvalidArgument
-// */
-// $app_container = new CachingContainer(
-// new CompositeContainer(
-// array_merge(
-// $additional_containers,
-// array( $container )
-// )
-// )
-// );
-// $proxy_container->setInnerContainer( $app_container );
-//
-// foreach ( $modules as $module ) {
-// /* @var $module ModuleInterface module */
-// $module->run( $app_container );
-// }
-//
-// return $app_container;
};
diff --git a/src/DhiiToModularityModule.php b/src/DhiiToModularityModule.php
index f584857a8..83baeccd8 100644
--- a/src/DhiiToModularityModule.php
+++ b/src/DhiiToModularityModule.php
@@ -1,6 +1,6 @@
modules = $modules;
}
+ /**
+ * Performs module-specific setup and initializes this object based on module service providers.
+ *
+ * @return void
+ * @throws Vendor\Dhii\Modular\Module\Exception\ModuleExceptionInterface Throws in case of module setup failure.
+ */
private function setup(): void {
if ( $this->is_initialized ) {
return;
}
- $this->services = array();
+ $this->services = array();
$this->extensions = array();
foreach ( $this->modules as $module ) {
@@ -85,9 +93,7 @@ private function setup(): void {
}
/**
- * Returns the services.
- *
- * @return array|callable[]
+ * {@inheritDoc}
*/
public function services(): array {
$this->setup();
@@ -95,24 +101,32 @@ public function services(): array {
}
/**
- * Returns the extensions.
- *
- * @return array|callable[]
+ * {@inheritDoc}
*/
public function extensions(): array {
$this->setup();
- $map = array_map( function ( $extension_group ) {
- return function ( $previous, ContainerInterface $container ) use ( $extension_group ) {
- $value = $previous;
- foreach ( $extension_group as $extension ) {
- $value = $extension( $container, $value );
- }
- return $value;
- };
- }, $this->extensions );
-
- return $map;
+ return array_map(
+ function ( $extension_group ) {
+ /**
+ * Maps Dhii extensions to modularity.
+ *
+ * @param mixed $previous The previous value.
+ * @return ContainerInterface $container The container instance.
+ *
+ * @psalm-suppress MissingClosureParamType
+ * @psalm-suppress MissingClosureReturnType
+ */
+ return function ( $previous, ContainerInterface $container ) use ( $extension_group ) {
+ $value = $previous;
+ foreach ( $extension_group as $extension ) {
+ $value = $extension( $container, $value );
+ }
+ return $value;
+ };
+ },
+ $this->extensions
+ );
}
/**
From 1a21ef0d21418b74460dbaedeab45d61782410d5 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 24 Nov 2023 18:24:30 +0000
Subject: [PATCH 3/4] Fix composer warning
---
composer.lock | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/composer.lock b/composer.lock
index 36ce94f4e..fd07c70cd 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "214504278540f7c3d05f10d1e67920b1",
+ "content-hash": "5c45654af6d2f3addad4a089950e515f",
"packages": [
{
"name": "container-interop/service-provider",
From 838449251fb67077c232d07e7136df55a2e0863e Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 14 Dec 2023 13:55:19 +0000
Subject: [PATCH 4/4] Fix tests.
---
tests/PHPUnit/bootstrap.php | 1 +
tests/inc/wp_functions.php | 41 +++++++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+)
create mode 100644 tests/inc/wp_functions.php
diff --git a/tests/PHPUnit/bootstrap.php b/tests/PHPUnit/bootstrap.php
index 75145ac11..8910b0241 100644
--- a/tests/PHPUnit/bootstrap.php
+++ b/tests/PHPUnit/bootstrap.php
@@ -4,6 +4,7 @@
define('TESTS_ROOT_DIR', dirname(__DIR__));
define('ROOT_DIR', dirname(TESTS_ROOT_DIR));
+require_once TESTS_ROOT_DIR . '/inc/wp_functions.php';
require_once ROOT_DIR . '/vendor/autoload.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway_CC.php';
diff --git a/tests/inc/wp_functions.php b/tests/inc/wp_functions.php
new file mode 100644
index 000000000..035e35885
--- /dev/null
+++ b/tests/inc/wp_functions.php
@@ -0,0 +1,41 @@
+