Source: includes/classes/Feature.php

<?php
/**
 * Feature class to be initiated for all features.
 *
 * All features extend this class.
 *
 * @since  2.1
 * @package elasticpress
 */

namespace ElasticPress;

use ElasticPress\FeatureRequirementsStatus;
use ElasticPress\Utils;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Feature abstract class
 */
abstract class Feature {
	/**
	 * Feature slug
	 *
	 * @var string
	 * @since  2.1
	 */
	public $slug;

	/**
	 * Feature pretty title
	 *
	 * @var string
	 * @since  2.1
	 */
	public $title;

	/**
	 * Short title
	 *
	 * @var string
	 * @since 4.4.1
	 */
	public $short_title;

	/**
	 * Feature summary
	 *
	 * @var string
	 * @since  4.0.0
	 */
	public $summary;

	/**
	 * URL to feature documentation.
	 *
	 * @var string
	 * @since  4.0.0
	 */
	public $docs_url;

	/**
	 * Optional feature default settings
	 *
	 * @since  2.2
	 * @var  array
	 */
	public $default_settings = [];

	/**
	 * True if the feature requires content reindexing after activating
	 *
	 * @since 2.1
	 * @var bool
	 */
	public $requires_install_reindex = false;

	/**
	 * The slug of a setting that requires content reindexing after activating.
	 *
	 * @since 4.5.0
	 * @var string
	 */
	public $setting_requires_install_reindex = '';

	/**
	 * The order in the features screen
	 *
	 * @var int
	 * @since  3.6.0
	 */
	public $order;

	/**
	 * Set if a feature should be on the left or right side
	 *
	 * @var string
	 * @since  3.6.0
	 */
	public $group_order;

	/**
	 * True if activation of this feature should be available during
	 * installation.
	 *
	 * @since 4.0.0
	 * @var boolean
	 */
	public $available_during_installation = false;

	/**
	 * Whether the feature should be always visible in the dashboard
	 *
	 * @since 4.5.0
	 * @var boolean
	 */
	protected $is_visible = true;

	/**
	 * Settings description
	 *
	 * @since 5.0.0
	 * @var array
	 */
	protected $settings_schema = [];

	/**
	 * The slug, or array of slugs, of a feature that is required to be active.
	 *
	 * @since 5.0.0
	 * @var false|string|array
	 */
	protected $requires_feature = false;

	/**
	 * Whether the feature is using ElasticPress.io.
	 *
	 * @since 5.0.0
	 * @var boolean
	 */
	protected $is_powered_by_epio = false;

	/**
	 * The name of a group that a feature may belong to.
	 *
	 * @since 5.3.0
	 * @var false|string
	 */
	public $group = false;

	/**
	 * Field groups available to a feature
	 *
	 * @since 5.3.0
	 * @var array
	 */
	protected $field_group_map = [];

	/**
	 * Run on every page load for feature to set itself up
	 *
	 * @since  2.1
	 */
	abstract public function setup();

	/**
	 * Create feature
	 *
	 * @since  3.0
	 */
	public function __construct() {
		/**
		 * Fires when Feature object is created
		 *
		 * @hook ep_feature_create
		 * @param {Feature} $feature Current feature
		 * @since  3.0
		 */
		do_action( 'ep_feature_create', $this );
	}

	/**
	 * Returns requirements status of feature
	 *
	 * @since  2.2
	 * @return FeatureRequirementsStatus
	 */
	public function requirements_status() {
		$status = new FeatureRequirementsStatus( 0 );

		/**
		 * Filter feature requirement status
		 *
		 * @hook ep_{indexable_slug}_index_kill
		 * @param  {FeatureRequirementStatus} $status Current feature requirement status
		 * @param {Feature} $feature Current feature
		 * @since  2.2
		 * @return {FeatureRequirementStatus}  New status
		 */
		return apply_filters( 'ep_feature_requirements_status', $status, $this );
	}

	/**
	 * Return feature settings
	 *
	 * @since  2.2.1, 4.5.0 started using default settings
	 * @return array
	 */
	public function get_settings() {
		$all_settings = Utils\get_option( 'ep_feature_settings', [] );

		$feature_settings = ( ! empty( $all_settings[ $this->slug ] ) ) ? (array) $all_settings[ $this->slug ] : [];

		$feature_settings = wp_parse_args( $feature_settings, $this->default_settings );

		return $feature_settings;
	}

