meta */ private $registry = array(); /** @var Bylt_Module[] slug => running module */ private $loaded = array(); /** @var array slug => error message */ private $failures = array(); public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Settings-screen groups, in display order. */ public static function categories() { return array( 'customers' => __( 'Customers', 'byltcommerce-toolkit' ), 'orders' => __( 'Orders', 'byltcommerce-toolkit' ), 'checkout' => __( 'Checkout', 'byltcommerce-toolkit' ), 'reports' => __( 'Reports', 'byltcommerce-toolkit' ), 'marketing' => __( 'Marketing', 'byltcommerce-toolkit' ), 'site' => __( 'Site', 'byltcommerce-toolkit' ), ); } public function boot() { Bylt_Audit_Log::maybe_upgrade(); self::migrate(); $this->discover(); $this->load_enabled(); if ( is_admin() ) { new Bylt_Admin( $this ); } do_action( 'bylt_loaded', $this ); } /** One-time changes to saved settings between versions. */ private static function migrate() { $enabled = get_option( self::OPTION, null ); // 0.20.0: gift cards split out of Smart Coupons; keep them on where they were. if ( is_array( $enabled ) && ! get_option( 'bylt_migrated_gift_cards' ) ) { if ( in_array( 'smart-coupons', $enabled, true ) && ! in_array( 'gift-cards', $enabled, true ) ) { $enabled[] = 'gift-cards'; update_option( self::OPTION, $enabled ); } update_option( 'bylt_migrated_gift_cards', 1, false ); } } private static function meta_defaults( $slug ) { return array( 'name' => $slug, 'description' => '', 'category' => 'customers', 'class' => '', 'file' => 'class-module.php', 'requires' => array(), 'default' => false, 'pro' => false, 'locked' => false, 'woocommerce' => true, // Needs WooCommerce to run. 'unavailable' => '', // Reason it can't run on this site, if any ('woocommerce', or a message). ); } private function scan( $dir, $url, $pro ) { $files = glob( $dir . '*/module.php' ); foreach ( $files ? $files : array() as $file ) { $meta = include $file; if ( ! is_array( $meta ) || empty( $meta['class'] ) ) { continue; } $slug = basename( dirname( $file ) ); if ( $pro ) { $meta['pro'] = true; $meta['path'] = dirname( $file ); $meta['url'] = $url . $slug; } $this->registry[ $slug ] = wp_parse_args( $meta, self::meta_defaults( $slug ) ); } } private function discover() { $this->scan( BYLT_DIR . 'modules/', BYLT_URL . 'modules/', false ); if ( Bylt_License::can_use_pro() ) { $this->scan( BYLT_DIR . 'pro__premium_only/modules/', BYLT_URL . 'pro__premium_only/modules/', true ); } // Modules that need WooCommerce, on a site without it. if ( ! class_exists( 'WooCommerce' ) ) { foreach ( $this->registry as $slug => $meta ) { if ( ! empty( $meta['woocommerce'] ) ) { $this->registry[ $slug ]['unavailable'] = 'woocommerce'; // Translated on display: this runs before init. } } } // Pro modules this site can't run yet: listed, locked, with an upgrade link. foreach ( Bylt_License::catalog() as $slug => $meta ) { if ( ! isset( $this->registry[ $slug ] ) ) { $this->registry[ $slug ] = wp_parse_args( array_merge( array( 'woocommerce' => true ), $meta, array( 'pro' => true, 'locked' => true ) ), self::meta_defaults( $slug ) ); } } /** * Lets add-on plugins register extra modules. Each entry needs a 'path' * key pointing at its folder when it lives outside this plugin. */ $this->registry = apply_filters( 'bylt_modules', $this->registry ); } public function registry() { return $this->registry; } /** * Slugs the store owner has switched on. */ public function enabled_slugs() { $saved = get_option( self::OPTION, null ); if ( ! is_array( $saved ) ) { $saved = array(); foreach ( $this->registry as $slug => $meta ) { if ( ! empty( $meta['default'] ) ) { $saved[] = $slug; } } } return array_values( array_intersect( $saved, $this->loadable_slugs() ) ); } /** Slugs that are actually installed and licensed (not locked). */ public function loadable_slugs() { $out = array(); foreach ( $this->registry as $slug => $meta ) { if ( empty( $meta['locked'] ) && empty( $meta['unavailable'] ) ) { $out[] = $slug; } } return $out; } public function save_enabled( array $slugs ) { // Keep the choice for locked Pro modules, so it comes back when a license is activated. $locked_on = array(); $saved = get_option( self::OPTION, array() ); foreach ( is_array( $saved ) ? $saved : array() as $slug ) { if ( isset( $this->registry[ $slug ] ) && ( ! empty( $this->registry[ $slug ]['locked'] ) || ! empty( $this->registry[ $slug ]['unavailable'] ) ) ) { $locked_on[] = $slug; } } $slugs = array_values( array_intersect( array_map( 'sanitize_key', $slugs ), $this->loadable_slugs() ) ); // Switch on anything an enabled module depends on. $changed = true; while ( $changed ) { $changed = false; foreach ( $slugs as $slug ) { foreach ( (array) $this->registry[ $slug ]['requires'] as $dep ) { if ( isset( $this->registry[ $dep ] ) && ! in_array( $dep, $slugs, true ) ) { $slugs[] = $dep; $changed = true; } } } } update_option( self::OPTION, array_values( array_unique( array_merge( $slugs, $locked_on ) ) ) ); } private function load_enabled() { $enabled = $this->enabled_slugs(); // A module that gained a dependency in an update (e.g. Wholesale now uses Forms): switch it on rather than fail. foreach ( $enabled as $slug ) { foreach ( (array) $this->registry[ $slug ]['requires'] as $dep ) { if ( ! in_array( $dep, $enabled, true ) && in_array( $dep, $this->loadable_slugs(), true ) ) { $this->save_enabled( $enabled ); $enabled = $this->enabled_slugs(); break 2; } } } foreach ( $enabled as $slug ) { $this->load( $slug, $enabled, array() ); } } /** * Load one module (and its dependencies). A module that throws while * loading is skipped and reported, instead of taking the site down. */ private function load( $slug, array $enabled, array $stack ) { if ( isset( $this->loaded[ $slug ] ) ) { return true; } if ( isset( $this->failures[ $slug ] ) || in_array( $slug, $stack, true ) ) { return false; } $meta = $this->registry[ $slug ]; $stack[] = $slug; foreach ( (array) $meta['requires'] as $dep ) { if ( ! isset( $this->registry[ $dep ] ) || ! in_array( $dep, $enabled, true ) || ! $this->load( $dep, $enabled, $stack ) ) { $this->failures[ $slug ] = sprintf( 'Needs the %s module, which is off or failed to load.', isset( $this->registry[ $dep ] ) ? $this->registry[ $dep ]['name'] : $dep ); return false; } } $base = ! empty( $meta['path'] ) ? trailingslashit( $meta['path'] ) : BYLT_DIR . 'modules/' . $slug . '/'; try { require_once $base . $meta['file']; if ( ! class_exists( $meta['class'] ) || ! is_subclass_of( $meta['class'], 'Bylt_Module' ) ) { throw new RuntimeException( sprintf( 'Class %s not found or does not extend Bylt_Module.', $meta['class'] ) ); } $module = new $meta['class']( $slug, $meta ); $module->init(); $this->loaded[ $slug ] = $module; return true; } catch ( Throwable $e ) { $this->failures[ $slug ] = $e->getMessage() . ' (' . basename( $e->getFile() ) . ':' . $e->getLine() . ')'; return false; } } /** * Is a module enabled AND running? */ public function is_active( $slug ) { return isset( $this->loaded[ $slug ] ); } /** * The loaded module object (a subclass of Bylt_Module), or null. * * @param string $slug Module slug. * @return mixed */ public function module( $slug ) { return isset( $this->loaded[ $slug ] ) ? $this->loaded[ $slug ] : null; } public function failures() { return $this->failures; } }