<?php

class Papi_Query {

	/**
	 * The query arguments.
	 *
	 * @var array
	 */
	protected $args;

	/**
	 * The default query arguments.
	 *
	 * @var array
	 */
	protected $default_args = [
		'entry_type' => ''
	];

	/**
	 * The query type.
	 *
	 * @var string
	 */
	protected $type;

	/**
	 * The query instance.
	 *
	 * @var WP_Term_Query|WP_Query
	 */
	protected $query;

	/**
	 * Query constructor.
	 *
	 * @param array  $args
	 * @param string $type
	 */
	public function __construct( array $args = [], $type = 'post' ) {
		$this->type  = $type === 'page' ? 'post' : $type;
		$this->query = $this->get_query_class();
		$this->args  = $args;
	}

	/**
	 * Dynamically access query properties.
	 *
	 * @param  string $key
	 *
	 * @return mixed
	 */
	public function __get( $key ) {
		switch ( $key ) {
			case 'posts':
			case 'terms':
				return $this->get_result();
			default:
				break;
		}
	}

	/**
	 * Get first item of result.
	 *
	 * @return array
	 */
	public function first() {
		$result = $this->get_result();

		return array_shift( $result );
	}

	/**
	 * Parse query arguments.
	 *
	 * @param  array $args
	 */
	public function parse_args( array $args ) {
		$args = array_merge( $this->default_args, $args );

		// Since a page type has defined post types we should use them.
		// With a fallback on `post_type` args or `any` value.
		if ( $this->type === 'post' ) {
			$args = $this->parse_post_args( $args );
		} else if ( $this->type === 'term' ) {
			$args = $this->parse_term_args( $args );
		}

		$this->args = $args;
	}

	/**
	 * Parse post query arguments.
	 *
	 * @param  array $args
	 *
	 * @return array
	 */
	protected function parse_post_args( array $args ) {
		if ( isset( $args['page_type'] ) ) {
			$args['entry_type'] = $args['page_type'];

			unset( $args['page_type'] );
		}

		$entry_type = papi_get_entry_type_by_id( $args['entry_type'] );

		if ( $entry_type instanceof Papi_Page_Type ) {
			$args['post_type'] = papi_to_array( $entry_type->post_type );
		} else {
			$args['post_type'] = isset( $args['post_type'] ) ? $args['post_type'] : '';
		}

		return $args;
	}

	/**
	 * Parse term query arguments.
	 *
	 * @param  array $args
	 *
	 * @return array
	 */
	protected function parse_term_args( array $args ) {
		if ( isset( $args['taxonomy_type'] ) ) {
			$args['entry_type'] = $args['taxonomy_type'];

			unset( $args['taxonomy_type'] );
		}

		$entry_type = papi_get_entry_type_by_id( $args['entry_type'] );

		if ( $entry_type instanceof Papi_Taxonomy_Type ) {
			$args['taxonomy'] = papi_to_array( $entry_type->taxonomy );
		} else {
			$args['taxonomy'] = isset( $args['taxonomy'] ) ? $args['taxonomy'] : '';
		}

		return $args;
	}

	/**
	 * Get query object for right query type.
	 *
	 * @return WP_Query|WP_Term_Query
	 */
	public function get_query_class() {
		switch ( $this->type ) {
			case 'post':
			case 'page':
				return new WP_Query;
			case 'term':
				// `WP_Term_Query` was added in WordPress 4.6.
				if ( class_exists( 'WP_Term_Query' ) ) {
					return new WP_Term_Query;
				}

				break;
			default:
				break;
		}
	}

	/**
	 * Get real query arguments without Papi Query specific arguments.
	 *
	 * @return array
	 */
	public function get_query_args() {
		$args = $this->args;

		if ( empty( $args['meta_query'] ) ) {
			// Add new meta key/value if `meta_key` or `meta_value` is empty.
			if ( empty( $args['meta_key'] ) || empty( $args['meta_value'] ) ) {
				$args['meta_key']   = papi_get_page_type_key();
				$args['meta_value'] = $args['entry_type'];
			} else if ( papi_entry_type_exists( $args['entry_type'] ) ) {
				$item = [
					'key'   => $args['meta_key'],
					'value' => $args['meta_value']
				];

				// Add `meta_compare` if set.
				if ( isset( $args['meta_compare'] ) ) {
					$item['compare'] = $args['meta_compare'];

					unset( $args['meta_compare'] );
				}

				// Add new meta query item.
				$args['meta_query'][] = $item;

				// Add Papi entry/page type meta query.
				$args['meta_query'][] = [
					'key'   => papi_get_page_type_key(),
					'value' => $args['entry_type']
				];

				// Add meta query relation when two query items.
				if ( isset( $args['relation'] ) ) {
					$args['meta_query']['relation'] = $args['relation'];
				} else {
					$args['meta_query']['relation'] = 'AND';
				}

				unset( $args['meta_key'] );
				unset( $args['meta_value'] );
			}
		} else if ( papi_entry_type_exists( $args['entry_type'] ) ) {
			// Add Papi entry/page type meta query.
			$args['meta_query'][] = [
				'key'   => papi_get_page_type_key(),
				'value' => $args['entry_type']
			];

			// Add meta query relation if not set.
			if ( ! isset( $args['meta_query']['relation'] ) ) {
				$args['meta_query']['relation'] = 'AND';
			}
		}

		// Since the real query classes don't support
		// custom arguments the should be deleted.
		foreach ( array_keys( $this->default_args ) as $key ) {
			if ( isset( $args[$key] ) ) {
				unset( $args[$key] );
			}
		}

		return $args;
	}

	/**
	 * Get result.
	 *
	 * Works for all query types.
	 *
	 * @return array
	 */
	public function get_result() {
		if ( ! method_exists( $this->query, 'query' ) ) {
			return [];
		}

		$this->parse_args( $this->args );

		return $this->query->query( $this->get_query_args() );
	}

	/**
	 * Get last item of result.
	 *
	 * @return array
	 */
	public function last() {
		$result = $this->get_result();

		return array_pop( $result );
	}
}


<?php

/**
 * Color property that implements WordPress color
 * picker as a property.
 */
class Papi_Property_Color extends Papi_Property {

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'change'        => false,
			'clear'         => false,
			'default_color' => false,
			'hide'          => false,
			'mode'          => 'hsv',
			'palettes'      => true,
			'slider'        => 'horizontal',
			'show_input'    => false,
			'type'          => 'full',
			'width'         => 255
		];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings = $this->get_settings();
		$value    = $this->get_value();

		papi_render_html_tag( 'div', [
			'class' => 'papi-property-color-picker',

			papi_html_tag( 'input', [
				'data-settings' => $settings,
				'id'            => $this->html_id(),
				'name'          => $this->html_name(),
				'type'          => $settings->show_input === true ? 'text' : 'hidden',
				'value'         => $value,
			] )

		] );
	}
}


<?php

/**
 * HTML checkbox property that can handle one or
 * more checkboxes.
 */
class Papi_Property_Checkbox extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'array';

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $value, $slug, $post_id ) {
		if ( is_string( $value ) && ! papi_is_empty( $value ) ) {
			return [papi_cast_string_value( $value )];
		}

		if ( ! is_array( $value ) ) {
			return $this->default_value;
		}

		return array_map( 'papi_cast_string_value', $value );
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'items'    => [],
			'selected' => []
		];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings = $this->get_settings();
		$value    = papi_cast_string_value( $this->get_value() );

		// Override selected setting with
		// database value if not empty.
		if ( ! papi_is_empty( $value ) ) {
			$settings->selected = $value;
		}

		$settings->selected = papi_to_array( $settings->selected );

		echo '
';

		foreach ( $settings->items as $key => $value ) {
			$key = is_numeric( $key ) ? $value : $key;

			papi_render_html_tag( 'p', [
				papi_html_tag( 'label', [
					'class' => 'light',
					'for'   => esc_attr( $this->html_id( $key ) ),

					papi_html_tag( 'input', [
						'id'      => esc_attr( $this->html_id( $key ) ),
						'name'    => esc_attr( $this->html_name() ) . '[]',
						'type'    => 'hidden'
					] ),

					papi_html_tag( 'input', [
						'checked' => in_array( $value, $settings->selected, true ),
						'id'      => esc_attr( $this->html_id( $key ) ),
						'name'    => esc_attr( $this->html_name() ) . '[]',
						'type'    => 'checkbox',
						'value'   => esc_attr( $value )
					] ),

					esc_html( papi_convert_to_string( $key ) )
				] )
			] );
		}

		echo '
';
	}
}


<?php

/**
 * Option store implementation of Papi meta store.
 */
class Papi_Option_Store extends Papi_Core_Meta_Store {

	/**
	 * The meta type.
	 *
	 * @var string
	 */
	protected $type = 'option';

	/**
	 * The constructor.
	 *
	 * @param int $id
	 */
	public function __construct( $id = 0 ) {
		// Options don't have a id so set it to zero.
		$this->id = 0;
	}

	/**
	 * Load property from page type.
	 *
	 * @param  string $slug
	 * @param  string $child_slug
	 *
	 * @return null|object
	 */
	public function get_property( $slug, $child_slug = '' ) {
		$entry_type_id = papi_get_qs( 'page' );

		if ( empty( $entry_type_id ) ) {
			$property   = null;
			$entry_types = papi_get_all_entry_types( [
				'types' => 'option'
			] );

			foreach ( $entry_types as $entry_type ) {
				if ( $property = $entry_type->get_property( $slug, $child_slug ) ) {
					break;
				}
			}

			if ( is_null( $property ) ) {
				return;
			}

			return $property;
		}

		$entry_type = papi_get_entry_type_by_id( $entry_type_id );

		if ( $entry_type instanceof Papi_Option_Type === false ) {
			return;
		}

		if ( $property = $entry_type->get_property( $slug, $child_slug ) ) {
			return $this->prepare_property( $property );
		}
	}

	/**
	 * Check if it's a valid store.
	 *
	 * @return bool
	 */
	public function valid() {
		return $this->id === 0;
	}
}


<?php

/**
 * Datetime property.
 */
class Papi_Property_Datetime extends Papi_Property {

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'format'           => 'YYYY-MM-DD hh:mm:ss',
			'first_day'        => 0,
			'show_seconds'     => false,
			'show_time'        => true,
			'show_time_first'  => false,
			'show_week_number' => false,
			'use_24_hours'     => get_locale() === 'sv_SE'
		];
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings = $this->get_settings();
		$value    = $this->get_value();

		$settings_json = [
			'i18n' => [
				'previousMonth' => __( 'Previous Month', 'papi' ),
				'nextMonth'     => __( 'Next Month', 'papi' ),
				'midnight'      => __( 'Midnight', 'papi' ),
				'months'        => [
					__( 'January', 'papi' ),
					__( 'February', 'papi' ),
					__( 'March', 'papi' ),
					__( 'April', 'papi' ),
					__( 'May', 'papi' ),
					__( 'June', 'papi' ),
					__( 'July', 'papi' ),
					__( 'August', 'papi' ),
					__( 'September', 'papi' ),
					__( 'October', 'papi' ),
					__( 'November', 'papi' ),
					__( 'December', 'papi' )
				],
				'noon'          => __( 'Noon', 'papi' ),
				'weekdays'      => [
					__( 'Sunday', 'papi' ),
					__( 'Monday', 'papi' ),
					__( 'Tuesday', 'papi' ),
					__( 'Wednesday', 'papi' ),
					__( 'Thursday', 'papi' ),
					__( 'Friday', 'papi' ),
					__( 'Saturday', 'papi' )
				],
				'weekdaysShort' => [
					__( 'Sun', 'papi' ),
					__( 'Mon', 'papi' ),
					__( 'Tue', 'papi' ),
					__( 'Wed', 'papi' ),
					__( 'Thu', 'papi' ),
					__( 'Fri', 'papi' ),
					__( 'Sat', 'papi' )
				]
			]
		];

		// Remove i18n setting if it exists.
		if ( isset( $settings->i18n ) ) {
			unset( $settings->i18n );
		}

		// Remove default time format if show time is false.
		if ( isset( $settings->show_time ) && ! $settings->show_time && isset( $settings->format ) ) {
			$settings->format = trim( str_replace( 'hh:mm:ss', '', $settings->format ) );
		}

		// Convert all sneak case key to camel case.
		foreach ( (array) $settings as $key => $val ) {
			if ( ! is_string( $key ) ) {
				continue;
			}

			if ( $key = papi_camel_case( $key ) ) {
				$settings_json[$key] = $val;
			}
		}

		// Papi has `use24Hours` as key and Pikaday has `use24hour`.
		// This code will fix it.
		if ( isset( $settings_json['use24Hours'] ) ) {
			$settings_json['use24hour'] = $settings_json['use24Hours'];
			unset( $settings_json['use24Hours'] );
		}

		papi_render_html_tag( 'input', [
			'class'         => 'papi-property-datetime',
			'data-settings' => (object) $settings_json,
			'id'            => $this->html_id(),
			'name'          => $this->html_name(),
			'type'          => 'text',
			'value'         => $value
		] );
	}
}