	/**
	 * Return a specific setting of the feature
	 *
	 * @since 4.5.0
	 * @param string $setting_name The setting name
	 * @return mixed
	 */
	public function get_setting( string $setting_name ) {
		$settings = $this->get_settings();

		return isset( $settings[ $setting_name ] ) ? $settings[ $setting_name ] : null;
	}

	/**
	 * Returns true if feature is active
	 *
	 * @since  2.2
	 * @return boolean
	 */
	public function is_active() {
		$feature_settings = Utils\get_option( 'ep_feature_settings', [] );

		$active = false;

		if ( ! empty( $feature_settings[ $this->slug ] ) && $feature_settings[ $this->slug ]['active'] ) {
			$active = true;
		}

		/**
		 * Filter whether a feature is active or not
		 *
		 * @hook ep_feature_active
		 * @param  {bool} $active Whether feature is active or not
		 * @param {array} $feature_settings Current feature settings
		 * @param  {Feature} $feature Current feature
		 * @since  2.2
		 * @return {bool}  New active value
		 */
		return apply_filters( 'ep_feature_active', $active, $feature_settings, $this );
	}

	/**
	 * Get the value of the setting that requires a reindex, if it exists.
	 *
	 * @since 4.5.0
	 * @return mixed
	 */
	public function get_reindex_setting() {
		$settings = $this->get_settings();
		$setting  = $this->setting_requires_install_reindex;

		return $settings && $setting && ! empty( $settings[ $setting ] )
			? $settings[ $setting ]
			: '';
	}

	/**
	 * To be run after initial feature activation
	 *
	 * @since 2.1
	 */
	public function post_activation() {
		/**
		 * Fires after feature is activated
		 *
		 * @hook ep_feature_post_activation
		 * @param  {string} $slug Feature slug
		 * @param {Feature} $feature Current feature
		 * @since  2.1
		 */
		do_action( 'ep_feature_post_activation', $this->slug, $this );
	}

	/**
	 * Returns the feature title.
	 *
	 * @since 4.4.1
	 * @return string
	 */
	public function get_title(): string {
		return $this->title;
	}

	/**
	 * Returns the feature short title.
	 *
	 * @since 4.4.1
	 * @return string
	 */
	public function get_short_title(): string {
		if ( ! empty( $this->short_title ) ) {
			return $this->short_title;
		}

		return $this->get_title();
	}

	/**
	 * Returns whether the feature is visible in the dashboard or not.
	 *
	 * By default, all active features are visible.
	 *
	 * @since 4.5.0
	 * @return boolean
	 */
	public function is_visible() {
		/**
		 * Filter whether a feature is visible or not in the dashboard.
		 *
		 * Example:
		 * ```
		 * add_filter(
		 *     'ep_feature_is_visible',
		 *     function ( $is_visible, $feature_slug ) {
		 *         return 'terms' === $feature_slug ? true : $is_visible;
		 *     },
		 *     10,
		 *     2
		 * );
		 * ```
		 *
		 * @hook ep_feature_is_visible
		 * @param {bool}    $is_visible   True to display the feature
		 * @param {string}  $feature_slug Feature slug
		 * @param {Feature} $feature      Feature object
		 * @since 4.5.0
		 * @return {bool} New $is_visible value
		 */
		return apply_filters( 'ep_feature_is_visible', $this->is_visible || $this->is_active(), $this->slug, $this );
	}

	/**
	 * Returns whether the feature is available or not.
	 *
	 * @since 4.5.0
	 * @return boolean
	 */
	public function is_available(): bool {
		$requirements_status = $this->requirements_status();
		/**
		 * Filter whether a feature is available or not.
		 *
		 * Example:
		 * ```
		 * add_filter(
		 *     'ep_feature_is_available',
		 *     function ( $is_available, $feature_slug ) {
		 *         return 'terms' === $feature_slug ? true : $is_available;
		 *     },
		 *     10,
		 *     2
		 * );
		 * ```
		 *
		 * @hook ep_feature_is_available
		 * @param {bool}    $is_available True if the feature is available
		 * @param {string}  $feature_slug Feature slug
		 * @param {Feature} $feature      Feature object
		 * @since 4.5.0
		 * @return {bool} New $is_available value
		 */
		return apply_filters( 'ep_feature_is_available', $this->is_visible() && 2 !== $requirements_status->code, $this->slug, $this );
	}

