From c5c7a9fc788615868b29efaf45a6259bc33b81c5 Mon Sep 17 00:00:00 2001 From: selul Date: Tue, 1 Sep 2026 15:44:32 +0300 Subject: [PATCH 1/3] fix: coerce object-shaped page profiles before viewport lookup Object-cache backends that JSON-decode without associative arrays store profiler payloads as stdClass, which fatals on ['af'] access during frontend lazyload. Normalize storage reads and guard device lookups. Co-authored-by: Cursor --- inc/v2/BgOptimizer/Lazyload.php | 18 +- inc/v2/PageProfiler/Profile.php | 65 ++- inc/v2/PageProfiler/Storage/Base.php | 34 +- inc/v2/PageProfiler/Storage/ObjectCache.php | 4 +- inc/v2/PageProfiler/Storage/Transients.php | 4 +- tests/test-page-profiler-shape.php | 441 ++++++++++++++++++++ 6 files changed, 547 insertions(+), 19 deletions(-) create mode 100644 tests/test-page-profiler-shape.php diff --git a/inc/v2/BgOptimizer/Lazyload.php b/inc/v2/BgOptimizer/Lazyload.php index a95dd154c..225f643ad 100644 --- a/inc/v2/BgOptimizer/Lazyload.php +++ b/inc/v2/BgOptimizer/Lazyload.php @@ -3,6 +3,7 @@ namespace OptimoleWP\BgOptimizer; use OptimoleWP\PageProfiler\Profile; +use OptimoleWP\PageProfiler\Storage\Base as ProfilerStorage; use OptimoleWP\Preload\Links; use Optml_Lazyload_Replacer; @@ -34,8 +35,21 @@ public static function get_personalized_css( $data ) { $css_selectors = []; $preload_urls = []; foreach ( Profile::get_active_devices() as $device ) { - $personalized_selectors = $data[ $device ]['bg'] ?? []; - $lcp_data = $data[ $device ]['lcp'] ?? []; + $device_data = $data[ $device ] ?? []; + if ( ! is_array( $device_data ) ) { + $device_data = ProfilerStorage::normalize_value( $device_data ); + $device_data = is_array( $device_data ) ? $device_data : []; + } + $personalized_selectors = $device_data['bg'] ?? []; + if ( ! is_array( $personalized_selectors ) ) { + $personalized_selectors = ProfilerStorage::normalize_value( $personalized_selectors ); + $personalized_selectors = is_array( $personalized_selectors ) ? $personalized_selectors : []; + } + $lcp_data = $device_data['lcp'] ?? []; + if ( ! is_array( $lcp_data ) ) { + $lcp_data = ProfilerStorage::normalize_value( $lcp_data ); + $lcp_data = is_array( $lcp_data ) ? $lcp_data : []; + } if ( OPTML_DEBUG ) { do_action( 'optml_log', 'personalized_selectors: ' . $device . ' ' . print_r( $personalized_selectors, true ) ); do_action( 'optml_log', 'LCP data: ' . $device . ' ' . print_r( $lcp_data, true ) ); diff --git a/inc/v2/PageProfiler/Profile.php b/inc/v2/PageProfiler/Profile.php index 31fa3634d..da13e0c18 100644 --- a/inc/v2/PageProfiler/Profile.php +++ b/inc/v2/PageProfiler/Profile.php @@ -226,7 +226,9 @@ public function store( string $id, int $device_type, array $above_fold_images, $ * @return array{w: int, h: int}|array{} The missing dimensions. */ public function get_missing_dimensions( int $image_id ): array { - return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['m'][ $image_id ] ?? []; + $dimensions = self::get_device_member( self::DEVICE_TYPE_GLOBAL, 'm' )[ $image_id ] ?? []; + + return is_array( $dimensions ) ? $dimensions : []; } /** @@ -237,7 +239,9 @@ public function get_missing_dimensions( int $image_id ): array { * @return array|array{} The missing srcsets. */ public function get_missing_srcsets( int $image_id ): array { - return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['s'][ $image_id ] ?? []; + $srcsets = self::get_device_member( self::DEVICE_TYPE_GLOBAL, 's' )[ $image_id ] ?? []; + + return is_array( $srcsets ) ? $srcsets : []; } /** @@ -248,7 +252,7 @@ public function get_missing_srcsets( int $image_id ): array { * @return bool The crop status. */ public function get_crop_status( int $image_id ): bool { - return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['c'][ $image_id ] ?? false; + return (bool) ( self::get_device_member( self::DEVICE_TYPE_GLOBAL, 'c' )[ $image_id ] ?? false ); } /** * Checks if profile data exists for all active device types. @@ -341,9 +345,9 @@ public function set_current_profile_data(): array { return self::$current_profile_data; } self::$current_profile_data = [ - self::DEVICE_TYPE_MOBILE => $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_MOBILE ), - self::DEVICE_TYPE_DESKTOP => $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_DESKTOP ), - self::DEVICE_TYPE_GLOBAL => $this->storage->get( self::get_current_profile_id() ), + self::DEVICE_TYPE_MOBILE => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_MOBILE ) ), + self::DEVICE_TYPE_DESKTOP => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_DESKTOP ) ), + self::DEVICE_TYPE_GLOBAL => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() ) ), ]; if ( OPTML_DEBUG ) { do_action( 'optml_log', 'Profile data: ' . print_r( self::$current_profile_data, true ) . ' for id: ' . self::get_current_profile_id() ); @@ -361,12 +365,14 @@ public function set_current_profile_data(): array { */ public function is_in_all_viewports( int $image_id ): bool { foreach ( self::get_active_devices() as $device ) { + $device_data = self::get_device_data( $device ); // If the data is not available for the device, return false. - if ( empty( self::$current_profile_data[ $device ] ?? null ) ) { + if ( empty( $device_data ) ) { return false; } // If the image is not in the viewport of the device, return false. - if ( ! ( self::$current_profile_data[ $device ]['af'][ $image_id ] ?? false ) ) { + $above_fold = self::get_device_member( $device, 'af' ); + if ( empty( $above_fold[ $image_id ] ) ) { return false; } } @@ -384,7 +390,8 @@ public function is_in_all_viewports( int $image_id ): bool { */ public function is_lcp_image_in_all_viewports( int $image_id ): bool { foreach ( self::get_active_devices() as $device ) { - if ( ( ( self::$current_profile_data[ $device ]['lcp']['type'] ?? '' ) === 'img' ) && ( self::$current_profile_data[ $device ]['lcp']['imageId'] === $image_id ) ) { + $lcp = self::get_device_member( $device, 'lcp' ); + if ( ( $lcp['type'] ?? '' ) === 'img' && ( $lcp['imageId'] ?? null ) === $image_id ) { return true; } } @@ -401,7 +408,8 @@ public function is_lcp_image_in_all_viewports( int $image_id ): bool { */ public function is_in_any_viewport( $image_id ) { foreach ( self::get_active_devices() as $device ) { - if ( self::$current_profile_data[ $device ]['af'][ $image_id ] ?? false ) { + $above_fold = self::get_device_member( $device, 'af' ); + if ( ! empty( $above_fold[ $image_id ] ) ) { return $device; } } @@ -419,7 +427,7 @@ public function is_in_any_viewport( $image_id ) { public function get_profile_data( $id ) { $profile_data = []; foreach ( self::get_active_devices() as $device ) { - $profile_data[ $device ] = $this->storage->get( $id . '_' . $device ); + $profile_data[ $device ] = Storage\Base::normalize_value( $this->storage->get( $id . '_' . $device ) ); } return $profile_data; @@ -454,7 +462,7 @@ public static function get_active_devices(): array { */ public function is_data_available(): bool { foreach ( self::get_active_devices() as $device ) { - if ( empty( self::$current_profile_data[ $device ] ) ) { + if ( empty( self::get_device_data( $device ) ) ) { return false; } } @@ -462,6 +470,39 @@ public function is_data_available(): bool { return true; } + /** + * Get array-shaped profile data for a device. + * + * Object-shaped cache values are coerced to arrays so viewport lookups never fatal. + * + * @param int $device Device type constant. + * @return array + */ + private static function get_device_data( int $device ): array { + $data = self::$current_profile_data[ $device ] ?? []; + if ( ! is_array( $data ) ) { + $data = Storage\Base::normalize_value( $data ); + } + + return is_array( $data ) ? $data : []; + } + + /** + * Get an array member from a device profile. + * + * @param int $device Device type constant. + * @param string $key Member key (af, bg, lcp, m, s, c). + * @return array + */ + private static function get_device_member( int $device, string $key ): array { + $member = self::get_device_data( $device )[ $key ] ?? []; + if ( ! is_array( $member ) ) { + $member = Storage\Base::normalize_value( $member ); + } + + return is_array( $member ) ? $member : []; + } + /** * Generate HTML comment with profile data and performance metrics. * diff --git a/inc/v2/PageProfiler/Storage/Base.php b/inc/v2/PageProfiler/Storage/Base.php index 750684ab6..82011a12d 100644 --- a/inc/v2/PageProfiler/Storage/Base.php +++ b/inc/v2/PageProfiler/Storage/Base.php @@ -22,10 +22,42 @@ abstract public function store( string $key, array $data ); * Retrieve data by key. * * @param string $key The unique identifier for the data to retrieve. - * @return array|false The stored data or null if not found. + * @return array|false The stored data or false if not found. */ abstract public function get( string $key ); + /** + * Coerce a stored profiler payload to an array. + * + * Object-cache backends that JSON-decode without associative arrays return stdClass. + * Nested objects (e.g. `af`, `bg`, `lcp`) are converted recursively. + * + * @param mixed $value Raw storage value. + * @return array|false + */ + public static function normalize_value( $value ) { + if ( false === $value || null === $value ) { + return false; + } + + if ( is_object( $value ) ) { + $value = get_object_vars( $value ); + } + + if ( ! is_array( $value ) ) { + return false; + } + + foreach ( $value as $key => $item ) { + if ( is_object( $item ) || is_array( $item ) ) { + $normalized_item = self::normalize_value( $item ); + $value[ $key ] = ( false !== $normalized_item ) ? $normalized_item : []; + } + } + + return $value; + } + /** * Delete data by key. * diff --git a/inc/v2/PageProfiler/Storage/ObjectCache.php b/inc/v2/PageProfiler/Storage/ObjectCache.php index 23075c4c2..9170a1bc6 100644 --- a/inc/v2/PageProfiler/Storage/ObjectCache.php +++ b/inc/v2/PageProfiler/Storage/ObjectCache.php @@ -55,10 +55,10 @@ public function store( string $key, array $data ) { * Retrieve data from the object cache. * * @param string $key The unique identifier for the data to retrieve. - * @return array|false The stored data or false if not found. + * @return array|false The stored data or false if not found. */ public function get( string $key ) { - return wp_cache_get( $key, self::GROUP ); + return self::normalize_value( wp_cache_get( $key, self::GROUP ) ); } /** diff --git a/inc/v2/PageProfiler/Storage/Transients.php b/inc/v2/PageProfiler/Storage/Transients.php index f1d139eb8..13c56a27b 100644 --- a/inc/v2/PageProfiler/Storage/Transients.php +++ b/inc/v2/PageProfiler/Storage/Transients.php @@ -65,10 +65,10 @@ public function store( string $key, array $data ) { * Retrieves data from a transient. * * @param string $key The key to retrieve data for. - * @return mixed The stored data or false if the transient doesn't exist or has expired. + * @return array|false The stored data or false if the transient doesn't exist or has expired. */ public function get( string $key ) { - return get_transient( $this->get_key( $key ) ); + return self::normalize_value( get_transient( $this->get_key( $key ) ) ); } /** diff --git a/tests/test-page-profiler-shape.php b/tests/test-page-profiler-shape.php new file mode 100644 index 000000000..ef1035bc1 --- /dev/null +++ b/tests/test-page-profiler-shape.php @@ -0,0 +1,441 @@ +update( + 'service_data', + [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com' ], + ] + ); + $settings->update( 'lazyload', 'enabled' ); + $settings->update( 'lazyload_type', 'viewport' ); + Optml_Url_Replacer::instance()->init(); + Optml_Tag_Replacer::instance()->init(); + Optml_Lazyload_Replacer::instance()->init(); + Optml_Manager::instance()->init(); + Profile::reset_current_profile(); + } + + /** + * Clean up after each test. + */ + public function tearDown(): void { + parent::tearDown(); + Profile::reset_current_profile(); + wp_cache_flush(); + } + + /** + * Build a valid device profile payload as an array. + * + * @param array $overrides Optional overrides. + * @return array + */ + private function arrayProfile( $overrides = [] ) { + return array_merge( + [ + 'af' => [ self::ABOVE_FOLD_IMAGE_ID => true ], + 'bg' => [ + '[style*="background-image:url("]' => [ + '.hero' => [ 'https://example.com/bg.jpg' ], + ], + ], + 'lcp' => [ + 'type' => 'img', + 'imageId' => self::ABOVE_FOLD_IMAGE_ID, + ], + ], + $overrides + ); + } + + /** + * JSON-decode a payload without associative arrays (Redis/JSON object-cache shape). + * + * @param array $payload Array payload. + * @return object + */ + private function objectProfile( $payload = null ) { + if ( null === $payload ) { + $payload = $this->arrayProfile(); + } + return json_decode( wp_json_encode( $payload ) ); + } + + /** + * Inject current profile data, including object-shaped values. + * + * @param mixed $mobile Mobile payload. + * @param mixed $desktop Desktop payload. + * @param mixed $global Global payload. + */ + private function injectCurrentProfileData( $mobile, $desktop, $global = false ) { + $reflection = new ReflectionClass( Profile::class ); + $property = $reflection->getProperty( 'current_profile_data' ); + $property->setAccessible( true ); + $property->setValue( + null, + [ + Profile::DEVICE_TYPE_MOBILE => $mobile, + Profile::DEVICE_TYPE_DESKTOP => $desktop, + Profile::DEVICE_TYPE_GLOBAL => $global, + ] + ); + } + + /** + * @dataProvider normalizeValueProvider + * @param mixed $input Raw value. + * @param mixed $expected Expected normalize result. + */ + public function test_normalize_value( $input, $expected ) { + $this->assertSame( $expected, ProfilerStorage::normalize_value( $input ) ); + } + + /** + * Data provider for normalize_value. + * + * @return array + */ + public function normalizeValueProvider() { + return [ + 'false' => [ false, false ], + 'null' => [ null, false ], + 'string' => [ 'corrupt', false ], + 'integer' => [ 0, false ], + 'empty_array' => [ [], [] ], + 'flat_array' => [ + [ 'af' => [ 1 => true ] ], + [ 'af' => [ 1 => true ] ], + ], + ]; + } + + /** + * Nested stdClass trees become associative arrays, including numeric keys. + */ + public function test_normalize_value_converts_nested_stdclass() { + $object = $this->objectProfile(); + $normalized = ProfilerStorage::normalize_value( $object ); + + $this->assertIsArray( $normalized ); + $this->assertIsArray( $normalized['af'] ); + $this->assertTrue( $normalized['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); + $this->assertIsArray( $normalized['bg'] ); + $this->assertIsArray( $normalized['lcp'] ); + $this->assertSame( 'img', $normalized['lcp']['type'] ); + $this->assertSame( self::ABOVE_FOLD_IMAGE_ID, $normalized['lcp']['imageId'] ); + } + + /** + * Top-level array with object-shaped members is still coerced. + */ + public function test_normalize_value_converts_object_members_inside_array() { + $payload = [ + 'af' => (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ], + 'bg' => (object) [], + 'lcp' => (object) [ 'type' => 'img', 'imageId' => self::ABOVE_FOLD_IMAGE_ID ], + ]; + $normalized = ProfilerStorage::normalize_value( $payload ); + + $this->assertIsArray( $normalized['af'] ); + $this->assertTrue( ! empty( $normalized['af'][ self::ABOVE_FOLD_IMAGE_ID ] ) ); + $this->assertIsArray( $normalized['lcp'] ); + $this->assertSame( 'img', $normalized['lcp']['type'] ); + } + + /** + * Object cache get() returns arrays when the backend stored stdClass. + */ + public function test_object_cache_get_normalizes_stdclass() { + $storage = new ObjectCache(); + $key = 'shape_oc_' . wp_generate_uuid4(); + wp_cache_set( $key, $this->objectProfile(), ObjectCache::GROUP ); + + $retrieved = $storage->get( $key ); + $this->assertIsArray( $retrieved ); + $this->assertTrue( $retrieved['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); + } + + /** + * Object cache miss stays false. + */ + public function test_object_cache_get_miss_returns_false() { + $storage = new ObjectCache(); + $this->assertFalse( $storage->get( 'missing_profiler_key_' . wp_generate_uuid4() ) ); + } + + /** + * Object cache still returns stored arrays unchanged. + */ + public function test_object_cache_get_preserves_arrays() { + $storage = new ObjectCache(); + $key = 'shape_oc_array_' . wp_generate_uuid4(); + $payload = $this->arrayProfile(); + $storage->store( $key, $payload ); + + $this->assertSame( $payload, $storage->get( $key ) ); + } + + /** + * Corrupt object-cache values are treated as a miss. + */ + public function test_object_cache_get_rejects_scalars() { + $storage = new ObjectCache(); + $key = 'shape_oc_bad_' . wp_generate_uuid4(); + wp_cache_set( $key, 'not-a-profile', ObjectCache::GROUP ); + + $this->assertFalse( $storage->get( $key ) ); + } + + /** + * Transient get() returns arrays when the stored value is stdClass. + */ + public function test_transients_get_normalizes_stdclass() { + $storage = new Transients(); + $key = 'shape_tr_' . wp_generate_uuid4(); + set_transient( Transients::PREFIX . $key, $this->objectProfile(), HOUR_IN_SECONDS ); + + $retrieved = $storage->get( $key ); + $this->assertIsArray( $retrieved ); + $this->assertTrue( $retrieved['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); + } + + /** + * Transient miss stays false. + */ + public function test_transients_get_miss_returns_false() { + $storage = new Transients(); + $this->assertFalse( $storage->get( 'missing_transient_' . wp_generate_uuid4() ) ); + } + + /** + * The reported crash: indexing object-shaped device data as an array. + */ + public function test_is_in_all_viewports_does_not_fatal_on_stdclass() { + $this->injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertTrue( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertFalse( $profiler->is_in_all_viewports( self::OTHER_IMAGE_ID ) ); + } + + /** + * Object-shaped above-fold map only (array wrapper, object `af`). + */ + public function test_is_in_all_viewports_with_object_shaped_af_member() { + $mobile = $this->arrayProfile(); + $desktop = $this->arrayProfile(); + $mobile['af'] = (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ]; + $desktop['af'] = (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ]; + $this->injectCurrentProfileData( $mobile, $desktop ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertTrue( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + } + + /** + * Missing or empty object-shaped device data is treated as unavailable. + */ + public function test_is_in_all_viewports_empty_object_is_unavailable() { + $this->injectCurrentProfileData( new stdClass(), $this->objectProfile() ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertFalse( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertFalse( $profiler->is_data_available() ); + } + + /** + * is_in_any_viewport must not fatal on object-shaped data. + */ + public function test_is_in_any_viewport_does_not_fatal_on_stdclass() { + $this->injectCurrentProfileData( $this->objectProfile(), false ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertSame( Profile::DEVICE_TYPE_MOBILE, $profiler->is_in_any_viewport( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertFalse( $profiler->is_in_any_viewport( self::OTHER_IMAGE_ID ) ); + } + + /** + * LCP lookup must not fatal on object-shaped `lcp`. + */ + public function test_is_lcp_image_in_all_viewports_does_not_fatal_on_stdclass() { + $this->injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertTrue( $profiler->is_lcp_image_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertFalse( $profiler->is_lcp_image_in_all_viewports( self::OTHER_IMAGE_ID ) ); + } + + /** + * Global missing-dimension lookups must not fatal on object-shaped global data. + */ + public function test_global_lookups_do_not_fatal_on_stdclass() { + $global = (object) [ + 'm' => (object) [ + (string) self::ABOVE_FOLD_IMAGE_ID => (object) [ 'w' => 100, 'h' => 80 ], + ], + 's' => (object) [ + (string) self::ABOVE_FOLD_IMAGE_ID => (object) [ + '200' => (object) [ + 'w' => 200, + 'h' => 160, + 'd' => 1, + 's' => 'https://example.com/i.jpg', + 'b' => 1, + ], + ], + ], + 'c' => (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ], + ]; + $this->injectCurrentProfileData( false, false, $global ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertSame( [ 'w' => 100, 'h' => 80 ], $profiler->get_missing_dimensions( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertSame( [], $profiler->get_missing_dimensions( self::OTHER_IMAGE_ID ) ); + $this->assertNotEmpty( $profiler->get_missing_srcsets( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertTrue( $profiler->get_crop_status( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertFalse( $profiler->get_crop_status( self::OTHER_IMAGE_ID ) ); + } + + /** + * Loading current profile data from object-shaped transients yields arrays. + */ + public function test_set_current_profile_data_normalizes_transients() { + $profile_id = 'shape_load_' . wp_generate_uuid4(); + foreach ( Profile::get_active_devices() as $device ) { + set_transient( + Transients::PREFIX . $profile_id . '_' . $device, + $this->objectProfile(), + HOUR_IN_SECONDS + ); + } + + Profile::set_current_profile_id( $profile_id ); + $data = Optml_Manager::instance()->page_profiler->set_current_profile_data(); + + $this->assertIsArray( $data[ Profile::DEVICE_TYPE_MOBILE ] ); + $this->assertIsArray( $data[ Profile::DEVICE_TYPE_DESKTOP ] ); + $this->assertTrue( $data[ Profile::DEVICE_TYPE_MOBILE ]['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); + $this->assertTrue( Optml_Manager::instance()->page_profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + } + + /** + * get_profile_data() also normalizes object-shaped storage. + */ + public function test_get_profile_data_normalizes_stdclass() { + $profile_id = 'shape_get_' . wp_generate_uuid4(); + set_transient( + Transients::PREFIX . $profile_id . '_' . Profile::DEVICE_TYPE_DESKTOP, + $this->objectProfile(), + HOUR_IN_SECONDS + ); + + $data = Optml_Manager::instance()->page_profiler->get_profile_data( $profile_id ); + $this->assertIsArray( $data[ Profile::DEVICE_TYPE_DESKTOP ] ); + $this->assertTrue( $data[ Profile::DEVICE_TYPE_DESKTOP ]['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); + } + + /** + * exists() is true after object-shaped data is normalized, false for scalars. + */ + public function test_exists_with_object_shaped_and_corrupt_storage() { + $profiler = Optml_Manager::instance()->page_profiler; + $good_id = 'shape_exists_good_' . wp_generate_uuid4(); + $bad_id = 'shape_exists_bad_' . wp_generate_uuid4(); + + set_transient( + Transients::PREFIX . $good_id . '_' . Profile::DEVICE_TYPE_DESKTOP, + $this->objectProfile(), + HOUR_IN_SECONDS + ); + set_transient( + Transients::PREFIX . $bad_id . '_' . Profile::DEVICE_TYPE_DESKTOP, + 'nope', + HOUR_IN_SECONDS + ); + + $this->assertTrue( $profiler->exists( $good_id, Profile::DEVICE_TYPE_DESKTOP ) ); + $this->assertFalse( $profiler->exists( $bad_id, Profile::DEVICE_TYPE_DESKTOP ) ); + $this->assertFalse( $profiler->exists_all( $good_id ) ); + } + + /** + * Personalized background CSS must not fatal on object-shaped profile data. + */ + public function test_personalized_css_does_not_fatal_on_stdclass() { + $data = [ + Profile::DEVICE_TYPE_MOBILE => $this->objectProfile(), + Profile::DEVICE_TYPE_DESKTOP => $this->objectProfile(), + ]; + + $css = Lazyload::get_personalized_css( $data ); + $this->assertIsString( $css ); + } + + /** + * Frontend HTML replacement must not fatal when transients hold stdClass profiles. + */ + public function test_replace_content_does_not_fatal_on_object_shaped_profile() { + $profile_id = 'shape_html_' . wp_generate_uuid4(); + add_filter( + 'optml_page_profile_id', + function () use ( $profile_id ) { + return $profile_id; + } + ); + + foreach ( Profile::get_active_devices() as $device ) { + set_transient( + Transients::PREFIX . $profile_id . '_' . $device, + $this->objectProfile(), + HOUR_IN_SECONDS + ); + } + + $html = Optml_Manager::instance()->replace_content( Test_Lazyload_Viewport::get_sample_html() ); + $this->assertNotEmpty( $html ); + $this->assertStringContainsString( 'injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); + Profile::set_current_profile_id( 'shape_can_lazy' ); + + $replacer = Optml_Lazyload_Replacer::instance(); + $url = 'https://example.com/test-image.jpg'; + $tag = 'test'; + + $this->assertIsBool( $replacer->can_lazyload_for( $url, $tag ) ); + } +} From b757f3069e717f636f23f9d3214aad49189a9b12 Mon Sep 17 00:00:00 2001 From: selul Date: Tue, 1 Sep 2026 15:51:58 +0300 Subject: [PATCH 2/3] fix: drop stale PHPStan baseline entries for typed storage get() Co-authored-by: Cursor --- phpstan-baseline.neon | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3141d6947..ba4bee0c5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3072,12 +3072,6 @@ parameters: count: 1 path: inc/v2/PageProfiler/Storage/Base.php - - - message: '#^Method OptimoleWP\\PageProfiler\\Storage\\Base\:\:get\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: inc/v2/PageProfiler/Storage/Base.php - - message: '#^Method OptimoleWP\\PageProfiler\\Storage\\Base\:\:store\(\) has no return type specified\.$#' identifier: missingType.return @@ -3090,12 +3084,6 @@ parameters: count: 1 path: inc/v2/PageProfiler/Storage/Base.php - - - message: '#^Method OptimoleWP\\PageProfiler\\Storage\\ObjectCache\:\:get\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: inc/v2/PageProfiler/Storage/ObjectCache.php - - message: '#^Method OptimoleWP\\PageProfiler\\Storage\\ObjectCache\:\:store\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue From 801b3519ccc0e405a7db524c6bec1c37743b8453 Mon Sep 17 00:00:00 2001 From: selul Date: Tue, 1 Sep 2026 16:00:33 +0300 Subject: [PATCH 3/3] refactor: keep object-shaped profile fix at the storage boundary Drop per-lookup Profile/Lazyload guards now that storage get() normalizes stdClass. Keep the LCP imageId null coalesce. Seed tests through transients instead of reflection. Co-authored-by: Cursor --- inc/v2/BgOptimizer/Lazyload.php | 18 +---- inc/v2/PageProfiler/Profile.php | 65 ++++------------- tests/test-page-profiler-shape.php | 112 ++++++++++++++--------------- 3 files changed, 68 insertions(+), 127 deletions(-) diff --git a/inc/v2/BgOptimizer/Lazyload.php b/inc/v2/BgOptimizer/Lazyload.php index 225f643ad..a95dd154c 100644 --- a/inc/v2/BgOptimizer/Lazyload.php +++ b/inc/v2/BgOptimizer/Lazyload.php @@ -3,7 +3,6 @@ namespace OptimoleWP\BgOptimizer; use OptimoleWP\PageProfiler\Profile; -use OptimoleWP\PageProfiler\Storage\Base as ProfilerStorage; use OptimoleWP\Preload\Links; use Optml_Lazyload_Replacer; @@ -35,21 +34,8 @@ public static function get_personalized_css( $data ) { $css_selectors = []; $preload_urls = []; foreach ( Profile::get_active_devices() as $device ) { - $device_data = $data[ $device ] ?? []; - if ( ! is_array( $device_data ) ) { - $device_data = ProfilerStorage::normalize_value( $device_data ); - $device_data = is_array( $device_data ) ? $device_data : []; - } - $personalized_selectors = $device_data['bg'] ?? []; - if ( ! is_array( $personalized_selectors ) ) { - $personalized_selectors = ProfilerStorage::normalize_value( $personalized_selectors ); - $personalized_selectors = is_array( $personalized_selectors ) ? $personalized_selectors : []; - } - $lcp_data = $device_data['lcp'] ?? []; - if ( ! is_array( $lcp_data ) ) { - $lcp_data = ProfilerStorage::normalize_value( $lcp_data ); - $lcp_data = is_array( $lcp_data ) ? $lcp_data : []; - } + $personalized_selectors = $data[ $device ]['bg'] ?? []; + $lcp_data = $data[ $device ]['lcp'] ?? []; if ( OPTML_DEBUG ) { do_action( 'optml_log', 'personalized_selectors: ' . $device . ' ' . print_r( $personalized_selectors, true ) ); do_action( 'optml_log', 'LCP data: ' . $device . ' ' . print_r( $lcp_data, true ) ); diff --git a/inc/v2/PageProfiler/Profile.php b/inc/v2/PageProfiler/Profile.php index da13e0c18..5cd8fa3e8 100644 --- a/inc/v2/PageProfiler/Profile.php +++ b/inc/v2/PageProfiler/Profile.php @@ -226,9 +226,7 @@ public function store( string $id, int $device_type, array $above_fold_images, $ * @return array{w: int, h: int}|array{} The missing dimensions. */ public function get_missing_dimensions( int $image_id ): array { - $dimensions = self::get_device_member( self::DEVICE_TYPE_GLOBAL, 'm' )[ $image_id ] ?? []; - - return is_array( $dimensions ) ? $dimensions : []; + return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['m'][ $image_id ] ?? []; } /** @@ -239,9 +237,7 @@ public function get_missing_dimensions( int $image_id ): array { * @return array|array{} The missing srcsets. */ public function get_missing_srcsets( int $image_id ): array { - $srcsets = self::get_device_member( self::DEVICE_TYPE_GLOBAL, 's' )[ $image_id ] ?? []; - - return is_array( $srcsets ) ? $srcsets : []; + return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['s'][ $image_id ] ?? []; } /** @@ -252,7 +248,7 @@ public function get_missing_srcsets( int $image_id ): array { * @return bool The crop status. */ public function get_crop_status( int $image_id ): bool { - return (bool) ( self::get_device_member( self::DEVICE_TYPE_GLOBAL, 'c' )[ $image_id ] ?? false ); + return self::$current_profile_data[ self::DEVICE_TYPE_GLOBAL ]['c'][ $image_id ] ?? false; } /** * Checks if profile data exists for all active device types. @@ -345,9 +341,9 @@ public function set_current_profile_data(): array { return self::$current_profile_data; } self::$current_profile_data = [ - self::DEVICE_TYPE_MOBILE => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_MOBILE ) ), - self::DEVICE_TYPE_DESKTOP => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_DESKTOP ) ), - self::DEVICE_TYPE_GLOBAL => Storage\Base::normalize_value( $this->storage->get( self::get_current_profile_id() ) ), + self::DEVICE_TYPE_MOBILE => $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_MOBILE ), + self::DEVICE_TYPE_DESKTOP => $this->storage->get( self::get_current_profile_id() . '_' . self::DEVICE_TYPE_DESKTOP ), + self::DEVICE_TYPE_GLOBAL => $this->storage->get( self::get_current_profile_id() ), ]; if ( OPTML_DEBUG ) { do_action( 'optml_log', 'Profile data: ' . print_r( self::$current_profile_data, true ) . ' for id: ' . self::get_current_profile_id() ); @@ -365,14 +361,12 @@ public function set_current_profile_data(): array { */ public function is_in_all_viewports( int $image_id ): bool { foreach ( self::get_active_devices() as $device ) { - $device_data = self::get_device_data( $device ); // If the data is not available for the device, return false. - if ( empty( $device_data ) ) { + if ( empty( self::$current_profile_data[ $device ] ?? null ) ) { return false; } // If the image is not in the viewport of the device, return false. - $above_fold = self::get_device_member( $device, 'af' ); - if ( empty( $above_fold[ $image_id ] ) ) { + if ( ! ( self::$current_profile_data[ $device ]['af'][ $image_id ] ?? false ) ) { return false; } } @@ -390,8 +384,7 @@ public function is_in_all_viewports( int $image_id ): bool { */ public function is_lcp_image_in_all_viewports( int $image_id ): bool { foreach ( self::get_active_devices() as $device ) { - $lcp = self::get_device_member( $device, 'lcp' ); - if ( ( $lcp['type'] ?? '' ) === 'img' && ( $lcp['imageId'] ?? null ) === $image_id ) { + if ( ( ( self::$current_profile_data[ $device ]['lcp']['type'] ?? '' ) === 'img' ) && ( ( self::$current_profile_data[ $device ]['lcp']['imageId'] ?? null ) === $image_id ) ) { return true; } } @@ -408,8 +401,7 @@ public function is_lcp_image_in_all_viewports( int $image_id ): bool { */ public function is_in_any_viewport( $image_id ) { foreach ( self::get_active_devices() as $device ) { - $above_fold = self::get_device_member( $device, 'af' ); - if ( ! empty( $above_fold[ $image_id ] ) ) { + if ( self::$current_profile_data[ $device ]['af'][ $image_id ] ?? false ) { return $device; } } @@ -427,7 +419,7 @@ public function is_in_any_viewport( $image_id ) { public function get_profile_data( $id ) { $profile_data = []; foreach ( self::get_active_devices() as $device ) { - $profile_data[ $device ] = Storage\Base::normalize_value( $this->storage->get( $id . '_' . $device ) ); + $profile_data[ $device ] = $this->storage->get( $id . '_' . $device ); } return $profile_data; @@ -462,7 +454,7 @@ public static function get_active_devices(): array { */ public function is_data_available(): bool { foreach ( self::get_active_devices() as $device ) { - if ( empty( self::get_device_data( $device ) ) ) { + if ( empty( self::$current_profile_data[ $device ] ) ) { return false; } } @@ -470,39 +462,6 @@ public function is_data_available(): bool { return true; } - /** - * Get array-shaped profile data for a device. - * - * Object-shaped cache values are coerced to arrays so viewport lookups never fatal. - * - * @param int $device Device type constant. - * @return array - */ - private static function get_device_data( int $device ): array { - $data = self::$current_profile_data[ $device ] ?? []; - if ( ! is_array( $data ) ) { - $data = Storage\Base::normalize_value( $data ); - } - - return is_array( $data ) ? $data : []; - } - - /** - * Get an array member from a device profile. - * - * @param int $device Device type constant. - * @param string $key Member key (af, bg, lcp, m, s, c). - * @return array - */ - private static function get_device_member( int $device, string $key ): array { - $member = self::get_device_data( $device )[ $key ] ?? []; - if ( ! is_array( $member ) ) { - $member = Storage\Base::normalize_value( $member ); - } - - return is_array( $member ) ? $member : []; - } - /** * Generate HTML comment with profile data and performance metrics. * diff --git a/tests/test-page-profiler-shape.php b/tests/test-page-profiler-shape.php index ef1035bc1..15dcf133e 100644 --- a/tests/test-page-profiler-shape.php +++ b/tests/test-page-profiler-shape.php @@ -88,24 +88,28 @@ private function objectProfile( $payload = null ) { } /** - * Inject current profile data, including object-shaped values. + * Seed transients with object-shaped payloads and load them through storage. * - * @param mixed $mobile Mobile payload. - * @param mixed $desktop Desktop payload. - * @param mixed $global Global payload. + * @param mixed $mobile Mobile payload, or false to skip. + * @param mixed $desktop Desktop payload, or false to skip. + * @param mixed $global Global payload, or false to skip. + * @return string Profile ID. */ - private function injectCurrentProfileData( $mobile, $desktop, $global = false ) { - $reflection = new ReflectionClass( Profile::class ); - $property = $reflection->getProperty( 'current_profile_data' ); - $property->setAccessible( true ); - $property->setValue( - null, - [ - Profile::DEVICE_TYPE_MOBILE => $mobile, - Profile::DEVICE_TYPE_DESKTOP => $desktop, - Profile::DEVICE_TYPE_GLOBAL => $global, - ] - ); + private function loadProfileFromStorage( $mobile, $desktop, $global = false ) { + $profile_id = 'shape_' . wp_generate_uuid4(); + if ( false !== $mobile ) { + set_transient( Transients::PREFIX . $profile_id . '_' . Profile::DEVICE_TYPE_MOBILE, $mobile, HOUR_IN_SECONDS ); + } + if ( false !== $desktop ) { + set_transient( Transients::PREFIX . $profile_id . '_' . Profile::DEVICE_TYPE_DESKTOP, $desktop, HOUR_IN_SECONDS ); + } + if ( false !== $global ) { + set_transient( Transients::PREFIX . $profile_id, $global, HOUR_IN_SECONDS ); + } + Profile::reset_current_profile(); + Profile::set_current_profile_id( $profile_id ); + Optml_Manager::instance()->page_profiler->set_current_profile_data(); + return $profile_id; } /** @@ -140,7 +144,7 @@ public function normalizeValueProvider() { * Nested stdClass trees become associative arrays, including numeric keys. */ public function test_normalize_value_converts_nested_stdclass() { - $object = $this->objectProfile(); + $object = $this->objectProfile(); $normalized = ProfilerStorage::normalize_value( $object ); $this->assertIsArray( $normalized ); @@ -156,7 +160,7 @@ public function test_normalize_value_converts_nested_stdclass() { * Top-level array with object-shaped members is still coerced. */ public function test_normalize_value_converts_object_members_inside_array() { - $payload = [ + $payload = [ 'af' => (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ], 'bg' => (object) [], 'lcp' => (object) [ 'type' => 'img', 'imageId' => self::ABOVE_FOLD_IMAGE_ID ], @@ -238,7 +242,7 @@ public function test_transients_get_miss_returns_false() { * The reported crash: indexing object-shaped device data as an array. */ public function test_is_in_all_viewports_does_not_fatal_on_stdclass() { - $this->injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertTrue( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); @@ -249,11 +253,11 @@ public function test_is_in_all_viewports_does_not_fatal_on_stdclass() { * Object-shaped above-fold map only (array wrapper, object `af`). */ public function test_is_in_all_viewports_with_object_shaped_af_member() { - $mobile = $this->arrayProfile(); - $desktop = $this->arrayProfile(); + $mobile = $this->arrayProfile(); + $desktop = $this->arrayProfile(); $mobile['af'] = (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ]; $desktop['af'] = (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ]; - $this->injectCurrentProfileData( $mobile, $desktop ); + $this->loadProfileFromStorage( $mobile, $desktop ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertTrue( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); @@ -263,7 +267,7 @@ public function test_is_in_all_viewports_with_object_shaped_af_member() { * Missing or empty object-shaped device data is treated as unavailable. */ public function test_is_in_all_viewports_empty_object_is_unavailable() { - $this->injectCurrentProfileData( new stdClass(), $this->objectProfile() ); + $this->loadProfileFromStorage( new stdClass(), $this->objectProfile() ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertFalse( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); @@ -274,7 +278,7 @@ public function test_is_in_all_viewports_empty_object_is_unavailable() { * is_in_any_viewport must not fatal on object-shaped data. */ public function test_is_in_any_viewport_does_not_fatal_on_stdclass() { - $this->injectCurrentProfileData( $this->objectProfile(), false ); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertSame( Profile::DEVICE_TYPE_MOBILE, $profiler->is_in_any_viewport( self::ABOVE_FOLD_IMAGE_ID ) ); @@ -285,13 +289,25 @@ public function test_is_in_any_viewport_does_not_fatal_on_stdclass() { * LCP lookup must not fatal on object-shaped `lcp`. */ public function test_is_lcp_image_in_all_viewports_does_not_fatal_on_stdclass() { - $this->injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertTrue( $profiler->is_lcp_image_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); $this->assertFalse( $profiler->is_lcp_image_in_all_viewports( self::OTHER_IMAGE_ID ) ); } + /** + * Missing LCP imageId must not warn or fatal. + */ + public function test_is_lcp_image_handles_missing_image_id() { + $payload = $this->arrayProfile(); + $payload['lcp'] = [ 'type' => 'img' ]; + $this->loadProfileFromStorage( $payload, $payload ); + $profiler = Optml_Manager::instance()->page_profiler; + + $this->assertFalse( $profiler->is_lcp_image_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + } + /** * Global missing-dimension lookups must not fatal on object-shaped global data. */ @@ -313,7 +329,7 @@ public function test_global_lookups_do_not_fatal_on_stdclass() { ], 'c' => (object) [ (string) self::ABOVE_FOLD_IMAGE_ID => true ], ]; - $this->injectCurrentProfileData( false, false, $global ); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile(), $global ); $profiler = Optml_Manager::instance()->page_profiler; $this->assertSame( [ 'w' => 100, 'h' => 80 ], $profiler->get_missing_dimensions( self::ABOVE_FOLD_IMAGE_ID ) ); @@ -327,22 +343,14 @@ public function test_global_lookups_do_not_fatal_on_stdclass() { * Loading current profile data from object-shaped transients yields arrays. */ public function test_set_current_profile_data_normalizes_transients() { - $profile_id = 'shape_load_' . wp_generate_uuid4(); - foreach ( Profile::get_active_devices() as $device ) { - set_transient( - Transients::PREFIX . $profile_id . '_' . $device, - $this->objectProfile(), - HOUR_IN_SECONDS - ); - } - - Profile::set_current_profile_id( $profile_id ); - $data = Optml_Manager::instance()->page_profiler->set_current_profile_data(); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); + $data = Profile::get_current_profile_data(); + $profiler = Optml_Manager::instance()->page_profiler; $this->assertIsArray( $data[ Profile::DEVICE_TYPE_MOBILE ] ); $this->assertIsArray( $data[ Profile::DEVICE_TYPE_DESKTOP ] ); $this->assertTrue( $data[ Profile::DEVICE_TYPE_MOBILE ]['af'][ self::ABOVE_FOLD_IMAGE_ID ] ); - $this->assertTrue( Optml_Manager::instance()->page_profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); + $this->assertTrue( $profiler->is_in_all_viewports( self::ABOVE_FOLD_IMAGE_ID ) ); } /** @@ -365,9 +373,9 @@ public function test_get_profile_data_normalizes_stdclass() { * exists() is true after object-shaped data is normalized, false for scalars. */ public function test_exists_with_object_shaped_and_corrupt_storage() { - $profiler = Optml_Manager::instance()->page_profiler; - $good_id = 'shape_exists_good_' . wp_generate_uuid4(); - $bad_id = 'shape_exists_bad_' . wp_generate_uuid4(); + $profiler = Optml_Manager::instance()->page_profiler; + $good_id = 'shape_exists_good_' . wp_generate_uuid4(); + $bad_id = 'shape_exists_bad_' . wp_generate_uuid4(); set_transient( Transients::PREFIX . $good_id . '_' . Profile::DEVICE_TYPE_DESKTOP, @@ -386,15 +394,12 @@ public function test_exists_with_object_shaped_and_corrupt_storage() { } /** - * Personalized background CSS must not fatal on object-shaped profile data. + * Personalized background CSS must not fatal when current profile came from stdClass storage. */ public function test_personalized_css_does_not_fatal_on_stdclass() { - $data = [ - Profile::DEVICE_TYPE_MOBILE => $this->objectProfile(), - Profile::DEVICE_TYPE_DESKTOP => $this->objectProfile(), - ]; + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); - $css = Lazyload::get_personalized_css( $data ); + $css = Lazyload::get_current_personalized_css(); $this->assertIsString( $css ); } @@ -402,7 +407,7 @@ public function test_personalized_css_does_not_fatal_on_stdclass() { * Frontend HTML replacement must not fatal when transients hold stdClass profiles. */ public function test_replace_content_does_not_fatal_on_object_shaped_profile() { - $profile_id = 'shape_html_' . wp_generate_uuid4(); + $profile_id = $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); add_filter( 'optml_page_profile_id', function () use ( $profile_id ) { @@ -410,14 +415,6 @@ function () use ( $profile_id ) { } ); - foreach ( Profile::get_active_devices() as $device ) { - set_transient( - Transients::PREFIX . $profile_id . '_' . $device, - $this->objectProfile(), - HOUR_IN_SECONDS - ); - } - $html = Optml_Manager::instance()->replace_content( Test_Lazyload_Viewport::get_sample_html() ); $this->assertNotEmpty( $html ); $this->assertStringContainsString( 'injectCurrentProfileData( $this->objectProfile(), $this->objectProfile() ); - Profile::set_current_profile_id( 'shape_can_lazy' ); + $this->loadProfileFromStorage( $this->objectProfile(), $this->objectProfile() ); $replacer = Optml_Lazyload_Replacer::instance(); $url = 'https://example.com/test-image.jpg';