From a52381d9f599748a24ea4c9edcb022f7cb7a65ef Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 21 Aug 2024 15:19:31 -0700 Subject: [PATCH 01/16] Allow URL metric schema to be extended --- .../class-od-url-metric.php | 102 ++++++++- .../tests/test-class-od-url-metric.php | 194 +++++++++++++++++- 2 files changed, 286 insertions(+), 10 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 83eca93e81..a0971c5759 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -121,7 +121,7 @@ public static function get_json_schema(): array { * Note that 'number' is used specifically instead of 'integer' because the values are all specified as * floats/doubles. */ - $properties = array_fill_keys( + $dom_rect_properties = array_fill_keys( array( 'width', 'height', @@ -139,17 +139,17 @@ public static function get_json_schema(): array { ); // The spec allows these to be negative but this doesn't make sense in the context of intersectionRect and boundingClientRect. - $properties['width']['minimum'] = 0.0; - $properties['height']['minimum'] = 0.0; + $dom_rect_properties['width']['minimum'] = 0.0; + $dom_rect_properties['height']['minimum'] = 0.0; $dom_rect_schema = array( 'type' => 'object', 'required' => true, - 'properties' => $properties, + 'properties' => $dom_rect_properties, 'additionalProperties' => false, ); - return array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'od-url-metric', 'type' => 'object', @@ -231,6 +231,98 @@ public static function get_json_schema(): array { ), 'additionalProperties' => false, ); + + /** + * Filters additional schema properties which should be allowed at the root of a URL metric. + * + * @since n.e.x.t + * + * @param array $additional_properties Additional properties. + */ + $additional_properties = (array) apply_filters( 'od_url_metric_schema_root_additional_properties', array() ); + if ( count( $additional_properties ) > 0 ) { + $schema['properties'] = self::extend_schema_with_optional_properties( $schema['properties'], $additional_properties, 'od_url_metric_schema_root_additional_properties' ); + } + + /** + * Filters additional schema properties which should be allowed for an elements item in a URL metric. + * + * @since n.e.x.t + * + * @param array $additional_properties Additional properties. + */ + $additional_properties = (array) apply_filters( 'od_url_metric_schema_element_item_additional_properties', array() ); + if ( count( $additional_properties ) > 0 ) { + $schema['properties']['elements']['items']['properties'] = self::extend_schema_with_optional_properties( + $schema['properties']['elements']['items']['properties'], + $additional_properties, + 'od_url_metric_schema_root_additional_properties' + ); + } + + return $schema; + } + + /** + * Extends a schema with additional optional properties. + * + * @since n.e.x.t + * + * @param array $properties_schema Properties schema to extend. + * @param array $additional_properties Additional properties. + * @param string $filter_name Filter name used to extend. + * + * @return array Extended schema. + */ + protected static function extend_schema_with_optional_properties( array $properties_schema, array $additional_properties, string $filter_name ): array { + $doing_it_wrong = static function ( string $message ) use ( $filter_name ): void { + _doing_it_wrong( + esc_html( "add_filter({$filter_name},...)" ), + esc_html( $message ), + 'Optimization Detective n.e.x.t' + ); + }; + foreach ( $additional_properties as $property_key => $property_schema ) { + if ( ! is_array( $property_schema ) ) { + continue; + } + if ( isset( $properties_schema[ $property_key ] ) ) { + $doing_it_wrong( + sprintf( + /* translators: property name */ + __( 'Disallowed override of existing schema property "%s".', 'optimization-detective' ), + $property_key + ) + ); + continue; + } + if ( ! isset( $property_schema['type'] ) || ! is_string( $property_schema['type'] ) ) { + $doing_it_wrong( + sprintf( + /* translators: 1: property name, 2: 'type' */ + __( 'Supplied schema property "%1$s" with missing "%2$s" key.', 'optimization-detective' ), + 'type', + $property_key + ) + ); + continue; + } + // TODO: Should 'default' be required? + if ( isset( $property_schema['required'] ) && false !== $property_schema['required'] ) { + $doing_it_wrong( + sprintf( + /* translators: 1: property name, 2: 'required' */ + __( 'Supplied schema property "%1$s" with truthy for "%2$s". All properties must be optional.', 'optimization-detective' ), + 'type', + $property_key + ) + ); + } + $property_schema['required'] = false; + + $properties_schema[ $property_key ] = $property_schema; + } + return $properties_schema; } /** diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index d301997d58..6a09d4fe6d 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -215,7 +215,189 @@ public function test_constructor( array $data, string $error = '' ): void { */ public function test_get_json_schema(): void { $schema = OD_URL_Metric::get_json_schema(); - $this->check_schema_subset( $schema, 'root' ); + $this->check_schema_subset( $schema, 'root', false ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_provider_to_test_get_json_schema_extensibility(): array { + return array( + 'missing_type' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'missing', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $this->assertSame( $original_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + ), + + 'bad_type' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'type' => 123, + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $this->assertSame( $original_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + ), + + 'bad_required' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'type' => 'string', + 'required' => true, + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $expected_schema = $original_schema; + $expected_schema['properties']['foo'] = array( + 'type' => 'string', + 'required' => false, + ); + $this->assertSame( $expected_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + ), + + 'bad_existing_root_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['uuid'] = array( + 'type' => 'number', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $this->assertSame( $original_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + ), + + 'bad_existing_element_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( $additional_properties ) { + $additional_properties['intersectionRatio'] = array( + 'type' => 'string', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $this->assertSame( $original_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + ), + + 'adding_root_string' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'type' => 'string', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $expected_schema = $original_schema; + $expected_schema['properties']['foo'] = array( + 'type' => 'string', + 'required' => false, + ); + $this->assertSame( $expected_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => null, + ), + + 'adding_root_and_element_string' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'type' => 'string', + ); + return $additional_properties; + } + ); + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( $additional_properties ) { + $additional_properties['bar'] = array( + 'type' => 'number', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $expected_schema = $original_schema; + $expected_schema['properties']['foo'] = array( + 'type' => 'string', + 'required' => false, + ); + $expected_schema['properties']['elements']['items']['properties']['bar'] = array( + 'type' => 'number', + 'required' => false, + ); + $this->assertSame( $expected_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => null, + ), + ); + } + + /** + * Tests get_json_schema() extensibility. + * + * @dataProvider data_provider_to_test_get_json_schema_extensibility + * + * @covers ::get_json_schema + */ + public function test_get_json_schema_extensibility( Closure $set_up, Closure $assert, ?string $expected_incorrect_usage ): void { + if ( null !== $expected_incorrect_usage ) { + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + } + $original_schema = OD_URL_Metric::get_json_schema(); + $set_up(); + $extended_schema = OD_URL_Metric::get_json_schema(); + $this->check_schema_subset( $extended_schema, 'root', true ); + $assert( $original_schema, $extended_schema ); } /** @@ -223,20 +405,22 @@ public function test_get_json_schema(): void { * * @param array $schema Schema subset. */ - protected function check_schema_subset( array $schema, string $path ): void { + protected function check_schema_subset( array $schema, string $path, bool $extended = false ): void { $this->assertArrayHasKey( 'required', $schema, $path ); - $this->assertTrue( $schema['required'], $path ); + if ( ! $extended ) { + $this->assertTrue( $schema['required'], $path ); + } $this->assertArrayHasKey( 'type', $schema, $path ); if ( 'object' === $schema['type'] ) { $this->assertArrayHasKey( 'properties', $schema, $path ); $this->assertArrayHasKey( 'additionalProperties', $schema, $path ); $this->assertFalse( $schema['additionalProperties'] ); foreach ( $schema['properties'] as $key => $property_schema ) { - $this->check_schema_subset( $property_schema, "$path/$key" ); + $this->check_schema_subset( $property_schema, "$path/$key", $extended ); } } elseif ( 'array' === $schema['type'] ) { $this->assertArrayHasKey( 'items', $schema, $path ); - $this->check_schema_subset( $schema['items'], "$path/items" ); + $this->check_schema_subset( $schema['items'], "$path/items", $extended ); } } } From e244b22f52b9ccf1044f193962cec3ade082d40f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 21 Aug 2024 15:56:24 -0700 Subject: [PATCH 02/16] Add get() and __get() methods to OD_URL_Metric Co-authored-by: felixarntz --- .../class-od-url-metric.php | 36 +++++++++++++++++++ .../tests/test-class-od-url-metric.php | 20 +++++++++++ 2 files changed, 56 insertions(+) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index a0971c5759..4c4db37711 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -44,6 +44,12 @@ * elements: ElementData[] * } * + * @property-read string $uuid + * @property-read string $url + * @property-read float $timestamp + * @property-read ViewportRect $viewport + * @property-read ElementData[] $elements + * * @since 0.1.0 * @access private */ @@ -325,6 +331,36 @@ protected static function extend_schema_with_optional_properties( array $propert return $properties_schema; } + /** + * Gets property value for an arbitrary key. + * + * This is particularly useful in conjunction with the `od_url_metric_schema_root_additional_properties` filter. + * + * @since n.e.x.t + * @todo Instead of returning null when the key doesn't exist, should the `default` value be returned as defined in the schema? + * + * @param string $key Property. + * @return mixed|null + */ + public function get( string $key ) { + return $this->data[ $key ] ?? null; + } + + /** + * Gets property value for an arbitrary key. + * + * This is useful with the `@property-read` annotations for the class. For accessing other data, + * it's likely the `get()` method will be more useful for static analysis reasons. + * + * @since n.e.x.t + * + * @param string $key Property. + * @return mixed|null + */ + public function __get( string $key ) { + return $this->get( $key ); + } + /** * Gets UUID. * diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index 6a09d4fe6d..5d32154cbe 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -178,6 +178,8 @@ public function data_provider(): array { /** * Tests construction. * + * @todo PHPStan complains when adding @covers tags for `::get()` and `::__get()` saying they are invalid method references. + * * @covers ::get_viewport * @covers ::get_viewport_width * @covers ::get_timestamp @@ -195,11 +197,29 @@ public function test_constructor( array $data, string $error = '' ): void { $this->expectExceptionMessage( $error ); } $url_metric = new OD_URL_Metric( $data ); + $this->assertSame( $data['viewport'], $url_metric->get_viewport() ); + $this->assertSame( $data['viewport'], $url_metric->get( 'viewport' ) ); + $this->assertSame( $data['viewport'], $url_metric->viewport ); $this->assertSame( $data['viewport']['width'], $url_metric->get_viewport_width() ); + $this->assertSame( $data['viewport']['width'], $url_metric->viewport['width'] ); + $this->assertSame( $data['timestamp'], $url_metric->get_timestamp() ); + $this->assertSame( $data['timestamp'], $url_metric->get( 'timestamp' ) ); + $this->assertSame( $data['timestamp'], $url_metric->timestamp ); + $this->assertSame( $data['elements'], $url_metric->get_elements() ); + $this->assertSame( $data['elements'], $url_metric->get( 'elements' ) ); + $this->assertSame( $data['elements'], $url_metric->elements ); + + $this->assertSame( $data['url'], $url_metric->get_url() ); + $this->assertSame( $data['url'], $url_metric->get( 'url' ) ); + $this->assertSame( $data['url'], $url_metric->url ); + $this->assertTrue( wp_is_uuid( $url_metric->get_uuid() ) ); + $this->assertSame( $url_metric->get_uuid(), $url_metric->uuid ); + $this->assertSame( $url_metric->get_uuid(), $url_metric->get( 'uuid' ) ); + $serialized = $url_metric->jsonSerialize(); if ( ! array_key_exists( 'uuid', $data ) ) { $this->assertTrue( wp_is_uuid( $serialized['uuid'] ) ); From 079e6d9f082cc7c98887052ed8710e1e6f35a063 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 22 Aug 2024 09:44:56 -0700 Subject: [PATCH 03/16] Account for type being an array --- plugins/optimization-detective/class-od-url-metric.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 4c4db37711..bb71a51ac1 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -302,7 +302,7 @@ protected static function extend_schema_with_optional_properties( array $propert ); continue; } - if ( ! isset( $property_schema['type'] ) || ! is_string( $property_schema['type'] ) ) { + if ( ! isset( $property_schema['type'] ) || ! ( is_string( $property_schema['type'] ) || is_array( $property_schema['type'] ) ) ) { $doing_it_wrong( sprintf( /* translators: 1: property name, 2: 'type' */ From bbc97dc350a71babb9c477c90a1fe21c2e46cb33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 22 Aug 2024 12:23:36 -0700 Subject: [PATCH 04/16] Add od_url_metric_collected action --- .../storage/rest-api.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index fb16ed1441..13348a7677 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -161,6 +161,33 @@ function od_handle_rest_request( WP_REST_Request $request ) { if ( $result instanceof WP_Error ) { return $result; } + $post_id = $result; + + /** + * Fires whenever a URL Metric was successfully collected. + * + * @since n.e.x.t + * + * @param array $context { + * Context about the successful URL Metric collection. + * + * @var int $post_id + * @var WP_REST_Request> $request + * @var OD_URL_Metric $url_metric + * @var OD_URL_Metrics_Group $group + * @var OD_URL_Metrics_Group_Collection $group_collection + * } + */ + do_action( + 'od_url_metric_collected', + array( + 'post_id' => $post_id, + 'request' => $request, + 'url_metric' => $url_metric, + 'url_metrics_group' => $group, + 'url_metrics_group_collection' => $group_collection, + ) + ); return new WP_REST_Response( array( From 70fd8258521933305c91ee123d025953c11a5531 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 22 Aug 2024 12:24:11 -0700 Subject: [PATCH 05/16] Add todos related to store_url_metric() --- .../storage/class-od-url-metrics-post-type.php | 1 + plugins/optimization-detective/storage/rest-api.php | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php index d51fbd450c..43079459a1 100644 --- a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php @@ -188,6 +188,7 @@ static function ( $url_metric_data ) use ( $trigger_error ) { * Stores URL metric by merging it with the other URL metrics which share the same normalized query vars. * * @since 0.1.0 + * @todo There is duplicate logic here with od_handle_rest_request(). * * @param string $slug Slug (hash of normalized query vars). * @param OD_URL_Metric $new_url_metric New URL metric. diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 13348a7677..17a2e4fc1d 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -153,6 +153,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { ); } + // TODO: This should be changed from store_url_metric($slug, $url_metric) instead be update_post( $slug, $group_collection ). As it stands, store_url_metric() is duplicating logic here. $result = OD_URL_Metrics_Post_Type::store_url_metric( $request->get_param( 'slug' ), $url_metric From 381c829ceea2417e422dd7b7d498273153641c0b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Sep 2024 12:57:11 -0700 Subject: [PATCH 06/16] Adopt Yoast SEO scheme for filter _doing_it_wrong --- plugins/optimization-detective/class-od-url-metric.php | 2 +- .../tests/test-class-od-url-metric.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index bb71a51ac1..d5667da888 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -283,7 +283,7 @@ public static function get_json_schema(): array { protected static function extend_schema_with_optional_properties( array $properties_schema, array $additional_properties, string $filter_name ): array { $doing_it_wrong = static function ( string $message ) use ( $filter_name ): void { _doing_it_wrong( - esc_html( "add_filter({$filter_name},...)" ), + esc_html( "Filter: '{$filter_name}'" ), esc_html( $message ), 'Optimization Detective n.e.x.t' ); diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index 5d32154cbe..0aa636f3c3 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -260,7 +260,7 @@ static function ( $additional_properties ) { 'assert' => function ( array $original_schema, $extended_schema ): void { $this->assertSame( $original_schema, $extended_schema ); }, - 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), 'bad_type' => array( @@ -278,7 +278,7 @@ static function ( $additional_properties ) { 'assert' => function ( array $original_schema, $extended_schema ): void { $this->assertSame( $original_schema, $extended_schema ); }, - 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), 'bad_required' => array( @@ -302,7 +302,7 @@ static function ( $additional_properties ) { ); $this->assertSame( $expected_schema, $extended_schema ); }, - 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), 'bad_existing_root_property' => array( @@ -320,7 +320,7 @@ static function ( $additional_properties ) { 'assert' => function ( array $original_schema, $extended_schema ): void { $this->assertSame( $original_schema, $extended_schema ); }, - 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), 'bad_existing_element_property' => array( @@ -338,7 +338,7 @@ static function ( $additional_properties ) { 'assert' => function ( array $original_schema, $extended_schema ): void { $this->assertSame( $original_schema, $extended_schema ); }, - 'expected_incorrect_usage' => 'add_filter(od_url_metric_schema_root_additional_properties,...)', + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), 'adding_root_string' => array( From e85c68337419bc512dd5f80cfed1c6e4f67a6b29 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Sep 2024 15:00:32 -0700 Subject: [PATCH 07/16] Fix and clarify message for disallowed required property in extended schema --- plugins/optimization-detective/class-od-url-metric.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index d5667da888..5717d1b24e 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -318,9 +318,9 @@ protected static function extend_schema_with_optional_properties( array $propert $doing_it_wrong( sprintf( /* translators: 1: property name, 2: 'required' */ - __( 'Supplied schema property "%1$s" with truthy for "%2$s". All properties must be optional.', 'optimization-detective' ), - 'type', - $property_key + __( 'Supplied schema property "%1$s" has a truthy value for "%2$s". All extended properties must be optional so that URL Metrics are not all immediately invalidated once an extension is deactivated..', 'optimization-detective' ), + $property_key, + 'required' ) ); } From 0cc91030d50bd0979f05ae289eca7dd9100ffe2e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Sep 2024 16:58:44 -0700 Subject: [PATCH 08/16] Add sanitization in addition to validation to ensure expected PHP types --- .../class-od-url-metric.php | 19 +++---- .../test-class-od-url-metrics-post-type.php | 2 +- .../tests/test-class-od-url-metric.php | 51 +++++++++++++++---- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 5717d1b24e..44ea0b513c 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -67,28 +67,28 @@ final class OD_URL_Metric implements JsonSerializable { * * @phpstan-param Data|array $data Valid data or invalid data (in which case an exception is thrown). * - * @param array $data URL metric data. - * * @throws OD_Data_Validation_Exception When the input is invalid. + * + * @param array $data URL metric data. */ public function __construct( array $data ) { if ( ! isset( $data['uuid'] ) ) { $data['uuid'] = wp_generate_uuid4(); } - $this->validate_data( $data ); - $this->data = $data; + $this->data = $this->prepare_data( $data ); } /** - * Validate data. + * Prepares data with validation and sanitization. * - * @phpstan-assert Data $data + * @throws OD_Data_Validation_Exception When the input is invalid. * * @param array $data Data to validate. - * @throws OD_Data_Validation_Exception When the input is invalid. + * @return Data Validated and sanitized data. */ - private function validate_data( array $data ): void { - $valid = rest_validate_object_value_from_schema( $data, self::get_json_schema(), self::class ); + private function prepare_data( array $data ): array { + $schema = static::get_json_schema(); + $valid = rest_validate_object_value_from_schema( $data, $schema, self::class ); if ( is_wp_error( $valid ) ) { throw new OD_Data_Validation_Exception( esc_html( $valid->get_error_message() ) ); } @@ -111,6 +111,7 @@ private function validate_data( array $data ): void { ) ); } + return rest_sanitize_value_from_schema( $data, $schema, self::class ); } /** diff --git a/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php b/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php index 8872d13156..135595194c 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php @@ -94,7 +94,7 @@ public function data_provider_test_get_url_metrics_from_post(): array { 'width' => 640, 'height' => 480, ), - 'timestamp' => (int) microtime( true ), // Integer to facilitate equality tests. + 'timestamp' => microtime( true ), 'elements' => array(), ), ); diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index 0aa636f3c3..96a34baa2b 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -48,6 +48,26 @@ public function data_provider(): array { ), ), ), + // This tests that sanitization converts values into their expected PHP types. + 'valid_but_props_are_strings' => array( + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => array_map( 'strval', $viewport ), + 'timestamp' => (string) microtime( true ), + 'elements' => array( + array_map( + static function ( $value ) { + if ( is_array( $value ) ) { + return array_map( 'strval', $value ); + } else { + return (string) $value; + } + }, + $valid_element + ), + ), + ), + ), 'bad_uuid' => array( 'data' => array( 'uuid' => 'foo', @@ -198,19 +218,26 @@ public function test_constructor( array $data, string $error = '' ): void { } $url_metric = new OD_URL_Metric( $data ); - $this->assertSame( $data['viewport'], $url_metric->get_viewport() ); - $this->assertSame( $data['viewport'], $url_metric->get( 'viewport' ) ); - $this->assertSame( $data['viewport'], $url_metric->viewport ); - $this->assertSame( $data['viewport']['width'], $url_metric->get_viewport_width() ); - $this->assertSame( $data['viewport']['width'], $url_metric->viewport['width'] ); + $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->get_viewport() ); + $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->get( 'viewport' ) ); + $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->viewport ); + $this->assertSame( (int) $data['viewport']['width'], $url_metric->get_viewport_width() ); + $this->assertSame( (int) $data['viewport']['width'], $url_metric->viewport['width'] ); - $this->assertSame( $data['timestamp'], $url_metric->get_timestamp() ); - $this->assertSame( $data['timestamp'], $url_metric->get( 'timestamp' ) ); - $this->assertSame( $data['timestamp'], $url_metric->timestamp ); + $this->assertSame( (float) $data['timestamp'], $url_metric->get_timestamp() ); + $this->assertSame( (float) $data['timestamp'], $url_metric->get( 'timestamp' ) ); + $this->assertSame( (float) $data['timestamp'], $url_metric->timestamp ); - $this->assertSame( $data['elements'], $url_metric->get_elements() ); - $this->assertSame( $data['elements'], $url_metric->get( 'elements' ) ); - $this->assertSame( $data['elements'], $url_metric->elements ); + $this->assertCount( count( $data['elements'] ), $url_metric->elements ); + for ( $i = 0, $length = count( $data['elements'] ); $i < $length; $i++ ) { + $this->assertSame( (bool) $data['elements'][ $i ]['isLCP'], $url_metric->elements[ $i ]['isLCP'] ); + $this->assertSame( (bool) $data['elements'][ $i ]['isLCPCandidate'], $url_metric->elements[ $i ]['isLCPCandidate'] ); + $this->assertSame( (float) $data['elements'][ $i ]['intersectionRatio'], $url_metric->elements[ $i ]['intersectionRatio'] ); + $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['boundingClientRect'] ), $url_metric->elements[ $i ]['boundingClientRect'] ); + $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['intersectionRect'] ), $url_metric->elements[ $i ]['intersectionRect'] ); + } + $this->assertSame( $url_metric->elements, $url_metric->get_elements() ); + $this->assertSame( $url_metric->elements, $url_metric->get( 'elements' ) ); $this->assertSame( $data['url'], $url_metric->get_url() ); $this->assertSame( $data['url'], $url_metric->get( 'url' ) ); @@ -225,6 +252,8 @@ public function test_constructor( array $data, string $error = '' ): void { $this->assertTrue( wp_is_uuid( $serialized['uuid'] ) ); unset( $serialized['uuid'] ); } + + // The use of assertEquals instead of assertSame ensures that lossy type comparisons are employed. $this->assertEquals( $data, $serialized ); } From 00d57b46c4a1a5105d3a0f5ca5bc30b46df15705 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Sep 2024 18:14:47 -0700 Subject: [PATCH 09/16] Persist unknown properties in stored URL Metrics but reject in new URL Metric storage requests --- .../class-od-strict-url-metric.php | 63 ++++++ .../class-od-url-metric.php | 17 +- plugins/optimization-detective/load.php | 1 + .../storage/rest-api.php | 30 +-- .../test-class-od-url-metrics-post-type.php | 17 +- .../tests/storage/test-rest-api.php | 60 +++++- .../tests/test-class-od-strict-url-metric.php | 62 ++++++ .../tests/test-class-od-url-metric.php | 183 +++++++++++++++++- 8 files changed, 402 insertions(+), 31 deletions(-) create mode 100644 plugins/optimization-detective/class-od-strict-url-metric.php create mode 100644 plugins/optimization-detective/tests/test-class-od-strict-url-metric.php diff --git a/plugins/optimization-detective/class-od-strict-url-metric.php b/plugins/optimization-detective/class-od-strict-url-metric.php new file mode 100644 index 0000000000..89d1196435 --- /dev/null +++ b/plugins/optimization-detective/class-od-strict-url-metric.php @@ -0,0 +1,63 @@ + Schema. + */ + public static function get_json_schema(): array { + return self::falsify_additional_properties( parent::get_json_schema() ); + } + + /** + * Recursively processes the schema to ensure that all objects have additionalProperties set to false. + * + * @since n.e.x.t + * + * @param mixed $schema Schema. + * @return mixed Processed schema. + */ + private static function falsify_additional_properties( $schema ) { + if ( ! isset( $schema['type'] ) ) { + return $schema; + } + if ( 'object' === $schema['type'] ) { + $schema['additionalProperties'] = false; + if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { + $schema['properties'] = array_map( + static function ( $property_schema ) { + return self::falsify_additional_properties( $property_schema ); + }, + $schema['properties'] + ); + } + } elseif ( 'array' === $schema['type'] && isset( $schema['items'] ) && is_array( $schema['items'] ) ) { + $schema['items'] = self::falsify_additional_properties( $schema['items'] ); + } + return $schema; + } +} diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 44ea0b513c..9017322f57 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -53,14 +53,14 @@ * @since 0.1.0 * @access private */ -final class OD_URL_Metric implements JsonSerializable { +class OD_URL_Metric implements JsonSerializable { /** * Data. * * @var Data */ - private $data; + protected $data; /** * Constructor. @@ -123,7 +123,7 @@ private function prepare_data( array $data ): array { */ public static function get_json_schema(): array { /* - * The intersectionRect and clientBoundingRect are both instances of the DOMRectReadOnly, which + * The intersectionRect and boundingClientRect are both instances of the DOMRectReadOnly, which * the following schema describes. See . * Note that 'number' is used specifically instead of 'integer' because the values are all specified as * floats/doubles. @@ -232,11 +232,18 @@ public static function get_json_schema(): array { 'intersectionRect' => $dom_rect_schema, 'boundingClientRect' => $dom_rect_schema, ), - 'additionalProperties' => false, + + // Additional properties may be added to the schema for items of elements via the od_url_metric_schema_element_item_additional_properties filter. + // See further explanation below. + 'additionalProperties' => true, ), ), ), - 'additionalProperties' => false, + // Additional root properties may be added to the schema via the od_url_metric_schema_root_additional_properties filter. + // Therefore, additionalProperties is set to true so that additional properties defined in the extended schema may persist + // in a stored URL Metric even when the extension is deactivated. For REST API requests, the OD_Strict_URL_Metric + // which sets this to false so that newly-submitted URL Metrics only ever include the known properties. + 'additionalProperties' => true, ); /** diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php index 59e5405e7f..52902fb31f 100644 --- a/plugins/optimization-detective/load.php +++ b/plugins/optimization-detective/load.php @@ -99,6 +99,7 @@ static function ( string $version ): void { require_once __DIR__ . '/class-od-data-validation-exception.php'; require_once __DIR__ . '/class-od-html-tag-processor.php'; require_once __DIR__ . '/class-od-url-metric.php'; + require_once __DIR__ . '/class-od-strict-url-metric.php'; require_once __DIR__ . '/class-od-url-metrics-group.php'; require_once __DIR__ . '/class-od-url-metrics-group-collection.php'; diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 17a2e4fc1d..77b7fda2d0 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -67,7 +67,7 @@ function od_register_endpoint(): void { 'methods' => 'POST', 'args' => array_merge( $args, - rest_get_endpoint_args_for_schema( OD_URL_Metric::get_json_schema() ) + rest_get_endpoint_args_for_schema( OD_Strict_URL_Metric::get_json_schema() ) ), 'callback' => static function ( WP_REST_Request $request ) { return od_handle_rest_request( $request ); @@ -128,19 +128,23 @@ function od_handle_rest_request( WP_REST_Request $request ) { OD_Storage_Lock::set_lock(); try { - $url_metric = new OD_URL_Metric( - array_merge( - wp_array_slice_assoc( - $request->get_params(), - array_keys( OD_URL_Metric::get_json_schema()['properties'] ) - ), - array( - // Now supply the readonly args which were omitted from the REST API params due to being `readonly`. - 'timestamp' => microtime( true ), - 'uuid' => wp_generate_uuid4(), - ) + $data = $request->get_params(); + // Remove params which are only used for the REST API request and which are not part of a URL Metric. + unset( + $data['slug'], + $data['nonce'] + ); + $data = array_merge( + $data, + array( + // Now supply the readonly args which were omitted from the REST API params due to being `readonly`. + 'timestamp' => microtime( true ), + 'uuid' => wp_generate_uuid4(), ) ); + + // The "strict" URL Metric class is being used here to ensure additionalProperties of all objects are disallowed. + $url_metric = new OD_Strict_URL_Metric( $data ); } catch ( OD_Data_Validation_Exception $e ) { return new WP_Error( 'rest_invalid_param', @@ -174,7 +178,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { * * @var int $post_id * @var WP_REST_Request> $request - * @var OD_URL_Metric $url_metric + * @var OD_Strict_URL_Metric $url_metric * @var OD_URL_Metrics_Group $group * @var OD_URL_Metrics_Group_Collection $group_collection * } diff --git a/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php b/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php index 135595194c..b9eec5d906 100644 --- a/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/tests/storage/test-class-od-url-metrics-post-type.php @@ -102,27 +102,34 @@ public function data_provider_test_get_url_metrics_from_post(): array { $valid_content_with_uuid = $valid_content; $valid_content_with_uuid[0]['uuid'] = wp_generate_uuid4(); + $valid_content_with_extra_property = $valid_content_with_uuid; + $valid_content_with_extra_property[0]['extra'] = 'foo'; + return array( - 'malformed_json' => array( + 'malformed_json' => array( 'post_content' => '{"bad":', 'expected_value' => array(), ), - 'not_array_json' => array( + 'not_array_json' => array( 'post_content' => '{"cool":"beans"}', 'expected_value' => array(), ), - 'missing_keys' => array( + 'missing_keys' => array( 'post_content' => '[{},{},{}]', 'expected_value' => array(), ), - 'valid_sans_uuid' => array( + 'valid_sans_uuid' => array( 'post_content' => wp_json_encode( $valid_content ), 'expected_value' => $valid_content, ), - 'valid_with_uuid' => array( + 'valid_with_uuid' => array( 'post_content' => wp_json_encode( $valid_content_with_uuid ), 'expected_value' => $valid_content_with_uuid, ), + 'valid_with_extra_prop' => array( + 'post_content' => wp_json_encode( $valid_content_with_extra_property ), + 'expected_value' => $valid_content_with_extra_property, + ), ); } diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 5661a469bc..603d40cd98 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -22,15 +22,49 @@ public function test_od_register_endpoint_hooked(): void { $this->assertSame( 10, has_action( 'rest_api_init', 'od_register_endpoint' ) ); } + /** + * Data provider. + * + * @return array + */ + public function data_provider_to_test_rest_request_good_params(): array { + return array( + 'not_extended' => array( + 'set_up' => function () { + return $this->get_valid_params(); + }, + ), + 'extended' => array( + 'set_up' => function () { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( array $properties ): array { + $properties['extra'] = array( + 'type' => 'string', + ); + return $properties; + } + ); + + $params = $this->get_valid_params(); + $params['extra'] = 'foo'; + return $params; + }, + ), + ); + } + /** * Test good params. * + * @dataProvider data_provider_to_test_rest_request_good_params + * * @covers ::od_register_endpoint * @covers ::od_handle_rest_request */ - public function test_rest_request_good_params(): void { + public function test_rest_request_good_params( Closure $set_up ): void { + $valid_params = $set_up(); $request = new WP_REST_Request( 'POST', self::ROUTE ); - $valid_params = $this->get_valid_params(); $this->assertCount( 0, get_posts( array( 'post_type' => OD_URL_Metrics_Post_Type::SLUG ) ) ); $request->set_body_params( $valid_params ); $response = rest_get_server()->dispatch( $request ); @@ -47,6 +81,13 @@ public function test_rest_request_good_params(): void { $this->assertCount( 1, $url_metrics, 'Expected number of URL metrics stored.' ); $this->assertSame( $valid_params['elements'], $url_metrics[0]->get_elements() ); $this->assertSame( $valid_params['viewport']['width'], $url_metrics[0]->get_viewport_width() ); + + $expected_data = $valid_params; + unset( $expected_data['nonce'], $expected_data['slug'] ); + $this->assertSame( + $expected_data, + wp_array_slice_assoc( $url_metrics[0]->jsonSerialize(), array_keys( $expected_data ) ) + ); } /** @@ -151,6 +192,19 @@ function ( $params ) { ), ), ), + 'invalid_root_property' => array( + 'is_touch' => false, + ), + 'invalid_element_property' => array( + 'elements' => array( + array_merge( + $valid_element, + array( + 'is_big' => true, + ) + ), + ), + ), ) ); } @@ -406,6 +460,7 @@ private function get_valid_params( array $extras = array() ): array { ), ) )->jsonSerialize(); + unset( $data['timestamp'], $data['uuid'] ); // Since these are readonly. $data = array_merge( array( 'slug' => $slug, @@ -413,7 +468,6 @@ private function get_valid_params( array $extras = array() ): array { ), $data ); - unset( $data['timestamp'] ); // Since provided by default args. if ( count( $extras ) > 0 ) { $data = $this->recursive_merge( $data, $extras ); } diff --git a/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php b/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php new file mode 100644 index 0000000000..33be2b8ffc --- /dev/null +++ b/plugins/optimization-detective/tests/test-class-od-strict-url-metric.php @@ -0,0 +1,62 @@ + 'object', + 'properties' => array( + 'hex' => array( + 'type' => 'string', + ), + ), + 'additionalProperties' => true, + ); + return $properties; + } + ); + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ) { + $properties['region'] = array( + 'type' => 'object', + 'properties' => array( + 'continent' => array( + 'type' => 'string', + ), + ), + 'additionalProperties' => true, + ); + return $properties; + } + ); + $loose_schema = OD_URL_Metric::get_json_schema(); + $this->assertTrue( $loose_schema['additionalProperties'] ); + $this->assertFalse( $loose_schema['properties']['viewport']['additionalProperties'] ); // The viewport is never extensible. Only the root and the elements are. + $this->assertTrue( $loose_schema['properties']['elements']['items']['additionalProperties'] ); + $this->assertTrue( $loose_schema['properties']['elements']['items']['properties']['region']['additionalProperties'] ); + $this->assertTrue( $loose_schema['properties']['colors']['additionalProperties'] ); + + $strict_schema = OD_Strict_URL_Metric::get_json_schema(); + $this->assertFalse( $strict_schema['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['viewport']['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['elements']['items']['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['elements']['items']['properties']['region']['additionalProperties'] ); + $this->assertFalse( $strict_schema['properties']['colors']['additionalProperties'] ); + } +} diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index 96a34baa2b..b678bdbf8e 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -14,7 +14,7 @@ class Test_OD_URL_Metric extends WP_UnitTestCase { * * @return array Data. */ - public function data_provider(): array { + public function data_provider_to_test_constructor(): array { $viewport = array( 'width' => 640, 'height' => 480, @@ -198,15 +198,15 @@ static function ( $value ) { /** * Tests construction. * - * @todo PHPStan complains when adding @covers tags for `::get()` and `::__get()` saying they are invalid method references. - * * @covers ::get_viewport * @covers ::get_viewport_width * @covers ::get_timestamp * @covers ::get_elements * @covers ::jsonSerialize + * @covers ::get + * @covers ::__get * - * @dataProvider data_provider + * @dataProvider data_provider_to_test_constructor * * @param array $data Data. * @param string $error Error. @@ -257,6 +257,175 @@ public function test_constructor( array $data, string $error = '' ): void { $this->assertEquals( $data, $serialized ); } + /** + * Data provider. + * + * @return array Data. + */ + public function data_provider_to_test_constructor_with_extended_schema(): array { + $viewport = array( + 'width' => 640, + 'height' => 480, + ); + $valid_element = array( + 'isLCP' => true, + 'isLCPCandidate' => true, + 'xpath' => '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]', + 'intersectionRatio' => 1.0, + 'intersectionRect' => $this->get_sample_dom_rect(), + 'boundingClientRect' => $this->get_sample_dom_rect(), + ); + + return array( + 'added_valid_root_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( array $properties ): array { + $properties['isTouch'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array(), + 'isTouch' => 1, // This should get cast to `true` after the extended schema has applied. + ), + 'assert' => function ( OD_URL_Metric $extended_url_metric, OD_URL_Metric $original_url_metric ): void { + $this->assertSame( $original_url_metric->get_viewport(), $extended_url_metric->get_viewport() ); + + $original_data = $original_url_metric->jsonSerialize(); + $this->assertArrayHasKey( 'isTouch', $original_data ); + $this->assertSame( 1, $original_data['isTouch'] ); + $this->assertSame( 1, $original_url_metric->get( 'isTouch' ) ); + + $extended_data = $extended_url_metric->jsonSerialize(); + $this->assertArrayHasKey( 'isTouch', $extended_data ); + $this->assertTrue( $extended_data['isTouch'] ); + $this->assertTrue( $extended_url_metric->get( 'isTouch' ) ); + }, + ), + + 'added_invalid_root_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( array $properties ): array { + $properties['isTouch'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array(), + 'isTouch' => array( 'cannot be cast to boolean' ), + ), + 'assert' => static function (): void {}, + 'error' => 'OD_URL_Metric[isTouch] is not of type boolean.', + ), + + 'added_valid_element_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['isColorful'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array( + array_merge( + $valid_element, + array( 'isColorful' => 'false' ) + ), + ), + ), + 'assert' => function ( OD_URL_Metric $extended_url_metric, OD_URL_Metric $original_url_metric ): void { + $this->assertSame( $original_url_metric->get_viewport(), $extended_url_metric->get_viewport() ); + + $original_data = $original_url_metric->jsonSerialize(); + $this->assertArrayHasKey( 'isColorful', $original_data['elements'][0] ); + $this->assertSame( 'false', $original_data['elements'][0]['isColorful'] ); + $this->assertSame( 'false', $original_url_metric->get_elements()[0]['isColorful'] ); + + $extended_data = $extended_url_metric->jsonSerialize(); + $this->assertArrayHasKey( 'isColorful', $extended_data['elements'][0] ); + $this->assertFalse( $extended_data['elements'][0]['isColorful'] ); + $this->assertFalse( $extended_url_metric->get_elements()[0]['isColorful'] ); + }, + ), + + 'added_invalid_element_property' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['isColorful'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array( + array_merge( + $valid_element, + array( 'isColorful' => array( 'cannot be cast to boolean' ) ) + ), + ), + ), + 'assert' => static function (): void {}, + 'error' => 'OD_URL_Metric[elements][0][isColorful] is not of type boolean.', + ), + ); + } + + /** + * Tests construction with extended schema. + * + * @covers ::jsonSerialize + * @covers ::get + * @covers ::__get + * + * @dataProvider data_provider_to_test_constructor_with_extended_schema + * + * @param Closure $set_up Set up to extend schema. + * @param array $data Data. + * @param Closure $assert Assert. + * @param string $error Error. + */ + public function test_constructor_with_extended_schema( Closure $set_up, array $data, Closure $assert, string $error = '' ): void { + if ( '' !== $error ) { + $this->expectException( OD_Data_Validation_Exception::class ); + $this->expectExceptionMessage( $error ); + } + $url_metric_sans_extended_schema = new OD_URL_Metric( $data ); + $set_up(); + $url_metric_with_extended_schema = new OD_URL_Metric( $data ); + $assert( $url_metric_with_extended_schema, $url_metric_sans_extended_schema ); + } + /** * Tests get_json_schema(). * @@ -463,7 +632,11 @@ protected function check_schema_subset( array $schema, string $path, bool $exten if ( 'object' === $schema['type'] ) { $this->assertArrayHasKey( 'properties', $schema, $path ); $this->assertArrayHasKey( 'additionalProperties', $schema, $path ); - $this->assertFalse( $schema['additionalProperties'] ); + if ( 'root/viewport' === $path || 'root/elements/items/intersectionRect' === $path || 'root/elements/items/boundingClientRect' === $path ) { + $this->assertFalse( $schema['additionalProperties'], "Path: $path" ); + } else { + $this->assertTrue( $schema['additionalProperties'], "Path: $path" ); + } foreach ( $schema['properties'] as $key => $property_schema ) { $this->check_schema_subset( $property_schema, "$path/$key", $extended ); } From 22711e42308b47cba5f61f1f51fb89dd89e8f889 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 09:58:02 -0700 Subject: [PATCH 10/16] Add return description Co-authored-by: Felix Arntz --- plugins/optimization-detective/class-od-url-metric.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 9017322f57..781c4cb0a1 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -348,7 +348,7 @@ protected static function extend_schema_with_optional_properties( array $propert * @todo Instead of returning null when the key doesn't exist, should the `default` value be returned as defined in the schema? * * @param string $key Property. - * @return mixed|null + * @return mixed|null The property value, or null if not set. */ public function get( string $key ) { return $this->data[ $key ] ?? null; From 384fa53f9de60b3080b515ab95a06070b50b1a04 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 11:31:00 -0700 Subject: [PATCH 11/16] Use properties instead of get methods --- .../class-od-url-metrics-group-collection.php | 4 ++-- .../optimization-detective/class-od-url-metrics-group.php | 6 +++--- .../storage/class-od-url-metrics-post-type.php | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 21bce9a0c5..dd2306a73b 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -193,7 +193,7 @@ private function create_groups(): array { */ public function add_url_metric( OD_URL_Metric $new_url_metric ): void { foreach ( $this->groups as $group ) { - if ( $group->is_viewport_width_in_range( $new_url_metric->get_viewport_width() ) ) { + if ( $group->is_viewport_width_in_range( $new_url_metric->viewport['width'] ) ) { $group->add_url_metric( $new_url_metric ); return; } @@ -416,7 +416,7 @@ public function get_all_element_max_intersection_ratios(): array { */ foreach ( $this->groups as $group ) { foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { + foreach ( $url_metric->elements as $element ) { $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) : $element['intersectionRatio']; diff --git a/plugins/optimization-detective/class-od-url-metrics-group.php b/plugins/optimization-detective/class-od-url-metrics-group.php index 97e1bf7cc7..79dd57cf61 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group.php +++ b/plugins/optimization-detective/class-od-url-metrics-group.php @@ -181,7 +181,7 @@ public function is_viewport_width_in_range( int $viewport_width ): bool { * @param OD_URL_Metric $url_metric URL metric. */ public function add_url_metric( OD_URL_Metric $url_metric ): void { - if ( ! $this->is_viewport_width_in_range( $url_metric->get_viewport_width() ) ) { + if ( ! $this->is_viewport_width_in_range( $url_metric->viewport['width'] ) ) { throw new InvalidArgumentException( esc_html__( 'URL metric is not in the viewport range for group.', 'optimization-detective' ) ); @@ -201,7 +201,7 @@ public function add_url_metric( OD_URL_Metric $url_metric ): void { usort( $this->url_metrics, static function ( OD_URL_Metric $a, OD_URL_Metric $b ): int { - return $b->get_timestamp() <=> $a->get_timestamp(); + return $b->timestamp <=> $a->timestamp; } ); @@ -283,7 +283,7 @@ public function get_lcp_element(): ?array { $breadcrumb_element = array(); foreach ( $this->url_metrics as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { + foreach ( $url_metric->elements as $element ) { if ( ! $element['isLCP'] ) { continue; } diff --git a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php index 43079459a1..8bfcccd096 100644 --- a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php @@ -200,7 +200,7 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me // multiple URL Metric instances, each of which also contains the URL for which the metric was captured. The URL // appearing in the post title is therefore the most recent URL seen for the URL Metrics which have the same // normalized query vars among them. - 'post_title' => $new_url_metric->get_url(), + 'post_title' => $new_url_metric->url, ); $post = self::get_post( $slug ); @@ -221,7 +221,7 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me ); try { - $group = $group_collection->get_group_for_viewport_width( $new_url_metric->get_viewport_width() ); + $group = $group_collection->get_group_for_viewport_width( $new_url_metric->viewport['width'] ); $group->add_url_metric( $new_url_metric ); } catch ( InvalidArgumentException $e ) { return new WP_Error( 'invalid_url_metric', $e->getMessage() ); From ed5815158552d27359a730e1f6dfa41ccbcc61c9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 11:36:31 -0700 Subject: [PATCH 12/16] Replace remaining use of get_timestamp --- plugins/optimization-detective/class-od-url-metrics-group.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-url-metrics-group.php b/plugins/optimization-detective/class-od-url-metrics-group.php index 79dd57cf61..6eb364aa19 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group.php +++ b/plugins/optimization-detective/class-od-url-metrics-group.php @@ -229,7 +229,7 @@ public function is_complete(): bool { } $current_time = microtime( true ); foreach ( $this->url_metrics as $url_metric ) { - if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) { + if ( $current_time > $url_metric->timestamp + $this->freshness_ttl ) { return false; } } From aa1e66334e52abb2e9e0acb168c080346abcc907 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 13:51:30 -0700 Subject: [PATCH 13/16] Disallow default value for schema --- .../class-od-url-metric.php | 16 +++- .../tests/test-class-od-url-metric.php | 96 +++++++++++++++++-- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 781c4cb0a1..261423b776 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -321,12 +321,11 @@ protected static function extend_schema_with_optional_properties( array $propert ); continue; } - // TODO: Should 'default' be required? if ( isset( $property_schema['required'] ) && false !== $property_schema['required'] ) { $doing_it_wrong( sprintf( /* translators: 1: property name, 2: 'required' */ - __( 'Supplied schema property "%1$s" has a truthy value for "%2$s". All extended properties must be optional so that URL Metrics are not all immediately invalidated once an extension is deactivated..', 'optimization-detective' ), + __( 'Supplied schema property "%1$s" has a truthy value for "%2$s". All extended properties must be optional so that URL Metrics are not all immediately invalidated once an extension is deactivated.', 'optimization-detective' ), $property_key, 'required' ) @@ -334,6 +333,18 @@ protected static function extend_schema_with_optional_properties( array $propert } $property_schema['required'] = false; + if ( isset( $property_schema['default'] ) ) { + $doing_it_wrong( + sprintf( + /* translators: 1: property name, 2: 'default' */ + __( 'Supplied schema property "%1$s" has disallowed "%2$s" specified.', 'optimization-detective' ), + $property_key, + 'default' + ) + ); + unset( $property_schema['default'] ); + } + $properties_schema[ $property_key ] = $property_schema; } return $properties_schema; @@ -345,7 +356,6 @@ protected static function extend_schema_with_optional_properties( array $propert * This is particularly useful in conjunction with the `od_url_metric_schema_root_additional_properties` filter. * * @since n.e.x.t - * @todo Instead of returning null when the key doesn't exist, should the `default` value be returned as defined in the schema? * * @param string $key Property. * @return mixed|null The property value, or null if not set. diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index b678bdbf8e..b3732b6ae7 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -277,7 +277,7 @@ public function data_provider_to_test_constructor_with_extended_schema(): array ); return array( - 'added_valid_root_property' => array( + 'added_valid_root_property_populated' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_schema_root_additional_properties', @@ -311,7 +311,38 @@ static function ( array $properties ): array { }, ), - 'added_invalid_root_property' => array( + 'added_valid_root_property_not_populated' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( array $properties ): array { + $properties['isTouch'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array(), + ), + 'assert' => function ( OD_URL_Metric $extended_url_metric, OD_URL_Metric $original_url_metric ): void { + $this->assertSame( $original_url_metric->get_viewport(), $extended_url_metric->get_viewport() ); + + $original_data = $original_url_metric->jsonSerialize(); + $this->assertArrayNotHasKey( 'isTouch', $original_data ); + $this->assertNull( $original_url_metric->get( 'isTouch' ) ); + + $extended_data = $extended_url_metric->jsonSerialize(); + $this->assertArrayNotHasKey( 'isTouch', $extended_data ); // If rest_sanitize_value_from_schema() took default into account (and we allowed defaults), this could be different. + $this->assertNull( $extended_url_metric->get( 'isTouch' ) ); + }, + ), + + 'added_invalid_root_property' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_schema_root_additional_properties', @@ -334,7 +365,7 @@ static function ( array $properties ): array { 'error' => 'OD_URL_Metric[isTouch] is not of type boolean.', ), - 'added_valid_element_property' => array( + 'added_valid_element_property_populated' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_schema_element_item_additional_properties', @@ -363,16 +394,45 @@ static function ( array $properties ): array { $original_data = $original_url_metric->jsonSerialize(); $this->assertArrayHasKey( 'isColorful', $original_data['elements'][0] ); $this->assertSame( 'false', $original_data['elements'][0]['isColorful'] ); - $this->assertSame( 'false', $original_url_metric->get_elements()[0]['isColorful'] ); + $this->assertSame( 'false', $original_url_metric->elements[0]['isColorful'] ); $extended_data = $extended_url_metric->jsonSerialize(); $this->assertArrayHasKey( 'isColorful', $extended_data['elements'][0] ); $this->assertFalse( $extended_data['elements'][0]['isColorful'] ); - $this->assertFalse( $extended_url_metric->get_elements()[0]['isColorful'] ); + $this->assertFalse( $extended_url_metric->elements[0]['isColorful'] ); + }, + ), + + 'added_valid_element_property_not_populated' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_element_item_additional_properties', + static function ( array $properties ): array { + $properties['isColorful'] = array( + 'type' => 'boolean', + ); + return $properties; + } + ); + }, + 'data' => array( + 'url' => home_url( '/' ), + 'viewport' => $viewport, + 'timestamp' => microtime( true ), + 'elements' => array( $valid_element ), + ), + 'assert' => function ( OD_URL_Metric $extended_url_metric, OD_URL_Metric $original_url_metric ): void { + $this->assertSame( $original_url_metric->get_viewport(), $extended_url_metric->get_viewport() ); + + $original_data = $original_url_metric->jsonSerialize(); + $this->assertArrayNotHasKey( 'isColorful', $original_data['elements'][0] ); + + $extended_data = $extended_url_metric->jsonSerialize(); + $this->assertArrayNotHasKey( 'isColorful', $extended_data['elements'][0] ); // If rest_sanitize_value_from_schema() took default into account (and we allowed defaults), this could be different. }, ), - 'added_invalid_element_property' => array( + 'added_invalid_element_property' => array( 'set_up' => static function (): void { add_filter( 'od_url_metric_schema_element_item_additional_properties', @@ -503,6 +563,30 @@ static function ( $additional_properties ) { 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', ), + 'bad_default' => array( + 'set_up' => static function (): void { + add_filter( + 'od_url_metric_schema_root_additional_properties', + static function ( $additional_properties ) { + $additional_properties['foo'] = array( + 'type' => 'string', + 'default' => 'bar', + ); + return $additional_properties; + } + ); + }, + 'assert' => function ( array $original_schema, $extended_schema ): void { + $expected_schema = $original_schema; + $expected_schema['properties']['foo'] = array( + 'type' => 'string', + 'required' => false, + ); + $this->assertSame( $expected_schema, $extended_schema ); + }, + 'expected_incorrect_usage' => 'Filter: 'od_url_metric_schema_root_additional_properties'', + ), + 'bad_existing_root_property' => array( 'set_up' => static function (): void { add_filter( From 8917ce990922a6ee28d3f482f7989df30e29926e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 13:57:01 -0700 Subject: [PATCH 14/16] Explicitly fork rest_default_additional_properties_to_false() --- .../class-od-strict-url-metric.php | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/plugins/optimization-detective/class-od-strict-url-metric.php b/plugins/optimization-detective/class-od-strict-url-metric.php index 89d1196435..d3e0a5a5da 100644 --- a/plugins/optimization-detective/class-od-strict-url-metric.php +++ b/plugins/optimization-detective/class-od-strict-url-metric.php @@ -30,34 +30,50 @@ final class OD_Strict_URL_Metric extends OD_URL_Metric { * @return array Schema. */ public static function get_json_schema(): array { - return self::falsify_additional_properties( parent::get_json_schema() ); + return self::set_additional_properties_to_false( parent::get_json_schema() ); } /** * Recursively processes the schema to ensure that all objects have additionalProperties set to false. * + * This is a forked version of `rest_default_additional_properties_to_false()` which isn't being used itself because + * it does not override `additionalProperties` to be false, but rather only sets it when it is empty. + * * @since n.e.x.t + * @see rest_default_additional_properties_to_false() * * @param mixed $schema Schema. * @return mixed Processed schema. */ - private static function falsify_additional_properties( $schema ) { + private static function set_additional_properties_to_false( $schema ) { if ( ! isset( $schema['type'] ) ) { return $schema; } - if ( 'object' === $schema['type'] ) { + + $type = (array) $schema['type']; + + if ( in_array( 'object', $type, true ) ) { + if ( isset( $schema['properties'] ) ) { + foreach ( $schema['properties'] as $key => $child_schema ) { + $schema['properties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + } + } + + if ( isset( $schema['patternProperties'] ) ) { + foreach ( $schema['patternProperties'] as $key => $child_schema ) { + $schema['patternProperties'][ $key ] = self::set_additional_properties_to_false( $child_schema ); + } + } + $schema['additionalProperties'] = false; - if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) { - $schema['properties'] = array_map( - static function ( $property_schema ) { - return self::falsify_additional_properties( $property_schema ); - }, - $schema['properties'] - ); + } + + if ( in_array( 'array', $type, true ) ) { + if ( isset( $schema['items'] ) ) { + $schema['items'] = self::set_additional_properties_to_false( $schema['items'] ); } - } elseif ( 'array' === $schema['type'] && isset( $schema['items'] ) && is_array( $schema['items'] ) ) { - $schema['items'] = self::falsify_additional_properties( $schema['items'] ); } + return $schema; } } From 907d8c3ba344d1247fa48a39c7b23512da2ba07a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Sep 2024 13:59:00 -0700 Subject: [PATCH 15/16] Add return description for magic getter --- plugins/optimization-detective/class-od-url-metric.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 261423b776..3c27631b44 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -373,7 +373,7 @@ public function get( string $key ) { * @since n.e.x.t * * @param string $key Property. - * @return mixed|null + * @return mixed|null The property value, or null if not set. */ public function __get( string $key ) { return $this->get( $key ); From 0db296f40002a7c2a13b88ca415928b2fa1644c4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 11 Sep 2024 15:08:34 -0700 Subject: [PATCH 16/16] Eliminate magic getter Co-authored-by: felixarntz --- .../class-od-url-metric.php | 21 ------------- .../class-od-url-metrics-group-collection.php | 4 +-- .../class-od-url-metrics-group.php | 8 ++--- .../class-od-url-metrics-post-type.php | 4 +-- .../tests/test-class-od-url-metric.php | 30 +++++++------------ 5 files changed, 19 insertions(+), 48 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 3c27631b44..a64db2f1dd 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -44,12 +44,6 @@ * elements: ElementData[] * } * - * @property-read string $uuid - * @property-read string $url - * @property-read float $timestamp - * @property-read ViewportRect $viewport - * @property-read ElementData[] $elements - * * @since 0.1.0 * @access private */ @@ -364,21 +358,6 @@ public function get( string $key ) { return $this->data[ $key ] ?? null; } - /** - * Gets property value for an arbitrary key. - * - * This is useful with the `@property-read` annotations for the class. For accessing other data, - * it's likely the `get()` method will be more useful for static analysis reasons. - * - * @since n.e.x.t - * - * @param string $key Property. - * @return mixed|null The property value, or null if not set. - */ - public function __get( string $key ) { - return $this->get( $key ); - } - /** * Gets UUID. * diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index dd2306a73b..21bce9a0c5 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -193,7 +193,7 @@ private function create_groups(): array { */ public function add_url_metric( OD_URL_Metric $new_url_metric ): void { foreach ( $this->groups as $group ) { - if ( $group->is_viewport_width_in_range( $new_url_metric->viewport['width'] ) ) { + if ( $group->is_viewport_width_in_range( $new_url_metric->get_viewport_width() ) ) { $group->add_url_metric( $new_url_metric ); return; } @@ -416,7 +416,7 @@ public function get_all_element_max_intersection_ratios(): array { */ foreach ( $this->groups as $group ) { foreach ( $group as $url_metric ) { - foreach ( $url_metric->elements as $element ) { + foreach ( $url_metric->get_elements() as $element ) { $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) : $element['intersectionRatio']; diff --git a/plugins/optimization-detective/class-od-url-metrics-group.php b/plugins/optimization-detective/class-od-url-metrics-group.php index 6eb364aa19..97e1bf7cc7 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group.php +++ b/plugins/optimization-detective/class-od-url-metrics-group.php @@ -181,7 +181,7 @@ public function is_viewport_width_in_range( int $viewport_width ): bool { * @param OD_URL_Metric $url_metric URL metric. */ public function add_url_metric( OD_URL_Metric $url_metric ): void { - if ( ! $this->is_viewport_width_in_range( $url_metric->viewport['width'] ) ) { + if ( ! $this->is_viewport_width_in_range( $url_metric->get_viewport_width() ) ) { throw new InvalidArgumentException( esc_html__( 'URL metric is not in the viewport range for group.', 'optimization-detective' ) ); @@ -201,7 +201,7 @@ public function add_url_metric( OD_URL_Metric $url_metric ): void { usort( $this->url_metrics, static function ( OD_URL_Metric $a, OD_URL_Metric $b ): int { - return $b->timestamp <=> $a->timestamp; + return $b->get_timestamp() <=> $a->get_timestamp(); } ); @@ -229,7 +229,7 @@ public function is_complete(): bool { } $current_time = microtime( true ); foreach ( $this->url_metrics as $url_metric ) { - if ( $current_time > $url_metric->timestamp + $this->freshness_ttl ) { + if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) { return false; } } @@ -283,7 +283,7 @@ public function get_lcp_element(): ?array { $breadcrumb_element = array(); foreach ( $this->url_metrics as $url_metric ) { - foreach ( $url_metric->elements as $element ) { + foreach ( $url_metric->get_elements() as $element ) { if ( ! $element['isLCP'] ) { continue; } diff --git a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php index 8bfcccd096..43079459a1 100644 --- a/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php +++ b/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php @@ -200,7 +200,7 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me // multiple URL Metric instances, each of which also contains the URL for which the metric was captured. The URL // appearing in the post title is therefore the most recent URL seen for the URL Metrics which have the same // normalized query vars among them. - 'post_title' => $new_url_metric->url, + 'post_title' => $new_url_metric->get_url(), ); $post = self::get_post( $slug ); @@ -221,7 +221,7 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me ); try { - $group = $group_collection->get_group_for_viewport_width( $new_url_metric->viewport['width'] ); + $group = $group_collection->get_group_for_viewport_width( $new_url_metric->get_viewport_width() ); $group->add_url_metric( $new_url_metric ); } catch ( InvalidArgumentException $e ) { return new WP_Error( 'invalid_url_metric', $e->getMessage() ); diff --git a/plugins/optimization-detective/tests/test-class-od-url-metric.php b/plugins/optimization-detective/tests/test-class-od-url-metric.php index b3732b6ae7..0f9370a0b1 100644 --- a/plugins/optimization-detective/tests/test-class-od-url-metric.php +++ b/plugins/optimization-detective/tests/test-class-od-url-metric.php @@ -204,7 +204,7 @@ static function ( $value ) { * @covers ::get_elements * @covers ::jsonSerialize * @covers ::get - * @covers ::__get + * @covers ::get_json_schema * * @dataProvider data_provider_to_test_constructor * @@ -220,31 +220,25 @@ public function test_constructor( array $data, string $error = '' ): void { $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->get_viewport() ); $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->get( 'viewport' ) ); - $this->assertSame( array_map( 'intval', $data['viewport'] ), $url_metric->viewport ); $this->assertSame( (int) $data['viewport']['width'], $url_metric->get_viewport_width() ); - $this->assertSame( (int) $data['viewport']['width'], $url_metric->viewport['width'] ); $this->assertSame( (float) $data['timestamp'], $url_metric->get_timestamp() ); $this->assertSame( (float) $data['timestamp'], $url_metric->get( 'timestamp' ) ); - $this->assertSame( (float) $data['timestamp'], $url_metric->timestamp ); - $this->assertCount( count( $data['elements'] ), $url_metric->elements ); + $this->assertCount( count( $data['elements'] ), $url_metric->get_elements() ); for ( $i = 0, $length = count( $data['elements'] ); $i < $length; $i++ ) { - $this->assertSame( (bool) $data['elements'][ $i ]['isLCP'], $url_metric->elements[ $i ]['isLCP'] ); - $this->assertSame( (bool) $data['elements'][ $i ]['isLCPCandidate'], $url_metric->elements[ $i ]['isLCPCandidate'] ); - $this->assertSame( (float) $data['elements'][ $i ]['intersectionRatio'], $url_metric->elements[ $i ]['intersectionRatio'] ); - $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['boundingClientRect'] ), $url_metric->elements[ $i ]['boundingClientRect'] ); - $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['intersectionRect'] ), $url_metric->elements[ $i ]['intersectionRect'] ); + $this->assertSame( (bool) $data['elements'][ $i ]['isLCP'], $url_metric->get_elements()[ $i ]['isLCP'] ); + $this->assertSame( (bool) $data['elements'][ $i ]['isLCPCandidate'], $url_metric->get_elements()[ $i ]['isLCPCandidate'] ); + $this->assertSame( (float) $data['elements'][ $i ]['intersectionRatio'], $url_metric->get_elements()[ $i ]['intersectionRatio'] ); + $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['boundingClientRect'] ), $url_metric->get_elements()[ $i ]['boundingClientRect'] ); + $this->assertSame( array_map( 'floatval', $data['elements'][ $i ]['intersectionRect'] ), $url_metric->get_elements()[ $i ]['intersectionRect'] ); } - $this->assertSame( $url_metric->elements, $url_metric->get_elements() ); - $this->assertSame( $url_metric->elements, $url_metric->get( 'elements' ) ); + $this->assertSame( $url_metric->get_elements(), $url_metric->get( 'elements' ) ); $this->assertSame( $data['url'], $url_metric->get_url() ); $this->assertSame( $data['url'], $url_metric->get( 'url' ) ); - $this->assertSame( $data['url'], $url_metric->url ); $this->assertTrue( wp_is_uuid( $url_metric->get_uuid() ) ); - $this->assertSame( $url_metric->get_uuid(), $url_metric->uuid ); $this->assertSame( $url_metric->get_uuid(), $url_metric->get( 'uuid' ) ); $serialized = $url_metric->jsonSerialize(); @@ -394,12 +388,12 @@ static function ( array $properties ): array { $original_data = $original_url_metric->jsonSerialize(); $this->assertArrayHasKey( 'isColorful', $original_data['elements'][0] ); $this->assertSame( 'false', $original_data['elements'][0]['isColorful'] ); - $this->assertSame( 'false', $original_url_metric->elements[0]['isColorful'] ); + $this->assertSame( 'false', $original_url_metric->get_elements()[0]['isColorful'] ); $extended_data = $extended_url_metric->jsonSerialize(); $this->assertArrayHasKey( 'isColorful', $extended_data['elements'][0] ); $this->assertFalse( $extended_data['elements'][0]['isColorful'] ); - $this->assertFalse( $extended_url_metric->elements[0]['isColorful'] ); + $this->assertFalse( $extended_url_metric->get_elements()[0]['isColorful'] ); }, ), @@ -464,9 +458,7 @@ static function ( array $properties ): array { /** * Tests construction with extended schema. * - * @covers ::jsonSerialize - * @covers ::get - * @covers ::__get + * @covers ::get_json_schema * * @dataProvider data_provider_to_test_constructor_with_extended_schema *