<?php

/**
 * Papi Entry Type is a base class that can
 * register boxes with properties.
 */
class Papi_Entry_Type extends Papi_Core_Type {

	/**
	 * The array of meta boxes to register.
	 *
	 * @var array
	 */
	protected $boxes = [];

	/**
	 * Menu to render entry type on.
	 *
	 * @var string
	 */
	public $menu = '';

	/**
	 * Load all boxes.
	 *
	 * @var bool
	 */
	protected $load_boxes = false;

	/**
	 * Show help tabs.
	 *
	 * @return bool
	 */
	public $show_help_tabs = true;

	/**
	 * Show screen options.
	 *
	 * @var bool
	 */
	public $show_screen_options = true;

	/**
	 * The type name.
	 *
	 * @var string
	 */
	public $type = 'entry';

	/**
	 * Add help tabs.
	 */
	public function add_help_tabs() {
		$help   = $this->help();
		$screen = get_current_screen();

		// No screen available.
		if ( $screen instanceof WP_Screen === false ) {
			return;
		}

		// Clean up all existing tabs.
		if ( ! $this->show_help_tabs || ! empty( $help ) ) {
			$screen->remove_help_tabs();
		}

		// No new help tabs available.
		if ( empty( $help ) ) {
			return;
		}

		// Add help sidebar content. By default it will be disabled
		// since `help_sidebar` method returns false.
		$help_sidebar = $this->help_sidebar();
		$help_sidebar = papi_maybe_get_callable_value( $help_sidebar );
		$screen->set_help_sidebar( wpautop( $help_sidebar ) );

		foreach ( $help as $key => $value ) {
			$args = [
				'id'    => papi_html_name( $key ),
				'title' => $key
			];

			if ( is_callable( $value ) ) {
				$args['callback'] = function () use ( $value ) {
					return wpautop( $value() );
				};
			} else {
				$args['content'] = wpautop( $value );
			}

			$screen->add_help_tab( $args );
		}
	}

	/**
	 * Get custom admin body classes.
	 *
	 * @return array
	 */
	public function body_classes() {
		return [];
	}

	/**
	 * Add new meta box with properties.
	 *
	 * @param mixed $file_or_options
	 * @param array $properties
	 */
	protected function box( $file_or_options = [], $properties = [] ) {
		if ( ! is_string( $file_or_options ) && ! is_array( $file_or_options ) && ! is_object( $file_or_options ) ) {
			return;
		}

		list( $options, $properties ) = papi_get_options_and_properties(
			$file_or_options,
			$properties,
			true
		);

		// Check so we have a post the to add the box to.
		// @codeCoverageIgnoreStart
		if ( ! $this->load_boxes ) {
			return;
		}
		// @codeCoverageIgnoreEnd

		if ( is_callable( $properties ) ) {
			$properties = call_user_func( $properties );
		}

		// Check and convert all non properties objects to properties objects.
		$properties = $this->convert_properties( $properties );

		// Create a core box instance and add it to the boxes array.
		array_push( $this->boxes, new Papi_Core_Box( $options, $properties ) );
	}

	/**
	 * Call parent register if it exists
	 * to collect boxes on the parent entry type.
	 */
	protected function call_parent_register() {
		$parent_class = get_parent_class( $this );

		if ( ! method_exists( $parent_class, 'register' ) ) {
			return;
		}

		$rc = new ReflectionClass( $parent_class );

		// Bail if not instantiable.
		if ( ! $rc->isinstantiable() ) {
			return;
		}

		$parent = $rc->newInstance();
		$parent->register();
		$this->boxes = $parent->get_boxes();
	}

	/**
	 * Convert properties to properties objects.
	 *
	 * @param  array|object $properties
	 *
	 * @return array
	 */
	protected function convert_properties( $properties ) {
		if ( is_array( $properties ) ) {
			if ( isset( $properties['type'] ) ) {
				$properties = [$properties];
			} else if ( isset( $properties[0] ) && $properties[0] instanceof Papi_Core_Tab ) {
				foreach ( $properties as $items ) {
					$items->properties = array_map( 'papi_get_property_type', $items->properties );
				}

				return $properties;
			}
		}

		if ( is_object( $properties ) ) {
			$properties = papi_get_property_type( $properties );
		}

		if ( papi_is_property( $properties ) ) {
			$properties = [$properties];
		}

		$properties = is_array( $properties ) ? $properties : [];
		$properties = array_map( 'papi_get_property_type', $properties );

		return array_filter( $properties, 'papi_is_property' );
	}

	/**
	 * Modify fields that are returned from the entry type.
	 *
	 * @param  array $fields
	 *
	 * @return array
	 */
	public function fields( $fields ) {
		return $fields;
	}

	/**
	 * Get admin body css classes.
	 *
	 * Use `body_classes` method to add custom admin body classes.
	 *
	 * @return array
	 */
	public function get_body_classes() {
		$arr = $this->body_classes();
		$arr = is_string( $arr ) ? [$arr] : $arr;
		$arr = is_array( $arr ) ? $arr : [];

		return array_merge( $arr, [] );
	}

	/**
	 * Get boxes from the page type.
	 *
	 * @return array
	 */
	public function get_boxes() {
		if ( empty( $this->boxes ) && $this->load_boxes === false ) {
			if ( ! method_exists( $this, 'register' ) ) {
				return [];
			}

			$this->load_boxes = true;

			$this->call_parent_register();
			$this->register();
		}

		// Merge boxes, e.g a parent box with a child box.
		$this->boxes = $this->merge_boxes( $this->boxes );

		/**
		 * Modify boxes array.
		 *
		 * @param array  $boxes
		 * @param string $id
		 */
		$this->boxes = apply_filters( 'papi/get_boxes', $this->boxes, $this->get_id() );
		$this->boxes = is_array( $this->boxes ) ? $this->boxes : [];

		// Go through all boxes and only add boxes
		// that is a array and remove boxes that isn't
		// a instance of core box class.
		foreach ( $this->boxes as $index => $box ) {
			if ( is_array( $box ) ) {
				if ( is_string( $index ) ) {
					$box['title'] = $index;
				}

				$this->box( $box );

				unset( $this->boxes[$index] );
				continue;
			}

			// Remove boxes that isn't a instanceof core box class.
			if ( $box instanceof Papi_Core_Box === false ) {
				unset( $this->boxes[$index] );
				continue;
			}

			// Remove box if site id isn't zero and isn't the current blog id.
			if ( $box->site_id !== 0 && $box->site_id !== get_current_blog_id() ) {
				unset( $this->boxes[$index] );
				continue;
			}
		}

		return papi_sort_order( array_reverse( $this->boxes ) );
	}

	/**
	 * Get labels that should be changed
	 * when using `fill_labels` option.
	 *
	 * @return array
	 */
	public function get_labels() {
		return [];
	}

	/**
	 * Get menu to render entry type on.
	 *
	 * @return string
	 */
	public function get_menu() {
		return $this->menu;
	}

	/**
	 * Get property from entry type.
	 *
	 * @param  string $slug
	 * @param  string $child_slug
	 *
	 * @return Papi_Property
	 */
	public function get_property( $slug, $child_slug = '' ) {
		$boxes = $this->get_boxes();
		$parts = preg_match( '/\[\d+\]/', $slug ) ? preg_split( '/\[\d+\]/', $slug ) : explode( '[', $slug );
		$parts = array_map( function ( $part ) {
			return preg_replace( '/(\[|\])/', '', $part );
		}, $parts );

		if ( count( $parts ) > 1 ) {
			$property = null;

			for ( $i = 0, $l = count( $parts ); $i < $l; $i++ ) {
				$child    = isset( $parts[$i + 1] ) ? $parts[$i + 1] : '';
				$property = $this->get_property( $parts[$i], $child );

				if ( isset( $parts[$i + 1] ) ) {
					$i++;
				}
			}

			/**
			 * Modify property.
			 *
			 * @param  Papi_Core_Property $property
			 */
			return apply_filters( 'papi/get_property', $property );
		}

		foreach ( $boxes as $box ) {
			foreach ( $box->properties as $property ) {
				$property = papi_get_property_type( $property );

				if ( papi_is_property( $property ) && $property->match_slug( $slug ) ) {
					if ( empty( $child_slug ) ) {
						/**
						 * Modify property.
						 *
						 * @param  Papi_Core_Property $property
						 */
						return apply_filters( 'papi/get_property', $property );
					}

					$property = $property->get_child_property( $child_slug );

					if ( papi_is_property( $property ) ) {
						/**
						 * Modify property.
						 *
						 * @param  Papi_Core_Property $property
						 */
						return apply_filters( 'papi/get_property', $property );
					}
				}
			}
		}
	}

	/**
	 * Get root properties.
	 *
	 * @return array
	 */
	public function get_properties() {
		$boxes = $this->get_boxes();
		$list  = [];

		foreach ( $boxes as $box ) {
			foreach ( $box->properties as $property ) {
				$list[] = $property;
			}
		}

		return $list;
	}

	/**
	 * Add admin help tabs.
	 *
	 * Example:
	 *   'My custom title' => 'My custom content'
	 *
	 * @return array
	 */
	public function help() {
		return [];
	}

	/**
	 * Add help sidebar content.
	 *
	 * By default we return false to disable
	 * the sidebar content.
	 *
	 * @return bool
	 */
	public function help_sidebar() {
		return false;
	}

	/**
	 * Merge boxes with same title.
	 *
	 * @param  array $boxes
	 *
	 * @return array
	 */
	protected function merge_boxes( array $boxes ) {
		$result = [];

		foreach ( $boxes as $box ) {
			if ( ! isset( $result[$box->id] ) ) {
				$result[$box->id] = $box;
				continue;
			}

			foreach ( $box->properties as $property ) {
				$result[$box->id]->properties[] = $property;
			}

			$result[$box->id]->properties = array_unique( $result[$box->id]->properties );
		}

		return array_values( $result );
	}

	/**
	 * Add new property to the page using array or rendering property template file.
	 *
	 * @param  array|string $file_or_options
	 * @param  array $values
	 *
	 * @return null|Papi_Property
	 */
	protected function property( $file_or_options = [], $values = [] ) {
		return papi_property( $file_or_options, $values );
	}

	/**
	 * Check if the entry type is a singleton.
	 *
	 * @return bool
	 */
	public function singleton() {
		return false;
	}

	/**
	 * Setup entry type.
	 */
	public function setup() {
		add_action( 'in_admin_header', [$this, 'add_help_tabs'] );
		add_filter( 'screen_options_show_screen', function () {
			return $this->show_screen_options;
		} );

		// @codeCoverageIgnoreStart
		if ( ! method_exists( $this, 'register' ) ) {
			return;
		}
		// @codeCoverageIgnoreEnd

		$this->register();

		foreach ( $this->get_boxes() as $box ) {
			new Papi_Admin_Meta_Box( $box );
		}
	}

	/**
	 * Add a new tab.
	 *
	 * @param  mixed $file_or_options
	 * @param  array $properties
	 *
	 * @return null|Papi_Core_Tab
	 */
	protected function tab( $file_or_options = [], $properties = [] ) {
		if ( ! is_string( $file_or_options ) && ! is_array( $file_or_options ) ) {
			return;
		}

		return papi_tab( $file_or_options, $properties );
	}

	/**
	 * Load template file.
	 *
	 * @param  string $file
	 * @param  array  $values
	 *
	 * @return array
	 */
	protected function template( $file, $values = [] ) {
		return papi_template( $file, $values );
	}
}


<?php

/**
 * HTML5 number input property.
 */
class Papi_Property_Number extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'int';

	/**
	 * Get convert type.
	 *
	 * @return string
	 */
	public function get_convert_type() {
		$value = $this->get_value();

		return is_float( $value ) ? 'float' : $this->convert_type;
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return float|int
	 */
	public function format_value( $value, $slug, $post_id ) {
		$value = is_string( $value ) && is_numeric( $value ) ? $value + 0 : $value;

		if ( is_float( $value ) ) {
			return floatval( $value );
		} else {
			return intval( $value );
		}
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'max'  => '',
			'min'  => '',
			'step' => 'any',
			'type' => 'number'
		];
	}

	/**
	 * Get value from the database.
	 *
	 * @return float|int
	 */
	public function get_value() {
		return $this->format_value(
			parent::get_value(),
			$this->get_slug(),
			papi_get_post_id()
		);
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$settings = $this->get_settings();
		$value    = $this->get_value();

		// If range type is used change the default values if empty.
		if ( $settings->type === 'range' ) {
			$settings->max  = papi_is_empty( $settings->max )
				? 100 : $settings->max;
			$settings->min  = papi_is_empty( $settings->min )
				? 0 : $settings->min;
			$settings->step = papi_is_empty( $settings->step )
				? 1 : $settings->step;
		}

		if ( $settings->min !== 0 && $value < $settings->min ) {
			$value = $settings->min;
		}

		papi_render_html_tag( 'input', [
			'id'    => $this->html_id(),
			'max'   => $settings->max,
			'min'   => $settings->min,
			'name'  => esc_attr( $this->html_name() ),
			'step'  => $settings->step,
			'type'  => esc_attr( $settings->type ),
			'value' => $value
		] );
	}
}


<?php

/**
 * WordPress link manager property.
 */
class Papi_Property_Link extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'object';

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * Link fields.
	 *
	 * @var array
	 */
	protected $link_fields = [
		'post_id',
		'url',
		'title',
		'target'
	];

	/**
	 * Delete value from the database.
	 *
	 * @param  string $slug
	 * @param  int    $post_id
	 * @param  string $type
	 *
	 * @return bool
	 */
	public function delete_value( $slug, $post_id, $type ) {
		$values = $this->load_value( null, $slug, $post_id );
		$values = is_object( $values ) ? (array) $values : $values;
		$result = true;

		foreach ( array_keys( $values ) as $key ) {
			$out    = papi_data_delete( $post_id, $slug . '_' . $key );
			$result = $out ? $result : $out;
		}

		if ( $result ) {
			$result = papi_data_delete( $post_id, $slug );
		}

		return $result;
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return mixed
	 */
	public function format_value( $value, $slug, $post_id ) {
		return (object) $this->prepare_link_array( $value, $slug );
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [];
	}

	/**
	 * Load value from database.
	 *
	 * @param  mixed  $value
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function load_value( $value, $slug, $post_id ) {
		if ( is_array( $value ) || is_object( $value ) ) {
			$values = $value;
		} else {
			$values = $this->link_fields;

			foreach ( $values as $index => $key ) {
				$values[$key] = papi_data_get(
					$post_id,
					sprintf( '%s_%s', $slug, $key ),
					$this->get_meta_type()
				);
				unset( $values[$index] );
			}
		}

		return (object) $this->prepare_link_array( $values, $slug );
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$value  = $this->get_value();
		$value  = is_array( $value ) || is_object( $value ) ? $value : [];
		$value  = (object) $value;
		$exists = ! empty( $value->url );
		?>
		<div class="papi-property-link" data-replace-slug="true" data-slug="<?php echo esc_attr( $this->html_name() ); ?>">
			<input type="hidden" name="<?php echo esc_attr( $this->html_name() ); ?>" value="<?php echo $exists ? 1 : ''; ?>">

			<?php if ( $exists ): ?>

							<?php esc_html_e( 'URL', 'papi' ); ?>

								<a href="<?php echo esc_attr( $value->url ); ?>" target="_blank"><?php echo esc_attr( $value->url ); ?>
								<input type="hidden" value="<?php echo esc_attr( $value->title . ' - ' . $value->url ); ?>" data-papi-rule="<?php echo esc_attr( $this->html_name() ); ?>">
								<input class="wp-link-url" type="hidden" value="<?php echo esc_attr( $value->url ); ?>" name="<?php echo esc_attr( $this->html_name() ); ?>[url]">

							<?php esc_html_e( 'Title', 'papi' ); ?>

								<?php echo esc_html( $value->title ); ?>
								<input class="wp-link-text" type="hidden" value="<?php echo esc_attr( $value->title ); ?>" name="<?php echo esc_attr( $this->html_name() ); ?>[title]">

							<?php esc_html_e( 'Target', 'papi' ); ?>

								<?php echo $value->target === '_blank' ? esc_html_e( 'New window', 'papi' ) : esc_html_e( 'Same window', 'papi' ); ?>
								<input class="wp-link-target" type="hidden" value="<?php echo esc_attr( $value->target ); ?>" name="<?php echo esc_attr( $this->html_name() ); ?>[target]">

			<?php endif; ?>

				<span class="<?php echo empty( $value->url ) ? '' : 'papi-hide'; ?>">
					<?php esc_html_e( 'No link selected', 'papi' ); ?>
					<button class="button" data-link-action="add"><?php esc_html_e( 'Add link', 'papi' ); ?></button>

				<span class="<?php echo empty( $value->url ) ? 'papi-hide' : ''; ?>">
					<button class="button" data-link-action="edit"><?php esc_html_e( 'Edit link', 'papi' ); ?></button>
					<button class="button" data-link-action="remove"><?php esc_html_e( 'Remove link', 'papi' ); ?></button>

		<?php
	}

	/**
	 * Prepare link array with post id. If it gets a post id
	 * bigger then zero it will use the permalink as url.
	 *
	 * @param  array|object $link
	 * @param  string       $slug
	 *
	 * @return array|object
	 */
	protected function prepare_link_array( $link, $slug ) {
		$array  = is_array( $link );
		$values = (array) $link;

		foreach ( $values as $key => $val ) {
			unset( $values[$key] );
			$key = preg_replace( '/^' . $slug . '\_/', '', $key );
			$values[$key] = $val;
		}

		$link = (object) $values;

		// Don't continue without a url.
		if ( ! isset( $link->url ) || empty( $link->url ) ) {
			return $array ? (array) $link : $link;
		}

		// Don't overwrite existing post id.
		if ( ! isset( $link->post_id ) || empty( $link->post_id ) ) {
			$link->post_id = url_to_postid( $link->url );
		}

		// If empty target set `_self` as default target.
		if ( empty( $link->target ) ) {
			$link->target = '_self';
		}

		// Remove slug if it exists.
		if ( isset( $link->$slug ) && is_numeric( $link->$slug ) ) {
			unset( $link->$slug );
		}

		return $array ? (array) $link : $link;
	}

	/**
	 * Render link template.
	 */
	public function render_link_template() {
		?>

		<?php
	}

	/**
	 * Prepare value for the database.
	 *
	 * @param  mixed  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function update_value( $values, $slug, $post_id ) {
		if ( is_object( $values ) ) {
			$values = (array) $values;
		}

		if ( ! isset( $values['url'] ) ) {
			$values = $this->link_fields;

			foreach ( $values as $index => $key ) {
				$values[sprintf( '%s_%s', $slug, $key )] = '';
				unset( $values[$index] );
			}

			// Delete the required field.
			$values[$slug] = '';

			return $values;
		}

		// If a url exists we can continue making the meta fields
		// that should be saved.
		$values = $this->prepare_link_array( $values, $slug );

		foreach ( $values as $key => $val ) {
			$values[sprintf( '%s_%s', $slug, $key )] = $val;
			unset( $values[$key] );
		}

		// Required field so Papi can load a value from the
		// original property slug.
		$values[$slug] = 1;

		return $values;
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_head', [$this, 'render_link_template'] );
	}
}


<?php

/**
 * Papi type that handle taxonomies.
 */
class Papi_Taxonomy_Type extends Papi_Entry_Type {

	/**
	 * Fill labels.
	 *
	 * @var bool
	 */
	public $fill_labels = false;

	/**
	 * Labels, the same labels that taxonomy type object uses.
	 *
	 * @var array
	 */
	public $labels = [];

	/**
	 * Redirect after create.
	 *
	 * @var bool
	 */
	public $redirect_after_create = false;

	/**
	 * The taxonomy.
	 *
	 * @var array
	 */
	public $taxonomy = [];

	/**
	 * The type name.
	 *
	 * @var string
	 */
	public $type = 'taxonomy';

	/**
	 * Determine if the taxonomy type is allowed by taxonomy type.
	 *
	 * @return bool
	 */
	public function allowed() {
		$args = func_get_args();

		if ( empty( $args ) ) {
			return parent::allowed();
		}

		return isset( $args[0] ) && in_array( $args[0], $this->taxonomy, true );
	}

	/**
	 * Should the Taxonomy Type be displayed in WordPress admin or not?
	 *
	 * @param  string $taxonomy
	 *
	 * @return bool
	 */
	public function display( $taxonomy ) {
		return true;
	}

	/**
	 * Get labels that should be changed
	 * when using `fill_labels` option.
	 *
	 * @return array
	 */
	public function get_labels() {
		if ( ! $this->fill_labels ) {
			return $this->labels;
		}

		return array_merge( $this->labels, [
			'add_new_item' => sprintf( '%s %s', __( 'Add New', 'papi' ), $this->name ),
			'edit_item'    => sprintf( '%s %s', __( 'Edit', 'papi' ), $this->name ),
			'view_item'    => sprintf( '%s %s', __( 'View', 'papi' ), $this->name )
		] );
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		foreach ( papi_to_array( $this->taxonomy ) as $taxonomy ) {
			if ( is_string( $taxonomy ) && taxonomy_exists( $taxonomy ) ) {
				add_action( $taxonomy . '_edit_form', [$this, 'edit_form'] );
			}
		}
	}

	/**
	 * Render term edit form.
	 */
	public function edit_form() {
		?>

			<?php wp_editor( '', 'papiHiddenEditor' ); ?>

				<?php
				foreach ( $this->boxes as $box ) {
					do_meta_boxes(
						$box->id,
						'normal',
						null
					);
				}
				?>

		<?php
	}

	/**
	 * Setup meta data.
	 */
	protected function setup_meta_data() {
		parent::setup_meta_data();
		$this->taxonomy = papi_to_array( $this->taxonomy );
	}
}


<?php

class Papi_REST_API_Post {

	/**
	 * REST API Post construct.
	 */
	public function __construct() {
		add_filter( 'the_post', [$this, 'get_post'] );
		add_action( 'rest_api_init', [$this, 'setup_fields'] );
	}

	/**
	 * Get page type.
	 *
	 * @param  array           $data
	 * @param  string          $field_name
	 * @param  WP_REST_Request $request
	 *
	 * @return array
	 */
	public function get_page_type( array $data, $field_name, $request ) {
		if ( ! isset( $data['ID'] ) ) {
			return '';
		}

		return papi_get_page_type_id( $data['ID'] ) ?: '';
	}

	/**
	 * Filter the post.
	 *
	 * Register all properties with `register_meta` and prepare response filter.
	 *
	 * @param  WP_Post $post
	 *
	 * @return WP_Post
	 */
	public function get_post( WP_Post $post ) {
		if ( ! ( $page_type = papi_get_entry_type_by_meta_id( $post->ID ) ) ) {
			return $post;
		}

		// Register all properties fields with register meta.
		foreach ( $page_type->get_properties() as $property ) {
			$property->register();
		}

		// Add filter to prepare the response for a post type.
		add_filter( 'rest_prepare_' . $post->post_type, [$this, 'prepare_response'] );

		return $post;
	}

	/**
	 * Prepare response.
	 *
	 * @param  WP_REST_Response $response
	 *
	 * @return WP_REST_Response
	 */
	public function prepare_response( $response ) {
		if ( ! isset( $response->data['meta'] ) ) {
			return $response;
		}

		foreach ( $response->data['meta'] as $key => $value ) {
			$response->data['meta'][$key] = papi_get_field( $key, $value, 'post' );
		}

		return $response;
	}

	/**
	 * Setup REST API fields.
	 */
	public function setup_fields() {
		if ( ! function_exists( 'register_rest_field' ) ) {
			return;
		}

		$post_types = papi_get_post_types();

		foreach ( $post_types as $post_type ) {
			register_rest_field( $post_type, 'page_type', [
				'get_callback' => [$this, 'get_page_type']
			] );
		}
	}
}

new Papi_REST_API_Post;


<?php

/**
 * Base class that must be extended by any Papi sub commands.
 */
class Papi_CLI_Command extends WP_CLI_Command {

	/**
	 * Get formatter object based on supplied arguments.
	 *
	 * @param  array $assoc_args Associative args from CLI to determine formattin
	 *
	 * @return \WP_CLI\Formatter
	 */
	protected function get_formatter( $assoc_args ) {
		$args = $this->get_format_args( $assoc_args );

		return new \WP_CLI\Formatter( $args );
	}

	/**
	 * Get default fields for formatter.
	 *
	 * Class that extends Papi_CLI_Command should override this method.
	 *
	 * @return null|string|array
	 */
	protected function get_default_format_fields() {
		return null;
	}

	/**
	 * Get format args that will be passed into CLI Formatter.
	 *
	 * @param  array $assoc_args Associative args from CLI
	 *
	 * @return array Formatter args
	 */
	protected function get_format_args( $assoc_args ) {
		$format_args = [
			'fields' => $this->get_default_format_fields(),
			'field'  => null,
			'format' => 'table',
		];

		if ( isset( $assoc_args['fields'] ) ) {
			$format_args['fields'] = $assoc_args['fields'];
		}

		if ( isset( $assoc_args['field'] ) ) {
			$format_args['field'] = $assoc_args['field'];
		}

		if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], ['count', 'ids', 'table', 'csv', 'json'], true ) ) {
			$format_args['format'] = $assoc_args['format'];
		}

		return $format_args;
	}
}


<?php

/**
 * Admin class that handle admin view rendering.
 */
final class Papi_Admin_View {

	/**
	 * Path to view dir.
	 *
	 * @var string
	 */
	protected $path = '';

	/**
	 * The constructor.
	 *
	 * @param string $path
	 */
	public function __construct( $path = '' ) {
		$this->path = empty( $path ) ? PAPI_PLUGIN_DIR . '/admin/views/' : $path;
	}

	/**
	 * Check if file exists.
	 *
	 * @param  string $file
	 *
	 * @return bool
	 */
	public function exists( $file ) {
		return file_exists( $this->file( $file ) );
	}

	/**
	 * Render file.
	 *
	 * @param  string $file
	 *
	 * @return string
	 */
	public function render( $file ) {
		if ( ! empty( $file ) && $this->exists( $file ) ) {
			require $this->file( $file );
		}
	}

	/**
	 * Get full path to file with php exstention.
	 *
	 * @param  string $file
	 *
	 * @return string
	 */
	protected function file( $file ) {
		return $this->path . $file . '.php';
	}
}


<?php

class Papi_Core_Box {

	/**
	 * Capabilities list.
	 *
	 * @var array
	 */
	public $capabilities = [];

	/**
	 * Context.
	 *
	 * @var string
	 */
	public $context = 'normal';

	/**
	 * Display box.
	 *
	 * @var boolean
	 */
	public $display = true;

	/**
	 * Display box with frame.
	 *
	 * @var bool
	 */
	public $frame = true;

	/**
	 * The core type identifier.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Box layout that all properties will inherit.
	 *
	 * Possible values are:
	 * - horizontal
	 * - vertical
	 *
	 * @var string
	 */
	public $layout = 'horizontal';

	/**
	 * Custom box options.
	 *
	 * @var array
	 */
	protected $options = [];

	/**
	 * Priority.
	 *
	 * @var string
	 */
	public $priority = 'default';

	/**
	 * Box properties.
	 *
	 * @var array
	 */
	public $properties = [];

	/**
	 * Site id.
	 *
	 * @var int
	 */
	public $site_id = 0;

	/**
	 * The sort order of the core box.
	 *
	 * @var int
	 */
	public $sort_order = 1000;

	/**
	 * The title of the box.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * The constructor.
	 *
	 * @param array $args
	 * @param array $properties
	 */
	public function __construct( array $args = [], array $properties = [] ) {
		$this->setup_args( $args );
		$this->setup_properties( $properties );
	}

	/**
	 * Get box option.
	 *
	 * @param  string $key
	 *
	 * @return mixed
	 */
	public function get_option( $key ) {
		return isset( $this->options[$key] ) ? $this->options[$key] : null;
	}

	/**
	 * Set box option.
	 *
	 * @param string $key
	 * @param mixed  $value
	 */
	public function set_option( $key, $value ) {
		$this->options[$key] = $value;
	}

	/**
	 * Setup arguments.
	 *
	 * @param  array $args
	 */
	protected function setup_args( array $args ) {
		$excluded_keys = ['options', 'properties'];

		foreach ( $args as $key => $value ) {
			if ( isset( $this->$key ) && ! in_array( $key, $excluded_keys, true ) ) {
				$this->$key = papi_esc_html( $value );
			}
		}

		if ( empty( $this->id ) ) {
			$this->id = papi_slugify( strtolower( papi_f( papi_underscorify( papify( $this->title ) ) ) ) );
			$this->id = sanitize_text_field( $this->id );
		}
	}

	/**
	 * Setup properties.
	 *
	 * @param  array $properties
	 */
	protected function setup_properties( array $properties ) {
		$this->properties = papi_populate_properties( $properties );
	}

	/**
	 * Get a string representation of the object.
	 *
	 * @return string
	 */
	public function __toString() {
		return $this->id;
	}
}


<?php

/**
 * Conditional rules class that contains
 * all rules.
 */
class Papi_Core_Conditional_Rules {

	/**
	 * The constructor.
	 */
	public function __construct() {
		$this->setup_filters();
	}

	/**
	 * Convert string bool to bool.
	 *
	 * @param  mixed $str
	 *
	 * @return mixed
	 */
	protected function convert_bool( $str ) {
		if ( ! is_string( $str ) ) {
			return $str;
		}

		switch ( $str ) {
			case 'false':
				return false;
			case 'true':
				return true;
			default:
				return $str;
		}
	}

	/**
	 * Convert value from a property.
	 *
	 * @param  mixed $value
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return mixed
	 */
	protected function convert_prop( $value, Papi_Core_Conditional_Rule $rule ) {
		$meta_id    = papi_get_meta_id();
		$meta_type  = papi_get_meta_type();
		$entry_type = papi_get_entry_type_by_meta_id( $meta_id, $meta_type );

		if ( ! papi_is_empty( $value ) && $entry_type instanceof Papi_Entry_Type !== false ) {
			$property = $entry_type->get_property( $rule->slug );

			if ( papi_is_property( $property ) ) {
				$prop_value = $property->format_value(
					$value,
					$property->slug,
					$meta_id
				);

				$prop_value = papi_filter_format_value(
					$property->type,
					$prop_value,
					$property->slug,
					$meta_id,
					$meta_type
				);

				$prop_value = $this->get_deep_value(
					$rule->slug,
					$prop_value
				);

				if ( gettype( $prop_value ) === gettype( $rule->value ) ) {
					return $prop_value;
				}
			}

			return $value;
		}

		return $value;
	}

	/**
	 * Convert string number to int or float.
	 *
	 * @param  string $str
	 *
	 * @return float|int
	 */
	protected function convert_number( $str ) {
		return papi_cast_string_value( $str );
	}

	/**
	 * Get converted value.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return array
	 */
	protected function get_converted_value( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		// Convert numeric values.
		if ( is_numeric( $value ) && is_numeric( $rule->value ) ) {
			return [
				$this->convert_number( $value ),
				$this->convert_number( $rule->value )
			];
		}

		// Convert bool value if it a string bool or return value.
		$value       = $this->convert_bool( $value );
		$rule->value = $this->convert_bool( $rule->value );

		// Try to convert the property to the same value as the rule value.
		return [
			$this->convert_prop( $value, $rule ),
			$rule->value
		];
	}

	/**
	 * Get deep value.
	 *
	 * @param  string $slug
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	protected function get_deep_value( $slug, $value ) {
		$slugs = explode( '.', $slug );
		array_shift( $slugs );
		return papi_field_value( $slugs, $value, $value );
	}

	/**
	 * Get property value.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return mixed
	 */
	protected function get_value( Papi_Core_Conditional_Rule $rule ) {
		if ( papi_doing_ajax() ) {
			$source     = $rule->get_source();
			$meta_id    = papi_get_meta_id();
			$entry_type = papi_get_entry_type_by_meta_id( $meta_id );

			if ( ! papi_is_empty( $source ) && $entry_type instanceof Papi_Entry_Type !== false ) {
				if ( papi_is_property( $entry_type->get_property( $rule->slug ) ) ) {
					return $this->get_deep_value( $rule->slug, $source );
				}
			}
		}

		if ( ! papi_is_empty( $rule->get_source() ) ) {
			return $this->get_deep_value( $rule->slug, $rule->get_source() );
		}

		$slug  = $rule->get_field_slug();
		$type  = papi_get_meta_type();
		$value = papi_get_field( $slug, null, $type );

		return $this->get_deep_value( $slug, $value );
	}

	/**
	 * Equal conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_equal( Papi_Core_Conditional_Rule $rule ) {
		list( $value, $rule_value ) = $this->get_converted_value( $rule );
		return $value === $rule_value;
	}

	/**
	 * Not equal conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_not_equal( Papi_Core_Conditional_Rule $rule ) {
		list( $value, $rule_value ) = $this->get_converted_value( $rule );
		return $value !== $rule_value;
	}

	/**
	 * Greater then conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_greater_then( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( is_array( $value ) ) {
			$value = count( $value );
		}

		if ( ! is_numeric( $value ) || ! is_numeric( $rule->value ) ) {
			return false;
		}

		return $this->convert_number( $value ) >
			$this->convert_number( $rule->value );
	}

	/**
	 * Greater then or equal conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_greater_then_or_equal( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( is_array( $value ) ) {
			$value = count( $value );
		}

		if ( ! is_numeric( $value ) || ! is_numeric( $rule->value ) ) {
			return false;
		}

		return $this->convert_number( $value ) >=
			$this->convert_number( $rule->value );
	}

	/**
	 * Less then conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_less_then( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( is_array( $value ) ) {
			$value = count( $value );
		}

		if ( ! is_numeric( $value ) || ! is_numeric( $rule->value ) ) {
			return false;
		}

		return $this->convert_number( $value ) <
			$this->convert_number( $rule->value );
	}

	/**
	 * Less then or equal conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_less_then_or_equal( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( is_array( $value ) ) {
			$value = count( $value );
		}

		if ( ! is_numeric( $value ) || ! is_numeric( $rule->value ) ) {
			return false;
		}

		return $this->convert_number( $value ) <=
			$this->convert_number( $rule->value );
	}

	/**
	 * In array conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_in( Papi_Core_Conditional_Rule $rule ) {
		list( $value, $rule_value ) = $this->get_converted_value( $rule );

		if ( ! is_array( $rule_value ) ) {
			return false;
		}

		return in_array( $value, $rule_value, true );
	}

	/**
	 * Not in array conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_not_in( Papi_Core_Conditional_Rule $rule ) {
		list( $value, $rule_value ) = $this->get_converted_value( $rule );

		if ( ! is_array( $rule_value ) ) {
			return false;
		}

		return ! in_array( $value, $rule_value, true );
	}

	/**
	 * Like conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_like( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( ! is_string( $value ) ) {
			$value = papi_convert_to_string( $value );
		}

		if ( papi_is_empty( $value ) ) {
			return false;
		}

		return strpos(
			strtolower( $value ),
			strtolower( $rule->value )
		) !== false;
	}

	/**
	 * Get between values.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return array
	 */
	protected function get_between_values( Papi_Core_Conditional_Rule $rule ) {
		$value = $this->get_value( $rule );

		if ( ! is_array( $rule->value ) ) {
			return [$rule, false];
		}

		foreach ( $rule->value as $index => $v ) {
			$v = $this->convert_number( $v );

			if ( is_numeric( $v ) ) {
				$rule->value[$index] = $v;
			} else {
				unset( $rule->value[$index] );
			}
		}

		if ( ! is_numeric( $value ) || count( $rule->value ) !== 2 ) {
			return [$rule, false];
		}

		return [$rule, $this->convert_number( $value )];
	}

	/**
	 * Between conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_between( Papi_Core_Conditional_Rule $rule ) {
		list( $rule, $value ) = $this->get_between_values( $rule );

		if ( $value === false ) {
			return false;
		}

		return $rule->value[0] <= $value && $value <= $rule->value[1];
	}

	/**
	 * Not between conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_not_between( Papi_Core_Conditional_Rule $rule ) {
		list( $rule, $value ) = $this->get_between_values( $rule );

		if ( $value === false ) {
			return false;
		}

		return ! ( $rule->value[0] <= $value && $value <= $rule->value[1] );
	}

	/**
	 * Exists conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_exists( Papi_Core_Conditional_Rule $rule ) {
		return ! in_array( $this->get_value( $rule ), [null, []], true );
	}

	/**
	 * Not exists conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_not_exists( Papi_Core_Conditional_Rule $rule ) {
		return in_array( $this->get_value( $rule ), [null, []], true );
	}

	/**
	 * Empty conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_empty( Papi_Core_Conditional_Rule $rule ) {
		return papi_is_empty( $this->get_value( $rule ) );
	}

	/**
	 * Empty conditional rule.
	 *
	 * @param  Papi_Core_Conditional_Rule $rule
	 *
	 * @return bool
	 */
	public function rule_not_empty( Papi_Core_Conditional_Rule $rule ) {
		return ! papi_is_empty( $this->get_value( $rule ) );
	}

	/**
	 * Setup filters.
	 */
	public function setup_filters() {
		add_filter( 'papi/conditional/rule/=', [$this, 'rule_equal'] );
		add_filter( 'papi/conditional/rule/!=', [$this, 'rule_not_equal'] );
		add_filter( 'papi/conditional/rule/>', [$this, 'rule_greater_then'] );
		add_filter( 'papi/conditional/rule/>=', [$this, 'rule_greater_then_or_equal'] );
		add_filter( 'papi/conditional/rule/<', [$this, 'rule_less_then'] );
		add_filter( 'papi/conditional/rule/<=', [$this, 'rule_less_then_or_equal'] );
		add_filter( 'papi/conditional/rule/IN', [$this, 'rule_in'] );
		add_filter( 'papi/conditional/rule/NOT IN', [$this, 'rule_not_in'] );
		add_filter( 'papi/conditional/rule/LIKE', [$this, 'rule_like'] );
		add_filter( 'papi/conditional/rule/BETWEEN', [$this, 'rule_between'] );
		add_filter( 'papi/conditional/rule/NOT BETWEEN', [$this, 'rule_not_between'] );
		add_filter( 'papi/conditional/rule/EXISTS', [$this, 'rule_exists'] );
		add_filter( 'papi/conditional/rule/NOT EXISTS', [$this, 'rule_not_exists'] );
		add_filter( 'papi/conditional/rule/EMPTY', [$this, 'rule_empty'] );
		add_filter( 'papi/conditional/rule/NOT EMPTY', [$this, 'rule_not_empty'] );
	}
}

new Papi_Core_Conditional_Rules();


<?php

/**
 * Relationship property that can handle more than
 * one relationship between posts or other data items.
 */
class Papi_Property_Relationship extends Papi_Property {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'array';

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * Convert WordPress post object to a item object.
	 *
	 * @param  WP_Post $post
	 *
	 * @return object
	 */
	protected function convert_post_to_item( WP_Post $post ) {
		return (object) [
			'id'    => $this->get_post_value( $post ),
			'title' => $post->post_title
		];
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $values, $slug, $post_id ) {
		if ( is_array( $values ) || is_object( $values ) ) {
			$items  = $this->get_setting( 'items' );
			$result = [];

			foreach ( $values as $id ) {
				// Backwards compatibility with array `id` and `id`.
				$id = is_object( $id ) ? $id->id : $id;
				$id = is_array( $id ) ? $id['id'] : $id;

				if ( empty( $id ) ) {
					continue;
				}

				if ( papi_is_empty( $items ) ) {
					if ( empty( $this->get_setting( 'meta_key' ) ) ) {
						$post = get_post( $id );
					} else {
						$args = [
							'fields'         => 'ids',
							'meta_key'       => $this->get_setting( 'meta_key' ),
							'meta_value'     => $id,
							'posts_per_page' => 1,
							'post_type'      => $this->get_setting( 'post_type' ),
						];

						$query = new WP_Query( $args );

						if ( ! empty( $query->posts ) ) {
							$post = get_post( $query->posts[0] );
						}
					}

					if ( empty( $post ) ) {
						continue;
					}

					$result[] = $post;
				} else {
					$id   = (int) $id;
					$item = null;

					foreach ( (array) $items as $value ) {
						$ids = wp_list_pluck( [$value], 'id' );
						$ids = count( $ids ) > 0 ? strval( $ids[0] ) : '';

						if ( $ids === (string) $id ) {
							$item = $value;
							break;
						}
					}

					if ( is_array( $item ) || is_object( $item ) ) {
						$result[] = papi_maybe_convert_to_object( $item );
					}
				}
			}

			$values = $this->sort_value( $result, $slug, $post_id );

			// Allow only id to be returned.
			if ( ! papi_is_admin() && $this->get_setting( 'fields' ) === 'ids' ) {
				return array_map( function ( $item ) {
					if ( $id = $this->get_post_value( $item ) ) {
						return $id;
					}

					$id = $item->ID;

					if ( is_numeric( $id ) ) {
						return $id == (int) $id ? (int) $id : (float) $id; // loose comparison
					}

					return $id;
				}, $values );
			}

			return $values;
		}

		return $this->default_value;
	}

	/**
	 * Get default settings.
	 *
	 * @return array
	 */
	public function get_default_settings() {
		return [
			'fields'       => '',
			'items'        => [],
			'limit'        => -1,
			'meta_key'     => '',
			'only_once'    => false,
			'post_type'    => 'page',
			'query'        => [],
			'show_sort_by' => true,
			'title'        => __( 'Post', 'papi' )
		];
	}

	/**
	 * Get sort option value.
	 *
	 * @param  int $post_id
	 *
	 * @return string
	 */
	public function get_sort_option( $post_id ) {
		$slug = $this->html_id( 'sort_option' );
		$slug = str_replace( '][', '_', $slug );
		$slug = str_replace( '[', '_', $slug );
		$slug = str_replace( ']', '', $slug );

		return papi_data_get( $post_id, $slug, $this->get_meta_type() );
	}

	/**
	 * Get sort options for relationship property.
	 *
	 * @return array
	 */
	public static function get_sort_options() {
		$sort_options = [];

		$sort_options[__( 'Select', 'papi' )] = null;

		$sort_options[__( 'Name (alphabetically)', 'papi' )] = function ( $a, $b ) {
			// Backwards compatibility with both `post_title` and `title`.
			return strcmp(
				strtolower( isset( $a->post_title ) ? $a->post_title : $a->title ),
				strtolower( isset( $b->post_title ) ? $b->post_title : $b->title )
			);
		};

		$sort_options[__( 'Post created date (ascending)', 'papi' )] = function ( $a, $b ) {
			return strtotime( $a->post_date ) > strtotime( $b->post_date );
		};

		$sort_options[__( 'Post created date (descending)', 'papi' )] = function ( $a, $b ) {
			return strtotime( $a->post_date ) < strtotime( $b->post_date );
		};

		$sort_options[__( 'Post id (ascending)', 'papi' )] = function ( $a, $b ) {
			// Backwards compatibility with both `ID` and `id`.
			return isset( $a->ID ) ? $a->ID > $b->ID : $a->id > $b->id;
		};

		$sort_options[__( 'Post id (descending)', 'papi' )] = function ( $a, $b ) {
			// Backwards compatibility with both `ID` and `id`.
			return isset( $a->ID ) ? $a->ID < $b->ID : $a->id < $b->id;
		};

		$sort_options[__( 'Post order value (ascending)', 'papi' )] = function ( $a, $b ) {
			return $a->menu_order > $b->menu_order;
		};

		$sort_options[__( 'Post order value (descending)', 'papi' )] = function ( $a, $b ) {
			return $a->menu_order < $b->menu_order;
		};

		$sort_options[__( 'Post modified date (ascending)', 'papi' )] = function ( $a, $b ) {
			return strtotime( $a->post_modified ) > strtotime( $b->post_modified );
		};

		$sort_options[__( 'Post modified date (descending)', 'papi' )] = function ( $a, $b ) {
			return strtotime( $a->post_modified ) < strtotime( $b->post_modified );
		};

		return apply_filters( 'papi/property/relationship/sort_options', $sort_options );
	}

	/**
	 * Get items to display from settings.
	 *
	 * @param  stdClass $settings
	 *
	 * @return array
	 */
	protected function get_items( $settings ) {
		if ( is_array( $settings->items ) && ! empty( $settings->items ) ) {
			$mapping = function ( $item ) {
				return is_array( $item ) ?
					isset( $item['id'], $item['title'] ) :
					isset( $item->id, $item->title );
			};

			return array_map(
				'papi_maybe_convert_to_object',
				array_filter( $settings->items, $mapping )
			);
		}

		// By default we add posts per page key with the value -1 (all).
		if ( ! isset( $settings->query['posts_per_page'] ) ) {
			$settings->query['posts_per_page'] = -1;
		}

		// Prepare arguments for WP_Query.
		$args = array_merge( [
			'post_status'            => 'any',
			'post_type'              => papi_to_array( $settings->post_type ),
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false
		], $settings->query );

		$items = ( new WP_Query( $args ) )->posts;

		return array_map( [$this, 'convert_post_to_item'], papi_get_only_objects( $items ) );
	}

	/**
	 * Get matching value based on key from a post.
	 *
	 * @param  mixed $value
	 *
	 * @return mixed
	 */
	protected function get_post_value( $value ) {
		$meta_key = $this->get_setting( 'meta_key' );

		if ( $value instanceof WP_Post === false ) {
			return 0;
		}

		if ( empty( $meta_key ) ) {
			return $value->ID;
		}

		if ( $value = get_post_meta( $value->ID, $meta_key, true ) ) {
			return $value;
		}

		return 0;
	}

	/**
	 * Render property html.
	 */
	public function html() {
		$post_id       = papi_get_post_id();
		$slug          = $this->html_name();
		$settings      = $this->get_settings();
		$settings_json = [];
		$sort_option   = $this->get_sort_option( $post_id );
		$sort_options  = static::get_sort_options();
		$values        = papi_get_only_objects( $this->get_value() );
		$items         = $this->get_items( $settings );

		if ( papi_is_empty( $settings->items ) ) {
			$values = array_map( [$this, 'convert_post_to_item'], $values );
		} else {
			foreach ( array_keys( $sort_options ) as $key ) {
				if ( strpos( $key, 'Post' ) === 0 ) {
					unset( $sort_options[$key] );
				}
			}
		}

		// Remove existing values if `only once` is active.
		if ( $this->get_setting( 'only_once' ) ) {
			$items = array_udiff( $items, $values, function( $a, $b ) {
				// Backwards compatibility with both `post_title` and `title`.
				return strcmp(
					strtolower( isset( $a->post_title ) ? $a->post_title : $a->title ),
					strtolower( isset( $b->post_title ) ? $b->post_title : $b->title )
				);
			} );
		}

		// Convert all sneak case key to camel case.
		foreach ( (array) $settings as $key => $val ) {
			if ( ! is_string( $key ) || ! in_array( $key, ['only_once', 'limit'], true ) ) {
				continue;
			}

			if ( $key = papi_camel_case( $key ) ) {
				$settings_json[$key] = $val;
			}
		}
		?>
		<div class="papi-property-relationship" data-settings='<?php echo esc_attr( papi_maybe_json_encode( $settings_json ) ); ?>'>
			<input type="hidden" name="<?php echo esc_attr( $slug ); ?>[]" data-papi-rule="<?php echo esc_attr( $slug ); ?>" />

					<label for="<?php echo esc_attr( $this->html_id( 'search' ) ); ?>"><?php esc_html_e( 'Search', 'papi' ); ?>
					<input id="<?php echo esc_attr( $this->html_id( 'search' ) ); ?>" type="search" />

					<?php if ( $settings->show_sort_by ): ?>
						<label for="<?php echo esc_attr( $this->html_id( 'sort_option' ) ); ?>"><?php esc_html_e( 'Sort by', 'papi' ); ?>
						<select id="<?php echo esc_attr( $this->html_id( 'sort_option' ) ); ?>" name="<?php echo esc_attr( $this->html_id( 'sort_option' ) ); ?>">
							<?php foreach ( array_keys( $sort_options ) as $key ): ?>
								<option value="<?php echo esc_attr( $key ); ?>" <?php echo $key === $sort_option ? 'selected="selected"' : ''; ?>><?php echo esc_html( $key ); ?></option>
							<?php endforeach; ?>
						</select>
					<?php endif; ?>

						<?php
						foreach ( $items as $item ):
							if ( ! empty( $item->title ) ):
								?>

									<input type="hidden" data-name="<?php echo esc_attr( $slug ); ?>[]" value="<?php echo esc_attr( $item->id ); ?>"/>
									<a href="#" title="<?php echo esc_attr( $item->title ); ?>"><?php echo esc_html( $item->title ); ?>

							<?php
							endif;
						endforeach;
						?>

						<?php foreach ( $values as $item ): ?>

								<input type="hidden" name="<?php echo esc_attr( $slug ); ?>[]" value="<?php echo esc_attr( $item->id ); ?>"/>
								<?php echo esc_attr( $item->title ); ?>

						<?php endforeach; ?>

	<?php
	}

	/**
	 * Change value after it's loaded from the database.
	 *
	 * @param  mixed  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return mixed
	 */
	public function load_value( $values, $slug, $post_id ) {
		$values = (array) papi_maybe_json_decode( maybe_unserialize( $values ), true );
		return array_map( 'papi_maybe_convert_to_object', $values );
	}

	/**
	 * Sort the values.
	 *
	 * @param  array  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function sort_value( $values, $slug, $post_id ) {
		$sort_option  = $this->get_sort_option( $post_id );
		$sort_options = static::get_sort_options();

		if ( empty( $sort_option ) || ! isset( $sort_options[$sort_option] ) || is_null( $sort_options[$sort_option] ) ) {
			return $values;
		}

		usort( $values, $sort_options[$sort_option] );

		return $values;
	}

	/**
	 * Sort the values on update.
	 *
	 * @param  mixed  $values
	 * @param  string $slug
	 * @param  int    $post_id
	 *
	 * @return string
	 */
	public function update_value( $values, $slug, $post_id ) {
		$values = $this->format_value( $values, $slug, $post_id );
		$values = array_map( function ( $item ) {
			if ( $item instanceof WP_Post ) {
				$item = $this->convert_post_to_item( $item );
			}

			if ( isset( $item->title ) ) {
				unset( $item->title );
			}

			return $item;
		}, $values );

		return papi_maybe_json_decode( $values );
	}
}


<?php

/**
 * Manage types.
 */
class Papi_CLI_Type_Command extends Papi_CLI_Command {

	/**
	 * Get default fields for formatter.
	 *
	 * @return array
	 */
	protected function get_default_format_fields() {
		return ['name', 'id', 'meta type value', 'template', 'db count', 'type'];
	}

	/**
	 * Get meta type value.
	 *
	 * @param  Papi_Entry_Type $entry_type
	 *
	 * @return string
	 */
	protected function get_meta_type_value( $entry_type ) {
		if ( in_array( $entry_type->get_type(), ['attachment'], true ) ) {
			return $entry_type->get_type();
		}

		switch ( papi_get_meta_type( $entry_type->get_type() ) ) {
			case 'post':
				return implode( ', ', $entry_type->post_type );
			case 'term':
				return implode( ', ', $entry_type->taxonomy );
			default:
				return 'n/a';
		}
	}

	/**
	 * List Papi types.
	 *
	 * ## Options
	 *
	 * [--<field>=<value>]
	 * : Filter types based on type property.
	 *
	 * [--field=<field>]
	 * : Prints the value of a single field for each type.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific type fields.
	 *
	 * [--format=<format>]
	 * : Acceptec values: table, csv, json, count, ids. Default: table.
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each type:
	 *
	 * * name
	 * * id
	 * * post_type
	 * * template
	 * * number_of_pages
	 * * type
	 *
	 * Not all fields exists on a Papi type so some fields will have `n/a`
	 * as value when no value can be displayed.
	 *
	 * ## EXAMPLES
	 *
	 *     wp papi type list
	 *
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {
		// Get all entry types.
		$entry_types = papi_get_all_entry_types();

		if ( empty( $entry_types ) ) {
			WP_CLI::error( 'No Papi types exists.' );
		}

		// Create type item with the fields that
		// will be displayed.
		$entry_types = array_map( function( $entry_type ) {
			return [
				'id'              => $entry_type->get_id(),
				'name'            => $entry_type->name,
				'meta type value' => $this->get_meta_type_value( $entry_type ),
				'template'        => empty( $entry_type->template ) ? 'n/a' : $entry_type->template,
				'type'            => $entry_type->get_type(),
				'db count'        => $entry_type->get_type() === 'option' ? 'n/a' : papi_get_entry_type_count( $entry_type )
			];
		}, $entry_types );

		// Render types as a table.
		$formatter = $this->get_formatter( $assoc_args );
		$formatter->display_items( $entry_types );
	}
}


<?php

/**
 * Admin class that handles admin tabs in
 * meta boxes.
 */
final class Papi_Admin_Meta_Box_Tabs {

	/**
	 * The tabs.
	 *
	 * @var array
	 */
	protected $tabs = [];

	/**
	 * The constructor.
	 *
	 * @param array $tabs
	 * @param bool  $render
	 */
	public function __construct( array $tabs = [], $render = true ) {
		if ( empty( $tabs ) ) {
			return;
		}

		$this->tabs = papi_tabs_setup( $tabs );

		if ( $render ) {
			$this->html();
		}
	}

	/**
	 * Get the tabs that are registered.
	 *
	 * @return array
	 */
	public function get_tabs() {
		return $this->tabs;
	}

	/**
	 * Generate html for tabs and properties.
	 */
	protected function html() {
		?>

				<?php
				foreach ( $this->tabs as $tab ):
					$css_classes = $this->tabs[0] === $tab ? 'active ' : '';

					if ( empty( $tab->background ) ) {
						// Find out if the first property has a sidebar or not. If the first property
						// don't have a sidebar the tab background should be white since it looks better.
						$no_sidebar = empty( $tab->properties ) ? false : $tab->properties[0]->sidebar;
						$css_classes .= ! empty( $tab->properties ) && $no_sidebar ? '' : 'white-tab';
					} else {
						$css_classes .= $tab->background === 'white' ? 'white-tab' : '';
					}
					?>
					<li class="<?php echo esc_attr( $css_classes ); ?>">
						<a href="#" data-papi-tab="<?php echo esc_attr( $tab->id ); ?>">
							<?php if ( ! empty( $tab->icon ) ): ?>
								<img src="<?php echo esc_attr( $tab->icon ); ?>" alt="<?php echo esc_attr( $tab->title ); ?>" />
							<?php endif;
							echo esc_html( $tab->title ); ?>

				<?php endforeach; ?>

				<?php
				foreach ( $this->tabs as $tab ):
					?>
					<div class="<?php echo $this->tabs[0] === $tab ? 'active' : ''; ?>" data-papi-tab="<?php echo esc_attr( $tab->id ); ?>">
						<?php papi_render_properties( $tab->properties ); ?>

					<?php
				endforeach;
				?>

		<?php
	}
}


<?php

/**
 * Flexible repeater property that can repeat multiple properties
 * with different properties per layout.
 */
class Papi_Property_Flexible extends Papi_Property_Repeater {

	/**
	 * The convert type.
	 *
	 * @var string
	 */
	public $convert_type = 'array';

	/**
	 * Flexible repeater counter number.
	 *
	 * @var int
	 */
	protected $counter = 0;

	/**
	 * The default value.
	 *
	 * @var array
	 */
	public $default_value = [];

	/**
	 * Exclude properties that is not allowed in a repeater.
	 *
	 * @var array
	 */
	protected $exclude_properties = ['flexible'];

	/**
	 * The layout key.
	 *
	 * @var string
	 */
	protected $layout_key = '_flexible_layout';

	/**
	 * Layout value regex.
	 *
	 * @var string
	 */
	protected $layout_value_regex = '/^\_flexible\_layout\_/';

	/**
	 * Delete value from the database.
	 *
	 * @param  string $slug
	 * @param  int    $post_id
	 * @param  string $type
	 *
	 * @return bool
	 */
	public function delete_value( $slug, $post_id, $type ) {
		$rows   = intval( papi_data_get( $post_id, $slug, $type ) );
		$value  = $this->load_value( $rows, $slug, $post_id );
		$value  = papi_property_to_array_slugs( $value, $slug );
		$result = true;

		foreach ( array_keys( $value ) as $key ) {
			$out    = papi_data_delete( $post_id, $key, $type );
			$result = $out ? $result : $out;
		}

		return $result;
	}

	/**
	 * Format the value of the property before it's returned
	 * to WordPress admin or the site.
	 *
	 * @param  mixed  $values
	 * @param  string $repeater_slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	public function format_value( $values, $repeater_slug, $post_id ) {
		if ( ! is_array( $values ) ) {
			return [];
		}

		foreach ( $values as $index => $layout ) {
			foreach ( $layout as $slug => $value ) {
				if ( is_string( $value ) && preg_match( $this->layout_value_regex, $value ) ) {
					if ( isset( $values[$index][$this->layout_key] ) ) {
						unset( $values[$index][$slug] );
						continue;
					}

					$values[$index][$this->layout_key] = $value;
					unset( $values[$index][$slug] );

					continue;
				}

				if ( papi_is_property_type_key( $slug ) ) {
					continue;
				}

				$property_type_slug = papi_get_property_type_key_f( $slug );

				if ( ! isset( $values[$index][$property_type_slug] ) ) {
					continue;
				}

				$property_type_value = $values[$index][$property_type_slug];
				$property_type       = papi_get_property_type( $property_type_value );

				if ( ! is_object( $property_type ) ) {
					continue;
				}

				// Set parent property.
				$property_type->set_parent_property( $this );

				// Get property child slug.
				$child_slug = $this->get_child_slug( $repeater_slug, $slug );

				// Create cache key.
				$cache_key = sprintf( '%s_%d_%s', $repeater_slug, $index, $slug );

				// Get raw value from cache if enabled.
				if ( $this->cache ) {
					$raw_value = papi_cache_get( $cache_key, $post_id, $this->get_meta_type() );
				} else {
					$raw_value = false;
				}

				// Load the value.
				if ( $raw_value === null || $raw_value === false ) {
					$values[$index][$slug] = $property_type->load_value( $value, $child_slug, $post_id );
					$values[$index][$slug] = papi_filter_load_value( $property_type->type, $values[$index][$slug], $child_slug, $post_id, papi_get_meta_type() );

					if ( ! papi_is_empty( $values[$index][$slug] ) && $this->cache ) {
						papi_cache_set( $cache_key, $post_id, $values[$index][$slug], $this->get_meta_type() );
					}
				} else {
					$values[$index][$slug] = $raw_value;
				}

				if ( strtolower( $property_type->type ) === 'repeater' ) {
					$property_type->cache = false;
				}

				// Format the value from the property class.
				$values[$index][$slug] = $property_type->format_value( $values[$index][$slug], $child_slug, $post_id );

				if ( ! papi_is_admin() ) {
					$values[$index][$slug] = papi_filter_format_value( $property_type->type, $values[$index][$slug], $child_slug, $post_id, papi_get_meta_type() );
				}

				$values[$index][$property_type_slug] = $property_type_value;

				if ( papi_is_empty( $values[$index][$slug] ) ) {
					$values[$index][$slug] = $property_type_value->get_option( 'default', $property_type_value->default_value );
				}
			}
		}

		if ( ! papi_is_admin() ) {
			foreach ( $values as $index => $row ) {
				foreach ( $row as $slug => $value ) {
					if ( is_string( $value ) && preg_match( $this->layout_value_regex, $value ) ) {
						unset( $values[$index][$slug] );
						$values[$index]['_layout'] = preg_replace( $this->layout_value_regex, '', $value );
					}

					if ( papi_is_property_type_key( $slug ) ) {
						unset( $values[$index][$slug] );
					}
				}
			}
		}

		return $values;
	}

	/**
	 * Check if the given key is a valid layout key.
	 *
	 * @param  string $key
	 *
	 * @return bool
	 */
	protected function is_layout_key( $key ) {
		return is_string( $key ) && preg_match( '/\\_layout|\\' . $this->layout_key . '$/', $key );
	}

	/**
	 * Generate layout slug.
	 *
	 * @param  string $key
	 * @param  string $extra
	 *
	 * @return string
	 */
	protected function get_json_id( $key, $extra = '' ) {
		return $this->get_slug() . '_' . papi_slugify( $key ) . ( empty( $extra ) ? '' : '_' . $extra );
	}

	/**
	 * Get layout by slug.
	 *
	 * @param  string $slug
	 *
	 * @return array
	 */
	protected function get_layout( $slug ) {
		$layouts = $this->get_settings_layouts();

		foreach ( $layouts as $layout ) {
			if ( $layout['slug'] === $slug ) {
				return $layout;
			}
		}

		return [];
	}

	/**
	 * Get layout value.
	 *
	 * @param  string $layout
	 *
	 * @return string
	 */
	protected function get_layout_value( $layout ) {
		if ( preg_match( $this->layout_value_regex, $layout ) ) {
			return $layout;
		}

		return sprintf( '_flexible_layout_%s', $layout );
	}

	/**
	 * Get results from the database.
	 *
	 * @param  int    $value
	 * @param  string $repeater_slug
	 * @param  int    $post_id
	 *
	 * @return array
	 */
	protected function get_results( $value, $repeater_slug, $post_id ) {
		global $wpdb;

		if ( $this->get_meta_type() === 'option' ) {
			$table = $wpdb->prefix . 'options';
			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE `option_name` LIKE '%s' ORDER BY `option_id` ASC",
				$repeater_slug . '_%'
			);
			// @codingStandardsIgnoreEnd
		} else {
			$table  = sprintf( '%s%smeta', $wpdb->prefix, $this->get_meta_type() );
			$column = papi_get_meta_id_column( $this->get_meta_type() );
			// @codingStandardsIgnoreStart
			$query = $wpdb->prepare(
				"SELECT * FROM `$table` WHERE `meta_key` LIKE '%s' AND `$column` = %s ORDER BY `meta_id` ASC", $repeater_slug . '_%',
				$post_id
			);
			// @codingStandardsIgnoreEnd
		}

		$dbresults = $wpdb->get_results( $query ); // WPCS: unprepared sql
		$value     = intval( $value );

		// Do not proceed with empty value or columns.
		if ( empty( $value ) ) {
			return [[], []];
		}

		$values  = [];
		$results = [];
		$trash   = [];

		// Get row results.
		$rows = $this->get_row_results( $dbresults );

		// Get columns, divde all items with two.
		$columns = array_map( function ( $row ) {
			return count( $row ) / 2;
		}, $rows );

		$rows = array_values( $rows );

		// Add repeater slug with number of rows to the values array.
		$values[$repeater_slug] = $value;

		for ( $i = 0; $i < $value; $i ++ ) {
			$no_trash = [];

			if ( ! isset( $columns[$i] ) || ! isset( $rows[$i] ) ) {
				continue;
			}

			foreach ( $rows[$i] as $slug => $meta ) {
				if ( ! is_string( $slug ) || ! isset( $rows[$i][$slug] ) ) {
					continue;
				}

				// Do not deal with layout meta object here since the property meta object will deal with it later.
				if ( is_string( $meta->meta_value ) && preg_match( $this->layout_value_regex, $meta->meta_value ) ) {
					if ( ! isset( $values[$slug] ) ) {
						$values[$slug] = $meta->meta_value;
					}

					continue;
				}

				// Add meta object to the no trash array.
				// so it won't be deleted.
				$no_trash[$slug] = $meta;

				// Serialize value if needed.
				$meta->meta_value = papi_maybe_json_decode( maybe_unserialize( $meta->meta_value ) );

				// Add property value and property type value.
				$values[$meta->meta_key] = $meta->meta_value;

				if ( isset( $rows[$i][$slug] ) ) {
					// Add the meta value.
					$values[$slug] = $rows[$i][$slug]->meta_value;
				}
			}

			// Get the meta keys to delete.
			$trash_diff = array_diff( array_keys( $rows[$i] ), array_keys( $no_trash ) );

			if ( ! empty( $trash_diff ) ) {
				// Find all trash meta objects from results array.
				foreach ( $trash_diff as $slug ) {
					if ( ! isset( $results[$i] ) || ! isset( $rows[$i][$slug] ) ) {
						continue;
					}

					$trash[$results[$i][$slug]->meta_key] = $rows[$i][$slug];
				}
			}
		}

		// Fetch one layout per row.
		// Since 3.0.0 this is backward compatibility.
		$dblayouts = [];
		foreach ( array_keys( $values ) as $slug ) {
			if ( $this->is_layout_key( $slug ) ) {
				$num = str_replace( $repeater_slug . '_', '', $slug );
				$num = explode( '_', $num );
				$num = intval( $num[0] );

				if ( ! isset( $dblayouts[$num] ) ) {
					$dblayouts[$num] = $num . $values[$slug];
				}
			}
		}

		$layouts = $this->get_settings_layouts();

		// Add empty rows that isn't saved to database.
		for ( $i = 0; $i < $value; $i ++ ) {
			foreach ( $layouts as $layout ) {
				$layout_slug = sprintf( '%s_%d%s', $this->get_slug( true ), $i, $this->layout_key );

				// Since 3.0.0 the `$dblayouts` check is only for backward compatibility.
				if ( isset( $layout['slug'] ) && ( ( isset( $values[$layout_slug] ) && $layout['slug'] === $values[$layout_slug] ) || in_array( $i . $layout['slug'], $dblayouts, true ) ) ) {
					foreach ( $layout['items'] as $prop ) {
						$slug = sprintf( '%s_%d_%s', $repeater_slug, $i, unpapify( $prop->slug ) );

						if ( ! isset( $values[$slug] ) ) {
							$values[$slug] = null;
						}
					}
				}
			}
		}

		return [$values, $trash];
	}

	/**
	 * Get layouts.
	 *
	 * @return array
	 */
	protected function get_settings_layouts() {
		$settings = $this->get_settings();

		return $this->prepare_properties( papi_to_array( $settings->items ) );
	}

	/**
	 * Change value after it's loaded from the database
	 * and populate every property in the flexible with the right property type.
	 *
	 * @param mixed  $value
	 * @param string $repeater_slug
	 * @param int    $post_id
	 *
	 * @return array
	 */
	public function load_value( $value, $repeater_slug, $post_id ) {
		if ( is_array( $value ) ) {
			return $value;
		}

		list( $results, $trash ) = $this->get_results( $value, $repeater_slug, $post_id );

		// Will not need this array.
		unset( $trash );

		$store   = $this->get_store();
		$results = papi_property_from_array_slugs( $results, unpapify( $repeater_slug ) );

		if ( is_null( $store ) ) {
			return $this->default_value;
		}

		return $this->load_child_properties( $results, $this );
	}

	/**
	 * Load child properties.
	 *
	 * @param  array              $results
	 * @param  Papi_Core_Property $property
	 *
	 * @return array
	 */
	protected function load_child_properties( array $results, $property = null ) {
		$layout_key = substr( $this->layout_key, 1 );

		foreach ( $results as $index => $row ) {
			foreach ( $row as $slug => $value ) {
				$children = [];

				if ( $layout_key === $slug ) {
					continue;
				}

				if ( isset( $results[$index][$layout_key] ) ) {
					$layout = $results[$index][$layout_key];
					$layout = $this->get_layout( $layout );

					if ( ! empty( $layout ) && isset( $layout['items'] ) ) {
						$children = $layout['items'];
					}
				}

				$child_property = null;

				foreach ( $children as $child ) {
					if ( $child->match_slug( $slug ) ) {
						$child_property = $child;
					}
				}

				if ( empty( $child_property ) ) {
					$child_property = $this->get_store()->get_property( $this->get_slug( true ), $slug );
				}

				if ( is_array( $value ) && papi_is_property( $child_property ) && ! empty( $child_property->get_child_properties() ) ) {
					$new_value = papi_from_property_array_slugs( $value, unpapify( $slug ) );

					if ( empty( $new_value ) ) {
						$results[$index][$slug] = $value;
					} else {
						$results[$index][$slug] = $this->load_child_properties( $new_value, $child_property );
					}
				}

				$type_key = papi_get_property_type_key_f( $slug );

				if ( $property->match_slug( $slug ) ) {
					$results[$index][$type_key] = $property;
				} else {
					$results[$index][$type_key] = $property->get_child_property( $slug, $children );
				}
			}
		}

		return $results;
	}

	/**
	 * Prepare properties.
	 *
	 * Not the best name for this function, but since
	 * property repeater using this we can't rename it.
	 *
	 * @param  array $layouts
	 *
	 * @return array
	 */
	protected function prepare_properties( $layouts ) {
		$layouts = array_map( function ( $layout ) {
			return (array) $layout;
		}, $layouts );

		foreach ( $layouts as $index => $layout ) {
			if ( ! $this->valid_layout( $layout ) ) {
				if ( is_array( $layout ) ) {
					unset( $layout[$index] );
				} else {
					unset( $layouts[$index] );
				}

				continue;
			}

			if ( ! isset( $layout['slug'] ) ) {
				$layout['slug'] = $layout['title'];
			}

			if ( ! isset( $layout['row_label'] ) ) {
				$layout['row_label'] = $layout['title'];
			}

			if ( ! isset( $layout['show_label'] ) ) {
				$layout['show_label'] = true;
			}

			$layouts[$index] = array_merge( $layouts[$index], $layout );

			$layouts[$index]['slug']  = papi_slugify( $layout['slug'] );
			$layouts[$index]['slug']  = $this->get_layout_value( $layouts[$index]['slug'] );
			$layouts[$index]['items'] = parent::prepare_properties( $layout['items'] );
		}

		return array_filter( $layouts );
	}

	/**
	 * Render AJAX request.
	 */
	public function render_ajax_request() {
		$items   = null;
		$layouts = $this->get_settings_layouts();

		if ( defined( 'DOING_PAPI_AJAX' ) && DOING_PAPI_AJAX ) {
			$counter         = papi_get_qs( 'counter' );
			$this->counter   = intval( $counter );
			$flexible_layout = papi_get_qs( 'flexible_layout' );

			foreach ( $layouts as $layout ) {
				if ( $layout['slug'] === $flexible_layout ) {
					$items = $layout;
					break;
				}
			}
		}

		if ( ! empty( $items ) ) {
			$this->render_properties( $items, false );
		}
	}

	/**
	 * Render layout JSON template.
	 *
	 * @param string $slug
	 */
	protected function render_json_template( $slug ) {
		$options = $this->get_options();

		$options->settings->items = papi_to_array( $options->settings->items );

		foreach ( $options->settings->items as $key => $value ) {
			if ( ! isset( $value['items'] ) ) {
				continue;
			}

			foreach ( $value['items'] as $index => $property ) {
				$property = $this->prepare_property_for_json( papi_property( $property ) );

				if ( $property === false ) {
					unset( $options->settings->items[$key]['items'][$index] );
					continue;
				}

				$options->settings->items[$key]['items'][$index] = $property;
			}
		}

		papi_render_html_tag( 'script', [
			'data-papi-json' => esc_attr( sprintf( '%s_repeater_json', $slug ) ),
			'type'           => 'application/json',
			papi_maybe_json_encode( [$options] )
		] );
	}