	/**
	 * Get a JSON representation of the feature
	 *
	 * @since 5.0.0
	 * @return string
	 */
	public function get_json() {
		$requirements_status = $this->requirements_status();

		$feature_desc = [
			'slug'              => $this->slug,
			'title'             => $this->get_title(),
			'shortTitle'        => $this->get_short_title(),
			'summary'           => $this->summary,
			'docsUrl'           => $this->docs_url,
			'defaultSettings'   => $this->default_settings,
			'order'             => $this->order,
			'isAvailable'       => $this->is_available(),
			'isPoweredByEpio'   => $this->is_powered_by_epio,
			'isVisible'         => $this->is_visible(),
			'reqStatusCode'     => $requirements_status->code,
			'reqStatusMessages' => (array) $requirements_status->message,
			'settingsSchema'    => $this->get_settings_schema(),
			'group'             => $this->group,
			'requiredFeature'   => $this->get_required_feature(),
			'fieldGroups'       => $this->get_field_group_map(),
		];

		return $feature_desc;
	}

	/**
	 * Return the feature settings schema
	 *
	 * @since 5.0.0
	 * @return array
	 */
	public function get_settings_schema() {
		// Settings were not set yet.
		if ( [] === $this->settings_schema ) {
			$this->set_settings_schema();
		}

		$active = [
			'default'          => false,
			'key'              => 'active',
			'label'            => __( 'Enable', 'elasticpress' ),
			'requires_feature' => $this->get_required_feature(),
			'requires_sync'    => $this->requires_install_reindex,
			'type'             => 'toggle',
		];

		$settings_schema = [
			$active,
			...$this->settings_schema,
		];

		/**
		 * Filter the settings schema of a feature
		 *
		 * @hook ep_feature_is_available
		 * @since 5.0.0
		 * @param {array}   $settings_schema True if the feature is available
		 * @param {string}  $feature_slug    Feature slug
		 * @param {Feature} $feature         Feature object
		 * @return {array} New $settings_schema value
		 */
		return apply_filters( 'ep_feature_settings_schema', $settings_schema, $this->slug, $this );
	}

	/**
	 * Default implementation of `set_settings_schema` based on the `default_settings` attribute
	 *
	 * @since 5.0.0
	 */
	protected function set_settings_schema() {
		if ( [] === $this->default_settings ) {
			return;
		}

		foreach ( $this->default_settings as $key => $default_value ) {
			$type = 'text';
			if ( in_array( $default_value, [ '0', '1' ], true ) ) {
				$type = 'checkbox';
			}
			if ( is_bool( $default_value ) ) {
				$type = 'toggle';
			}

			$this->settings_schema[] = [
				'default' => $default_value,
				'key'     => $key,
				'label'   => $key,
				'type'    => $type,
			];
		}
	}

	/**
	 * Sets the i18n strings for the feature.
	 *
	 * @return void
	 * @since 5.2.0
	 */
	public function set_i18n_strings(): void {
	}

	/**
	 * Get all features required by this feature
	 *
	 * @since 5.3.0
	 * @return array List of required feature slugs
	 */
	public function get_required_feature() {
		return $this->requires_feature ? array_unique( (array) $this->requires_feature ) : [];
	}

	/**
	 * Get the field group map for the feature.
	 *
	 * @since 5.3.0
	 * @return array
	 */
	public function get_field_group_map(): array {
		/**
		 * Filter available field groups.
		 *
		 * @hook ep_feature_field_groups
		 * @since 5.3.0
		 * @param  {array} $field_groups Current field groups
		 * @return {array} New field groups
		 */
		return apply_filters( 'ep_feature_field_groups', $this->field_group_map );
	}

	/**
	 * Get the feature slug.
	 *
	 * @since 5.3.0
	 * @return string Feature slug.
	 */
	public function get_feature_slug(): string {
		return $this->slug;
	}
}