File: /volume1/web/qrcodesforeveryone/wp-content/plugins/gravityview/src/Search/SearchFieldCollection.php
<?php
/**
* The Search Field Collection class.
*
* Represents a collection of search fields.
*
* @package GravityKit\GravityView\Search
* @since 3.0.0
*/
namespace GravityKit\GravityView\Search;
use ArrayIterator;
use GravityView_Widget_Search;
use GV\Collection;
use GV\Collection_Position_Aware;
use GV\Grid;
use GV\Plugin;
use GV\Search\Fields\Search_Field;
use GV\Search\Fields\Search_Field_All;
use GV\Search\Fields\Search_Field_Created_By;
use GV\Search\Fields\Search_Field_Entry_Date;
use GV\Search\Fields\Search_Field_Entry_ID;
use GV\Search\Fields\Search_Field_Gravity_Forms;
use GV\Search\Fields\Search_Field_Is_Approved;
use GV\Search\Fields\Search_Field_Is_Read;
use GV\Search\Fields\Search_Field_Is_Starred;
use GV\Search\Fields\Search_Field_Search_Mode;
use GV\Search\Fields\Search_Field_Submit;
use GV\Template_Context;
use GV\View;
use IteratorAggregate;
use JsonException;
use function gravityview;
/**
* Represents a collection of search fields.
*
* @since 2.42
* @since 3.0.0 Migrated to PSR-4 namespace.
*
* @extends Collection<Search_Field>
* @implements IteratorAggregate<Search_Field>
*/
final class SearchFieldCollection extends \GV\Collection implements \GV\Collection_Position_Aware, \IteratorAggregate {
/**
* Micro cache to avoid multiple DB and filter calls.
*
* @since 2.42
*
* @var array<int, self>
*/
private static array $available_fields_cache = [];
/**
* Contains any additional context used for filters.
*
* @since 2.42
*
* @var array
*/
private array $context;
/**
* Weather a parent collection has searchable fields.
*
* @since 2.42
*
* @var bool|null
*/
private ?bool $base_has_visible_fields = null;
/**
* The position what this collection was filtered by.
*
* @since 2.42
*
* @var string|null
*/
private ?string $position = null;
/**
* Holds the configuration per position.
*
* @since 2.44
*
* @var array
*/
private array $area_configuration = [];
/**
* Creates a collection of fields.
*
* @since 2.42
*
* @param Search_Field[] $fields The fields.
* @param array $context The additional context.
*/
private function __construct( array $fields = [], array $context = [] ) {
$this->storage = $fields;
$this->context = $context;
}
/**
* Returns the default search fields.
*
* @since 2.42
*
* @param int $form_id The form ID.
* @param string|null $section The section to check.
*
* @return self
*/
public static function available_fields( int $form_id = 0, ?string $section = null ): self {
$section = $section ?? 'search';
$cache_key = $form_id . '_' . $section;
if ( $form_id > 0 && isset( self::$available_fields_cache[ $cache_key ] ) ) {
return self::$available_fields_cache[ $cache_key ];
}
$fields = [
new Search_Field_All(),
new Search_Field_Search_Mode(),
new Search_Field_Submit(),
new Search_Field_Entry_Date(),
new Search_Field_Entry_ID(),
new Search_Field_Created_By(),
new Search_Field_Is_Starred(),
new Search_Field_Is_Read(),
];
if ( gravityview()->plugin->supports( Plugin::FEATURE_GFQUERY ) ) {
$fields[] = new Search_Field_Is_Approved();
}
if ( $section && 'search' !== $section ) {
$fields = array_values(
array_filter(
$fields,
static fn( Search_Field $field ) => $field->is_allowed_for_section( $section )
)
);
}
$fields = (array) apply_filters( 'gk/gravityview/search/available-fields', $fields, $form_id, $section );
$collection = new self( array_filter( $fields, static fn( $field ) => $field instanceof Search_Field ) );
self::$available_fields_cache[ $cache_key ] = $collection;
return $collection;
}
/**
* Returns a collection from a stored configuration.
*
* @since 2.42
*
* @param array $configuration The configuration.
* @param View|null $view The View object.
* @param array $additional_context Additional params passed along to every field (e.g. Context, Widget args).
*
* @return self The collection.
*/
public static function from_configuration(
array $configuration,
?View $view = null,
array $additional_context = []
): self {
$collection = new self( [], $additional_context );
foreach ( $configuration as $position => $_fields ) {
if ( ! $_fields ) {
continue;
}
foreach ( $_fields as $uid => $_configuration ) {
if ( 'area_settings' === $uid ) {
$collection->set_area_configuration( $position, $_configuration );
continue;
}
$_configuration['UID'] = $uid;
$_configuration['position'] = $position;
$field = Search_Field::from_configuration( $_configuration, $view, $additional_context );
if ( ! $field ) {
continue;
}
$collection->add( $field );
}
}
return $collection;
}
/**
* Creates a collection based on the legacy configuration.
*
* @since 2.42
*
* @param array $configuration The legacy configuration.
* @param View|null $view The View.
* @param array $additional_context Additional params passed along to every field (e.g. Context, Widget args).
*
* @return self The collection.
*/
public static function from_legacy_configuration(
array $configuration,
?View $view,
array $additional_context = []
): self {
$collection = new self( [], $additional_context );
try {
$search_fields = json_decode( $configuration['search_fields'] ?? '[]', true, 512, JSON_THROW_ON_ERROR );
} catch ( JsonException $e ) {
return $collection;
}
if ( [] === $search_fields ) {
return $collection;
}
$row = Grid::get_row_by_type( '100' );
if ( [] === $row ) {
return $collection;
}
$form_id = (int) ( $view ? $view->form->ID : ( $configuration['form_id'] ?? 0 ) );
$shared_data = [
'sieve_choices' => (bool) ( $configuration['sieve_choices'] ?? false ),
];
$areas = array_keys( $row );
$area_count = count( $areas );
// Transform legacy fields into Search Fields.
foreach ( $search_fields as $i => $field ) {
// Automatically loop through all available areas.
$area_key = $areas[ $i % $area_count ];
$field_id = (string) ( $field['field'] ?? '' );
$form_id = (string) ( $field['form_id'] ?? $form_id );
$field_data = array_merge(
$shared_data,
[
'id' => $field_id,
'form_id' => $form_id,
'input_type' => $field['input'] ?? 'input_text',
'custom_label' => $field['label'] ?? '',
'position' => 'search-general_' . ( $row[ $area_key ][0]['areaid'] ?? '' ),
'date_range_mode' => $field['date_range_mode'] ?? 'separate',
]
);
$search_field = Search_Field::from_configuration( $field_data, $view, $additional_context );
if ( ! $search_field ) {
$field_data['id'] = Search_Field_Gravity_Forms::generate_field_id( $form_id, $field_id );
$search_field = Search_Field::from_configuration( $field_data, $view, $additional_context );
}
if ( $search_field ) {
$collection->add( $search_field );
}
}
// Only add the required fields if there are search fields.
if ( $collection->count() > 0 ) {
$collection = $collection->ensure_required_search_fields( $configuration );
}
if ( 'horizontal' === ( $configuration['search_layout'] ?? null ) ) {
foreach ( $row as $areas ) {
foreach ( $areas as $area ) {
$collection->set_area_configuration(
'search-general_' . $area['areaid'],
[ 'layout' => 'row' ]
);
}
}
}
return $collection;
}
/**
* Return the available field instance by field ID, if it exists.
*
* @since 2.42
*
* @param int $form_id The form ID.
* @param string $field_id The field ID.
*
* @return Search_Field|null The field.
*/
public static function get_field_by_field_id( int $form_id, string $field_id ): ?Search_Field {
$available_fields = self::available_fields( $form_id );
foreach ( $available_fields as $field ) {
if ( $field->is_of_type( $field_id ) ) {
return $field;
}
}
return null;
}
/**
* Returns the search field collection as a configuration array.
*
* @since 2.42
*
* @return array
*/
public function to_configuration(): array {
$configuration = [];
foreach ( $this->storage as $field ) {
$data = $field->to_configuration();
if ( ! isset( $data['position'], $data['UID'] ) ) {
continue;
}
$configuration[ $data['position'] ] ??= [];
$configuration[ $data['position'] ][ $data['UID'] ] = $data;
}
foreach ( $configuration as $position => $_ ) {
$settings = $this->get_area_configuration( $position );
if ( $settings ) {
// The area settings are stored like a field with a fixed ID.
// So these values are here to keep the renderer happy.
$settings['id'] = 'area_settings';
$settings['label'] = esc_html__( 'Column', 'gk-gravityview' );
$configuration[ $position ]['area_settings'] = $settings;
}
}
return $configuration;
}
/**
* Returns The iterator.
*
* @since 2.42
*
* @return ArrayIterator<Search_Field>
*/
public function getIterator(): ArrayIterator {
$fields = [];
foreach ( $this->storage as $field ) {
$configuration = $field->to_configuration();
$fields[ $configuration['type'] ] = $field;
}
return new ArrayIterator( $fields );
}
/**
* @inheritDoc
* @since 2.42
*/
public function by_position( $position ) {
$clone = clone $this;
$clone->base_has_visible_fields = $this->has_visible_fields();
$clone->position = $position;
$search = implode( '.*', array_map( 'preg_quote', explode( '*', $position ) ) );
$clone->storage = array_values(
array_filter(
$clone->storage,
static fn( Search_Field $field ): bool => preg_match( "#^{$search}$#", $field->position ),
)
);
return $clone;
}
/**
* Returns whether this collection contains a date field.
*
* @since 2.42
*
* @return bool Whether the collection contains a date field.
*/
public function has_date_field(): bool {
$date_field_types = [ 'date', 'date_range', 'entry_date' ];
foreach ( $this->storage as $field ) {
$input_type = $field->to_template_data()['input'] ?? '';
if ( in_array( $input_type, $date_field_types, true ) ) {
return true;
}
}
return false;
}
/**
* Returns whether this collection contains a date-range search field that uses the combined picker.
*
* Entry Date fields default to the range input, so they are also considered range fields
* unless explicitly rendered as a single date. Range fields configured to render as two
* separate single-date pickers do not require the combined date-range picker bundle.
*
* @since 3.0.0
*
* @return bool Whether the collection contains a combined date-range search field.
*/
public function has_date_range_field(): bool {
foreach ( $this->storage as $field ) {
$template_data = $field->to_template_data();
$input = $template_data['input'] ?? '';
$mode = $template_data['date_range_mode'] ?? 'separate';
if ( 'separate' === $mode ) {
continue;
}
if ( 'date_range' === $input ) {
return true;
}
// Entry Date reports `entry_date` for `input` and carries the configured mode in `input_type`.
if ( 'entry_date' === $input && 'date_range' === ( $template_data['input_type'] ?? 'date_range' ) ) {
return true;
}
}
return false;
}
/**
* Returns whether this collection contains a field that needs the single-date picker.
*
* Includes fields whose input type is `date`, Entry Date configured as a single date, and
* any date-range field that is rendered as two separate single-date pickers.
*
* @since 3.0.0
*
* @return bool Whether the collection contains a field that needs the single-date picker.
*/
public function has_date_picker_field(): bool {
foreach ( $this->storage as $field ) {
$template_data = $field->to_template_data();
$input = $template_data['input'] ?? '';
$mode = $template_data['date_range_mode'] ?? 'separate';
if ( 'date' === $input ) {
return true;
}
if ( 'date_range' === $input && 'separate' === $mode ) {
return true;
}
$entry_date_input = $template_data['input_type'] ?? 'date_range';
if ( 'entry_date' !== $input ) {
continue;
}
if ( 'date' === $entry_date_input ) {
return true;
}
if ( 'date_range' === $entry_date_input && 'separate' === $mode ) {
return true;
}
}
return false;
}
/**
* Returns whether one of the visible fields has a request value.
*
* @since 2.42
*
* @return bool Whether one of the visible fields has a request value.
*/
public function has_request_values(): bool {
foreach ( $this->storage as $field ) {
if ( ! $field->is_visible() ) {
continue;
}
if ( $field->has_request_value() ) {
return true;
}
}
return false;
}
/**
* Returns whether the current collection has any fields of the provided type.
*
* @since 2.42
*
* @param string $type The type to check.
*
* @return bool Whether the field type is in the collection.
*/
public function has_fields_of_type( string $type ): bool {
foreach ( $this->storage as $field ) {
if ( $field->is_of_type( $type ) ) {
return true;
}
}
return false;
}
/**
* Ensures required fields are added to the collection.
*
* @since 2.42
*
* @param array $configuration The configuration.
*
* @return self A new collection with the required fields.
*/
public function ensure_required_search_fields( array $configuration = [] ): self {
$collection = clone $this;
// Add Submit and Search Mode fields in a separate row.
$submit_row = Grid::get_row_by_type( '50/50' );
$search_mode_position = 'search-general_' . ( $submit_row['1-2 right'][0]['areaid'] ?? '' );
foreach ( $collection->storage as $field ) {
if ( $field->is_of_type( 'submit' ) ) {
// If only the search mode is missing, add it with an existing Submit field.
$search_mode_position = $field->position;
}
}
/** @var Search_Field[] $required_fields */
$required_fields = [
Search_Field_Submit::from_configuration(
[
'position' => 'search-general_' . ( $submit_row['1-2 left'][0]['areaid'] ?? '' ),
'search_clear' => (bool) ( $configuration['search_clear'] ?? false ),
]
),
Search_Field_Search_Mode::from_configuration(
[
'position' => $search_mode_position,
'mode' => $configuration['search_mode'] ?? 'any',
'input_type' => 'hidden',
]
),
];
foreach ( $required_fields as $field ) {
if ( ! $collection->has_fields_of_type( $field->get_type() ) ) {
$collection->add( $field );
}
}
return $collection;
}
/**
* Checks if the collection has any visible searchable fields.
*
* This excludes Submit and Search Mode fields as they are not actual search fields.
*
* @since 2.42
*
* @return bool Whether there are visible searchable fields.
*/
public function has_visible_fields( bool $ignore_base = false ): bool {
if ( ! $ignore_base && true === $this->base_has_visible_fields ) {
return true;
}
foreach ( $this->storage as $field ) {
if ( $field->is_visible() && $field->is_searchable_field() ) {
return true;
}
}
return false;
}
/**
* Returns the fields as filtered template data.
*
* @since 2.42
*
* @return array The template data.
*/
public function to_template_data(): array {
if ( ! $this->has_visible_fields() ) {
return [];
}
$search_fields = [];
foreach ( $this->storage as $field ) {
if ( ! $field->is_visible() ) {
continue;
}
$search_fields[] = $field->to_template_data();
}
/**
* Modify what fields are shown. The order of the fields in the $search_filters array controls the order as displayed in the search bar widget.
*
* @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
* @param GravityView_Widget_Search $widget Current widget object
* @param array |null $widget_args Args passed to this method. {@since 1.8}
* @param null|string|Template_Context $context {@since 2.0}
* @param string|null $position The search field position {@since 2.42}
*
* @type array The filtered search filters.
*/
$search_fields = apply_filters(
'gravityview_widget_search_filters',
$search_fields,
$this->context['widget'] ?? null,
$this->context['widget_args'] ?? null,
$this->context['context'] ?? null,
$this->position,
);
gravityview()->log->debug( 'Calculated Search Fields: ', [ 'data' => $search_fields ] );
return $search_fields;
}
/**
* Returns a collection of fields of a specific type.
*
* @since 2.42
*
* @param string $type The search type or class name.
*
* @return self A new collection.
*/
public function by_type( string $type ): self {
$clone = clone $this;
$clone->storage = array_filter(
$clone->storage,
static function ( Search_Field $field ) use ( $type ): bool {
if ( is_subclass_of( $type, Search_Field::class ) ) {
return $field instanceof $type;
}
return $field->is_of_type( $type );
}
);
return $clone;
}
/**
* Adds multiple fields to the collection.
*
* @since 2.42
*
* @param Search_Field $value The field to add.
* @param Search_Field ...$fields Additional fields to add.
*/
public function add( $value /*, ...$fields */ ): void {
$fields = func_get_args();
foreach ( $fields as $field ) {
if ( ! $field instanceof Search_Field ) {
continue;
}
parent::add( $field );
}
}
/**
* Sets the position configuration.
*
* @since 2.44
*
* @param string $position The position.
* @param array $configuration The configuration.
*/
private function set_area_configuration( string $position, array $configuration ): void {
$this->area_configuration[ $position ] = $configuration;
}
/**
* Returns the position configuration.
*
* @since 2.44
*
* @param string $position The position.
*
* @return array The configuration.
*/
public function get_area_configuration( string $position ): array {
return $this->area_configuration[ $position ] ?? [];
}
/**
* Clears the internal caches.
*
* @since 3.0.0
*
* @internal Used for testing purposes only.
*/
public static function clear_cache(): void {
self::$available_fields_cache = [];
}
}