Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 35 additions & 17 deletions config/data.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
<?php

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Enumerable;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Spatie\LaravelData\Casts\EnumCast;
use Spatie\LaravelData\Casts\EnumerableCast;
use Spatie\LaravelData\Normalizers\ArrayableNormalizer;
use Spatie\LaravelData\Normalizers\ArrayNormalizer;
use Spatie\LaravelData\Normalizers\JsonNormalizer;
use Spatie\LaravelData\Normalizers\ModelNormalizer;
use Spatie\LaravelData\Normalizers\ObjectNormalizer;
use Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer;
use Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer;
use Spatie\LaravelData\RuleInferrers\NullableRuleInferrer;
use Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer;
use Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer;
use Spatie\LaravelData\Support\Creation\ValidationStrategy;
use Spatie\LaravelData\Transformers\ArrayableTransformer;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
use Spatie\LaravelData\Transformers\EnumTransformer;

return [
/*
Expand Down Expand Up @@ -38,19 +56,19 @@
* types.
*/
'transformers' => [
DateTimeInterface::class => \Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer::class,
\Illuminate\Contracts\Support\Arrayable::class => \Spatie\LaravelData\Transformers\ArrayableTransformer::class,
BackedEnum::class => Spatie\LaravelData\Transformers\EnumTransformer::class,
DateTimeInterface::class => DateTimeInterfaceTransformer::class,
Arrayable::class => ArrayableTransformer::class,
BackedEnum::class => EnumTransformer::class,
],

/*
* Global casts will cast values into complex types when creating a data
* object from simple types.
*/
'casts' => [
DateTimeInterface::class => Spatie\LaravelData\Casts\DateTimeInterfaceCast::class,
BackedEnum::class => Spatie\LaravelData\Casts\EnumCast::class,
Enumerable::class => Spatie\LaravelData\Casts\EnumerableCast::class,
DateTimeInterface::class => DateTimeInterfaceCast::class,
BackedEnum::class => EnumCast::class,
Enumerable::class => EnumerableCast::class,
],

/*
Expand All @@ -59,11 +77,11 @@
* the type of the property.
*/
'rule_inferrers' => [
Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\NullableRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer::class,
Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer::class,
SometimesRuleInferrer::class,
NullableRuleInferrer::class,
RequiredRuleInferrer::class,
BuiltInTypesRuleInferrer::class,
AttributesRuleInferrer::class,
],

/*
Expand All @@ -72,12 +90,12 @@
* every data object, unless overridden in a specific data object class.
*/
'normalizers' => [
Spatie\LaravelData\Normalizers\ModelNormalizer::class,
ModelNormalizer::class,
// Spatie\LaravelData\Normalizers\FormRequestNormalizer::class,
Spatie\LaravelData\Normalizers\ArrayableNormalizer::class,
Spatie\LaravelData\Normalizers\ObjectNormalizer::class,
Spatie\LaravelData\Normalizers\ArrayNormalizer::class,
Spatie\LaravelData\Normalizers\JsonNormalizer::class,
ArrayableNormalizer::class,
ObjectNormalizer::class,
ArrayNormalizer::class,
JsonNormalizer::class,
],

/*
Expand Down Expand Up @@ -126,7 +144,7 @@
* method. By default, only when a request is passed the data is being validated. This
* behaviour can be changed to always validate or to completely disable validation.
*/
'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::OnlyRequests->value,
'validation_strategy' => ValidationStrategy::OnlyRequests->value,

/*
* A data object can map the names of its properties when transforming (output) or when
Expand Down
15 changes: 14 additions & 1 deletion src/DTOs/Google/ImmersiveProductResults.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public function __construct(
public readonly ?ImmersiveProductAboutTheProduct $about_the_product = null,
public readonly ?array $thumbnails = [],
public readonly ?int $reviews = null,
public readonly ?int $rating = null,
public readonly ?float $rating = null,
public readonly ?string $stores_next_page_token = null,
public readonly ?array $ratings = null,
/* @var null|ImmersiveProductStore[] $stores */
Expand All @@ -28,4 +28,17 @@ public function __construct(
public readonly ?array $more_options = null,
public readonly ?array $variants = null,
) {}

/**
* SerpApi can send `title` as a non-string map rather than the product title.
* A title that is not a string carries no product name, so it maps to null.
*/
public static function prepareForPipeline(array $properties): array
{
if (isset($properties['title']) && ! is_string($properties['title'])) {
$properties['title'] = null;
}

return $properties;
}
}
38 changes: 38 additions & 0 deletions tests/Feature/SerpApiClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,42 @@ public function test_google_immersive_product_async()
$this->assertEquals('68f12e7fcc56d34e74b53c3e', $immersiveProductResponse->search_metadata->id);
$this->assertEquals('Folgers Classic Roast Ground Coffee', $immersiveProductResponse->product_results->title);
}

