*/ private array $registered = array(); /** * Whether the plugin has already been booted. * * @var bool */ private bool $booted = false; /** * Constructor. * * Private to enforce the singleton pattern. WordPress gives a plugin one * process wide entry point, so the container behind it is a single object * as well; everything past this class receives its dependencies instead of * reaching for them. */ private function __construct() { $this->container = new Container(); $this->container->instance( Container::class, $this->container ); } /** * Returns the shared instance. */ public static function instance(): Plugin { if ( ! self::$instance instanceof Plugin ) { self::$instance = new self(); } return self::$instance; } /** * Boots the plugin. */ public function run(): void { if ( $this->booted ) { return; } $this->booted = true; Services::register( $this->container ); $this->register_modules( Services::modules() ); /** * Fires once every plugin module has been registered. * * @since 1.0.0 * * @param Plugin $plugin The plugin instance. */ do_action( 'formpilot_loaded', $this ); } /** * Returns the service container. * * The one place add-ons reach into the plugin: resolve a repository or a * service instead of building your own. */ public function container(): Container { return $this->container; } /** * Resolves the modules and lets each attach its callbacks. * * A module that cannot be built is skipped rather than fatal: one broken * add-on should not take the whole site down. * * @param array $classes Module class names. */ private function register_modules( array $classes ): void { /** * Filters the modules that attach to WordPress. * * Every entry must implement {@see Hookable}. This is where an add-on * adds a module of its own; bind whatever it depends on beforehand * through the container returned by {@see Plugin::container()}. * * @since 1.0.0 * * @param array $classes Module class names, in load order. */ $classes = (array) apply_filters( 'formpilot_modules', $classes ); foreach ( $classes as $class_name ) { if ( ! is_string( $class_name ) || isset( $this->registered[ $class_name ] ) ) { continue; } if ( ! is_subclass_of( $class_name, Hookable::class ) ) { continue; } try { $module = $this->container->get( $class_name ); if ( ! $module instanceof Hookable ) { continue; } /* * Inside the try as well. `register_hooks()` used to run outside it, * so a module that built cleanly and then threw while registering — * a TypeError, a missing constant, anything an add-on does — was * fatal, which is precisely what this block exists to prevent. */ $module->register_hooks(); $this->registered[ $class_name ] = true; } catch ( \Throwable $exception ) { /* * Keep the site up, but never fail silently: a module that cannot * be built is a wiring bug, and without this the feature simply * disappears with nothing to explain why. * * Announced as well as logged, so it reaches the plugin's own log * when the logging module started and the error log when it did * not — which is the case this has to survive. */ do_action( 'formpilot_log', sprintf( 'Module %1$s could not be built and was skipped. %2$s', $class_name, $exception->getMessage() ), 'error', 'errors', array( 'module' => $class_name ) ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug only, and the alternative is a feature vanishing without trace. sprintf( 'FormPilot — Smart Form Builder: module %1$s was skipped. %2$s', $class_name, $exception->getMessage() ) ); } continue; } } } /** * Prevents cloning of the singleton. */ private function __clone() {} }