# elementor/4.3.0-beta3/modules/mcp/abilities/utils/overridable-props-builder.php

Elementor Website Builder – more than just a page builder, version 4.3.0-beta3. 569 lines.

- Page: https://pluginprobe.com/plugins/elementor/4.3.0-beta3/code/modules/mcp/abilities/utils/overridable-props-builder.php
- Raw: https://pluginprobe.com/plugins/elementor/4.3.0-beta3/raw/modules/mcp/abilities/utils/overridable-props-builder.php
- Modified: 2026-09-01T11:47:36+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/elementor/4.3.0-beta3/code/modules/mcp/abilities/utils/overridable-props-builder.php#L10-L20`.

```php
<?php

namespace Elementor\Modules\Mcp\Abilities\Utils;

use Elementor\Modules\Components\Components_Repository;
use Elementor\Modules\Components\Documents\Component_Overridable_Prop;
use Elementor\Modules\Components\PropTypes\Component_Override_Parser;
use Elementor\Modules\Components\PropTypes\Overridable_Prop_Type;
use Elementor\Modules\Components\PropTypes\Override_Prop_Type;
use Elementor\Modules\Components\PropTypes\Overrides_Prop_Type;
use Elementor\Modules\Components\Utils\Parsing_Utils;
use Elementor\Modules\Components\Widgets\Component_Instance;
use WP_Http;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Builds native component overridable props from MCP definitions.
 */
class Overridable_Props_Builder {

	const DEFAULT_GROUP_LABEL = 'Default';
	const GROUP_ID_PREFIX = 'group';
	const GROUP_ID_SUFFIX_LENGTH = 7;

	private ?Components_Repository $repository;

	public function __construct( ?Components_Repository $repository = null ) {
		$this->repository = $repository;
	}

	public static function make( ?Components_Repository $repository = null ): self {
		return new self( $repository );
	}

	/**
	 * @param array $elements         Elements tree containing the referenced targets.
	 * @param array $prop_definitions Overridable prop definitions keyed by override key.
	 *
	 * @return array{props: array, groups: array{items: array, order: array}}|\WP_Error
	 */
	public function build( array &$elements, array $prop_definitions ) {
		if ( empty( $prop_definitions ) ) {
			return [
				'props' => [],
				'groups' => [
					'items' => [],
					'order' => [],
				],
			];
		}

		$props = [];
		$groups = [];
		$group_order = [];
		$group_ids_by_label = [];
		$errors = [];
		$updated_elements = $elements;
		$override_keys_by_element_prop = [];

		foreach ( $prop_definitions as $override_key => $definition ) {
			$prop = $this->build_prop(
				(string) $override_key,
				$definition,
				$updated_elements,
				$group_ids_by_label,
				$groups,
				$group_order,
				$override_keys_by_element_prop
			);

			if ( is_wp_error( $prop ) ) {
				$errors[] = $prop->get_error_message();
				continue;
			}

			$props[ $override_key ] = $prop;
		}

		if ( ! empty( $errors ) ) {
			return new \WP_Error(
				'invalid_overridable_props',
				implode( ' ', $errors ),
				[ 'status' => WP_Http::BAD_REQUEST ]
			);
		}

		$elements = $updated_elements;

		return [
			'props' => $props,
			'groups' => [
				'items' => $groups,
				'order' => $group_order,
			],
		];
	}

	/**
	 * @return array|\WP_Error
	 */
	private function build_prop(
		string $override_key,
		$definition,
		array &$elements,
		array &$group_ids_by_label,
		array &$groups,
		array &$group_order,
		array &$override_keys_by_element_prop
	) {
		$common = $this->validate_common_definition( $override_key, $definition );
		if ( is_wp_error( $common ) ) {
			return $common;
		}
		[ 'target' => $target, 'prop_key' => $prop_key, 'label' => $label, 'group_label' => $group_label ] = $common;

		$element = &$this->find_element_ref( $elements, $target );

		if ( null === $element ) {
			return $this->invalid_definition( sprintf( '[%s] target "%s" was not found in the element tree.', $override_key, $target ) );
		}

		$element_id = (string) ( $element['id'] ?? $target );

		if ( isset( $override_keys_by_element_prop[ $element_id ][ $prop_key ] ) ) {
			return $this->invalid_definition(
				sprintf(
					'[%1$s] target "%2$s" and prop_key "%3$s" are duplicated by override key "%4$s".',
					$override_key,
					$target,
					$prop_key,
					$override_keys_by_element_prop[ $element_id ][ $prop_key ]
				)
			);
		}

		$override_keys_by_element_prop[ $element_id ][ $prop_key ] = $override_key;

		$widget_type = (string) ( $element['widgetType'] ?? '' );

		if ( Component_Instance::get_element_type() === $widget_type ) {
			return $this->build_nested_component_prop(
				$override_key,
				$prop_key,
				$label,
				$group_label,
				$element,
				$group_ids_by_label,
				$groups,
				$group_order
			);
		}

		return $this->build_raw_widget_prop(
			$override_key,
			$prop_key,
			$label,
			$group_label,
			$element,
			$group_ids_by_label,
			$groups,
			$group_order
		);
	}

	/**
	 * @return array{target: string, prop_key: string, label: string, group_label: string}|\WP_Error
	 */
	private function validate_common_definition( string $override_key, $definition ) {
		if ( ! is_array( $definition ) ) {
			return $this->invalid_definition( sprintf( '[%s] overridable_props entry must be an object.', $override_key ) );
		}

		if ( sanitize_key( $override_key ) !== $override_key ) {
			return $this->invalid_definition( sprintf( '[%s] override keys must be slugs: lowercase letters, digits, dashes and underscores only.', $override_key ) );
		}

		$target = $definition['target'] ?? null;
		$prop_key = $definition['prop_key'] ?? null;
		$label = $definition['label'] ?? null;
		$group_label = is_string( $definition['group'] ?? null ) && '' !== $definition['group']
			? $definition['group']
			: self::DEFAULT_GROUP_LABEL;

		if ( ! is_string( $target ) || '' === $target || ! is_string( $prop_key ) || '' === $prop_key || ! is_string( $label ) || '' === $label ) {
			return $this->invalid_definition( sprintf( '[%s] overridable_props entries require a non-empty target, prop_key, and label.', $override_key ) );
		}

		return [
			'target' => $target,
			'prop_key' => $prop_key,
			'label' => $label,
			'group_label' => $group_label,
		];
	}

	/**
	 * @return array|\WP_Error
	 */
	private function build_raw_widget_prop(
		string $override_key,
		string $prop_key,
		string $label,
		string $group_label,
		array &$element,
		array &$group_ids_by_label,
		array &$groups,
		array &$group_order
	) {
		$el_type = (string) ( $element['elType'] ?? '' );
		$widget_type = (string) ( $element['widgetType'] ?? '' );

		try {
			$prop_type = Parsing_Utils::get_prop_type( $el_type, $widget_type, $prop_key );
		} catch ( \Exception $e ) {
			return $this->invalid_definition( sprintf( '[%s] %s', $override_key, $e->getMessage() ) );
		}

		$origin_value = $element['settings'][ $prop_key ] ?? $prop_type->get_default();

		$element['settings'][ $prop_key ] = [
			'$$type' => Overridable_Prop_Type::get_key(),
			'value' => [
				'override_key' => $override_key,
				'origin_value' => $origin_value,
			],
		];

		$group_id = $this->resolve_group_id( $group_label, $group_ids_by_label, $groups, $group_order );
		$groups[ $group_id ]['props'][] = $override_key;

		return [
			'overrideKey' => $override_key,
			'label' => $label,
			'elementId' => (string) $element['id'],
			'elType' => $el_type,
			'widgetType' => $this->resolve_widgets_cache_key( $el_type, $widget_type ),
			'propKey' => $prop_key,
			'originValue' => $origin_value,
			'groupId' => $group_id,
		];
	}

	/**
	 * Expose-further path. `$prop_key` names the **inner component's** exposed override key,
	 * not a real setting key on the `<e-component>` element (which only has `component_instance`).
	 *
	 * @return array|\WP_Error
	 */
	private function build_nested_component_prop(
		string $override_key,
		string $inner_override_key,
		string $label,
		string $group_label,
		array &$element,
		array &$group_ids_by_label,
		array &$groups,
		array &$group_order
	) {
		$inner_component_id = $this->extract_inner_component_id( $element );

		if ( null === $inner_component_id ) {
			return new \WP_Error(
				'invalid_overridable_prop_definition',
				sprintf(
					'[%s] target "%s" is an <e-component> instance but has no valid component_instance settings. Set component_id on the instance (via element_config) before exposing an override through it.',
					$override_key,
					(string) ( $element['id'] ?? '' )
				)
			);
		}

		$inner_component = $this->get_repository()->get( $inner_component_id, false );

		if ( ! $inner_component ) {
			return $this->invalid_definition(
				sprintf( '[%s] inner component %d referenced by target "%s" was not found.', $override_key, $inner_component_id, (string) $element['id'] )
			);
		}

		$inner_props = $inner_component->get_overridable_props()->props;
		$inner_prop = $inner_props[ $inner_override_key ] ?? null;

		if ( ! $inner_prop ) {
			$available = empty( $inner_props ) ? '(none)' : implode( ', ', array_keys( $inner_props ) );
			return $this->invalid_definition(
				sprintf(
					'[%s] component %d has no exposed override "%s". Available: %s. Use one of these as prop_key when the target is an <e-component>, or expose the underlying raw widget on the inner component first.',
					$override_key,
					$inner_component_id,
					$inner_override_key,
					$available
				)
			);
		}

		$origin_prop_fields = $this->resolve_origin_prop_fields( $inner_prop );

		$inner_override_value = $this->take_existing_override_value( $element, $inner_override_key );

		$override_envelope = $this->build_override_envelope( $inner_override_key, $inner_component_id, $inner_override_value );

		$overridable_envelope = [
			'$$type' => Overridable_Prop_Type::get_key(),
			'value' => [
				'override_key' => $override_key,
				'origin_value' => $override_envelope,
			],
		];

		$this->upsert_component_instance_override( $element, $override_key, $overridable_envelope );

		$group_id = $this->resolve_group_id( $group_label, $group_ids_by_label, $groups, $group_order );
		$groups[ $group_id ]['props'][] = $override_key;

		return [
			'overrideKey' => $override_key,
			'label' => $label,
			'elementId' => (string) $element['id'],
			'elType' => 'widget',
			'widgetType' => Component_Instance::get_element_type(),
			'propKey' => $inner_override_key,
			'originValue' => $inner_override_value,
			'groupId' => $group_id,
			'originPropFields' => $origin_prop_fields,
		];
	}

	private function extract_inner_component_id( array $element ): ?int {
		$raw = $element['settings']['component_instance']['value']['component_id']['value'] ?? null;

		if ( ! is_numeric( $raw ) || (int) $raw <= 0 ) {
			return null;
		}

		return (int) $raw;
	}

	/**
	 * N-level chaining: if the inner overridable prop is itself a chained expose (already carries
	 * `origin_prop_fields`), thread those through so the outer entry points all the way at the raw
	 * widget's schema. Otherwise, derive the fields from the inner prop's own element metadata.
	 */
	private function resolve_origin_prop_fields( Component_Overridable_Prop $inner_prop ): array {
		if ( $inner_prop->origin_prop_fields ) {
			return [
				'elType' => (string) $inner_prop->origin_prop_fields['el_type'],
				'widgetType' => (string) $inner_prop->origin_prop_fields['widget_type'],
				'propKey' => (string) $inner_prop->origin_prop_fields['prop_key'],
				'elementId' => (string) $inner_prop->origin_prop_fields['element_id'],
			];
		}

		return [
			'elType' => (string) $inner_prop->el_type,
			'widgetType' => (string) $inner_prop->widget_type,
			'propKey' => (string) $inner_prop->prop_key,
			'elementId' => (string) $inner_prop->element_id,
		];
	}

	/**
	 * @param string     $inner_override_key   Override key exposed by the inner component.
	 * @param int        $inner_component_id   Component the inner override key belongs to.
	 * @param array|null $inner_override_value Per-instance value for the inner override, or null to inherit
	 *                                         the inner component's own origin value.
	 */
	private function build_override_envelope( string $inner_override_key, int $inner_component_id, ?array $inner_override_value ): array {
		return [
			'$$type' => Override_Prop_Type::get_key(),
			'value' => [
				'override_key' => $inner_override_key,
				'override_value' => $inner_override_value,
				'schema_source' => [
					'type' => Component_Override_Parser::get_override_type(),
					'id' => $inner_component_id,
				],
			],
		];
	}

	/**
	 * Both a literal `override` (placed via `element_config`) and the `overridable` envelope we are about to
	 * write address the nested instance by the same inner override key. Two entries for one key make the
	 * resolvers last-write-wins, silently discarding whichever came first, so the existing entry is consumed
	 * here: its value moves into the new envelope's `override_value` and the old entry is dropped.
	 *
	 * @return array|null The value carried by the removed entry, or null when there was none.
	 */
	private function take_existing_override_value( array &$element, string $inner_override_key ): ?array {
		$this->ensure_component_instance_shape( $element );

		$overrides_list = &$element['settings']['component_instance']['value']['overrides']['value'];
		$taken_value = null;

		foreach ( $overrides_list as $index => $existing ) {
			if ( $inner_override_key !== $this->resolve_inner_override_key( $existing ) ) {
				continue;
			}

			$taken_value = $this->resolve_inner_override_value( $existing );
			unset( $overrides_list[ $index ] );
		}

		$overrides_list = array_values( $overrides_list );

		return $taken_value;
	}

	/**
	 * An entry addresses the nested instance either directly (`override`) or through an already-exposed
	 * wrapper (`overridable` whose `origin_value` is the `override`).
	 */
	private function resolve_inner_override( $entry ): ?array {
		if ( ! is_array( $entry ) ) {
			return null;
		}

		if ( Overridable_Prop_Type::get_key() === ( $entry['$$type'] ?? null ) ) {
			$entry = $entry['value']['origin_value'] ?? null;
		}

		if ( ! is_array( $entry ) || Override_Prop_Type::get_key() !== ( $entry['$$type'] ?? null ) ) {
			return null;
		}

		return $entry;
	}

	private function resolve_inner_override_key( $entry ): ?string {
		$override = $this->resolve_inner_override( $entry );

		return isset( $override['value']['override_key'] ) ? (string) $override['value']['override_key'] : null;
	}

	private function resolve_inner_override_value( $entry ): ?array {
		$override = $this->resolve_inner_override( $entry );
		$value = $override['value']['override_value'] ?? null;

		return is_array( $value ) ? $value : null;
	}

	/**
	 * Replace-or-append semantics on the nested instance's `overrides` list, keyed by the outer
	 * override_key stored on the `Overridable_Prop_Type` envelope — mirrors how the editor's
	 * `getMatchingOverride`/`setInstanceValue` flow keys entries when the user re-edits an
	 * exposed-further prop.
	 */
	private function upsert_component_instance_override( array &$element, string $override_key, array $overridable_envelope ): void {
		$this->ensure_component_instance_shape( $element );

		$overrides_list = &$element['settings']['component_instance']['value']['overrides']['value'];

		foreach ( $overrides_list as $index => $existing ) {
			$is_overridable = ( $existing['$$type'] ?? null ) === Overridable_Prop_Type::get_key();
			$existing_key = $existing['value']['override_key'] ?? null;

			if ( $is_overridable && $existing_key === $override_key ) {
				$overrides_list[ $index ] = $overridable_envelope;
				return;
			}
		}

		$overrides_list[] = $overridable_envelope;
	}

	private function ensure_component_instance_shape( array &$element ): void {
		if ( ! isset( $element['settings']['component_instance']['value']['overrides'] ) ) {
			$element['settings']['component_instance']['value']['overrides'] = [
				'$$type' => Overrides_Prop_Type::get_key(),
				'value' => [],
			];
			return;
		}

		if ( ! isset( $element['settings']['component_instance']['value']['overrides']['value'] )
			|| ! is_array( $element['settings']['component_instance']['value']['overrides']['value'] ) ) {
			$element['settings']['component_instance']['value']['overrides']['value'] = [];
		}
	}

	private function get_repository(): Components_Repository {
		if ( ! $this->repository ) {
			$this->repository = new Components_Repository();
		}

		return $this->repository;
	}

	private function invalid_definition( string $message ): \WP_Error {
		return new \WP_Error( 'invalid_overridable_prop_definition', $message );
	}

	/**
	 * The editor's `getWidgetsCache()` is keyed by the atomic widget/element type name (e.g. `e-heading`,
	 * `e-flexbox`). Widgets carry it on `widgetType`; atomic elements carry it on `elType` and leave
	 * `widgetType` unset. `OverrideControl` looks the persisted `widgetType` up in the cache to find the
	 * origin prop type — it must be a real cache key, or the panel throws "Prop type not found".
	 */
	private function resolve_widgets_cache_key( string $el_type, string $widget_type ): string {
		return '' !== $widget_type ? $widget_type : $el_type;
	}

	private function resolve_group_id( string $label, array &$group_ids_by_label, array &$groups, array &$group_order ): string {
		if ( isset( $group_ids_by_label[ $label ] ) ) {
			return $group_ids_by_label[ $label ];
		}

		$group_id = $this->generate_group_id();

		$group_ids_by_label[ $label ] = $group_id;
		$groups[ $group_id ] = [
			'id' => $group_id,
			'label' => $label,
			'props' => [],
		];
		$group_order[] = $group_id;

		return $group_id;
	}

	/**
	 * Mirrors the editor's `generateUniqueId( 'group' )` format. Group ids must not be derived from the
	 * label: `@elementor/ui`'s `SortableGroup` treats the id "default" as "no group id" and hands the
	 * whole multi-group map back to the props panel, which then crashes on `items.map`.
	 */
	private function generate_group_id(): string {
		return sprintf(
			'%s-%d-%s',
			self::GROUP_ID_PREFIX,
			(int) round( microtime( true ) * 1000 ),
			strtolower( wp_generate_password( self::GROUP_ID_SUFFIX_LENGTH, false, false ) )
		);
	}

	/**
	 * Matches the persisted element id, or the configuration-id the element was compiled from —
	 * compiled trees get machine-generated ids, so the caller can only address them by the
	 * configuration-id it already used in `xml_structure`.
	 *
	 * @return array|null
	 */
	private function &find_element_ref( array &$elements, string $target ) {
		$not_found = null;

		foreach ( $elements as &$element ) {
			$configuration_id = $element['editor_settings']['title'] ?? null;

			if ( ( $element['id'] ?? null ) === $target || $configuration_id === $target ) {
				return $element;
			}

			if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
				$found = &$this->find_element_ref( $element['elements'], $target );

				if ( null !== $found ) {
					unset( $element );
					return $found;
				}
			}
		}
		unset( $element );

		return $not_found;
	}
}

```
