diff --git a/.github/workflows/deploy-tag.yml b/.github/workflows/deploy-tag.yml index 64fff3b..1ceeb23 100644 --- a/.github/workflows/deploy-tag.yml +++ b/.github/workflows/deploy-tag.yml @@ -9,8 +9,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - name: normal build - run: composer install - name: remove installers run: composer remove composer/installers - name: optimized build diff --git a/.github/workflows/test_php8.yml b/.github/workflows/test_php8.yml index a5cadb1..2a82dc9 100644 --- a/.github/workflows/test_php8.yml +++ b/.github/workflows/test_php8.yml @@ -61,7 +61,7 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install dependencies - run: composer install --prefer-dist --no-interaction --ignore-platform-reqs + run: composer install --prefer-dist --no-interaction --no-scripts --ignore-platform-reqs - name: Install tests run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wp-versions }} diff --git a/.github/workflows/test_rocketcdn.yml b/.github/workflows/test_rocketcdn.yml index e51ab38..2523778 100644 --- a/.github/workflows/test_rocketcdn.yml +++ b/.github/workflows/test_rocketcdn.yml @@ -62,7 +62,7 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install dependencies - run: composer install --prefer-dist --no-interaction --ignore-platform-reqs + run: composer install --prefer-dist --no-interaction --no-scripts - name: Install tests run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wp-versions }} diff --git a/.gitignore b/.gitignore index 8b92a69..0fd16bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ composer.phar /vendor/ -src/Dependencies # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # composer.lock diff --git a/composer.json b/composer.json index fceff48..37d7c2a 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require-dev": { "php": "^7 || ^8", "brain/monkey": "^2.0", - "coenjacobs/mozart": "^0.5.1", + "coenjacobs/mozart": "^0.7", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "league/container": "^3.3", "phpcompatibility/phpcompatibility-wp": "^2.0", diff --git a/composer.lock b/composer.lock index c680817..1470814 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": "22246a4fde2b733124fb799855ae6182", + "content-hash": "05d14a7ac2d918dedecab2a7125d6243", "packages": [ { "name": "composer/installers", @@ -279,26 +279,29 @@ }, { "name": "coenjacobs/mozart", - "version": "0.5.1", + "version": "0.7.1", "source": { "type": "git", "url": "https://github.com/coenjacobs/mozart.git", - "reference": "4bdde231f3309d9299c87b2246166e87acc93653" + "reference": "dbcdeb992d20d9c8914eef090f9a0d684bb1102c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/4bdde231f3309d9299c87b2246166e87acc93653", - "reference": "4bdde231f3309d9299c87b2246166e87acc93653", + "url": "https://api.github.com/repos/coenjacobs/mozart/zipball/dbcdeb992d20d9c8914eef090f9a0d684bb1102c", + "reference": "dbcdeb992d20d9c8914eef090f9a0d684bb1102c", "shasum": "" }, "require": { "league/flysystem": "^1.0", - "php": "^7.2", + "php": "^7.3|^8.0", "symfony/console": "^4|^5", "symfony/finder": "^4|^5" }, "require-dev": { - "phpunit/phpunit": "^8.5" + "mheap/phpunit-github-actions-printer": "^1.4", + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" }, "bin": [ "bin/mozart" @@ -322,9 +325,15 @@ "description": "Composes all dependencies as a package inside a WordPress plugin", "support": { "issues": "https://github.com/coenjacobs/mozart/issues", - "source": "https://github.com/coenjacobs/mozart/tree/0.5.1" + "source": "https://github.com/coenjacobs/mozart/tree/0.7.1" }, - "time": "2019-12-23T12:24:56+00:00" + "funding": [ + { + "url": "https://github.com/coenjacobs", + "type": "github" + } + ], + "time": "2021-02-02T21:37:03+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", diff --git a/src/Dependencies/LaunchpadCore/Activation/Activation.php b/src/Dependencies/LaunchpadCore/Activation/Activation.php new file mode 100644 index 0000000..47be36c --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Activation/Activation.php @@ -0,0 +1,158 @@ + $value ) { + self::$container->add( $key, $value ); + } + + $container->share( 'dispatcher', self::$dispatcher ); + + $container->inflector( PrefixAwareInterface::class )->invokeMethod( 'set_prefix', [ key_exists( 'prefix', self::$params ) ? self::$params['prefix'] : '' ] ); + $container->inflector( DispatcherAwareInterface::class )->invokeMethod( 'set_dispatcher', [ $container->get( 'dispatcher' ) ] ); + + $providers = array_filter( + self::$providers, + function ( $provider ) { + if ( is_string( $provider ) ) { + $provider = new $provider(); + } + + if ( ! $provider instanceof ActivationServiceProviderInterface && ( ! $provider instanceof HasInflectorInterface || count( $provider->get_inflectors() ) === 0 ) ) { + return false; + } + + return $provider; + } + ); + + /** + * Activation providers. + * + * @param AbstractServiceProvider[] $providers Providers. + * @return AbstractServiceProvider[] + */ + $providers = apply_filters( "{$container->get('prefix')}deactivate_providers", $providers ); + + $providers = array_map( + function ( $provider ) { + if ( is_string( $provider ) ) { + return new $provider(); + } + return $provider; + }, + $providers + ); + + foreach ( $providers as $provider ) { + self::$container->addServiceProvider( $provider ); + } + + foreach ( $providers as $service_provider ) { + if ( ! $service_provider instanceof HasInflectorInterface ) { + continue; + } + $service_provider->register_inflectors(); + } + + foreach ( $providers as $provider ) { + if ( ! $provider instanceof HasActivatorServiceProviderInterface ) { + continue; + } + + foreach ( $provider->get_activators() as $activator ) { + $activator_instance = self::$container->get( $activator ); + if ( ! $activator_instance instanceof ActivationInterface ) { + continue; + } + $activator_instance->activate(); + } + } + } +} diff --git a/src/Dependencies/LaunchpadCore/Activation/ActivationInterface.php b/src/Dependencies/LaunchpadCore/Activation/ActivationInterface.php new file mode 100644 index 0000000..784145d --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Activation/ActivationInterface.php @@ -0,0 +1,14 @@ +provides; + } + + /** + * Returns a boolean if checking whether this provider provides a specific + * service or returns an array of provided services if no argument passed. + * + * @param string $alias Class searched. + * + * @return boolean + */ + public function provides( string $alias ): bool { + if ( ! $this->loaded ) { + $this->loaded = true; + $this->define(); + } + + return parent::provides( $alias ); + } + + /** + * Return IDs from front subscribers. + * + * @return string[] + */ + public function get_front_subscribers(): array { + return []; + } + + /** + * Return IDs from admin subscribers. + * + * @return string[] + */ + public function get_admin_subscribers(): array { + return []; + } + + /** + * Return IDs from common subscribers. + * + * @return string[] + */ + public function get_common_subscribers(): array { + return []; + } + + /** + * Return IDs from init subscribers. + * + * @return string[] + */ + public function get_init_subscribers(): array { + return []; + } + + /** + * Register service into the provider. + * + * @param string $classname Class to register. + * @param callable|null $method Method called when registering. + * @param string $concrete Concrete class when necessary. + * @return Registration + */ + public function register_service( string $classname, callable $method = null, string $concrete = '' ): Registration { + + $registration = new Registration( $classname ); + + if( $method ) { + $registration->set_definition( $method ); + } + + + + if ( $concrete ) { + $registration->set_concrete( $concrete ); + } + + $this->services_to_load[] = $registration; + + if ( ! in_array( $classname, $this->provides, true ) ) { + $this->provides[] = $classname; + } + + return $registration; + } + + /** + * Define classes. + * + * @return mixed + */ + abstract protected function define(); + + /** + * Register classes provided by the service provider. + * + * @return void + */ + public function register() { + foreach ( $this->services_to_load as $registration ) { + $registration->register( $this->getLeagueContainer() ); + } + } +} diff --git a/src/Dependencies/LaunchpadCore/Container/HasInflectorInterface.php b/src/Dependencies/LaunchpadCore/Container/HasInflectorInterface.php new file mode 100644 index 0000000..90d2269 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Container/HasInflectorInterface.php @@ -0,0 +1,20 @@ + + */ + public function get_inflectors(): array; + + /** + * Register inflectors. + * + * @return void + */ + public function register_inflectors(): void; +} diff --git a/src/Dependencies/LaunchpadCore/Container/InflectorServiceProviderTrait.php b/src/Dependencies/LaunchpadCore/Container/InflectorServiceProviderTrait.php new file mode 100644 index 0000000..2e20159 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Container/InflectorServiceProviderTrait.php @@ -0,0 +1,46 @@ + + */ + public function get_inflectors(): array { + return []; + } + + /** + * Register inflectors. + * + * @return void + */ + public function register_inflectors(): void { + foreach ( $this->get_inflectors() as $class => $data ) { + if ( ! is_array( $data ) || ! key_exists( 'method', $data ) ) { + continue; + } + $method = $data['method']; + + if ( ! key_exists( 'args', $data ) || ! is_array( $data['args'] ) ) { + $this->getLeagueContainer()->inflector( $class )->invokeMethod( $method, [] ); + continue; + } + + $this->getLeagueContainer()->inflector( $class )->invokeMethod( $method, $data['args'] ); + } + } + + /** + * Get the container. + * + * @return Container + */ + abstract public function getLeagueContainer(): Container; // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid +} diff --git a/src/Dependencies/LaunchpadCore/Container/IsOptimizableServiceProvider.php b/src/Dependencies/LaunchpadCore/Container/IsOptimizableServiceProvider.php new file mode 100644 index 0000000..0fae980 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Container/IsOptimizableServiceProvider.php @@ -0,0 +1,8 @@ +prefix = $prefix; + } +} diff --git a/src/Dependencies/LaunchpadCore/Container/PrefixAwareInterface.php b/src/Dependencies/LaunchpadCore/Container/PrefixAwareInterface.php new file mode 100644 index 0000000..78c6d4c --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Container/PrefixAwareInterface.php @@ -0,0 +1,13 @@ +id = $id; + $this->value = $id; + } + + /** + * Define a callback definition for the class. + * + * @param callable $definition Callback definition for the class. + * @return $this + */ + public function set_definition( callable $definition ): Registration { + $this->definition = $definition; + return $this; + } + + /** + * Set a concrete class. + * + * @param mixed $concrete Concrete class. + * @return $this + */ + public function set_concrete( $concrete ): Registration { + $this->value = $concrete; + return $this; + } + + /** + * Make a definition shared. + * + * @return $this + */ + public function share(): Registration { + $this->shared = true; + return $this; + } + + /** + * Register a definition on a container. + * + * @param Container $container Container to register on. + * @return void + */ + public function register( Container $container ) { + $class_registration = $container->add( $this->id, $this->value, $this->shared ); + + if ( ! $this->definition ) { + return; + } + + ( $this->definition )( $class_registration ); + } +} diff --git a/src/Dependencies/LaunchpadCore/Container/ServiceProviderInterface.php b/src/Dependencies/LaunchpadCore/Container/ServiceProviderInterface.php new file mode 100644 index 0000000..a5c1eee --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Container/ServiceProviderInterface.php @@ -0,0 +1,44 @@ + $value ) { + $container->add( $key, $value ); + } + + $container->share( 'dispatcher', self::$dispatcher ); + + $container->inflector( PrefixAwareInterface::class )->invokeMethod( 'set_prefix', [ key_exists( 'prefix', self::$params ) ? self::$params['prefix'] : '' ] ); + $container->inflector( DispatcherAwareInterface::class )->invokeMethod( 'set_dispatcher', [ $container->get( 'dispatcher' ) ] ); + + $providers = array_filter( + self::$providers, + function ( $provider ) { + if ( is_string( $provider ) ) { + $provider = new $provider(); + } + + if ( ! $provider instanceof DeactivationServiceProviderInterface && ( ! $provider instanceof HasInflectorInterface || count( $provider->get_inflectors() ) === 0 ) ) { + return false; + } + + return $provider; + } + ); + + $providers = array_map( + function ( $provider ) { + if ( is_string( $provider ) ) { + return new $provider(); + } + return $provider; + }, + $providers + ); + + foreach ( $providers as $provider ) { + $container->addServiceProvider( $provider ); + } + + foreach ( $providers as $service_provider ) { + if ( ! $service_provider instanceof HasInflectorInterface ) { + continue; + } + $service_provider->register_inflectors(); + } + + /** + * Deactivation providers. + * + * @param AbstractServiceProvider[] $providers Providers. + * @return AbstractServiceProvider[] + */ + $providers = apply_filters( "{$container->get('prefix')}deactivate_providers", $providers ); + + foreach ( $providers as $provider ) { + if ( ! $provider instanceof HasDeactivatorServiceProviderInterface ) { + continue; + } + + foreach ( $provider->get_deactivators() as $deactivator ) { + $deactivator_instance = self::$container->get( $deactivator ); + if ( ! $deactivator_instance instanceof DeactivationInterface ) { + continue; + } + $deactivator_instance->deactivate(); + } + } + } +} diff --git a/src/Dependencies/LaunchpadCore/Deactivation/DeactivationInterface.php b/src/Dependencies/LaunchpadCore/Deactivation/DeactivationInterface.php new file mode 100644 index 0000000..27cbb47 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Deactivation/DeactivationInterface.php @@ -0,0 +1,13 @@ +dispatcher = $dispatcher; + } +} diff --git a/src/Dependencies/LaunchpadCore/Dispatcher/Sanitizer/SubscriberSignaturesSanitizer.php b/src/Dependencies/LaunchpadCore/Dispatcher/Sanitizer/SubscriberSignaturesSanitizer.php new file mode 100644 index 0000000..0cc03fa --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Dispatcher/Sanitizer/SubscriberSignaturesSanitizer.php @@ -0,0 +1,55 @@ +is_default = false; + + if ( ! is_array( $value ) ) { + $this->is_default = true; + return false; + } + + $output = []; + + foreach ( $value as $subscriber ) { + if ( ! is_string( $subscriber ) && ! is_object( $subscriber ) ) { + continue; + } + + $output [] = $subscriber; + } + + return $output; + } + + /** + * Should return default value. + * + * @param mixed $value Current value. + * @param mixed $original Original value. + * + * @return bool + */ + public function is_default( $value, $original ): bool { + return $this->is_default; + } +} diff --git a/src/Dependencies/LaunchpadCore/EventManagement/ClassicSubscriberInterface.php b/src/Dependencies/LaunchpadCore/EventManagement/ClassicSubscriberInterface.php new file mode 100644 index 0000000..c40d558 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/EventManagement/ClassicSubscriberInterface.php @@ -0,0 +1,24 @@ + 'method_name') + * * array('hook_name' => array('method_name', $priority)) + * * array('hook_name' => array('method_name', $priority, $accepted_args)) + * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) + * + * @return array + */ + public function get_subscribed_events(); +} diff --git a/src/Dependencies/LaunchpadCore/EventManagement/EventManager.php b/src/Dependencies/LaunchpadCore/EventManagement/EventManager.php new file mode 100644 index 0000000..f95a001 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/EventManagement/EventManager.php @@ -0,0 +1,135 @@ + + */ +class EventManager { + /** + * Adds a callback to a specific hook of the WordPress plugin API. + * + * @uses add_filter() + * + * @param string $hook_name Name of the hook. + * @param callable $callback Callback function. + * @param int $priority Priority. + * @param int $accepted_args Number of arguments. + */ + public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { + add_filter( $hook_name, $callback, $priority, $accepted_args ); + } + + /** + * Add an event subscriber. + * + * The event manager registers all the hooks that the given subscriber + * wants to register with the WordPress Plugin API. + * + * @param ClassicSubscriberInterface $subscriber Subscriber_Interface implementation. + */ + public function add_subscriber( ClassicSubscriberInterface $subscriber ) { + if ( $subscriber instanceof EventManagerAwareSubscriberInterface ) { + $subscriber->set_event_manager( $this ); + } + + $events = $subscriber->get_subscribed_events(); + + if ( empty( $events ) ) { + return; + } + + foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { + $this->add_subscriber_callback( $subscriber, $hook_name, $parameters ); + } + } + + /** + * Checks the WordPress plugin API to see if the given hook has + * the given callback. The priority of the callback will be returned + * or false. If no callback is given will return true or false if + * there's any callbacks registered to the hook. + * + * @uses has_filter() + * + * @param string $hook_name Hook name. + * @param mixed $callback Callback. + * + * @return bool|int + */ + public function has_callback( $hook_name, $callback = false ) { + return has_filter( $hook_name, $callback ); + } + + /** + * Removes the given callback from the given hook. The WordPress plugin API only + * removes the hook if the callback and priority match a registered hook. + * + * @uses remove_filter() + * + * @param string $hook_name Hook name. + * @param callable $callback Callback. + * @param int $priority Priority. + * + * @return bool + */ + public function remove_callback( $hook_name, $callback, $priority = 10 ) { + return remove_filter( $hook_name, $callback, $priority ); + } + + /** + * Remove an event subscriber. + * + * The event manager removes all the hooks that the given subscriber + * wants to register with the WordPress Plugin API. + * + * @param SubscriberInterface $subscriber Subscriber_Interface implementation. + */ + public function remove_subscriber( SubscriberInterface $subscriber ) { + foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { + $this->remove_subscriber_callback( $subscriber, $hook_name, $parameters ); + } + } + + /** + * Adds the given subscriber's callback to a specific hook + * of the WordPress plugin API. + * + * @param SubscriberInterface $subscriber Subscriber_Interface implementation. + * @param string $hook_name Hook name. + * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. + */ + private function add_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { + if ( is_string( $parameters ) ) { + $this->add_callback( $hook_name, [ $subscriber, $parameters ] ); + } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { + foreach ( $parameters as $parameter ) { + $this->add_subscriber_callback( $subscriber, $hook_name, $parameter ); + } + } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { + $this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 ); + } + } + + /** + * Removes the given subscriber's callback to a specific hook + * of the WordPress plugin API. + * + * @param SubscriberInterface $subscriber Subscriber_Interface implementation. + * @param string $hook_name Hook name. + * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. + */ + private function remove_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { + if ( is_string( $parameters ) ) { + $this->remove_callback( $hook_name, [ $subscriber, $parameters ] ); + } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { + foreach ( $parameters as $parameter ) { + $this->remove_subscriber_callback( $subscriber, $hook_name, $parameter ); + } + } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { + $this->remove_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10 ); + } + } +} diff --git a/src/Dependencies/LaunchpadCore/EventManagement/EventManagerAwareSubscriberInterface.php b/src/Dependencies/LaunchpadCore/EventManagement/EventManagerAwareSubscriberInterface.php new file mode 100644 index 0000000..1a6af03 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/EventManagement/EventManagerAwareSubscriberInterface.php @@ -0,0 +1,13 @@ + 'method_name') + * * array('hook_name' => array('method_name', $priority)) + * * array('hook_name' => array('method_name', $priority, $accepted_args)) + * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) + * + * @return array + */ + public static function get_subscribed_events(); +} diff --git a/src/Dependencies/LaunchpadCore/EventManagement/SubscriberInterface.php b/src/Dependencies/LaunchpadCore/EventManagement/SubscriberInterface.php new file mode 100644 index 0000000..9263f3c --- /dev/null +++ b/src/Dependencies/LaunchpadCore/EventManagement/SubscriberInterface.php @@ -0,0 +1,8 @@ +prefix = $prefix; + } + + /** + * Wrap a subscriber will the common interface for subscribers. + * + * @param object $instance Any class subscriber. + * + * @return SubscriberInterface + * @throws ReflectionException Error is the class name is not valid. + */ + public function wrap( $instance ): SubscriberInterface { + if ( $instance instanceof OptimizedSubscriberInterface ) { + return new WrappedSubscriber( $instance, $instance->get_subscribed_events() ); + } + + $methods = get_class_methods( $instance ); + $reflection_class = new ReflectionClass( get_class( $instance ) ); + $events = []; + foreach ( $methods as $method ) { + $method_reflection = $reflection_class->getMethod( $method ); + $doc_comment = $method_reflection->getDocComment(); + if ( ! $doc_comment ) { + continue; + } + $pattern = '#@hook\s(?[a-zA-Z\\\-_$/]+)(\s(?[0-9]+))?#'; + + preg_match_all( $pattern, $doc_comment, $matches, PREG_PATTERN_ORDER ); + if ( ! $matches ) { + continue; + } + + foreach ( $matches[0] as $index => $match ) { + $hook = str_replace( '$prefix', $this->prefix, $matches['name'][ $index ] ); + + $events[ $hook ][] = [ + $method, + key_exists( 'priority', $matches ) && key_exists( $index, $matches['priority'] ) && '' !== $matches['priority'][ $index ] ? (int) $matches['priority'][ $index ] : 10, + $method_reflection->getNumberOfParameters(), + ]; + } + } + + return new WrappedSubscriber( $instance, $events ); + } +} diff --git a/src/Dependencies/LaunchpadCore/EventManagement/Wrapper/WrappedSubscriber.php b/src/Dependencies/LaunchpadCore/EventManagement/Wrapper/WrappedSubscriber.php new file mode 100644 index 0000000..cb953e4 --- /dev/null +++ b/src/Dependencies/LaunchpadCore/EventManagement/Wrapper/WrappedSubscriber.php @@ -0,0 +1,72 @@ +instance = $instance; + $this->events = $events; + } + + /** + * Returns an array of events that this subscriber wants to listen to. + * + * The array key is the event name. The value can be: + * + * * The method name + * * An array with the method name and priority + * * An array with the method name, priority and number of accepted arguments + * + * For instance: + * + * * array('hook_name' => 'method_name') + * * array('hook_name' => array('method_name', $priority)) + * * array('hook_name' => array('method_name', $priority, $accepted_args)) + * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) + * + * @return array + */ + public function get_subscribed_events(): array { + return $this->events; + } + + /** + * Delegate callbacks to the actual subscriber. + * + * @param string $name Name from the method. + * @param array $arguments Parameters from the method. + * + * @return mixed + */ + public function __call( $name, $arguments ) { + + if ( method_exists( $this, $name ) ) { + return $this->{$name}( ...$arguments ); + } + + return $this->instance->{$name}( ...$arguments ); + } +} diff --git a/src/Dependencies/LaunchpadCore/Plugin.php b/src/Dependencies/LaunchpadCore/Plugin.php new file mode 100644 index 0000000..94d5a6e --- /dev/null +++ b/src/Dependencies/LaunchpadCore/Plugin.php @@ -0,0 +1,267 @@ +container = $container; + $this->event_manager = $event_manager; + $this->subscriber_wrapper = $subscriber_wrapper; + $this->dispatcher = $dispatcher; + } + + /** + * Returns the Rocket container instance. + * + * @return ContainerInterface + */ + public function get_container() { + return $this->container; + } + + /** + * Loads the plugin into WordPress. + * + * @param array $params Parameters to pass to the container. + * @param array $providers List of providers from the plugin. + * + * @return void + * + * @throws ContainerExceptionInterface Error from the container. + * @throws NotFoundExceptionInterface Error when a class is not found on the container. + * @throws ReflectionException Error when a classname is invalid. + */ + public function load( array $params, array $providers = [] ) { + + foreach ( $params as $key => $value ) { + $this->container->share( $key, $value ); + } + + /** + * Runs before the plugin is loaded. + */ + $this->dispatcher->do_action( "{$this->container->get('prefix')}before_load" ); + + add_filter( "{$this->container->get('prefix')}container", [ $this, 'get_container' ] ); + + $this->container->share( 'event_manager', $this->event_manager ); + $this->container->share( 'dispatcher', $this->dispatcher ); + + $this->container->inflector( PrefixAwareInterface::class )->invokeMethod( 'set_prefix', [ $this->container->get( 'prefix' ) ] ); + $this->container->inflector( DispatcherAwareInterface::class )->invokeMethod( 'set_dispatcher', [ $this->container->get( 'dispatcher' ) ] ); + + $providers = array_map( + function ( $classname ) { + if ( is_string( $classname ) ) { + return new $classname(); + } + + return $classname; + }, + $providers + ); + + $providers = $this->optimize_service_providers( $providers ); + + foreach ( $providers as $service_provider ) { + $this->container->addServiceProvider( $service_provider ); + } + + foreach ( $providers as $service_provider ) { + if ( ! $service_provider instanceof HasInflectorInterface ) { + continue; + } + $service_provider->register_inflectors(); + } + + foreach ( $providers as $service_provider ) { + $this->load_init_subscribers( $service_provider ); + } + + foreach ( $providers as $service_provider ) { + $this->load_subscribers( $service_provider ); + } + + /** + * Runs after the plugin is loaded. + */ + $this->dispatcher->do_action( "{$this->container->get('prefix')}after_load" ); + } + + /** + * Optimize service providers to keep only the ones we need to load. + * + * @param ServiceProviderInterface[] $providers Providers given to the plugin. + * + * @return ServiceProviderInterface[] + * + * @throws ContainerExceptionInterface Error from the container. + * @throws NotFoundExceptionInterface Error when a class is not found on the container. + */ + protected function optimize_service_providers( array $providers ): array { + $optimized_providers = []; + + foreach ( $providers as $provider ) { + if ( ! $provider instanceof IsOptimizableServiceProvider ) { + $optimized_providers[] = $provider; + continue; + } + $subscribers = array_merge( $provider->get_common_subscribers(), $provider->get_init_subscribers(), is_admin() ? $provider->get_admin_subscribers() : $provider->get_front_subscribers() ); + + /** + * Plugin Subscribers from a provider. + * + * @param SubscriberInterface[] $subscribers Subscribers. + * @param AbstractServiceProvider $provider Provider. + * + * @return SubscriberInterface[] + */ + $subscribers = $this->dispatcher->apply_filters( "{$this->container->get('prefix')}load_provider_subscribers", new SubscriberSignaturesSanitizer(), $subscribers, $provider ); + + if ( count( $subscribers ) === 0 ) { + continue; + } + + $optimized_providers[] = $provider; + } + + return $optimized_providers; + } + + /** + * Load list of event subscribers from service provider. + * + * @param ServiceProviderInterface $service_provider_instance Instance of service provider. + * + * @return void + * + * @throws ContainerExceptionInterface Error from the container. + * @throws NotFoundExceptionInterface Error when a class is not found on the container. + * @throws ReflectionException Error when a classname is invalid. + */ + private function load_init_subscribers( ServiceProviderInterface $service_provider_instance ) { + $subscribers = $service_provider_instance->get_init_subscribers(); + + /** + * Plugin Init Subscribers. + * + * @param SubscriberInterface[] $subscribers Subscribers. + * + * @return SubscriberInterface[] + */ + $subscribers = $this->dispatcher->apply_filters( "{$this->container->get('prefix')}load_init_subscribers", new SubscriberSignaturesSanitizer(), $subscribers ); + + if ( empty( $subscribers ) ) { + return; + } + + foreach ( $subscribers as $subscriber ) { + $subscriber_object = $this->container->get( $subscriber ); + if ( ! $subscriber_object instanceof ClassicSubscriberInterface ) { + $subscriber_object = $this->subscriber_wrapper->wrap( $subscriber_object ); + } + + $this->event_manager->add_subscriber( $subscriber_object ); + } + } + + /** + * Load list of event subscribers from service provider. + * + * @param ServiceProviderInterface $service_provider_instance Instance of service provider. + * + * @return void + * + * @throws ContainerExceptionInterface Error from the container. + * @throws NotFoundExceptionInterface Error when a class is not found on the container. + * @throws ReflectionException Error when a classname is invalid. + */ + private function load_subscribers( ServiceProviderInterface $service_provider_instance ) { + + $subscribers = $service_provider_instance->get_common_subscribers(); + + if ( ! is_admin() ) { + $subscribers = array_merge( $subscribers, $service_provider_instance->get_front_subscribers() ); + } else { + $subscribers = array_merge( $subscribers, $service_provider_instance->get_admin_subscribers() ); + } + + /** + * Plugin Subscribers. + * + * @param SubscriberInterface[] $subscribers Subscribers. + * @param AbstractServiceProvider $service_provider_instance Provider. + * + * @return SubscriberInterface[] + */ + $subscribers = $this->dispatcher->apply_filters( "{$this->container->get('prefix')}load_subscribers", new SubscriberSignaturesSanitizer(), $subscribers, $service_provider_instance ); + + if ( empty( $subscribers ) ) { + return; + } + + foreach ( $subscribers as $subscriber ) { + $subscriber_object = $this->container->get( $subscriber ); + if ( ! $subscriber_object instanceof ClassicSubscriberInterface ) { + $subscriber_object = $this->subscriber_wrapper->wrap( $subscriber_object ); + } + + $this->event_manager->add_subscriber( $subscriber_object ); + } + } +} diff --git a/src/Dependencies/LaunchpadCore/boot.php b/src/Dependencies/LaunchpadCore/boot.php new file mode 100644 index 0000000..d17f37a --- /dev/null +++ b/src/Dependencies/LaunchpadCore/boot.php @@ -0,0 +1,99 @@ +load( $params, $providers ); + } + ); + + Deactivation::set_container( new Container() ); + Deactivation::set_dispatcher( new Dispatcher() ); + Deactivation::set_params( $params ); + Deactivation::set_providers( $providers ); + + register_deactivation_hook( $plugin_launcher_file, [ Deactivation::class, 'deactivate_plugin' ] ); + + Activation::set_container( new Container() ); + Activation::set_dispatcher( new Dispatcher() ); + Activation::set_params( $params ); + Activation::set_providers( $providers ); + + register_activation_hook( $plugin_launcher_file, [ Activation::class, 'activate_plugin' ] ); +} diff --git a/src/Dependencies/LaunchpadDispatcher/Dispatcher.php b/src/Dependencies/LaunchpadDispatcher/Dispatcher.php new file mode 100644 index 0000000..d155f3f --- /dev/null +++ b/src/Dependencies/LaunchpadDispatcher/Dispatcher.php @@ -0,0 +1,102 @@ +call_deprecated_actions($name, ...$parameters); + do_action($name, ...$parameters); + } + + public function apply_filters(string $name, SanitizerInterface $sanitizer, $default, ...$parameters) + { + $result_deprecated = $this->call_deprecated_filters($name, $default, ...$parameters); + + $result = apply_filters($name, $result_deprecated, ...$parameters); + + $sanitized_result = $sanitizer->sanitize($result); + + if( false === $sanitized_result && $sanitizer->is_default($sanitized_result, $result) ) { + return $default; + } + + return $sanitized_result; + } + + public function apply_string_filters(string $name, string $default, ...$parameters): string + { + return $this->apply_filters($name, new StringSanitizer(), $default, ...$parameters); + } + + public function apply_bool_filters(string $name, bool $default, ...$parameters): bool + { + return $this->apply_filters($name, new BoolSanitizer(), $default, ...$parameters); + } + + public function apply_int_filters(string $name, int $default, ...$parameters): int + { + return $this->apply_filters($name, new IntSanitizer(), $default, ...$parameters); + } + + public function apply_float_filters(string $name, float $default, ...$parameters): float + { + return $this->apply_filters($name, new FloatSanitizer(), $default, ...$parameters); + } + + public function add_deprecated_action(string $name, string $deprecated_name, string $version, string $message = '') + { + $this->deprecated_actions[$name][] = [ + 'name' => $deprecated_name, + 'version' => $version, + 'message' => $message + ]; + } + + public function add_deprecated_filter(string $name, string $deprecated_name, string $version, string $message = '') + { + $this->deprecated_filters[$name][] = [ + 'name' => $deprecated_name, + 'version' => $version, + 'message' => $message + ]; + } + + protected function call_deprecated_actions(string $name, ...$parameters) + { + if( ! key_exists($name, $this->deprecated_actions)) { + return; + } + + foreach ($this->deprecated_actions[$name] as $action) { + do_action_deprecated($action['name'], $parameters, $action['version'], $name, $action['message']); + $this->call_deprecated_actions($action['name'], ...$parameters); + } + } + + protected function call_deprecated_filters(string $name, $default, ...$parameters) + { + if( ! key_exists($name, $this->deprecated_filters)) { + return $default; + } + + foreach ($this->deprecated_filters[$name] as $filter) { + $filter_parameters = array_merge([$default], $parameters); + $default = apply_filters_deprecated($filter['name'], $filter_parameters, $filter['version'], $name, $filter['message']); + $default = $this->call_deprecated_filters($filter['name'], $default, ...$parameters); + } + + return $default; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadDispatcher/Interfaces/SanitizerInterface.php b/src/Dependencies/LaunchpadDispatcher/Interfaces/SanitizerInterface.php new file mode 100644 index 0000000..b784264 --- /dev/null +++ b/src/Dependencies/LaunchpadDispatcher/Interfaces/SanitizerInterface.php @@ -0,0 +1,10 @@ +register_service(OptionsInterface::class) + ->share() + ->set_concrete(Options::class) + ->set_definition(function (DefinitionInterface $definition) { + $definition->addArgument('prefix'); + }); + + $this->register_service(TransientsInterface::class) + ->share() + ->set_concrete(Transients::class) + ->set_definition(function (DefinitionInterface $definition) { + $definition->addArgument('prefix'); + }); + + $this->register_service(SettingsInterface::class) + ->share() + ->set_concrete(Settings::class) + ->set_definition(function (DefinitionInterface $definition) { + $prefix = $this->container->get('prefix'); + $definition->addArguments([OptionsInterface::class, "{$prefix}settings"]); + }); + } + + /** + * Returns inflectors. + * + * @return array[] + */ + public function get_inflectors(): array + { + return [ + OptionsAwareInterface::class => [ + 'method' => 'set_options', + 'args' => [ + OptionsInterface::class, + ], + ], + TransientsAwareInterface::class => [ + 'method' => 'set_transients', + 'args' => [ + TransientsInterface::class, + ], + ], + SettingsAwareInterface::class => [ + 'method' => 'set_settings', + 'args' => [ + SettingsInterface::class, + ], + ], + ]; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadFrameworkOptions/Traits/OptionsAwareTrait.php b/src/Dependencies/LaunchpadFrameworkOptions/Traits/OptionsAwareTrait.php new file mode 100644 index 0000000..2ac1d50 --- /dev/null +++ b/src/Dependencies/LaunchpadFrameworkOptions/Traits/OptionsAwareTrait.php @@ -0,0 +1,26 @@ +options = $options; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadFrameworkOptions/Traits/SettingsAwareTrait.php b/src/Dependencies/LaunchpadFrameworkOptions/Traits/SettingsAwareTrait.php new file mode 100644 index 0000000..33e407e --- /dev/null +++ b/src/Dependencies/LaunchpadFrameworkOptions/Traits/SettingsAwareTrait.php @@ -0,0 +1,26 @@ +settings = $settings; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadFrameworkOptions/Traits/TransientsAwareTrait.php b/src/Dependencies/LaunchpadFrameworkOptions/Traits/TransientsAwareTrait.php new file mode 100644 index 0000000..0f10433 --- /dev/null +++ b/src/Dependencies/LaunchpadFrameworkOptions/Traits/TransientsAwareTrait.php @@ -0,0 +1,26 @@ +transients = $transients; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadOptions/Interfaces/Actions/DeleteInterface.php b/src/Dependencies/LaunchpadOptions/Interfaces/Actions/DeleteInterface.php new file mode 100644 index 0000000..714f360 --- /dev/null +++ b/src/Dependencies/LaunchpadOptions/Interfaces/Actions/DeleteInterface.php @@ -0,0 +1,15 @@ + $values Values to import. + * + * @return void + */ + public function import(array $values); + + /** + * Export settings values. + * + * @return array + */ + public function dumps(): array; +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadOptions/Interfaces/TransientsInterface.php b/src/Dependencies/LaunchpadOptions/Interfaces/TransientsInterface.php new file mode 100644 index 0000000..756dc2f --- /dev/null +++ b/src/Dependencies/LaunchpadOptions/Interfaces/TransientsInterface.php @@ -0,0 +1,25 @@ +prefix = $prefix; + } + + /** + * Gets the option for the given name. Returns the default value if the value does not exist. + * + * @param string $name Name of the option to get. + * @param mixed $default Default value to return if the value does not exist. + * + * @return mixed + */ + public function get( string $name, $default = null ) { + $option = get_option( $this->get_full_key( $name ), $default ); + + if ( is_array( $default ) && ! is_array( $option ) ) { + $option = (array) $option; + } + + return $option; + } + + /** + * Sets the value of an option. Update the value if the option for the given name already exists. + * + * @param string $name Name of the option to set. + * @param mixed $value Value to set for the option. + * + * @return void + */ + public function set( string $name, $value ) { + update_option( $this->get_full_key( $name ), $value ); + } + + /** + * Deletes the option with the given name. + * + * @param string $name Name of the option to delete. + * + * @return void + */ + public function delete( string $name ) { + delete_option( $this->get_full_key( $name ) ); + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadOptions/Settings.php b/src/Dependencies/LaunchpadOptions/Settings.php new file mode 100644 index 0000000..b3eb57a --- /dev/null +++ b/src/Dependencies/LaunchpadOptions/Settings.php @@ -0,0 +1,142 @@ +options = $options; + $this->settings_key = $settings_key; + $this->settings = (array) $this->options->get($settings_key, []); + } + + /** + * @inheritDoc + */ + public function get(string $name, $default = null) + { + /** + * Pre-filter any setting before read + * + * @param mixed $default The default value. + */ + $value = apply_filters( "pre_get_{$this->settings_key}_" . $name, null, $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + if ( null !== $value ) { + return $value; + } + + if( ! $this->has($name)) { + return $default; + } + + /** + * Filter any setting after read + * + * @param mixed $default The default value. + */ + return apply_filters( "get_{$this->settings_key}" . $name, $this->settings[$name], $default ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + } + + /** + * @inheritDoc + */ + public function set(string $name, $value) + { + $this->settings[$name] = $value; + + $this->persist(); + } + + /** + * @inheritDoc + */ + public function delete(string $name) + { + unset($this->settings[$name]); + + $this->persist(); + } + + /** + * @inheritDoc + */ + public function has(string $name): bool + { + return key_exists($name, $this->settings); + } + + /** + * Persist the settings into the database. + * @return void + */ + protected function persist() + { + do_action("pre_persist_{$this->settings_key}", $this->settings); + + $this->options->set($this->settings_key, $this->settings); + + do_action("persist_{$this->settings_key}", $this->settings); + } + + /** + * Import multiple values at once. + * + * @param array $values Values to import. + * + * @return void + */ + public function import(array $values) + { + foreach ($values as $name => $value) { + $this->settings[$name] = $value; + } + + $this->persist(); + } + + /** + * Export settings values. + * + * @return array + */ + public function dumps(): array + { + $output = []; + + foreach ($this->settings as $name => $value) { + $output[$name] = $this->get($name); + } + + return $output; + } +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadOptions/Traits/PrefixedKeyTrait.php b/src/Dependencies/LaunchpadOptions/Traits/PrefixedKeyTrait.php new file mode 100644 index 0000000..89b6133 --- /dev/null +++ b/src/Dependencies/LaunchpadOptions/Traits/PrefixedKeyTrait.php @@ -0,0 +1,45 @@ +prefix . $name; + } + + /** + * Checks if the option with the given name exists. + * + * @param string $name Name of the option to check. + * + * @return bool + */ + public function has( string $name ): bool { + return null !== $this->get( $name ); + } + + /** + * Gets the option for the given name. Returns the default value if the value does not exist. + * + * @param string $name Name of the option to get. + * @param mixed $default Default value to return if the value does not exist. + * + * @return mixed + */ + abstract public function get( string $name, $default = null ); +} \ No newline at end of file diff --git a/src/Dependencies/LaunchpadOptions/Transients.php b/src/Dependencies/LaunchpadOptions/Transients.php new file mode 100644 index 0000000..1baf132 --- /dev/null +++ b/src/Dependencies/LaunchpadOptions/Transients.php @@ -0,0 +1,64 @@ +prefix = $prefix; + } + + /** + * Gets the transient for the given name. Returns the default value if the value does not exist. + * + * @param string $name Name of the transient to get. + * @param mixed $default Default value to return if the value does not exist. + * + * @return mixed + */ + public function get(string $name, $default = null) + { + $transient = get_transient( $this->get_full_key( $name ), $default ); + + if ( is_array( $default ) && ! is_array( $transient ) ) { + $option = (array) $transient; + } + + return $transient; + } + + /** + * Sets the value of an transient. Update the value if the transient for the given name already exists. + * + * @param string $name Name of the transient to set. + * @param mixed $value Value to set for the transient. + * @param int $expiration Time until expiration in seconds. Default 0 (no expiration). + * + * @return void + */ + public function set(string $name, $value, int $expiration = 0) + { + set_transient( $this->get_full_key( $name ), $value, $expiration ); + } + + /** + * Deletes the transient with the given name. + * + * @param string $name Name of the transient to delete. + * + * @return void + */ + public function delete(string $name) + { + delete_transient( $this->get_full_key( $name ) ); + } +} \ No newline at end of file diff --git a/src/Dependencies/League/Container/Argument/ArgumentResolverInterface.php b/src/Dependencies/League/Container/Argument/ArgumentResolverInterface.php new file mode 100644 index 0000000..d6cabf4 --- /dev/null +++ b/src/Dependencies/League/Container/Argument/ArgumentResolverInterface.php @@ -0,0 +1,28 @@ +getValue(); + } elseif ($argument instanceof ClassNameInterface) { + $id = $argument->getClassName(); + } elseif (!is_string($argument)) { + return $argument; + } else { + $justStringValue = true; + $id = $argument; + } + + $container = null; + + try { + $container = $this->getLeagueContainer(); + } catch (ContainerException $e) { + if ($this instanceof ReflectionContainer) { + $container = $this; + } + } + + if ($container !== null) { + try { + return $container->get($id); + } catch (NotFoundException $exception) { + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + if ($justStringValue) { + return $id; + } + + throw $exception; + } + } + + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + // Just a string value. + return $id; + }, $arguments); + } + + /** + * {@inheritdoc} + */ + public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []) : array + { + $arguments = array_map(function (ReflectionParameter $param) use ($method, $args) { + $name = $param->getName(); + $type = $param->getType(); + + if (array_key_exists($name, $args)) { + return new RawArgument($args[$name]); + } + + if ($type) { + if (PHP_VERSION_ID >= 70100) { + $typeName = $type->getName(); + } else { + $typeName = (string) $type; + } + + $typeName = ltrim($typeName, '?'); + + if ($param->isDefaultValueAvailable()) { + return new ClassNameWithOptionalValue($typeName, $param->getDefaultValue()); + } + + return new ClassName($typeName); + } + + if ($param->isDefaultValueAvailable()) { + return new RawArgument($param->getDefaultValue()); + } + + throw new NotFoundException(sprintf( + 'Unable to resolve a value for parameter (%s) in the function/method (%s)', + $name, + $method->getName() + )); + }, $method->getParameters()); + + return $this->resolveArguments($arguments); + } + + /** + * @return ContainerInterface + */ + abstract public function getContainer() : ContainerInterface; + + /** + * @return Container + */ + abstract public function getLeagueContainer() : Container; +} diff --git a/src/Dependencies/League/Container/Argument/ClassName.php b/src/Dependencies/League/Container/Argument/ClassName.php new file mode 100644 index 0000000..e283336 --- /dev/null +++ b/src/Dependencies/League/Container/Argument/ClassName.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getClassName() : string + { + return $this->value; + } +} diff --git a/src/Dependencies/League/Container/Argument/ClassNameInterface.php b/src/Dependencies/League/Container/Argument/ClassNameInterface.php new file mode 100644 index 0000000..2e26f78 --- /dev/null +++ b/src/Dependencies/League/Container/Argument/ClassNameInterface.php @@ -0,0 +1,13 @@ +className = $className; + $this->optionalValue = $optionalValue; + } + + /** + * @inheritDoc + */ + public function getClassName(): string + { + return $this->className; + } + + public function getOptionalValue() + { + return $this->optionalValue; + } +} diff --git a/src/Dependencies/League/Container/Argument/RawArgument.php b/src/Dependencies/League/Container/Argument/RawArgument.php new file mode 100644 index 0000000..6c4a584 --- /dev/null +++ b/src/Dependencies/League/Container/Argument/RawArgument.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Dependencies/League/Container/Argument/RawArgumentInterface.php b/src/Dependencies/League/Container/Argument/RawArgumentInterface.php new file mode 100644 index 0000000..d81c312 --- /dev/null +++ b/src/Dependencies/League/Container/Argument/RawArgumentInterface.php @@ -0,0 +1,13 @@ +definitions = $definitions ?? new DefinitionAggregate; + $this->providers = $providers ?? new ServiceProviderAggregate; + $this->inflectors = $inflectors ?? new InflectorAggregate; + + if ($this->definitions instanceof ContainerAwareInterface) { + $this->definitions->setLeagueContainer($this); + } + + if ($this->providers instanceof ContainerAwareInterface) { + $this->providers->setLeagueContainer($this); + } + + if ($this->inflectors instanceof ContainerAwareInterface) { + $this->inflectors->setLeagueContainer($this); + } + } + + /** + * Add an item to the container. + * + * @param string $id + * @param mixed $concrete + * @param boolean $shared + * + * @return DefinitionInterface + */ + public function add(string $id, $concrete = null, bool $shared = null) : DefinitionInterface + { + $concrete = $concrete ?? $id; + $shared = $shared ?? $this->defaultToShared; + + return $this->definitions->add($id, $concrete, $shared); + } + + /** + * Proxy to add with shared as true. + * + * @param string $id + * @param mixed $concrete + * + * @return DefinitionInterface + */ + public function share(string $id, $concrete = null) : DefinitionInterface + { + return $this->add($id, $concrete, true); + } + + /** + * Whether the container should default to defining shared definitions. + * + * @param boolean $shared + * + * @return self + */ + public function defaultToShared(bool $shared = true) : ContainerInterface + { + $this->defaultToShared = $shared; + + return $this; + } + + /** + * Get a definition to extend. + * + * @param string $id [description] + * + * @return DefinitionInterface + */ + public function extend(string $id) : DefinitionInterface + { + if ($this->providers->provides($id)) { + $this->providers->register($id); + } + + if ($this->definitions->has($id)) { + return $this->definitions->getDefinition($id); + } + + throw new NotFoundException( + sprintf('Unable to extend alias (%s) as it is not being managed as a definition', $id) + ); + } + + /** + * Add a service provider. + * + * @param ServiceProviderInterface|string $provider + * + * @return self + */ + public function addServiceProvider($provider) : self + { + $this->providers->add($provider); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($id, bool $new = false) + { + if ($this->definitions->has($id)) { + $resolved = $this->definitions->resolve($id, $new); + return $this->inflectors->inflect($resolved); + } + + if ($this->definitions->hasTag($id)) { + $arrayOf = $this->definitions->resolveTagged($id, $new); + + array_walk($arrayOf, function (&$resolved) { + $resolved = $this->inflectors->inflect($resolved); + }); + + return $arrayOf; + } + + if ($this->providers->provides($id)) { + $this->providers->register($id); + + if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { + throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); + } + + return $this->get($id, $new); + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + $resolved = $delegate->get($id); + return $this->inflectors->inflect($resolved); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + if ($this->definitions->has($id)) { + return true; + } + + if ($this->definitions->hasTag($id)) { + return true; + } + + if ($this->providers->provides($id)) { + return true; + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + return true; + } + } + + return false; + } + + /** + * Allows for manipulation of specific types on resolution. + * + * @param string $type + * @param callable|null $callback + * + * @return InflectorInterface + */ + public function inflector(string $type, callable $callback = null) : InflectorInterface + { + return $this->inflectors->add($type, $callback); + } + + /** + * Delegate a backup container to be checked for services if it + * cannot be resolved via this container. + * + * @param ContainerInterface $container + * + * @return self + */ + public function delegate(ContainerInterface $container) : self + { + $this->delegates[] = $container; + + if ($container instanceof ContainerAwareInterface) { + $container->setLeagueContainer($this); + } + + return $this; + } +} diff --git a/src/Dependencies/League/Container/ContainerAwareInterface.php b/src/Dependencies/League/Container/ContainerAwareInterface.php new file mode 100644 index 0000000..8a9f8a6 --- /dev/null +++ b/src/Dependencies/League/Container/ContainerAwareInterface.php @@ -0,0 +1,40 @@ +container = $container; + + return $this; + } + + /** + * Get the container. + * + * @return ContainerInterface + */ + public function getContainer() : ContainerInterface + { + if ($this->container instanceof ContainerInterface) { + return $this->container; + } + + throw new ContainerException('No container implementation has been set.'); + } + + /** + * Set a container. + * + * @param Container $container + * + * @return self + */ + public function setLeagueContainer(Container $container) : ContainerAwareInterface + { + $this->container = $container; + $this->leagueContainer = $container; + + return $this; + } + + /** + * Get the container. + * + * @return Container + */ + public function getLeagueContainer() : Container + { + if ($this->leagueContainer instanceof Container) { + return $this->leagueContainer; + } + + throw new ContainerException('No container implementation has been set.'); + } +} diff --git a/src/Dependencies/League/Container/Definition/Definition.php b/src/Dependencies/League/Container/Definition/Definition.php new file mode 100644 index 0000000..ff5c0c7 --- /dev/null +++ b/src/Dependencies/League/Container/Definition/Definition.php @@ -0,0 +1,278 @@ +alias = $id; + $this->concrete = $concrete; + } + + /** + * {@inheritdoc} + */ + public function addTag(string $tag) : DefinitionInterface + { + $this->tags[$tag] = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + return isset($this->tags[$tag]); + } + + /** + * {@inheritdoc} + */ + public function setAlias(string $id) : DefinitionInterface + { + $this->alias = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAlias() : string + { + return $this->alias; + } + + /** + * {@inheritdoc} + */ + public function setShared(bool $shared = true) : DefinitionInterface + { + $this->shared = $shared; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isShared() : bool + { + return $this->shared; + } + + /** + * {@inheritdoc} + */ + public function getConcrete() + { + return $this->concrete; + } + + /** + * {@inheritdoc} + */ + public function setConcrete($concrete) : DefinitionInterface + { + $this->concrete = $concrete; + $this->resolved = null; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArgument($arg) : DefinitionInterface + { + $this->arguments[] = $arg; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArguments(array $args) : DefinitionInterface + { + foreach ($args as $arg) { + $this->addArgument($arg); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCall(string $method, array $args = []) : DefinitionInterface + { + $this->methods[] = [ + 'method' => $method, + 'arguments' => $args + ]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCalls(array $methods = []) : DefinitionInterface + { + foreach ($methods as $method => $args) { + $this->addMethodCall($method, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function resolve(bool $new = false) + { + $concrete = $this->concrete; + + if ($this->isShared() && $this->resolved !== null && $new === false) { + return $this->resolved; + } + + if (is_callable($concrete)) { + $concrete = $this->resolveCallable($concrete); + } + + if ($concrete instanceof RawArgumentInterface) { + $this->resolved = $concrete->getValue(); + + return $concrete->getValue(); + } + + if ($concrete instanceof ClassNameInterface) { + $concrete = $concrete->getClassName(); + } + + if (is_string($concrete) && class_exists($concrete)) { + $concrete = $this->resolveClass($concrete); + } + + if (is_object($concrete)) { + $concrete = $this->invokeMethods($concrete); + } + + if (is_string($concrete) && $this->getContainer()->has($concrete)) { + $concrete = $this->getContainer()->get($concrete); + } + + $this->resolved = $concrete; + + return $concrete; + } + + /** + * Resolve a callable. + * + * @param callable $concrete + * + * @return mixed + */ + protected function resolveCallable(callable $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + + return call_user_func_array($concrete, $resolved); + } + + /** + * Resolve a class. + * + * @param string $concrete + * + * @return object + * + * @throws ReflectionException + */ + protected function resolveClass(string $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + $reflection = new ReflectionClass($concrete); + + return $reflection->newInstanceArgs($resolved); + } + + /** + * Invoke methods on resolved instance. + * + * @param object $instance + * + * @return object + */ + protected function invokeMethods($instance) + { + foreach ($this->methods as $method) { + $args = $this->resolveArguments($method['arguments']); + + /** @var callable $callable */ + $callable = [$instance, $method['method']]; + call_user_func_array($callable, $args); + } + + return $instance; + } +} diff --git a/src/Dependencies/League/Container/Definition/DefinitionAggregate.php b/src/Dependencies/League/Container/Definition/DefinitionAggregate.php new file mode 100644 index 0000000..73e1d4b --- /dev/null +++ b/src/Dependencies/League/Container/Definition/DefinitionAggregate.php @@ -0,0 +1,124 @@ +definitions = array_filter($definitions, function ($definition) { + return ($definition instanceof DefinitionInterface); + }); + } + + /** + * {@inheritdoc} + */ + public function add(string $id, $definition, bool $shared = false) : DefinitionInterface + { + if (!$definition instanceof DefinitionInterface) { + $definition = new Definition($id, $definition); + } + + $this->definitions[] = $definition + ->setAlias($id) + ->setShared($shared) + ; + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function has(string $id) : bool + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getDefinition(string $id) : DefinitionInterface + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return $definition->setLeagueContainer($this->getLeagueContainer()); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); + } + + /** + * {@inheritdoc} + */ + public function resolve(string $id, bool $new = false) + { + return $this->getDefinition($id)->resolve($new); + } + + /** + * {@inheritdoc} + */ + public function resolveTagged(string $tag, bool $new = false) : array + { + $arrayOf = []; + + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $arrayOf[] = $definition->setLeagueContainer($this->getLeagueContainer())->resolve($new); + } + } + + return $arrayOf; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->definitions); + + for ($i = 0; $i < $count; $i++) { + yield $this->definitions[$i]; + } + } +} diff --git a/src/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php b/src/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php new file mode 100644 index 0000000..7069f2f --- /dev/null +++ b/src/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php @@ -0,0 +1,67 @@ +type = $type; + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function getType() : string + { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function invokeMethod(string $name, array $args) : InflectorInterface + { + $this->methods[$name] = $args; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function invokeMethods(array $methods) : InflectorInterface + { + foreach ($methods as $name => $args) { + $this->invokeMethod($name, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperty(string $property, $value) : InflectorInterface + { + $this->properties[$property] = $this->resolveArguments([$value])[0]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperties(array $properties) : InflectorInterface + { + foreach ($properties as $property => $value) { + $this->setProperty($property, $value); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + $properties = $this->resolveArguments(array_values($this->properties)); + $properties = array_combine(array_keys($this->properties), $properties); + + // array_combine() can technically return false + foreach ($properties ?: [] as $property => $value) { + $object->{$property} = $value; + } + + foreach ($this->methods as $method => $args) { + $args = $this->resolveArguments($args); + + /** @var callable $callable */ + $callable = [$object, $method]; + call_user_func_array($callable, $args); + } + + if ($this->callback !== null) { + call_user_func($this->callback, $object); + } + } +} diff --git a/src/Dependencies/League/Container/Inflector/InflectorAggregate.php b/src/Dependencies/League/Container/Inflector/InflectorAggregate.php new file mode 100644 index 0000000..644668e --- /dev/null +++ b/src/Dependencies/League/Container/Inflector/InflectorAggregate.php @@ -0,0 +1,58 @@ +inflectors[] = $inflector; + + return $inflector; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->inflectors); + + for ($i = 0; $i < $count; $i++) { + yield $this->inflectors[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + foreach ($this->getIterator() as $inflector) { + $type = $inflector->getType(); + + if (! $object instanceof $type) { + continue; + } + + $inflector->setLeagueContainer($this->getLeagueContainer()); + $inflector->inflect($object); + } + + return $object; + } +} diff --git a/src/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php b/src/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php new file mode 100644 index 0000000..7309e01 --- /dev/null +++ b/src/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php @@ -0,0 +1,27 @@ +cacheResolutions === true && array_key_exists($id, $this->cache)) { + return $this->cache[$id]; + } + + if (! $this->has($id)) { + throw new NotFoundException( + sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) + ); + } + + $reflector = new ReflectionClass($id); + $construct = $reflector->getConstructor(); + + if ($construct && !$construct->isPublic()) { + throw new NotFoundException( + sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id) + ); + } + + $resolution = $construct === null + ? new $id + : $resolution = $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) + ; + + if ($this->cacheResolutions === true) { + $this->cache[$id] = $resolution; + } + + return $resolution; + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + return class_exists($id); + } + + /** + * Invoke a callable via the container. + * + * @param callable $callable + * @param array $args + * + * @return mixed + * + * @throws ReflectionException + */ + public function call(callable $callable, array $args = []) + { + if (is_string($callable) && strpos($callable, '::') !== false) { + $callable = explode('::', $callable); + } + + if (is_array($callable)) { + if (is_string($callable[0])) { + $callable[0] = $this->getContainer()->get($callable[0]); + } + + $reflection = new ReflectionMethod($callable[0], $callable[1]); + + if ($reflection->isStatic()) { + $callable[0] = null; + } + + return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); + } + + if (is_object($callable)) { + $reflection = new ReflectionMethod($callable, '__invoke'); + + return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); + } + + $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); + + return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); + } + + /** + * Whether the container should default to caching resolutions and returning + * the cache on following calls. + * + * @param boolean $option + * + * @return self + */ + public function cacheResolutions(bool $option = true) : ContainerInterface + { + $this->cacheResolutions = $option; + + return $this; + } +} diff --git a/src/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php b/src/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php new file mode 100644 index 0000000..1b356af --- /dev/null +++ b/src/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php @@ -0,0 +1,46 @@ +provides, true); + } + + /** + * {@inheritdoc} + */ + public function setIdentifier(string $id) : ServiceProviderInterface + { + $this->identifier = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() : string + { + return $this->identifier ?? get_class($this); + } +} diff --git a/src/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php b/src/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php new file mode 100644 index 0000000..895f4be --- /dev/null +++ b/src/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php @@ -0,0 +1,14 @@ +getContainer()->has($provider)) { + $provider = $this->getContainer()->get($provider); + } elseif (is_string($provider) && class_exists($provider)) { + $provider = new $provider; + } + + if (in_array($provider, $this->providers, true)) { + return $this; + } + + if ($provider instanceof ContainerAwareInterface) { + $provider->setLeagueContainer($this->getLeagueContainer()); + } + + if ($provider instanceof BootableServiceProviderInterface) { + $provider->boot(); + } + + if ($provider instanceof ServiceProviderInterface) { + $this->providers[] = $provider; + + return $this; + } + + throw new ContainerException( + 'A service provider must be a fully qualified class name or instance ' . + 'of (\RocketCDN\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface)' + ); + } + + /** + * {@inheritdoc} + */ + public function provides(string $service) : bool + { + foreach ($this->getIterator() as $provider) { + if ($provider->provides($service)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->providers); + + for ($i = 0; $i < $count; $i++) { + yield $this->providers[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function register(string $service) + { + if (false === $this->provides($service)) { + throw new ContainerException( + sprintf('(%s) is not provided by a service provider', $service) + ); + } + + foreach ($this->getIterator() as $provider) { + if (in_array($provider->getIdentifier(), $this->registered, true)) { + continue; + } + + if ($provider->provides($service)) { + $this->registered[] = $provider->getIdentifier(); + $provider->register(); + } + } + } +} diff --git a/src/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php b/src/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php new file mode 100644 index 0000000..a0eb57e --- /dev/null +++ b/src/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php @@ -0,0 +1,36 @@ +leagueContainer property or the `getLeagueContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register(); + + /** + * Set a custom id for the service provider. This enables + * registering the same service provider multiple times. + * + * @param string $id + * + * @return self + */ + public function setIdentifier(string $id) : ServiceProviderInterface; + + /** + * The id of the service provider uniquely identifies it, so + * that we can quickly determine if it has already been registered. + * Defaults to get_class($provider). + * + * @return string + */ + public function getIdentifier() : string; +} diff --git a/src/Dependencies/Psr/Container/ContainerExceptionInterface.php b/src/Dependencies/Psr/Container/ContainerExceptionInterface.php new file mode 100644 index 0000000..c0ddbc0 --- /dev/null +++ b/src/Dependencies/Psr/Container/ContainerExceptionInterface.php @@ -0,0 +1,12 @@ +