php_Fields.php

<?php

namespace FindStr;

/**
 * Class Fields
 *
 * This class is responsible for managing the fields in the FindStr application.
 * It provides methods for getting, updating, and deleting fields.
 * It also provides methods for getting field types and data sources.
 */
class Fields {

  public array $field_types      = array();
  public string $field_save_file = 'findstr/fields.json';
  public array $data_sources     = array();

  public function __construct() {

    //default fields type
    $this->field_types = array(
      'search'          => array(
        'label' => __( 'Search', 'findstr' ),
        'type'  => 'search',
      ),
      'checkbox'        => array(
        'label' => _x( 'Checkbox', 'field type', 'findstr' ),
        'type'  => 'checkbox',
      ),
      'dropdown'        => array(
        'label' => _x( 'Dropdown', 'field type', 'findstr' ),
        'type'  => 'select',
      ),
      'toggle'          => array(
        'label' => _x( 'Toggle', 'field type', 'findstr' ),
        'type'  => 'toggle',
      ),
      'datepicker'      => array(
        'label' => _x( 'Date Picker', 'field type', 'findstr' ),
        'type'  => 'datepicker',
      ),
      'results-count'   => array(
        'label' => _x( 'Results Count', 'field type', 'findstr' ),
        'type'  => 'results-count',
      ),
      'sort'            => array(
        'label' => _x( 'Sort', 'field type', 'findstr' ),
        'type'  => 'sort',
      ),
      'pagination'      => array(
        'label' => _x( 'Pagination', 'field type', 'findstr' ),
        'type'  => 'pagination',
      ),
      'loadMore'        => array(
        'label' => _x( 'Load More', 'field type', 'findstr' ),
        'type'  => 'loadMore',
      ),
      'selectedFilters' => array(
        'label' => _x( 'Selected Filters', 'field type', 'findstr' ),
        'type'  => 'selectedFilters',
      ),
      'reset'           => array(
        'label' => _x( 'Reset', 'field type', 'findstr' ),
        'type'  => 'reset',
      ),
    );

    /**
     * Filter the path where the fields are saved
     * This filter allows you to change the path where the fields are saved.
     *
     * @hook findstr_fields_save_path
     *
     * @param {string} $path
     *
     * @return {string} $path
     */
    $this->field_save_file = apply_filters( 'findstr_fields_save_path', trailingslashit( get_stylesheet_directory() ) . $this->field_save_file );

  }

  /**
   * Get all fields
   *
   * @return array
   */
  public function get_fields(): array {

    if ( is_file( $this->field_save_file ) ) {
      $fields_json = file_get_contents( $this->field_save_file, true );
      if ( ! empty( $fields_json ) ) {
        $fields = json_decode( $fields_json, true );
        if ( ! empty( $fields ) ) {

          /**
           * This filter allows you to modify the fields before they are returned.
           *
           * @hook findstr_get_fields
           *
           * @param {array} $fields
           *
           * @return {array} $fields
           *
           */
          return (array) apply_filters( 'findstr_get_fields', $fields );
        }
      }
    }

    $fields = (array) get_option( 'findstr_fields', array() );

    /**
     * @see findstr_get_fields hook
     */
    return (array) apply_filters( 'findstr_get_fields', $fields );
  }


  public function update_fields( $fields ) {

    /**
     * This filter allows you to modify the fields before they are saved.
     *
     * @hook findstr_update_fields
     *
     * @param {array} $fields
     *
     * @return {array} $fields
     */
    $fields = apply_filters( 'findstr_update_fields', $fields );

    $fields_json = wp_json_encode( $fields, JSON_PRETTY_PRINT );

    $file_stored = ( new Settings() )->is_file_stored();
    if ( $file_stored ) {
      Helpers::file_put_content( $this->field_save_file, $fields_json );
    }

    update_option( 'findstr_fields', $fields );

    return $fields;
  }

  public function update_field( $field ) {

    if ( empty( $field['id'] ) ) {
      return false;
    }
    $fields = $this->get_fields();

    //remove fields with the fieldSlug property starting with and underscore,
    //because they are reserved for internal use
    $fields = array_filter(
      $fields,
      function ( $f ) {
        return 0 !== strpos( $f['fieldSlug'], '_' );
      }
    );

    $fields[ $field['id'] ] = $field;
    $this->update_fields( $fields );

    return $fields;
  }

  public function get_field( string $slug ) {

    $fields = $this->get_fields();

    if ( empty( $fields ) ) {
      return false;
    }

    $key = array_search( $slug, array_column( $fields, 'fieldSlug', 'id' ), true );

    if ( ! empty( $key ) ) {

      $field = $fields[ $key ];

      return new Field( $field );
    }

    return false;
  }

  public function delete_field( $field_id ) {

    if ( empty( $field_id ) ) {
      return false;
    }

    $fields = $this->get_fields();

    unset( $fields[ $field_id ] );

    $this->update_fields( $fields );

    return $fields;
  }


  /**
   * Get field types
   *
   * @return array
   */
  public function get_field_types(): array {
    /**
     * FindStr field types
     *
     * This filter allows you to add or remove field types from the FindStr plugin.
     *
     * @hook findstr_field_types
     *
     * @param {array} $field_types
     *
     * @returns {array} $field_types
     *
     */
    return apply_filters( 'findstr_field_types', $this->field_types );
  }

