Refactor app container to modularity.

This commit is contained in:
Pedro Silva 2023-11-23 17:43:56 +00:00
parent 42f995222d
commit 3be8563870
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
22 changed files with 2367 additions and 45 deletions

View file

@ -11,8 +11,11 @@ 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;
return function (
string $root_dir,
@ -34,38 +37,47 @@ return function (
*/
$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();
foreach ( $modules as $module ) {
/* @var $module ModuleInterface module */
$module->run( $app_container );
}
return $bootstrap->container();
return $app_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;
};

View file

@ -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
}

70
composer.lock generated
View file

@ -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",

View file

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerExceptionInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class ContainerConfigurator
{
/**
* @var array<string, callable(ContainerInterface $container):mixed>
*/
private $services = [];
/**
* @var array<string, bool>
*/
private $factoryIds = [];
/**
* @var array<string, array<callable(mixed $service, ContainerInterface $container):mixed>>
*/
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;
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Package;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerExceptionInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class PackageProxyContainer implements ContainerInterface
{
/**
* @var Package
*/
private $package;
/**
* @var ContainerInterface|null
*/
private $container;
/**
* @param Package $package
*/
public function __construct(Package $package)
{
$this->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 {
};
}
}

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\NotFoundExceptionInterface;
class ReadOnlyContainer implements ContainerInterface
{
/**
* @var array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
private $services;
/**
* @var array<string, bool>
*/
private $factoryIds;
/**
* @var array<string, array<callable(mixed, ContainerInterface $container):mixed>>
*/
private $extensions;
/**
* Resolved factories.
*
* @var array<string, mixed>
*/
private $resolvedServices = [];
/**
* @var ContainerInterface[]
*/
private $containers;
/**
* ReadOnlyContainer constructor.
*
* @param array<string, callable(ContainerInterface $container):mixed> $services
* @param array<string, bool> $factoryIds
* @param array<string, array<callable(mixed, ContainerInterface $container):mixed>> $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;
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
interface ExecutableModule extends Module
{
/**
* Perform actions with objects retrieved from the container. Usually, adding WordPress hooks.
* Return true to signal a success, false to signal a failure.
*
* @param ContainerInterface $container
*
* @return bool true when successfully booted, otherwise false.
*/
public function run(ContainerInterface $container): bool;
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface ExtendingModule extends Module
{
/**
* Return application services' extensions.
*
* Array keys will be services' IDs in the container, array values are callback that
* accepts as parameters the original service and a PSR-11 container and return an instance of
* the extended service.
*
* It is possible to explicitly extend extensions made by other modules.
* That is done by using as ID (array key in the `extensions` method) the target module ID
* and the service ID.
*
* @return array<string, callable(mixed $service, \WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function extensions(): array;
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface FactoryModule extends Module
{
/**
* Return application factories.
*
* Similar to `services`, but object created by given factories are not "cached", but a *new*
* instance is returned everytime `get()` is called in the container.
*
* @return array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function factories(): array;
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
/**
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module
*/
interface Module
{
/**
* Unique identifier for your Module.
*
* @return string
*/
public function id(): string;
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
/**
* Trait ModuleClassNameIdTrait
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module
*/
trait ModuleClassNameIdTrait
{
/**
* @return string
* @see Module::id()
*/
public function id(): string
{
return __CLASS__;
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface ServiceModule extends Module
{
/**
* Return application services' factories.
*
* Array keys will be services' IDs in the container, array values are callback that
* accepts a PSR-11 container as parameter and return an instance of the service.
* Services are "cached", so the given factory is called once the first time `get()` is called
* in the container, and on subsequent `get()` the same instance is returned again and again.
*
* @return array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function services(): array;
}

View file

@ -0,0 +1,727 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container\ContainerConfigurator;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container\PackageProxyContainer;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\FactoryModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\Module;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties\Properties;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class Package
{
/**
* All the hooks fired in this class use this prefix.
* @var string
*/
private const HOOK_PREFIX = 'inpsyde.modularity.';
/**
* Identifier to access Properties in Container.
*
* @example
* <code>
* $package = Package::new();
* $package->boot();
*
* $container = $package->container();
* $container->has(Package::PROPERTIES);
* $container->get(Package::PROPERTIES);
* </code>
*
* @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
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_INIT),
* $callback
* );
* </code>
*/
public const ACTION_INIT = 'init';
/**
* Custom action which is triggered after the application
* is booted to access container and properties.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_READY),
* $callback
* );
* </code>
*/
public const ACTION_READY = 'ready';
/**
* Custom action which is triggered when a failure happens during the building stage.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_FAILED_BUILD),
* $callback
* );
* </code>
*/
public const ACTION_FAILED_BUILD = 'failed-build';
/**
* Custom action which is triggered when a failure happens during the booting stage.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_FAILED_BOOT),
* $callback
* );
* </code>
*/
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
* <code>
* $package = Package::new();
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // false
* $package->boot(new SomeModule());
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // true
* </code>
*/
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
* <code>
* $package = Package::new();
* $package->statusIs(Package::IDLE); // true
* $package->boot();
* $package->statusIs(Package::BOOTED); // true
* </code>
*/
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<string, list<string>>
*/
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<string, bool>
*/
private $connectedPackages = [];
/**
* @var list<ExecutableModule>
*/
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<string> $ids */
$ids[] = $id;
}
);
/** @var list<string> $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<string>|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<string, list<string>>
*/
public function modulesStatus(): array
{
return $this->moduleStatus;
}
/**
* @return array<string, bool>
*/
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);
}
}
}

View file

@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
class BaseProperties implements Properties
{
/**
* @var null|bool
*/
protected $isDebug = null;
/**
* @var string
*/
protected $baseName;
/**
* @var string
*/
protected $basePath;
/**
* @var string|null
*/
protected $baseUrl;
/**
* @var array
*/
protected $properties;
/**
* @param string $baseName
* @param string $basePath
* @param string|null $baseUrl
* @param array $properties
*/
protected function __construct(
string $baseName,
string $basePath,
string $baseUrl = null,
array $properties = []
) {
$baseName = $this->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;
}
}

View file

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class LibraryProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*/
class LibraryProperties extends BaseProperties
{
/**
* Allowed configuration in composer.json "extra.modularity".
*
* @var array
*/
public const EXTRA_KEYS = [
self::PROP_DOMAIN_PATH,
self::PROP_NAME,
self::PROP_TEXTDOMAIN,
self::PROP_URI,
self::PROP_VERSION,
self::PROP_REQUIRES_WP,
];
/**
* @param string $composerJsonFile
* @param string|null $baseUrl
*
* @return LibraryProperties
*
* @throws \Exception
* @psalm-suppress MixedArrayAccess
*/
public static function new(string $composerJsonFile, ?string $baseUrl = null): LibraryProperties
{
if (!\is_file($composerJsonFile) || !\is_readable($composerJsonFile)) {
throw new \Exception("File {$composerJsonFile} does not exist or is not readable.");
}
$content = (string) file_get_contents($composerJsonFile);
/** @var array $composerJsonData */
$composerJsonData = json_decode($content, true);
$properties = Properties::DEFAULT_PROPERTIES;
$properties[self::PROP_DESCRIPTION] = $composerJsonData['description'] ?? '';
$properties[self::PROP_TAGS] = $composerJsonData['keywords'] ?? [];
$authors = $composerJsonData['authors'] ?? [];
$names = [];
foreach ((array) $authors as $author) {
$name = $author['name'] ?? null;
if ($name && is_string($name)) {
$names[] = $name;
}
$url = $author['homepage'] ?? null;
if ($url && !$properties['authorUri'] && is_string($url)) {
$properties[self::PROP_AUTHOR_URI] = $url;
}
}
if (count($names) > 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;
}
}

View file

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class PluginProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*
* @psalm-suppress PossiblyFalseArgument, InvalidArgument
*/
class PluginProperties extends BaseProperties
{
/**
* Custom properties for Plugins.
*/
public const PROP_NETWORK = 'network';
/**
* Available methods of Properties::__call()
* from plugin headers.
*
* @link https://developer.wordpress.org/reference/functions/get_plugin_data/
*/
protected const HEADERS = [
self::PROP_AUTHOR => '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;
}
}

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
interface Properties
{
public const PROP_AUTHOR = 'author';
public const PROP_AUTHOR_URI = 'authorUri';
public const PROP_DESCRIPTION = 'description';
public const PROP_DOMAIN_PATH = 'domainPath';
public const PROP_NAME = 'name';
public const PROP_TEXTDOMAIN = 'textDomain';
public const PROP_URI = 'uri';
public const PROP_VERSION = 'version';
public const PROP_REQUIRES_WP = 'requiresWp';
public const PROP_REQUIRES_PHP = 'requiresPhp';
public const PROP_TAGS = 'tags';
/**
* @var array
*/
public const DEFAULT_PROPERTIES = [
self::PROP_AUTHOR => '',
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;
}

View file

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class ThemeProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*
* @psalm-suppress PossiblyFalseArgument, InvalidArgument
*/
class ThemeProperties extends BaseProperties
{
/**
* Additional properties specific for themes.
*/
public const PROP_STATUS = 'status';
public const PROP_TEMPLATE = 'template';
/**
* Available methods of Properties::__call()
* from theme headers.
*
* @link https://developer.wordpress.org/reference/classes/wp_theme/
*/
protected const HEADERS = [
self::PROP_AUTHOR => '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());
}
}

View file

@ -1,7 +1,4 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;

View file

@ -1,7 +1,6 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;
@ -20,7 +19,7 @@ interface ContainerInterface
*
* @return mixed Entry.
*/
public function get($id);
public function get(string $id);
/**
* Returns true if the container can return an entry for the given identifier.
@ -33,5 +32,5 @@ interface ContainerInterface
*
* @return bool
*/
public function has($id);
public function has(string $id);
}

View file

@ -1,7 +1,4 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;

View file

@ -0,0 +1,128 @@
<?php
/**
* The wrapper module.
*
* @package WooCommerce\PayPalCommerce
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class WrapperModule
*/
class DhiiToModularityModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* The Dhii Modules.
*
* @var array|ModuleInterface[]
*/
private $modules;
/**
* The services.
*
* @var array
*/
private $services = array();
/**
* The extensions.
*
* @var array
*/
private $extensions = array();
/**
* The services
*
* @var bool
*/
private $is_initialized = false;
/**
* @param array|ModuleInterface[] $modules
*/
public function __construct( array $modules ) {
$this->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;
}
}