| @@ -89,8 +89,10 @@ | ||
| 89 | 89 | } |
| 90 | 90 | |
| 91 | 91 | $this->features[ $options['name'] ] = $experimental_data; |
| 92 | 92 | |
| 93 | + $this->resolve_registered_dependants( $options['name'], $experimental_data ); | |
| 94 | + | |
| 93 | 95 | if ( $experimental_data['mutable'] && is_admin() ) { |
| 94 | 96 | $feature_option_key = $this->get_feature_option_key( $options['name'] ); |
| 95 | 97 | |
| 96 | 98 | $on_state_change_callback = function( $old_state, $new_state ) use ( $experimental_data, $feature_option_key ) { |
| @@ -251,9 +253,9 @@ | ||
| 251 | 253 | * @access public |
| 252 | 254 | * |
| 253 | 255 | * @param string $feature_name Experiment feature name. |
| 254 | 256 | * @param bool $check_dependencies When true, also require dependency experiments to be active. |
| 255 | - * Missing or hidden dependencies are treated as active for compatibility. | |
| 257 | + * Missing dependencies always fail closed. Hidden dependencies are treated as satisfied. | |
| 256 | 258 | * |
| 257 | 259 | * @return bool |
| 258 | 260 | */ |
| 259 | 261 | public function is_feature_active( $feature_name, $check_dependencies = false ) { |
| @@ -262,17 +264,33 @@ | ||
| 262 | 264 | if ( ! $feature || self::STATE_ACTIVE !== $this->get_feature_actual_state( $feature ) ) { |
| 263 | 265 | return false; |
| 264 | 266 | } |
| 265 | 267 | |
| 266 | - if ( $check_dependencies && isset( $feature['dependencies'] ) && is_array( $feature['dependencies'] ) ) { | |
| 268 | + if ( isset( $feature['dependencies'] ) && is_array( $feature['dependencies'] ) ) { | |
| 267 | 269 | foreach ( $feature['dependencies'] as $dependency ) { |
| 268 | - if ( $dependency instanceof Non_Existing_Dependency ) { | |
| 270 | + $dependent_feature = $this->get_features( $dependency->get_name() ); | |
| 271 | + | |
| 272 | + // A dependency that Core still does not ship can never be verified as active. | |
| 273 | + // Non existing dependencies are re-resolved first to cover later registrations. | |
| 274 | + if ( $dependency instanceof Non_Existing_Dependency && ! $dependent_feature ) { | |
| 275 | + return false; | |
| 276 | + } | |
| 277 | + | |
| 278 | + if ( ! $dependent_feature ) { | |
| 279 | + // A Core experiment dependency that no longer resolves fails closed. | |
| 280 | + // Class dependencies are not experiment names and stay exempt. | |
| 281 | + if ( $dependency instanceof Wrap_Core_Dependency ) { | |
| 282 | + return false; | |
| 283 | + } | |
| 284 | + | |
| 269 | 285 | continue; |
| 270 | 286 | } |
| 271 | 287 | |
| 272 | - $dependent_feature = $this->get_features( $dependency->get_name() ); | |
| 288 | + if ( ! $check_dependencies ) { | |
| 289 | + continue; | |
| 290 | + } | |
| 273 | 291 | |
| 274 | - if ( $this->is_removed_or_hidden_dependency( $dependent_feature ) ) { | |
| 292 | + if ( ! empty( $dependent_feature[ static::TYPE_HIDDEN ] ) ) { | |
| 275 | 293 | continue; |
| 276 | 294 | } |
| 277 | 295 | |
| 278 | 296 | $feature_state = self::STATE_ACTIVE === $this->get_feature_actual_state( $dependent_feature ); |
| @@ -1018,8 +1036,31 @@ | ||
| 1018 | 1036 | return true; |
| 1019 | 1037 | } |
| 1020 | 1038 | |
| 1021 | 1039 | return ! empty( $dependency_feature[ static::TYPE_HIDDEN ] ); |
| 1040 | + } | |
| 1041 | + | |
| 1042 | + /** | |
| 1043 | + * Re-resolve non existing dependency placeholders of dependants that were | |
| 1044 | + * registered before this feature existed. | |
| 1045 | + * | |
| 1046 | + * @param string $feature_name Newly registered feature name. | |
| 1047 | + * @param array $feature_data Newly registered feature data. | |
| 1048 | + */ | |
| 1049 | + private function resolve_registered_dependants( string $feature_name, array $feature_data ): void { | |
| 1050 | + foreach ( $this->features as $name => $registered_feature ) { | |
| 1051 | + if ( $name === $feature_name || empty( $registered_feature['dependencies'] ) || ! is_array( $registered_feature['dependencies'] ) ) { | |
| 1052 | + continue; | |
| 1053 | + } | |
| 1054 | + | |
| 1055 | + foreach ( $registered_feature['dependencies'] as $key => $dependency ) { | |
| 1056 | + if ( ! ( $dependency instanceof Non_Existing_Dependency ) || $dependency->get_name() !== $feature_name ) { | |
| 1057 | + continue; | |
| 1058 | + } | |
| 1059 | + | |
| 1060 | + $this->features[ $name ]['dependencies'][ $key ] = $this->create_dependency_class( $feature_name, $feature_data ); | |
| 1061 | + } | |
| 1062 | + } | |
| 1022 | 1063 | } |
| 1023 | 1064 | |
| 1024 | 1065 | private function warn_removed_or_hidden_dependency( string $message ): void { |
| 1025 | 1066 | if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { |