group relation * Used when searching for fields in a specific complex field group */ const HIERARCHY_GROUP_SEPARATOR = ':'; /** * Visual layout type constants */ const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal'; const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical'; /** * Stores if the container is active on the current page * * @see activate() * @var bool */ protected $active = false; /** * List of registered unique field names for this container instance * * @see register_field_name() * @var array */ protected $registered_field_names = array(); /** * Complex field layout * * @var string static::LAYOUT_* constant */ protected $layout = self::LAYOUT_TABBED_HORIZONTAL; /** * Tabs available */ protected $tabs = array(); /** * List of default container settings * * @see init() * @var array */ public $settings = array(); /** * Unique ID of the container * * @var string */ public $id; /** * Title of the container * * @var string */ public $title = ''; /** * Type of the container * * @var string */ public $type; /** * List of notification messages to be displayed on the front-end * * @var array */ protected $notifications = array(); /** * List of error messages to be displayed on the front-end * * @var array */ protected $errors = array(); /** * List of container fields * * @see add_fields() * @var array */ protected $fields = array(); /** * Array of custom CSS classes. * * @see set_classes() * @see get_classes() * @var array */ protected $classes = array(); /** * Container datastores. Propagated to all container fields * * @see set_datastore() * @see get_datastore() * @var Datastore_Interface */ protected $datastore; /** * Flag whether the datastore is the default one or replaced with a custom one * * @see set_datastore() * @see get_datastore() * @var boolean */ protected $has_default_datastore = true; /** * Fulfillable_Collection to use when checking attachment/saving conditions * * @var Fulfillable_Collection */ protected $condition_collection; /** * Translator to use when translating conditions to json * * @var \Carbon_Fields\Container\Fulfillable\Translator\Translator */ protected $condition_translator; /** * Create a new container of type $type and name $name. * * @param string $raw_type * @param string $id Unique id for the container. Optional * @param string $name Human-readable name of the container * @return Container $container */ public static function factory( $raw_type, $id, $name = '' ) { // no name provided - switch input around as the id is optionally generated based on the name if ( $name === '' ) { $name = $id; $id = ''; } $type = Helper::normalize_type( $raw_type ); $repository = Carbon_Fields::resolve( 'container_repository' ); $id = $repository->get_unique_container_id( ( $id !== '' ) ? $id : $name ); if ( ! Helper::is_valid_entity_id( $id ) ) { Incorrect_Syntax_Exception::raise( 'Container IDs can only contain lowercase alphanumeric characters, dashes and underscores ("' . $id . '" passed).' ); return null; } if ( ! $repository->is_unique_container_id( $id ) ) { Incorrect_Syntax_Exception::raise( 'The passed container id is already taken ("' . $id . '" passed).' ); return null; } $container = null; if ( Carbon_Fields::has( $type, 'containers' ) ) { $container = Carbon_Fields::resolve_with_arguments( $type, array( 'id' => $id, 'name' => $name, 'type' => $type, ), 'containers' ); } else { // Fallback to class name-based resolution $class = Helper::type_to_class( $type, __NAMESPACE__, '_Container' ); if ( ! class_exists( $class ) ) { Incorrect_Syntax_Exception::raise( 'Unknown container "' . $raw_type . '".' ); $class = __NAMESPACE__ . '\\Broken_Container'; } $fulfillable_collection = Carbon_Fields::resolve( 'container_condition_fulfillable_collection' ); $condition_translator = Carbon_Fields::resolve( 'container_condition_translator_json' ); $container = new $class( $id, $name, $type, $fulfillable_collection, $condition_translator ); } $repository->register_container( $container ); return $container; } /** * An alias of factory(). * * @see Container::factory() * @return Container */ public static function make() { return call_user_func_array( array( get_class(), 'factory' ), func_get_args() ); } /** * Create a new container * * @param string $id Unique id of the container * @param string $title Title of the container * @param string $type Type of the container * @param Fulfillable_Collection $condition_collection * @param \Carbon_Fields\Container\Fulfillable\Translator\Translator $condition_translator */ public function __construct( $id, $title, $type, $condition_collection, $condition_translator ) { Carbon_Fields::verify_boot(); if ( empty( $title ) ) { Incorrect_Syntax_Exception::raise( 'Empty container title is not supported' ); } $this->id = $id; $this->title = $title; $this->type = $type; $this->condition_collection = $condition_collection; $this->condition_collection->set_condition_type_list( array_merge( $this->get_condition_types( true ), $this->get_condition_types( false ) ), true ); $this->condition_translator = $condition_translator; } /** * Return the container id * * @return string */ public function get_id() { return $this->id; } /** * Get array of all static condition types * * @param boolean $static * @return array */ protected function get_condition_types( $static ) { $group = $static ? 'static' : 'dynamic'; $container_type = Helper::class_to_type( get_class( $this ), '_Container' ); $condition_types = array(); $condition_types = apply_filters( 'carbon_fields_' . $container_type . '_container_' . $group . '_condition_types', $condition_types, $container_type, $this ); $condition_types = apply_filters( 'carbon_fields_container_' . $group . '_condition_types', $condition_types, $container_type, $this ); return $condition_types; } /** * Return whether the container is active */ public function is_active() { return $this->active; } /** * Activate the container and trigger an action */ protected function activate() { $this->active = true; $this->boot(); do_action( 'carbon_fields_container_activated', $this ); $fields = $this->get_fields(); foreach ( $fields as $field ) { $field->activate(); } } /** * Perform instance initialization */ abstract public function init(); /** * Boot the container once it's attached. */ protected function boot() { } /** * Load the value for each field in the container. * Could be used internally during container rendering */ public function load() { foreach ( $this->fields as $field ) { $field->load(); } } /** * Called first as part of the container save procedure. * Responsible for checking the request validity and * calling the container-specific save() method * * @see save() * @see is_valid_save() */ public function _save() { $param = func_get_args(); if ( call_user_func_array( array( $this, '_is_valid_save' ), $param ) ) { call_user_func_array( array( $this, 'save' ), $param ); } } /** * Load submitted data and save each field in the container * * @see is_valid_save() */ public function save( $data = null ) { foreach ( $this->fields as $field ) { $field->set_value_from_input( Helper::input() ); $field->save(); } } /** * Checks whether the current save request is valid * * @return bool */ final protected function _is_valid_save() { $params = func_get_args(); $is_valid_save = call_user_func_array( array( $this, 'is_valid_save' ), $params ); return apply_filters( 'carbon_fields_container_is_valid_save', $is_valid_save, $this ); } /** * Checks whether the current save request is valid * * @return bool */ abstract protected function is_valid_save(); /** * Called first as part of the container attachment procedure. * Responsible for checking it's OK to attach the container * and if it is, calling the container-specific attach() method * * @see attach() * @see is_valid_attach() */ public function _attach() { if ( ! $this->is_valid_attach() ) { return; } $param = func_get_args(); call_user_func_array( array( $this, 'attach' ), $param ); // Allow containers to initialize but not activate (useful in cases such as theme options) if ( $this->should_activate() ) { $this->activate(); } } /** * Attach the container rendering and helping methods * to concrete WordPress Action hooks */ public function attach() {} /** * Perform checks whether the container should be attached during the current request * * @return bool True if the container is allowed to be attached */ final public function is_valid_attach() { $is_valid_attach = $this->is_valid_attach_for_request(); return apply_filters( 'carbon_fields_container_is_valid_attach', $is_valid_attach, $this ); } /** * Get environment array for page request (in admin) * * @return array */ abstract protected function get_environment_for_request(); /** * Check container attachment rules against current page request (in admin) * * @return bool */ abstract protected function is_valid_attach_for_request(); /** * Check if conditions pass for request * * @return bool */ protected function static_conditions_pass() { $environment = $this->get_environment_for_request(); $static_condition_collection = $this->condition_collection->evaluate( $this->get_condition_types( false ), true ); return $static_condition_collection->is_fulfilled( $environment ); } /** * Get environment array for object id * * @param integer $object_id * @return array */ abstract protected function get_environment_for_object( $object_id ); /** * Check container attachment rules against object id * * @param int $object_id * @return bool */ abstract public function is_valid_attach_for_object( $object_id ); /** * Check if all conditions pass for object * * @param integer $object_id * @return bool */ protected function all_conditions_pass( $object_id ) { $environment = $this->get_environment_for_object( $object_id ); return $this->condition_collection->is_fulfilled( $environment ); } /** * Whether this container is currently viewed. */ public function should_activate() { return true; } /** * Perform a check whether the current container has fields * * @return bool */ public function has_fields() { return (bool) $this->fields; } /** * Returns the private container array of fields. * Use only if you are completely aware of what you are doing. * * @return Field[] */ public function get_fields() { return $this->fields; } /** * Return root field from container with specified name * * @example crb_complex * * @param string $field_name * @return Field */ public function get_root_field_by_name( $field_name ) { $fields = $this->get_fields(); foreach ( $fields as $field ) { if ( $field->get_base_name() === $field_name ) { return $field; } } return null; } /** * Get a regex to match field name patterns used to fetch specific fields * * @return string */ protected function get_field_pattern_regex() { $field_name_characters = Helper::get_field_name_characters_pattern(); // matches: // field_name // field_name[0] // field_name[0]:group_name // field_name:group_name $regex = '/ \A (?P[' . $field_name_characters . ']+) (?:\[(?P\d+)\])? (?:' . preg_quote( static::HIERARCHY_GROUP_SEPARATOR, '/' ). '(?P[' . $field_name_characters . ']+))? \z /x'; return $regex; } /** * Return field from container with specified name * * @example $field_name = 'crb_complex/text_field' * @example $field_name = 'crb_complex/complex_2' * @example $field_name = 'crb_complex/complex_2:text_group/text_field' * @example $field_name = 'crb_complex[3]/complex_2[1]:text_group/text_field' * * @param string $field_name * @return Field */ public function get_field_by_name( $field_name ) { $hierarchy = array_filter( explode( static::HIERARCHY_FIELD_SEPARATOR, $field_name ) ); $field = null; $field_group = $this->get_fields(); $hierarchy_left = $hierarchy; $field_pattern_regex = $this->get_field_pattern_regex(); $hierarchy_index = array(); while ( ! empty( $hierarchy_left ) ) { $segment = array_shift( $hierarchy_left ); $segment_pieces = array(); if ( ! preg_match( $field_pattern_regex, $segment, $segment_pieces ) ) { return null; } $segment_field_name = $segment_pieces['field_name']; $segment_group_index = isset( $segment_pieces['group_index'] ) ? $segment_pieces['group_index'] : 0; $segment_group_name = isset( $segment_pieces['group_name'] ) ? $segment_pieces['group_name'] : Group_Field::DEFAULT_GROUP_NAME; foreach ( $field_group as $f ) { if ( $f->get_base_name() !== $segment_field_name ) { continue; } if ( empty( $hierarchy_left ) ) { $field = clone $f; $field->set_hierarchy_index( $hierarchy_index ); } else { if ( ! ( $f instanceof \Carbon_Fields\Field\Complex_Field ) ) { return null; } $group = $f->get_group_by_name( $segment_group_name ); if ( ! $group ) { return null; } $field_group = $group->get_fields(); $hierarchy_index[] = $segment_group_index; } break; } } return $field; } /** * Perform checks whether there is a field registered with the name $name. * If not, the field name is recorded. * * @param string $name * @return boolean */ protected function register_field_name( $name ) { if ( in_array( $name, $this->registered_field_names ) ) { Incorrect_Syntax_Exception::raise( 'Field name "' . $name . '" already registered' ); return false; } $this->registered_field_names[] = $name; return true; } /** * Return whether the datastore instance is the default one or has been overriden * * @return boolean */ public function has_default_datastore() { return $this->has_default_datastore; } /** * Set datastore instance * * @param Datastore_Interface $datastore * @param bool $set_as_default (optional) * @return Container $this */ public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) { if ( $set_as_default && ! $this->has_default_datastore() ) { return $this; // datastore has been overriden with a custom one - abort changing to a default one } $this->datastore = $datastore; $this->has_default_datastore = $set_as_default; foreach ( $this->fields as $field ) { $field->set_datastore( $this->get_datastore(), true ); } return $this; } /** * Get the DataStore instance * * @return Datastore_Interface $datastore */ public function get_datastore() { return $this->datastore; } /** * Return WordPress nonce name used to identify the current container instance * * @return string */ protected function get_nonce_name() { return $this->get_id() . '_nonce'; } /** * Return WordPress nonce name used to identify the current container instance * * @return string */ protected function get_nonce_value() { return wp_create_nonce( $this->get_nonce_name() ); } /** * Check if the nonce is present in the request and that it is verified * * @return bool */ protected function verified_nonce_in_request() { $input = Helper::input(); $nonce_name = $this->get_nonce_name(); $nonce_value = isset( $input[ $nonce_name ] ) ? $input[ $nonce_name ] : ''; return wp_verify_nonce( $nonce_value, $nonce_name ); } /** * Internal function that creates the tab and associates it with particular field set * * @param string $tab_name * @param array $fields * @param int $queue_end * @return object $this */ private function create_tab( $tab_name, $fields, $queue_end = self::TABS_TAIL ) { if ( isset( $this->tabs[ $tab_name ] ) ) { Incorrect_Syntax_Exception::raise( "Tab name duplication for $tab_name" ); } if ( $queue_end === static::TABS_TAIL ) { $this->tabs[ $tab_name ] = array(); } else if ( $queue_end === static::TABS_HEAD ) { $this->tabs = array_merge( array( $tab_name => array() ), $this->tabs ); } foreach ( $fields as $field ) { $field_name = $field->get_name(); $this->tabs[ $tab_name ][ $field_name ] = $field; } $this->settings['tabs'] = $this->get_tabs_json(); } /** * Whether the container is tabbed or not * * @return bool */ public function is_tabbed() { return (bool) $this->tabs; } /** * Retrieve all fields that are not defined under a specific tab * * @return array */ protected function get_untabbed_fields() { $tabbed_fields_names = array(); foreach ( $this->tabs as $tab_fields ) { $tabbed_fields_names = array_merge( $tabbed_fields_names, array_keys( $tab_fields ) ); } $untabbed_fields = array_filter( $this->fields, function( $field ) use ( $tabbed_fields_names ) { return ! in_array( $field->get_name(), $tabbed_fields_names ); } ); return $untabbed_fields; } /** * Retrieve all tabs. * Create a default tab if there are any untabbed fields. * * @return array */ protected function get_tabs() { $untabbed_fields = $this->get_untabbed_fields(); if ( ! empty( $untabbed_fields ) ) { $this->create_tab( apply_filters( 'carbon_fields_untabbed_fields_tab_title', __( 'General', 'carbon-fields' ), $this ), $untabbed_fields, static::TABS_HEAD ); } return $this->tabs; } /** * Build the tabs JSON * * @return array */ protected function get_tabs_json() { $tabs_json = array(); $tabs = $this->get_tabs(); foreach ( $tabs as $tab_name => $fields ) { foreach ( $fields as $field_name => $field ) { $tabs_json[ $tab_name ][] = $field_name; } } return $tabs_json; } /** * Get custom CSS classes. * * @return array */ public function get_classes() { return $this->classes; } /** * Set CSS classes that the container should use. * * @param string|array $classes * @return Container $this */ public function set_classes( $classes ) { $this->classes = Helper::sanitize_classes( $classes ); return $this; } /** * Returns an array that holds the container data, suitable for JSON representation. * * @param bool $load Should the value be loaded from the database or use the value from the current instance. * @return array */ public function to_json( $load ) { $conditions = $this->condition_collection->evaluate( $this->get_condition_types( true ), $this->get_environment_for_request(), array( 'CUSTOM' ) ); $conditions = $this->condition_translator->fulfillable_to_foreign( $conditions ); $container_data = array( 'id' => $this->get_id(), 'type' => $this->type, 'title' => $this->title, 'layout' => $this->layout, 'classes' => $this->get_classes(), 'settings' => $this->settings, 'conditions' => $conditions, 'fields' => array(), 'nonce' => array( 'name' => $this->get_nonce_name(), 'value' => $this->get_nonce_value(), ), ); $fields = $this->get_fields(); foreach ( $fields as $field ) { $field_data = $field->to_json( $load ); $container_data['fields'][] = $field_data; } return $container_data; } /** * COMMON USAGE METHODS */ /** * Append array of fields to the current fields set. All items of the array * must be instances of Field and their names should be unique for all * Carbon containers. * If a field does not have DataStore already, the container datastore is * assigned to them instead. * * @param array $fields * @return Container $this */ public function add_fields( $fields ) { foreach ( $fields as $field ) { if ( ! ( $field instanceof Field ) ) { Incorrect_Syntax_Exception::raise( 'Object must be of type Carbon_Fields\\Field\\Field' ); return $this; } $unique = $this->register_field_name( $field->get_name() ); if ( ! $unique ) { return $this; } $field->set_context( $this->type ); if ( ! $field->get_datastore() ) { $field->set_datastore( $this->get_datastore(), $this->has_default_datastore() ); } } $this->fields = array_merge( $this->fields, $fields ); return $this; } /** * Configuration function for adding tab with fields * * @param string $tab_name * @param array $fields * @return Container $this */ public function add_tab( $tab_name, $fields ) { $this->add_fields( $fields ); $this->create_tab( $tab_name, $fields ); return $this; } /** * Proxy function to set attachment conditions * * @see Fulfillable_Collection::where() * @return Container $this */ public function where() { call_user_func_array( array( $this->condition_collection, 'where' ), func_get_args() ); return $this; } /** * Proxy function to set attachment conditions * * @see Fulfillable_Collection::or_where() * @return Container $this */ public function or_where() { call_user_func_array( array( $this->condition_collection, 'or_where' ), func_get_args() ); return $this; } /** * Modify the layout of this field. * * @param string $layout * @return self $this */ public function set_layout( $layout ) { $available_layouts = array( static::LAYOUT_TABBED_HORIZONTAL, static::LAYOUT_TABBED_VERTICAL, ); if ( ! in_array( $layout, $available_layouts ) ) { $error_message = 'Incorrect layout ``' . $layout . '" specified. ' . 'Available layouts: ' . implode( ', ', $available_layouts ); Incorrect_Syntax_Exception::raise( $error_message ); return $this; } $this->layout = $layout; return $this; } }