/**
* SerpApi can return product_results carrying only a non-string title map and no stores.
*
* @throws GuzzleException
* @throws Throwable
*/
public function test_google_immersive_product_result_with_non_string_title()
{
Http::fake([
'https://serpapi.com/searches/68f12e7fcc56d34e74b53c3f.json?api_key=tests' => Http::response($this->getFixtureJsonContent('google_immersive_product_response_title_map.json'), 200),
]);

$client = new SerpApiClient(apiKey: 'tests');

$immersiveProductResponse = $client->retrieveGoogleImmersiveProductResult('68f12e7fcc56d34e74b53c3f');

$this->assertEquals('68f12e7fcc56d34e74b53c3f', $immersiveProductResponse->search_metadata->id);
$this->assertNull($immersiveProductResponse->product_results->title);
$this->assertNull($immersiveProductResponse->product_results->stores);
}

/**
* @throws GuzzleException
* @throws Throwable
*/
public function test_google_immersive_product_fractional_rating()
{
Http::fake([
'https://serpapi.com/search.json?engine=google_immersive_product&page_token=asdf&api_key=tests' => Http::response($this->getFixtureJsonContent('google_immersive_product_response.json'), 200),
]);

$client = new SerpApiClient(apiKey: 'tests');

$immersiveProductResponse = $client->makeGoogleImmersiveProductRequest('asdf');

$this->assertSame(4.6, $immersiveProductResponse->product_results->rating);
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/google_immersive_product_response_title_map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"search_metadata": {
"id": "68f12e7fcc56d34e74b53c3f",
"status": "Success",
"json_endpoint": "https://serpapi.com/searches/f4d4ca32e4122e52/68f12e7fcc56d34e74b53c3e.json",
"created_at": "2025-10-16 17:42:23 UTC",
"processed_at": "2025-10-16 17:42:23 UTC",
"google_immersive_product_url": "https://www.google.com/async/oapv?yv=3&ei=145qZ7CeKu7n5NoPmrjS8Qo&q=+Coffee&gl=us&hl=en&cs=1&async=catalogid:16914877625280977865,gpcid:3245476400833551948,headlineOfferDocid:3238224844624912169,rds:PC_3245476400833551948|PROD_PC_3245476400833551948,imageDocid:476749915906327914,isp:true,query:%20Coffee,mid:576462612280130481,pvt:hg,sori:0,mno:3,_fmt:jspb",
"raw_html_file": "https://serpapi.com/searches/f4d4ca32e4122e52/68f12e7fcc56d34e74b53c3e.html",
"prettify_html_file": "https://serpapi.com/searches/f4d4ca32e4122e52/68f12e7fcc56d34e74b53c3e.prettify",
"total_time_taken": 2.41
},
"search_parameters": {
"engine": "google_immersive_product",
"page_token": "eyJlaSI6IjE0NXFaN0NlS3U3bjVOb1BtcmpTOFFvIiwicHJvZHVjdGlkIjoiIiwiY2F0YWxvZ2lkIjoiMTY5MTQ4Nzc2MjUyODA5Nzc4NjUiLCJoZWFkbGluZU9mZmVyRG9jaWQiOiIzMjM4MjI0ODQ0NjI0OTEyMTY5IiwiaW1hZ2VEb2NpZCI6IjQ3Njc0OTkxNTkwNjMyNzkxNCIsInJkcyI6IlBDXzMyNDU0NzY0MDA4MzM1NTE5NDh8UFJPRF9QQ18zMjQ1NDc2NDAwODMzNTUxOTQ4IiwicXVlcnkiOiIrQ29mZmVlIiwiZ3BjaWQiOiIzMjQ1NDc2NDAwODMzNTUxOTQ4IiwibWlkIjoiNTc2NDYyNjEyMjgwMTMwNDgxIiwicHZ0IjoiaGciLCJ1dWxlIjpudWxsfQ==",
"more_stores": true
},
"product_results": {
"title": {
"108": 1
}
}
}