diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 0e2fc394232..edaaa686499 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -12,11 +12,12 @@ 'admin.onboarding_menu' => \AmpProject\AmpWP\Admin\OnboardingWizardSubmenu::class, 'admin.onboarding_wizard' => \AmpProject\AmpWP\Admin\OnboardingWizardSubmenuPage::class, 'admin.options_menu' => \AmpProject\AmpWP\Admin\OptionsMenu::class, - 'admin.polyfills' => \AmpProject\AmpWP\Admin\Polyfills::class, 'admin.paired_browsing' => \AmpProject\AmpWP\Admin\PairedBrowsing::class, - 'admin.validation_counts' => \AmpProject\AmpWP\Admin\ValidationCounts::class, 'admin.plugin_row_meta' => \AmpProject\AmpWP\Admin\PluginRowMeta::class, + 'admin.polyfills' => \AmpProject\AmpWP\Admin\Polyfills::class, + 'admin.validation_counts' => \AmpProject\AmpWP\Admin\ValidationCounts::class, 'amp_slug_customization_watcher' => \AmpProject\AmpWP\AmpSlugCustomizationWatcher::class, + 'background_task_deactivator' => \AmpProject\AmpWP\BackgroundTask\BackgroundTaskDeactivator::class, 'cli.command_namespace' => \AmpProject\AmpWP\CliCli\CommandNamespaceRegistration::class, 'cli.optimizer_command' => \AmpProject\AmpWP\CliCli\OptimizerCommand::class, 'cli.transformer_command' => \AmpProject\AmpWP\CliCli\TransformerCommand::class, @@ -33,10 +34,13 @@ 'editor.editor_support' => \AmpProject\AmpWP\Editor\EditorSupport::class, 'extra_theme_and_plugin_headers' => \AmpProject\AmpWP\ExtraThemeAndPluginHeaders::class, 'injector' => \AmpProject\AmpWP\Infrastructure\Injector::class, + 'loading_error' => \AmpProject\AmpWP\LoadingError::class, 'mobile_redirection' => \AmpProject\AmpWP\MobileRedirection::class, 'obsolete_block_attribute_remover' => \AmpProject\AmpWP\ObsoleteBlockAttributeRemover::class, 'optimizer' => \AmpProject\AmpWP\Optimizer\OptimizerService::class, 'optimizer.hero_candidate_filtering' => \AmpProject\AmpWP\Optimizer\HeroCandidateFiltering::class, + 'paired_routing' => \AmpProject\AmpWP\PairedRouting::class, + 'paired_url' => \AmpProject\AmpWP\PairedUrl::class, 'plugin_activation_notice' => \AmpProject\AmpWP\Admin\PluginActivationNotice::class, 'plugin_registry' => \AmpProject\AmpWP\PluginRegistry::class, 'plugin_suppression' => \AmpProject\AmpWP\PluginSuppression::class, @@ -44,15 +48,12 @@ 'reader_theme_support_features' => \AmpProject\AmpWP\ReaderThemeSupportFeatures::class, 'rest.options_controller' => \AmpProject\AmpWP\OptionsRESTController::class, 'rest.validation_counts_controller' => \AmpProject\AmpWP\Validation\ValidationCountsRestController::class, + 'save_post_validation_event' => \AmpProject\AmpWP\Validation\SavePostValidationEvent::class, 'server_timing' => \AmpProject\AmpWP\Instrumentation\ServerTiming::class, 'site_health_integration' => \AmpProject\AmpWP\Admin\SiteHealth::class, - 'validated_url_stylesheet_gc' => \AmpProject\AmpWP\BackgroundTask\ValidatedUrlStylesheetDataGarbageCollection::class, 'url_validation_cron' => \AmpProject\AmpWP\Validation\URLValidationCron::class, - 'save_post_validation_event' => \AmpProject\AmpWP\Validation\SavePostValidationEvent::class, - 'background_task_deactivator' => \AmpProject\AmpWP\BackgroundTask\BackgroundTaskDeactivator::class, - 'paired_routing' => \AmpProject\AmpWP\PairedRouting::class, - 'paired_url' => \AmpProject\AmpWP\PairedUrl::class, - 'loading_error' => \AmpProject\AmpWP\LoadingError::class, + 'url_validation_rest_controller' => \AmpProject\AmpWP\Validation\URLValidationRESTController::class, + 'validated_url_stylesheet_gc' => \AmpProject\AmpWP\BackgroundTask\ValidatedUrlStylesheetDataGarbageCollection::class, ] ) ); diff --git a/codecov.yml b/codecov.yml index a82746ffacc..f705980f67a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,10 +1,5 @@ # See documentation at https://docs.codecov.io/docs/codecovyml-reference -codecov: - notify: - # Wait for Jest & PHPUnit $ Behat reports before notifying - after_n_builds: 3 - coverage: status: # Patch-level coverage (how well is the PR tested) diff --git a/src/Admin/PairedBrowsing.php b/src/Admin/PairedBrowsing.php index ec0338581d0..156ab603a4a 100644 --- a/src/Admin/PairedBrowsing.php +++ b/src/Admin/PairedBrowsing.php @@ -12,6 +12,7 @@ use AMP_Validation_Manager; use AMP_Validated_URL_Post_Type; use AmpProject\AmpWP\Infrastructure\Conditional; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use AmpProject\AmpWP\Option; @@ -29,7 +30,7 @@ * @since 2.1 * @internal */ -final class PairedBrowsing implements Service, Registerable, Conditional { +final class PairedBrowsing implements Service, Registerable, Conditional, HasRequirements { /** * Query var for requests to open the app. @@ -73,6 +74,17 @@ public static function is_needed() { ); } + /** + * Get the list of service IDs required for this service to be registered. + * + * @return string[] List of required services. + */ + public static function get_requirements() { + return [ + 'dependency_support', + ]; + } + /** * PairedBrowsing constructor. * diff --git a/src/Admin/Polyfills.php b/src/Admin/Polyfills.php index ce5dfe3b261..78c9e34cdd5 100644 --- a/src/Admin/Polyfills.php +++ b/src/Admin/Polyfills.php @@ -11,6 +11,7 @@ use AmpProject\AmpWP\Infrastructure\Conditional; use AmpProject\AmpWP\Infrastructure\Delayed; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use AmpProject\AmpWP\Services; @@ -23,7 +24,7 @@ * @since 2.0 * @internal */ -final class Polyfills implements Conditional, Delayed, Service, Registerable { +final class Polyfills implements Conditional, Delayed, Service, Registerable, HasRequirements { /** * Get the action to use for registering the service. @@ -43,6 +44,17 @@ public static function is_needed() { return ! Services::get( 'dependency_support' )->has_support(); } + /** + * Get the list of service IDs required for this service to be registered. + * + * @return string[] List of required services. + */ + public static function get_requirements() { + return [ + 'dependency_support', + ]; + } + /** * Runs on instantiation. */ diff --git a/src/Admin/ValidationCounts.php b/src/Admin/ValidationCounts.php index b155034b5df..78585a88f28 100644 --- a/src/Admin/ValidationCounts.php +++ b/src/Admin/ValidationCounts.php @@ -9,6 +9,7 @@ use AmpProject\AmpWP\Infrastructure\Conditional; use AmpProject\AmpWP\Infrastructure\Delayed; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use AmpProject\AmpWP\Services; @@ -20,7 +21,7 @@ * @since 2.1 * @internal */ -final class ValidationCounts implements Service, Registerable, Conditional, Delayed { +final class ValidationCounts implements Service, Registerable, Conditional, Delayed, HasRequirements { /** * Assets handle. @@ -38,6 +39,18 @@ public static function get_registration_action() { return 'admin_enqueue_scripts'; } + /** + * Get the list of service IDs required for this service to be registered. + * + * @return string[] List of required services. + */ + public static function get_requirements() { + return [ + 'dependency_support', + 'dev_tools.user_access', + ]; + } + /** * Check whether the conditional object is currently needed. * diff --git a/src/AmpWpPlugin.php b/src/AmpWpPlugin.php index e1997bba6ef..c2dd276f58e 100644 --- a/src/AmpWpPlugin.php +++ b/src/AmpWpPlugin.php @@ -67,23 +67,24 @@ final class AmpWpPlugin extends ServiceBasedPlugin { * @var string[] */ const SERVICES = [ - 'dependency_support' => DependencySupport::class, // Needs to be registered first as other services depend on it. 'admin.analytics_menu' => Admin\AnalyticsOptionsSubmenu::class, 'admin.google_fonts' => Admin\GoogleFonts::class, 'admin.onboarding_menu' => Admin\OnboardingWizardSubmenu::class, 'admin.onboarding_wizard' => Admin\OnboardingWizardSubmenuPage::class, 'admin.options_menu' => Admin\OptionsMenu::class, - 'admin.polyfills' => Admin\Polyfills::class, 'admin.paired_browsing' => Admin\PairedBrowsing::class, - 'admin.validation_counts' => Admin\ValidationCounts::class, 'admin.plugin_row_meta' => Admin\PluginRowMeta::class, + 'admin.polyfills' => Admin\Polyfills::class, + 'admin.validation_counts' => Admin\ValidationCounts::class, 'amp_slug_customization_watcher' => AmpSlugCustomizationWatcher::class, + 'background_task_deactivator' => BackgroundTaskDeactivator::class, 'cli.command_namespace' => Cli\CommandNamespaceRegistration::class, 'cli.optimizer_command' => Cli\OptimizerCommand::class, 'cli.transformer_command' => Cli\TransformerCommand::class, 'cli.validation_command' => Cli\ValidationCommand::class, 'css_transient_cache.ajax_handler' => Admin\ReenableCssTransientCachingAjaxAction::class, 'css_transient_cache.monitor' => BackgroundTask\MonitorCssTransientCaching::class, + 'dependency_support' => DependencySupport::class, 'dev_tools.block_sources' => DevTools\BlockSources::class, 'dev_tools.callback_reflection' => DevTools\CallbackReflection::class, 'dev_tools.error_page' => DevTools\ErrorPage::class, @@ -92,10 +93,13 @@ final class AmpWpPlugin extends ServiceBasedPlugin { 'dev_tools.user_access' => DevTools\UserAccess::class, 'editor.editor_support' => Editor\EditorSupport::class, 'extra_theme_and_plugin_headers' => ExtraThemeAndPluginHeaders::class, + 'loading_error' => LoadingError::class, 'mobile_redirection' => MobileRedirection::class, 'obsolete_block_attribute_remover' => ObsoleteBlockAttributeRemover::class, 'optimizer' => OptimizerService::class, 'optimizer.hero_candidate_filtering' => HeroCandidateFiltering::class, + 'paired_routing' => PairedRouting::class, + 'paired_url' => PairedUrl::class, 'plugin_activation_notice' => Admin\PluginActivationNotice::class, 'plugin_registry' => PluginRegistry::class, 'plugin_suppression' => PluginSuppression::class, @@ -103,16 +107,12 @@ final class AmpWpPlugin extends ServiceBasedPlugin { 'reader_theme_support_features' => ReaderThemeSupportFeatures::class, 'rest.options_controller' => OptionsRESTController::class, 'rest.validation_counts_controller' => Validation\ValidationCountsRestController::class, + 'save_post_validation_event' => SavePostValidationEvent::class, 'server_timing' => Instrumentation\ServerTiming::class, 'site_health_integration' => Admin\SiteHealth::class, - 'validated_url_stylesheet_gc' => BackgroundTask\ValidatedUrlStylesheetDataGarbageCollection::class, - 'url_validation_rest_controller' => Validation\URLValidationRESTController::class, 'url_validation_cron' => URLValidationCron::class, - 'save_post_validation_event' => SavePostValidationEvent::class, - 'background_task_deactivator' => BackgroundTaskDeactivator::class, - 'paired_routing' => PairedRouting::class, - 'paired_url' => PairedUrl::class, - 'loading_error' => LoadingError::class, + 'url_validation_rest_controller' => Validation\URLValidationRESTController::class, + 'validated_url_stylesheet_gc' => BackgroundTask\ValidatedUrlStylesheetDataGarbageCollection::class, ]; /** diff --git a/src/Infrastructure/HasRequirements.php b/src/Infrastructure/HasRequirements.php new file mode 100644 index 00000000000..e85f2f7ab3e --- /dev/null +++ b/src/Infrastructure/HasRequirements.php @@ -0,0 +1,27 @@ +validate_services( $filtered_services, $services ); } - foreach ( $services as $id => $class ) { - $id = $this->maybe_resolve( $id ); - $class = $this->maybe_resolve( $class ); + while ( null !== key( $services ) ) { + $id = $this->maybe_resolve( key( $services ) ); + $class = $this->maybe_resolve( current( $services ) ); + + // Delay registering the service until all requirements are met. + if ( + is_a( $class, HasRequirements::class, true ) + && + ! $this->requirements_are_met( $id, $class, $services ) + ) { + next( $services ); + continue; + } // Allow the services to delay their registration. if ( is_a( $class, Delayed::class, true ) ) { $registration_action = $class::get_registration_action(); - if ( did_action( $registration_action ) ) { + if ( \did_action( $registration_action ) ) { $this->register_service( $id, $class ); + } else { + \add_action( + $registration_action, + function () use ( $id, $class ) { + $this->register_service( $id, $class ); + } + ); + } + + next( $services ); + continue; + } + + $this->register_service( $id, $class ); + + next( $services ); + } + } + + /** + * Determine if the requirements for a service to be registered are met. + * + * This also hooks up another check in the future to the registration action(s) of its requirements. + * + * @param string $id Service ID of the service with requirements. + * @param string $class Service FQCN of the service with requirements. + * @param string[] $services List of services to be registered. + * + * @throws InvalidService If the required service is not recognized. + * + * @return bool Whether the requirements for the service has been met. + */ + protected function requirements_are_met( $id, $class, &$services ) { + $missing_requirements = $this->collect_missing_requirements( $class, $services ); + + if ( empty( $missing_requirements ) ) { + return true; + } + + foreach ( $missing_requirements as $missing_requirement ) { + if ( is_a( $missing_requirement, Delayed::class, true ) ) { + $action = $missing_requirement::get_registration_action(); + if ( \did_action( $action ) ) { continue; } + /* + * The current service depends on another service that is Delayed and hasn't been registered yet + * and for which the registration action has not yet passed. + * + * Therefore, we postpone the registration of the current service up until the requirement's + * action has passed. + * + * We don't register the service right away, though, we will first check whether at that point, + * the requirements have been met. + * + * Note that badly configured requirements can lead to services that will never register at all. + */ + \add_action( - $class::get_registration_action(), - function () use ( $id, $class ) { + $action, + function () use ( $id, $class, $services ) { + if ( ! $this->requirements_are_met( $id, $class, $services ) ) { + return; + } + $this->register_service( $id, $class ); - } + }, + PHP_INT_MAX ); + return false; + } + } + + /* + * The registration actions from all of the requirements were already processed. This means that the missing + * requirement(s) are about to be registered, they just weren't encountered yet while traversing the services + * map. Therefore, we skip registration for now and move this particular service to the end of the service map. + */ + unset( $services[ $id ] ); + $services[ $id ] = $class; + + return false; + } + + /** + * Collect the list of missing requirements for a service which has requirements. + * + * @param string $class Service FQCN of the service with requirements. + * @param string[] $services List of services to register. + * + * @throws InvalidService If the required service is not recognized. + * + * @return string[] List of missing requirements as a $service_id => $service_class mapping. + */ + protected function collect_missing_requirements( $class, $services ) { + $requirements = $class::get_requirements(); + + $missing = []; + + foreach ( $requirements as $requirement ) { + // Bail if it requires a service that is not recognized. + if ( ! array_key_exists( $requirement, $services ) ) { + throw InvalidService::from_service_id( $requirement ); + } + + if ( $this->get_container()->has( $requirement ) ) { continue; } - $this->register_service( $id, $class ); + $missing[ $requirement ] = $services[ $requirement ]; } + + return $missing; } /** diff --git a/tests/php/src/Admin/PairedBrowsingTest.php b/tests/php/src/Admin/PairedBrowsingTest.php index 652f691053b..71475e4cacd 100644 --- a/tests/php/src/Admin/PairedBrowsingTest.php +++ b/tests/php/src/Admin/PairedBrowsingTest.php @@ -11,6 +11,7 @@ use AMP_Theme_Support; use AmpProject\AmpWP\Admin\PairedBrowsing; use AmpProject\AmpWP\Infrastructure\Conditional; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use AmpProject\AmpWP\Option; @@ -52,12 +53,18 @@ public function test_is_needed() { $this->assertSame( Services::get( 'dependency_support' )->has_support(), PairedBrowsing::is_needed() ); } + /** @covers ::get_requirements() */ + public function test_get_requirements() { + $this->assertSame( [ 'dependency_support' ], PairedBrowsing::get_requirements() ); + } + /** @covers ::__construct() */ public function test__construct() { $this->assertInstanceOf( PairedBrowsing::class, $this->instance ); $this->assertInstanceOf( Service::class, $this->instance ); $this->assertInstanceOf( Registerable::class, $this->instance ); $this->assertInstanceOf( Conditional::class, $this->instance ); + $this->assertInstanceOf( HasRequirements::class, $this->instance ); } /** @covers ::register() */ diff --git a/tests/php/src/Admin/PolyfillsTest.php b/tests/php/src/Admin/PolyfillsTest.php index 91ad01f35fa..4706efc2f02 100644 --- a/tests/php/src/Admin/PolyfillsTest.php +++ b/tests/php/src/Admin/PolyfillsTest.php @@ -10,6 +10,7 @@ use AmpProject\AmpWP\Admin\Polyfills; use AmpProject\AmpWP\Infrastructure\Conditional; use AmpProject\AmpWP\Infrastructure\Delayed; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use WP_Scripts; @@ -49,6 +50,12 @@ public function test__construct() { $this->assertInstanceOf( Delayed::class, $this->instance ); $this->assertInstanceOf( Conditional::class, $this->instance ); $this->assertInstanceOf( Registerable::class, $this->instance ); + $this->assertInstanceOf( HasRequirements::class, $this->instance ); + } + + /** @covers ::get_requirements() */ + public function test_get_requirements() { + $this->assertSame( [ 'dependency_support' ], Polyfills::get_requirements() ); } /** diff --git a/tests/php/src/Admin/ValidationCountsTest.php b/tests/php/src/Admin/ValidationCountsTest.php index 3eedaaa33c8..ea7f78dc1d9 100644 --- a/tests/php/src/Admin/ValidationCountsTest.php +++ b/tests/php/src/Admin/ValidationCountsTest.php @@ -14,6 +14,7 @@ use AmpProject\AmpWP\DevTools\UserAccess; use AmpProject\AmpWP\Infrastructure\Conditional; use AmpProject\AmpWP\Infrastructure\Delayed; +use AmpProject\AmpWP\Infrastructure\HasRequirements; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; use AmpProject\AmpWP\Option; @@ -51,6 +52,7 @@ public function test__construct() { $this->assertInstanceOf( Conditional::class, $this->instance ); $this->assertInstanceOf( Service::class, $this->instance ); $this->assertInstanceOf( Registerable::class, $this->instance ); + $this->assertInstanceOf( HasRequirements::class, $this->instance ); } /** @@ -62,6 +64,14 @@ public function test_get_registration_action() { self::assertEquals( 'admin_enqueue_scripts', ValidationCounts::get_registration_action() ); } + /** @covers ::get_requirements() */ + public function test_get_requirements() { + $this->assertSame( + [ 'dependency_support', 'dev_tools.user_access' ], + ValidationCounts::get_requirements() + ); + } + /** * Test ::register(). * diff --git a/tests/php/src/Fixture/DummyServiceWithDelay.php b/tests/php/src/Fixture/DummyServiceWithDelay.php new file mode 100644 index 00000000000..ea3e3bc5598 --- /dev/null +++ b/tests/php/src/Fixture/DummyServiceWithDelay.php @@ -0,0 +1,18 @@ + List of required services. + */ + public static function get_requirements() { + return [ 'service_a' ]; + } +} diff --git a/tests/php/src/Infrastructure/ServiceBasedPluginTest.php b/tests/php/src/Infrastructure/ServiceBasedPluginTest.php index 7c1c38467a9..34a4183a91a 100644 --- a/tests/php/src/Infrastructure/ServiceBasedPluginTest.php +++ b/tests/php/src/Infrastructure/ServiceBasedPluginTest.php @@ -8,6 +8,8 @@ use AmpProject\AmpWP\Infrastructure\ServiceContainer\SimpleServiceContainer; use AmpProject\AmpWP\Tests\Fixture\DummyService; use AmpProject\AmpWP\Tests\Fixture\DummyServiceBasedPlugin; +use AmpProject\AmpWP\Tests\Fixture\DummyServiceWithDelay; +use AmpProject\AmpWP\Tests\Fixture\DummyServiceWithRequirements; use WP_UnitTestCase; final class ServiceBasedPluginTest extends WP_UnitTestCase { @@ -151,6 +153,115 @@ static function ( $services ) { $this->assertInstanceof( DummyService::class, $container->get( 'filtered_service' ) ); } + public function test_it_registers_service_with_requirements() { + $container = new SimpleServiceContainer(); + $plugin = $this->getMockBuilder( DummyServiceBasedPlugin::class ) + ->enableOriginalConstructor() + ->setConstructorArgs( [ true, null, $container ] ) + ->setMethodsExcept( + [ + 'collect_missing_requirements', + 'register', + 'register_services', + 'requirements_are_met', + 'get_container', + 'get_service_classes', + ] + ) + ->getMock(); + + $service_callback = static function ( $services ) { + return array_merge( + $services, + [ 'service_with_requirements' => DummyServiceWithRequirements::class ] + ); + }; + + add_filter( 'services', $service_callback ); + + $plugin->register(); + + $this->assertEquals( 4, count( $container ) ); + $this->assertTrue( $container->has( 'service_a' ) ); + $this->assertInstanceof( DummyService::class, $container->get( 'service_a' ) ); + $this->assertTrue( $container->has( 'service_b' ) ); + $this->assertInstanceof( DummyService::class, $container->get( 'service_b' ) ); + $this->assertTrue( $container->has( 'service_with_requirements' ) ); + $this->assertInstanceof( DummyServiceWithRequirements::class, $container->get( 'service_with_requirements' ) ); + } + + public function test_it_handles_delays_for_requirements() { + $container = new SimpleServiceContainer(); + $plugin = $this->getMockBuilder( DummyServiceBasedPlugin::class ) + ->enableOriginalConstructor() + ->setConstructorArgs( [ true, null, $container ] ) + ->setMethodsExcept( + [ + 'collect_missing_requirements', + 'register', + 'register_services', + 'requirements_are_met', + 'get_container', + 'get_service_classes', + ] + ) + ->getMock(); + + $service_callback = static function ( $services ) { + return array_merge( + $services, + [ + 'service_a' => DummyServiceWithDelay::class, + 'service_with_requirements' => DummyServiceWithRequirements::class, + ] + ); + }; + + add_filter( 'services', $service_callback ); + + $plugin->register(); + + $this->assertEquals( 2, count( $container ) ); + $this->assertFalse( $container->has( 'service_a' ) ); + $this->assertTrue( $container->has( 'service_b' ) ); + $this->assertFalse( $container->has( 'service_with_requirements' ) ); + $this->assertInstanceof( DummyService::class, $container->get( 'service_b' ) ); + + do_action( 'some_action' ); + + $this->assertEquals( 4, count( $container ) ); + $this->assertTrue( $container->has( 'service_a' ) ); + $this->assertInstanceof( DummyServiceWithDelay::class, $container->get( 'service_a' ) ); + $this->assertTrue( $container->has( 'service_b' ) ); + $this->assertInstanceof( DummyService::class, $container->get( 'service_b' ) ); + $this->assertTrue( $container->has( 'service_with_requirements' ) ); + $this->assertInstanceof( DummyServiceWithRequirements::class, $container->get( 'service_with_requirements' ) ); + } + + public function test_it_throws_an_exception_if_unrecognized_service_is_required() { + $container = new SimpleServiceContainer(); + $plugin = $this->getMockBuilder( DummyServiceBasedPlugin::class ) + ->enableOriginalConstructor() + ->setConstructorArgs( [ true, null, $container ] ) + ->setMethodsExcept( + [ + 'register', + 'register_services', + 'get_service_classes', + ] + ) + ->getMock(); + + $service_callback = static function () { + return [ 'service_with_requirements' => DummyServiceWithRequirements::class ]; + }; + + add_filter( 'services', $service_callback ); + + $this->expectExceptionMessage( 'The service ID "service_a" is not recognized and cannot be retrieved.' ); + $plugin->register(); + } + public function test_it_generates_identifiers_as_needed() { $container = new SimpleServiceContainer(); $plugin = $this->getMockBuilder( ServiceBasedPlugin::class )