	/**
	 * Render layout input.
	 *
	 * @param string $value
	 */
	protected function render_layout_input( $value ) {
		$slug = sprintf( '%s[%d][%s]', $this->get_slug(), $this->counter, $this->layout_key );
		?>
		<input type="hidden" name="<?php echo esc_attr( $slug ); ?>" value="<?php echo esc_attr( $value ); ?>" />
		<?php
	}

	/**
	 * Render properties.
	 *
	 * @param array      $row
	 * @param array|bool $value
	 */
	protected function render_properties( $row, $value ) {
		$has_value     = $value !== false;
		$render_layout = $this->get_setting( 'layout' );
		$layout_slug   = isset( $row['slug'] ) ? $row['slug'] : false;
		$layout_slug   = empty( $layout_slug ) && isset( $value['_layout'] ) ? $value['_layout'] : $layout_slug;
		$layout_slug   = empty( $layout_slug ) && isset( $value[$this->layout_key] ) ? $value[$this->layout_key] : $layout_slug;
		$row           = isset( $row['items'] ) ? $row['items'] : $row;
		$layout        = $this->get_layout( $layout_slug );

		// Render one hidden input for layout slug.
		$this->render_layout_input( $layout_slug );
		?>
		<td class="repeater-column flexible-column <?php echo $render_layout === 'table' ? 'flexible-layout-table' : 'flexible-layout-row'; ?>">

				<?php // flexible label table layout ?>
				<?php if ( $render_layout === 'table' && ! empty( $layout['row_label'] ) && isset( $layout['show_label'] ) && $layout['show_label'] ): ?>
					<?php echo esc_html( $layout['row_label'] ); ?>
				<?php endif; ?>
				<table class="<?php echo $render_layout === 'table' ? 'flexible-table' : 'papi-table'; ?>">
					<?php
					if ( $render_layout === 'table' ):
						echo '
';
						for ( $i = 0, $l = count( $row ); $i < $l; $i ++ ) {
							// Don't show the property if it's disabled.
							if ( $row[$i]->disabled() ) {
								continue;
							}

							if ( $row[$i]->sidebar ) {
								echo 'display() ? '' : 'papi-hide' ) . '">';
								echo sprintf(
									'%s',
									esc_attr( $this->html_id( $row[$i], $this->counter ) ),
									esc_html( $row[$i]->title )
								);
								echo '	';
							}
						}
						echo '
';
					endif;

					echo '
';

					if ( $render_layout === 'table' ):
						echo '
';
					endif;

					for ( $i = 0, $l = count( $row ); $i < $l; $i ++ ) {
						// Don't show the property if it's disabled.
						if ( $row[$i]->disabled() ) {
							continue;
						}

						$render_property = clone $row[$i]->get_options();
						$value_slug      = $row[$i]->get_slug( true );

						if ( $has_value ) {
							if ( array_key_exists( $value_slug, $value ) ) {
								$render_property->value = $value[$value_slug];
							} else {
								if ( array_key_exists( $row[$i]->get_slug(), $value ) ) {
									$render_property->value = $row[$i]->default_value;
								} else {
									continue;
								}
							}
						}

						$render_property->slug = $this->html_name(
							$render_property,
							$this->counter
						);
						$render_property->raw  = $render_layout === 'table';

						if ( $render_layout === 'table' ) {
							echo 'display() ? '' : 'papi-hide' ) . '">';
						} else if ( $i === 0 && ! empty( $layout['row_label'] ) && isset( $layout['show_label'] ) && $layout['show_label'] ) {
							// flexible label row layout
							echo sprintf( '%s', esc_html( $layout['row_label'] ) );
						}

						papi_render_property( $render_property );

						if ( $render_layout === 'table' ) {
							echo '	';
						}
					}

					if ( $render_layout === 'table' ):
						echo '
';
					endif;

					echo '
';
					?>

				<?php
				if ( ! empty( $layout['title'] ) ) {
					echo esc_html( $layout['title'] );
				}
				?>

		<?php
	}

	/**
	 * Render repeater html.
	 *
	 * @param object $options
	 */
	protected function render_repeater( $options ) {
		$layouts = $this->get_settings_layouts();
		?>
		<div class="papi-property-flexible papi-property-repeater-top" data-limit="<?php echo esc_attr( $this->get_setting( 'limit' ) ); ?>">

				<?php $this->render_repeater_row(); ?>

							<?php
							foreach ( $layouts as $layout ) {
								papi_render_html_tag( 'li', [
									papi_html_tag( 'a', [
										'data-layout'    => esc_html( $layout['slug'] ),
										'data-papi-json' => sprintf( '%s_repeater_json', $options->slug ),
										'href'           => '#',
										'role'           => 'button',
										'tabindex'       => 0,
										esc_html( $layout['title'] )
									] )
								] );
							}
							?>

					<?php
					papi_render_html_tag( 'button', [
						'class' => 'button button-primary',
						'type'  => 'button',
						esc_html( $this->get_setting( 'add_new_label' ) )
					] );
					?>

			<?php /* Default repeater value */ ?>

			<input type="hidden" data-papi-rule="<?php echo esc_attr( $options->slug ); ?>" name="<?php echo esc_attr( $this->get_slug() ); ?>[]" />

		<?php
	}

	/**
	 * Render repeater row.
	 */
	protected function render_repeater_row() {
		$layouts = $this->get_settings_layouts();
		$values  = $this->get_value();

		// Fetch all slugs in all layouts.
		$slugs = [];
		foreach ( $layouts as $index => $layout ) {
			foreach ( $layout['items'] as $item ) {
				$slugs[] = unpapify( $item->slug );
			}
		}

		// Remove values that don't exists in the slugs array.
		foreach ( $values as $index => $row ) {
			$keys = array_keys( $row );

			foreach ( array_keys( $row ) as $slug ) {
				if ( in_array( $slug, $keys, true ) || papi_is_property_type_key( $slug ) || $this->is_layout_key( $slug ) ) {
					continue;
				}

				unset( $values[$index][$slug] );
			}
		}

		$values      = array_filter( $values );
		$closed_rows = $this->get_setting( 'closed_rows', true );

		foreach ( $values as $index => $row ):
			?>

			<tr <?php echo $closed_rows ? 'class="closed"' : ''; ?>>

					<?php echo esc_html( $this->counter + 1 ); ?>

				<?php
				foreach ( $layouts as $layout ) {
					// Don't render layouts that don't have a valid value in the database.
					if ( ! isset( $row[$this->layout_key] ) || $layout['slug'] !== $this->get_layout_value( $row[$this->layout_key] ) ) {
						continue;
					}

					// Render all properties in the layout
					$this->render_properties( $layout['items'], $row );
				}

				$this->counter ++;
				?>

						<a title="<?php esc_html_e( 'Remove', 'papi' ); ?>" href="#" class="repeater-remove-item">x

			<?php
		endforeach;
	}

	/**
	 * Render repeater row template.
	 */
	public function render_repeater_row_template() {
		?>

		<?php
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_head', [$this, 'render_repeater_row_template'] );
	}

	/**
	 * Check if the layout is valid or not.
	 *
	 * @param  array $layout
	 *
	 * @return bool
	 */
	protected function valid_layout( array $layout ) {
		return isset( $layout['title'], $layout['items'] );
	}
}


<?php

/**
 * Admin class that handles taxonomy modifications.
 */
class Papi_Admin_Entry_Taxonomy extends Papi_Admin_Entry {

	/**
	 * All taxonomy types.
	 *
	 * @var array
	 */
	protected $taxonomy_types = [];

	/**
	 * The construct.
	 */
	public function __construct() {
		$this->setup_actions();
	}

	/**
	 * Add form fields to edit tags page.
	 */
	public function add_form_fields() {
		$html_name       = esc_attr( papi_get_page_type_key() );
		$taxonomy        = papi_get_qs( 'taxonomy' );
		$taxonomy_object = get_taxonomy( $taxonomy );

		// Get only the taxonomy types that has the taxonomy.
		$taxonomy_types = array_filter( $this->taxonomy_types, function ( $taxonomy_type ) use ( $taxonomy ) {
			return in_array( $taxonomy, $taxonomy_type->taxonomy, true ) && $taxonomy_type->display( $taxonomy );
		} );
		$taxonomy_types = array_values( $taxonomy_types );

		// Do not display empty select if no taxonomy types.
		if ( empty( $taxonomy_types ) ) {
			return;
		}

		// Prepare taxonomy types with standard taxonomy type.
		$taxonomy_types = $this->prepare_taxonomy_types( $taxonomy_types );

		// Render a dropdown if more than one taxonomy types
		// exists on the taxonomy.
		if ( count( $taxonomy_types ) > 1 ):
			?>
			<div class="form-field">
				<label for="<?php echo esc_attr( $html_name ); ?>">
					<?php echo esc_html( sprintf( __( '%s type', 'papi' ), $taxonomy_object->labels->singular_name ) ); ?>
				</label>
				<select name="<?php echo esc_attr( $html_name ); ?>" id="<?php echo esc_attr( $html_name ); ?>" data-papi-page-type-key="true">
					<?php
					foreach ( $taxonomy_types as $taxonomy_type ) {
						papi_render_html_tag( 'option', [
							'data-redirect' => $taxonomy_type->redirect_after_create,
							'value'         => esc_attr( $taxonomy_type->get_id() ),
							esc_html( $taxonomy_type->name )
						] );
					}
					?>
				</select>
			</div>
			<?php
		else:
			papi_render_html_tag( 'input', [
				'data-redirect'           => $taxonomy_types[0]->redirect_after_create,
				'data-papi-page-type-key' => true,
				'name'                    => esc_attr( $html_name ),
				'type'                    => 'hidden',
				'value'                   => esc_attr( $taxonomy_types[0]->get_id() )
			] );
		endif;
	}

	/**
	 * Prepare taxonomy types, add standard taxonomy if it should be added.
	 *
	 * @param  array $taxonomy_types
	 *
	 * @return array
	 */
	protected function prepare_taxonomy_types( array $taxonomy_types ) {
		$taxonomy = papi_get_qs( 'taxonomy' );

		if ( papi_filter_settings_show_standard_taxonomy_type( $taxonomy ) ) {
			$id                      = sprintf( 'papi-standard-%s-type', $taxonomy );
			$taxonomy_type           = new Papi_Taxonomy_Type( $id );
			$taxonomy_type->id       = $id;
			$taxonomy_type->name     = papi_filter_settings_standard_taxonomy_type_name( $taxonomy );
			$taxonomy_type->taxonomy = [$taxonomy];
			$taxonomy_types[]        = $taxonomy_type;
		}

		usort( $taxonomy_types, function ( $a, $b ) {
			return strcmp( $a->name, $b->name );
		} );

		return papi_sort_order( array_reverse( $taxonomy_types ) );
	}

	/**
	 * Setup actions.
	 */
	protected function setup_actions() {
		add_action( 'admin_init', [$this, 'setup_taxonomies_hooks'] );
	}

	/**
	 * Setup hooks for all taxonomies.
	 */
	public function setup_taxonomies_hooks() {
		$this->taxonomy_types = papi_get_all_entry_types( [
			'types' => 'taxonomy'
		] );

		$taxonomies = array_reduce( $this->taxonomy_types, function ( $taxonomies, $taxonomy_type ) {
			return array_merge( $taxonomies, $taxonomy_type->taxonomy );
		}, [] );
		$taxonomies = array_unique( $taxonomies );

		foreach ( $taxonomies as $taxonomy ) {
			if ( is_string( $taxonomy ) && taxonomy_exists( $taxonomy ) ) {
				add_action( $taxonomy . '_add_form_fields', [$this, 'add_form_fields'] );
			}
		}
	}
}

if ( papi_is_admin() ) {
	Papi_Admin_Entry_Taxonomy::instance();
}