  public function get_data_sources( $post_types = array() ): array {

    global $wpdb;

    if ( empty( $post_types ) ) {
      $settings   = new Settings();
      $post_types = $settings->get( 'postTypes' );
    }

    // At this point, $post_types is an array of post types.
    // It's empty if no post types are selected in the settings.
    if ( empty( $post_types ) ) {
      return array();
    }

    if ( ! is_array( $post_types ) ) {
      $post_types = array( $post_types );
    }

    if ( ! empty( $this->data_sources ) ) {
      $sources = $this->data_sources;
    } else {

      $sources = array(
        'posts'         => array(
          'label'   => __( 'Posts', 'findstr' ),
          'options' => array(
            array(
              'value' => 'post_title',
              'label' => __( 'Post Title', 'findstr' ),
            ),
            array(
              'value' => 'post_content',
              'label' => __( 'Post Content', 'findstr' ),
            ),
            array(
              'value' => 'post_type',
              'label' => __( 'Post Type', 'findstr' ),
            ),
            array(
              'value' => 'post_date',
              'label' => __( 'Post Date', 'findstr' ),
            ),
            array(
              'value' => 'post_modified',
              'label' => __( 'Post Modified', 'findstr' ),
            ),
            array(
              'value' => 'post_author',
              'label' => __( 'Post Author', 'findstr' ),
            ),
            array(
              'value' => 'post_parent',
              'label' => __( 'Post Parent', 'findstr' ),
            ),
          ),
          'weight'  => 10,
        ),
        'taxonomies'    => array(
          'label'   => __( 'Taxonomies', 'findstr' ),
          'options' => array(),
          'weight'  => 20,
        ),
        'custom_fields' => array(
          'label'   => __( 'Custom Fields', 'findstr' ),
          'options' => array(),
          'weight'  => 30,
        ),
      );

      if ( function_exists( 'acf_get_field_groups' ) ) {
        $sources['acf_fields'] = array(
          'label'   => __( 'Advanced Custom Fields', 'findstr' ),
          'options' => array(),
          'weight'  => 40,
        );
      }

      // Get taxonomies
      foreach ( $post_types as $post_type ) {
        $taxonomies = get_object_taxonomies( $post_type, 'object' );

        foreach ( $taxonomies as $tax ) {
          if ( true === $tax->publicly_queryable ) {
            $sources['taxonomies']['options'][] = array(
              'value' => 'tax/' . $tax->name,
              'label' => $tax->labels->name . ' (' . $tax->name . ')',
            );
          }
        }
      }

      //ninja trick to remove duplicates
      $sources['taxonomies']['options'] = array_values( array_unique( $sources['taxonomies']['options'], SORT_REGULAR ) );

      //Get custom fields
      $meta_keys = get_transient( 'findstr_meta_keys' );
      if ( empty( $meta_keys ) ) {
        $meta_keys = $wpdb->get_col(
          $wpdb->prepare(
            "SELECT DISTINCT pm.meta_key
                  FROM {$wpdb->postmeta} pm
                  LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
                  WHERE p.post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )
                  AND pm.meta_key NOT LIKE %s
                  ORDER BY pm.meta_key',
            array_merge( $post_types, array( '\_%' ) )
          )
        );

        set_transient( 'findstr_meta_keys', $meta_keys, 4 * HOUR_IN_SECONDS );
      }

      /**
       * Filter to exclude custom fields.
       * This filter allows you to exclude specified data sources from UI.
       *
       * @hook findstr_excluded_custom_fields
       *
       * @param {array} $excluded_fields
       *
       * @return {array} $excluded_fields
       *
       */
      $excluded_fields = apply_filters(
        'findstr_excluded_custom_fields',
        array(
          '_edit_last',
          '_edit_lock',
        )
      );

      $custom_fields = array_diff( $meta_keys, $excluded_fields );

      foreach ( $custom_fields as $cf ) {
        if ( 0 !== strpos( $cf, '_oembed_' ) ) {
          $sources['custom_fields']['options'][] = array(
            'value' => 'cf/' . $cf,
            'label' => $cf,
          );
        }
      }

      //get acf fields
      if ( function_exists( 'acf_get_field_groups' ) ) {
        $acf_groups = acf_get_field_groups();
        $acf_fields = array();

        foreach ( $acf_groups as $group ) {
          $fields = acf_get_fields( $group['key'] );

          foreach ( $fields as $field ) {
            $sources['acf_fields']['options'][] = array(
              'value' => 'acf/' . $field['name'],
              'label' => $group['title'] . ' - ' . $field['label'] . ' (' . $field['name'] . ')',
            );
          }
        }
      }

      $this->data_sources = $sources;
    }

    /**
     * This filter allows you to modify the data sources before they are returned.
     *
     * @hook findstr_fields_sources
     *
     * @param {array} $sources
     * @param {array} $post_types
     *
     * @return {array} $sources
     */
    $sources = apply_filters( 'findstr_fields_sources', $sources, $post_types );

    uasort( $sources, array( $this, 'sort_by_weight' ) );

    return $sources;
  }

  public function sort_by_weight( $a, $b ): int {
    $a['weight'] = $a['weight'] ?? 10;
    $b['weight'] = $b['weight'] ?? 10;

    if ( $a['weight'] === $b['weight'] ) {
      return 0;
    }

    return ( $a['weight'] < $b['weight'] ) ? - 1 : 1;
  }

  public static function get_field_source_name( $field ) {

    if ( ! empty( $field['options']['sourceName'] ) ) {

      $field_source = explode( '/', $field['options']['sourceName'] );
      return end( $field_source );
    }

    return false;
  }

}