diff --git a/.github/workflows/serpapi-php.yml b/.github/workflows/serpapi-php.yml
new file mode 100644
index 0000000..17faf9a
--- /dev/null
+++ b/.github/workflows/serpapi-php.yml
@@ -0,0 +1,66 @@
+name: PHP build
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.5'
+ extensions: mbstring, intl, curl
+ tools: composer
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run linter
+ run: composer run-script lint
+
+ run:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring, intl, curl
+ ini-values: post_max_size=256M, max_execution_time=180
+
+ - name: Verify required PHP extensions
+ run: |
+ php -m | grep -i curl
+ php -m | grep -i json
+
+ - name: Validate composer.json
+ run: composer validate
+
+ - name: Cache Composer packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.composer/cache
+ key: ${{ runner.os }}-php-${{ matrix.php-versions }}-composer-${{ hashFiles('composer.json') }}
+ restore-keys: |
+ ${{ runner.os }}-php-${{ matrix.php-versions }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run test suite
+ run: composer run-script test
+ env:
+ API_KEY: ${{ secrets.API_KEY }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..04b4bda
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+vendor/*
+.vscode/*
+
+# PHPUnit
+.phpunit.result.cache
+
+# MacOS Specific
+.DS_Store
+
+composer.lock
+composer.phar
+tmp/
+
+.env
+.env.*
\ No newline at end of file
diff --git a/MIT-LICENSE.txt b/MIT-LICENSE.txt
new file mode 100644
index 0000000..01ae8df
--- /dev/null
+++ b/MIT-LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018-2026 SerpApi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9cc42ed
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+# Default target
+all: install readme lint test
+
+# Clean up the project
+clean:
+ rm -rf vendor/
+
+# Install the necessary libraries and dependencies
+install:
+ composer install --prefer-dist --no-progress
+
+# Run PSR12 lint
+lint:
+ vendor/bin/phpcs
+
+# Run the tests
+test:
+ vendor/bin/phpunit -c phpunit.xml
+
+# Generate README from ERB template
+readme:
+ erb -T '-' README.md.erb > README.md
diff --git a/README.md b/README.md
index 5f1499d..098bd62 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,554 @@
-# serpapi-php
-SerpApi PHP official package [draft]
+# SerpApi PHP Library
+
+[](https://packagist.org/packages/serpapi/serpapi-php)
+[](https://www.php.net)
+[](https://github.com/serpapi/serpapi-php/blob/master/MIT-LICENSE.txt)
+
+Integrate search data into your PHP application. This library is the official wrapper for [SerpApi](https://serpapi.com).
+
+SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and [more](https://serpapi.com).
+
+This is the new library provided by SerpApi as a replacement for our old library that can be found [here](https://github.com/serpapi/google-search-results-php). Feel free to contact us in case you need any help: contact@serpapi.com
+
+[The full documentation is available here.](https://serpapi.com/search-api)
+
+The following services are provided:
+ * [Search API](https://serpapi.com/search-api)
+ * [Location API](https://serpapi.com/locations-api)
+ * [Search Archive API](https://serpapi.com/search-archive-api)
+ * [Account API](https://serpapi.com/account-api)
+
+SerpApi provides a [script builder](https://serpapi.com/playground) to get you started quickly.
+
+## Installation
+
+PHP 7.2+ with `ext-curl` and `ext-json` must be installed along with [composer](https://getcomposer.org/) dependency management tool.
+
+Tested PHP versions:
+* 7.2
+* 7.3
+* 7.4
+* 8.0
+* 8.1
+* 8.2
+* 8.3
+* 8.4
+* 8.5
+
+Package available from [packagist](https://packagist.org/packages/serpapi/serpapi-php).
+
+## Quick start
+
+```bash
+composer require serpapi/serpapi-php
+```
+
+## Simple Usage
+
+```php
+require 'vendor/autoload.php';
+
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+This example runs a search for "coffee" on Google. It returns the results as a PHP object decoded from JSON.
+
+See the [playground](https://serpapi.com/playground) to generate your own code.
+
+## Configuration
+
+### API key
+
+The API key can be set in the constructor or later via `setApiKey`:
+
+```php
+use SerpApi\Client;
+
+// via constructor
+$client = new Client('Your Private Key');
+
+// or later
+$client = new Client();
+$client->setApiKey('Your Private Key');
+```
+
+Get your API key from [serpapi.com/dashboard](https://serpapi.com/dashboard).
+
+### Engine
+
+The default engine is `google`. You can change it via the second constructor parameter:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'), 'bing');
+$results = $client->search(['q' => 'coffee']);
+```
+
+### Timeout
+
+The default request timeout is 120 seconds. Customize it via the third constructor parameter:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'), 'google', 30);
+```
+
+## Search API
+
+### Search Google
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google',
+ 'tbm' => 'isch',
+ 'q' => 'coffee',
+]);
+
+print_r($results->images_results);
+```
+
+ * source: [tests/ExampleSearchGoogleTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleTest.php)
+see: [https://serpapi.com/search-api](https://serpapi.com/search-api)
+
+### Search Google Scholar
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_scholar',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchGoogleScholarTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleScholarTest.php)
+see: [https://serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
+
+### Search Google Autocomplete
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_autocomplete',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchGoogleAutocompleteTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleAutocompleteTest.php)
+see: [https://serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
+
+### Search Google Shopping
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_shopping',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchGoogleShoppingTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleShoppingTest.php)
+see: [https://serpapi.com/google-shopping-api](https://serpapi.com/google-shopping-api)
+
+### Search Google Maps
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_maps',
+ 'q' => 'pizza',
+ 'll' => '@40.7455096,-74.0083012,15.1z',
+ 'type' => 'search',
+]);
+
+print_r($results->local_results);
+```
+
+ * source: [tests/ExampleSearchGoogleMapsTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleMapsTest.php)
+see: [https://serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
+
+### Search Google Jobs
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_jobs',
+ 'q' => 'coffee',
+]);
+
+print_r($results->jobs_results);
+```
+
+ * source: [tests/ExampleSearchGoogleJobsTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleJobsTest.php)
+see: [https://serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
+
+### Search Google Events
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_events',
+ 'q' => 'Events in Austin',
+ 'location' => 'Austin, Texas, United States',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchGoogleEventsTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleEventsTest.php)
+see: [https://serpapi.com/google-events-api](https://serpapi.com/google-events-api)
+
+### Search Google Lens
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_lens',
+ 'url' => 'https://i.imgur.com/5bGzZi7.jpg',
+ 'gl' => 'us',
+ 'hl' => 'en',
+]);
+
+print_r($results->visual_matches);
+```
+
+ * source: [tests/ExampleSearchGoogleLensTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleLensTest.php)
+see: [https://serpapi.com/google-lens-api](https://serpapi.com/google-lens-api)
+
+### Search Google Play
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_play',
+ 'q' => 'kite',
+ 'store' => 'apps',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchGooglePlayTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGooglePlayTest.php)
+see: [https://serpapi.com/google-play-api](https://serpapi.com/google-play-api)
+
+### Search Google Local Services
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'google_local_services',
+ 'q' => 'electrician',
+ 'data_cid' => '6745062158417646970',
+]);
+
+print_r($results->local_ads);
+```
+
+ * source: [tests/ExampleSearchGoogleLocalServicesTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchGoogleLocalServicesTest.php)
+see: [https://serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
+
+### Search Bing
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'bing',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchBingTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchBingTest.php)
+see: [https://serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
+
+### Search Baidu
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'baidu',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchBaiduTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchBaiduTest.php)
+see: [https://serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
+
+### Search Yahoo
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'yahoo',
+ 'p' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchYahooTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchYahooTest.php)
+see: [https://serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
+
+### Search YouTube
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'youtube',
+ 'search_query' => 'coffee',
+]);
+
+print_r($results->video_results);
+```
+
+ * source: [tests/ExampleSearchYoutubeTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchYoutubeTest.php)
+see: [https://serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
+
+### Search Walmart
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'walmart',
+ 'query' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchWalmartTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchWalmartTest.php)
+see: [https://serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
+
+### Search eBay
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'ebay',
+ '_nkw' => 'water',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchEbayTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchEbayTest.php)
+see: [https://serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
+
+### Search Naver
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'naver',
+ 'query' => 'coffee',
+]);
+
+print_r($results->ads_results);
+```
+
+ * source: [tests/ExampleSearchNaverTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchNaverTest.php)
+see: [https://serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
+
+### Search Home Depot
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'home_depot',
+ 'q' => 'table',
+]);
+
+print_r($results->products);
+```
+
+ * source: [tests/ExampleSearchHomeDepotTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchHomeDepotTest.php)
+see: [https://serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
+
+### Search Apple App Store
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'apple_app_store',
+ 'term' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchAppleAppStoreTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchAppleAppStoreTest.php)
+see: [https://serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
+
+### Search DuckDuckGo
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'engine' => 'duckduckgo',
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+ * source: [tests/ExampleSearchDuckduckgoTest.php](https://github.com/serpapi/serpapi-php/blob/master/tests/ExampleSearchDuckduckgoTest.php)
+see: [https://serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
+
+## APIs supported
+
+### Location API
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$locations = $client->location(['q' => 'Austin', 'limit' => 3]);
+
+echo "Number of locations: " . count($locations) . "\n";
+print_r($locations);
+```
+
+NOTE: `api_key` is not required for this endpoint.
+
+### Search Archive API
+
+First, run a search and save the search ID:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'q' => 'Coffee',
+ 'location' => 'Austin, Texas',
+]);
+$search_id = $results->search_metadata->id;
+```
+
+Now retrieve the previous search from the archive (free of charge):
+
+```php
+$archived = $client->searchArchive($search_id);
+print_r($archived);
+```
+
+### Account API
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$account = $client->account();
+print_r($account);
+```
+
+### HTML results
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$html = $client->html(['q' => 'Coffee']);
+
+echo strlen($html) . " bytes of HTML\n";
+```
+
+## Error handling
+
+`SerpApiException` includes structured context for HTTP and API errors (status code, endpoint, search params, search id).
+
+```php
+use SerpApi\Client;
+use SerpApi\SerpApiException;
+
+try {
+ $client = new Client('invalid_key');
+ $client->search(['q' => 'test']);
+} catch (SerpApiException $exception) {
+ echo $exception->getMessage() . "\n";
+ // HTTP request failed with status: 401 error: Invalid API key... from url: https://serpapi.com/search
+
+ echo $exception->getSerpApiError() . "\n";
+ echo $exception->getResponseStatus() . "\n";
+ echo $exception->getSearchId() . "\n";
+ print_r($exception->getSearchParams());
+ print_r($exception->toArray());
+}
+```
+
+## Testing
+
+We love "true open source", "continuous integration", and Test Driven Development (TDD).
+We use PHPUnit to test our infrastructure around the clock using [GitHub Actions](https://github.com/serpapi/serpapi-php/actions/workflows/serpapi-php.yml) to achieve the best QoS (Quality Of Service).
+
+The `tests/` directory includes specifications which serve the dual purposes of examples and functional tests.
+
+Set your secret API key in your shell before running tests:
+
+```bash
+export API_KEY="your_secret_key"
+```
+
+Install dependencies and run the test suite:
+
+```bash
+make install
+make test
+```
+
+Contributions are welcome. Feel free to submit a pull request!
+
+## Change log
+
+ * 1.0 - First stable version
+
+## Conclusion
+
+SerpApi supports all the major search engines. Google has the more advanced support with all the major services available: Images, News, Shopping and more...
+
+[The full documentation is available here.](https://serpapi.com/search-api)
+
+Authors: Victor Benarbia victor@serpapi.com, Alaa Abdulridha alaa@serpapi.com
+For more information: https://serpapi.com
+
+## License
+
+[MIT](MIT-LICENSE.txt)
diff --git a/README.md.erb b/README.md.erb
new file mode 100644
index 0000000..9384cab
--- /dev/null
+++ b/README.md.erb
@@ -0,0 +1,330 @@
+<%-
+def snippet(path)
+ source = File.read(path)
+
+ params_match = source.match(/\$this->searchParams\s*=\s*(\[.*?\]);/m)
+ raise "Could not find $this->searchParams in #{path}" unless params_match
+ raw_params = params_match[1]
+
+ # Re-indent: keep opening/closing bracket at column 0, indent inner lines by 2 spaces
+ raw_lines = raw_params.lines.map { |l| l.strip }
+ params = raw_lines.each_with_index.map { |l, i|
+ (i == 0 || i == raw_lines.length - 1) ? l : " #{l}"
+ }.join("\n")
+
+ property = source[/assertResponseHasProperty\(\$response,\s*'([^']+)'/, 1] || 'organic_results'
+
+ code = "use SerpApi\\Client;\n\n"
+ code += "$client = new Client(getenv('API_KEY'));\n"
+ code += "$results = $client->search(#{params});\n\n"
+ code += "print_r($results->#{property});\n"
+
+ "```php\n#{code}```\n\n * source: [#{path}](https://github.com/serpapi/serpapi-php/blob/master/#{path})"
+end
+-%>
+# SerpApi PHP Library
+
+[](https://packagist.org/packages/serpapi/serpapi-php)
+[](https://www.php.net)
+[](https://github.com/serpapi/serpapi-php/blob/master/MIT-LICENSE.txt)
+
+Integrate search data into your PHP application. This library is the official wrapper for [SerpApi](https://serpapi.com).
+
+SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and [more](https://serpapi.com).
+
+This is the new library provided by SerpApi as a replacement for our old library that can be found [here](https://github.com/serpapi/google-search-results-php). Feel free to contact us in case you need any help: contact@serpapi.com
+
+[The full documentation is available here.](https://serpapi.com/search-api)
+
+The following services are provided:
+ * [Search API](https://serpapi.com/search-api)
+ * [Location API](https://serpapi.com/locations-api)
+ * [Search Archive API](https://serpapi.com/search-archive-api)
+ * [Account API](https://serpapi.com/account-api)
+
+SerpApi provides a [script builder](https://serpapi.com/playground) to get you started quickly.
+
+## Installation
+
+PHP 7.2+ with `ext-curl` and `ext-json` must be installed along with [composer](https://getcomposer.org/) dependency management tool.
+
+Tested PHP versions:
+* 7.2
+* 7.3
+* 7.4
+* 8.0
+* 8.1
+* 8.2
+* 8.3
+* 8.4
+* 8.5
+
+Package available from [packagist](https://packagist.org/packages/serpapi/serpapi-php).
+
+## Quick start
+
+```bash
+composer require serpapi/serpapi-php
+```
+
+## Simple Usage
+
+```php
+require 'vendor/autoload.php';
+
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'q' => 'coffee',
+]);
+
+print_r($results->organic_results);
+```
+
+This example runs a search for "coffee" on Google. It returns the results as a PHP object decoded from JSON.
+
+See the [playground](https://serpapi.com/playground) to generate your own code.
+
+## Configuration
+
+### API key
+
+The API key can be set in the constructor or later via `setApiKey`:
+
+```php
+use SerpApi\Client;
+
+// via constructor
+$client = new Client('Your Private Key');
+
+// or later
+$client = new Client();
+$client->setApiKey('Your Private Key');
+```
+
+Get your API key from [serpapi.com/dashboard](https://serpapi.com/dashboard).
+
+### Engine
+
+The default engine is `google`. You can change it via the second constructor parameter:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'), 'bing');
+$results = $client->search(['q' => 'coffee']);
+```
+
+### Timeout
+
+The default request timeout is 120 seconds. Customize it via the third constructor parameter:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'), 'google', 30);
+```
+
+## Search API
+
+### Search Google
+<%= snippet('tests/ExampleSearchGoogleTest.php') %>
+see: [https://serpapi.com/search-api](https://serpapi.com/search-api)
+
+### Search Google Scholar
+<%= snippet('tests/ExampleSearchGoogleScholarTest.php') %>
+see: [https://serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
+
+### Search Google Autocomplete
+<%= snippet('tests/ExampleSearchGoogleAutocompleteTest.php') %>
+see: [https://serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
+
+### Search Google Shopping
+<%= snippet('tests/ExampleSearchGoogleShoppingTest.php') %>
+see: [https://serpapi.com/google-shopping-api](https://serpapi.com/google-shopping-api)
+
+### Search Google Maps
+<%= snippet('tests/ExampleSearchGoogleMapsTest.php') %>
+see: [https://serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
+
+### Search Google Jobs
+<%= snippet('tests/ExampleSearchGoogleJobsTest.php') %>
+see: [https://serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
+
+### Search Google Events
+<%= snippet('tests/ExampleSearchGoogleEventsTest.php') %>
+see: [https://serpapi.com/google-events-api](https://serpapi.com/google-events-api)
+
+### Search Google Lens
+<%= snippet('tests/ExampleSearchGoogleLensTest.php') %>
+see: [https://serpapi.com/google-lens-api](https://serpapi.com/google-lens-api)
+
+### Search Google Play
+<%= snippet('tests/ExampleSearchGooglePlayTest.php') %>
+see: [https://serpapi.com/google-play-api](https://serpapi.com/google-play-api)
+
+### Search Google Local Services
+<%= snippet('tests/ExampleSearchGoogleLocalServicesTest.php') %>
+see: [https://serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
+
+### Search Bing
+<%= snippet('tests/ExampleSearchBingTest.php') %>
+see: [https://serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
+
+### Search Baidu
+<%= snippet('tests/ExampleSearchBaiduTest.php') %>
+see: [https://serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
+
+### Search Yahoo
+<%= snippet('tests/ExampleSearchYahooTest.php') %>
+see: [https://serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
+
+### Search YouTube
+<%= snippet('tests/ExampleSearchYoutubeTest.php') %>
+see: [https://serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
+
+### Search Walmart
+<%= snippet('tests/ExampleSearchWalmartTest.php') %>
+see: [https://serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
+
+### Search eBay
+<%= snippet('tests/ExampleSearchEbayTest.php') %>
+see: [https://serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
+
+### Search Naver
+<%= snippet('tests/ExampleSearchNaverTest.php') %>
+see: [https://serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
+
+### Search Home Depot
+<%= snippet('tests/ExampleSearchHomeDepotTest.php') %>
+see: [https://serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
+
+### Search Apple App Store
+<%= snippet('tests/ExampleSearchAppleAppStoreTest.php') %>
+see: [https://serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
+
+### Search DuckDuckGo
+<%= snippet('tests/ExampleSearchDuckduckgoTest.php') %>
+see: [https://serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
+
+## APIs supported
+
+### Location API
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$locations = $client->location(['q' => 'Austin', 'limit' => 3]);
+
+echo "Number of locations: " . count($locations) . "\n";
+print_r($locations);
+```
+
+NOTE: `api_key` is not required for this endpoint.
+
+### Search Archive API
+
+First, run a search and save the search ID:
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$results = $client->search([
+ 'q' => 'Coffee',
+ 'location' => 'Austin, Texas',
+]);
+$search_id = $results->search_metadata->id;
+```
+
+Now retrieve the previous search from the archive (free of charge):
+
+```php
+$archived = $client->searchArchive($search_id);
+print_r($archived);
+```
+
+### Account API
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$account = $client->account();
+print_r($account);
+```
+
+### HTML results
+
+```php
+use SerpApi\Client;
+
+$client = new Client(getenv('API_KEY'));
+$html = $client->html(['q' => 'Coffee']);
+
+echo strlen($html) . " bytes of HTML\n";
+```
+
+## Error handling
+
+`SerpApiException` includes structured context for HTTP and API errors (status code, endpoint, search params, search id).
+
+```php
+use SerpApi\Client;
+use SerpApi\SerpApiException;
+
+try {
+ $client = new Client('invalid_key');
+ $client->search(['q' => 'test']);
+} catch (SerpApiException $exception) {
+ echo $exception->getMessage() . "\n";
+ // HTTP request failed with status: 401 error: Invalid API key... from url: https://serpapi.com/search
+
+ echo $exception->getSerpApiError() . "\n";
+ echo $exception->getResponseStatus() . "\n";
+ echo $exception->getSearchId() . "\n";
+ print_r($exception->getSearchParams());
+ print_r($exception->toArray());
+}
+```
+
+## Testing
+
+We love "true open source", "continuous integration", and Test Driven Development (TDD).
+We use PHPUnit to test our infrastructure around the clock using [GitHub Actions](https://github.com/serpapi/serpapi-php/actions/workflows/serpapi-php.yml) to achieve the best QoS (Quality Of Service).
+
+The `tests/` directory includes specifications which serve the dual purposes of examples and functional tests.
+
+Set your secret API key in your shell before running tests:
+
+```bash
+export API_KEY="your_secret_key"
+```
+
+Install dependencies and run the test suite:
+
+```bash
+make install
+make test
+```
+
+Contributions are welcome. Feel free to submit a pull request!
+
+## Change log
+
+ * 1.0 - First stable version
+
+## Conclusion
+
+SerpApi supports all the major search engines. Google has the more advanced support with all the major services available: Images, News, Shopping and more...
+
+[The full documentation is available here.](https://serpapi.com/search-api)
+
+Authors: Victor Benarbia victor@serpapi.com, Alaa Abdulridha alaa@serpapi.com
+For more information: https://serpapi.com
+
+## License
+
+[MIT](MIT-LICENSE.txt)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3cb274b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,67 @@
+{
+ "name": "serpapi/serpapi-php",
+ "description": "Get Google, Bing, Baidu, eBay, Yahoo, Yandex, Home Depot, Naver, Apple, DuckDuckGo, Yelp and YouTube search results via SerpApi.com",
+ "type": "library",
+ "keywords": [
+ "Google",
+ "Serp",
+ "Search",
+ "Result",
+ "YouTube",
+ "Walmart",
+ "Yandex",
+ "Localisation",
+ "REST",
+ "api",
+ "client",
+ "curl",
+ "JSON",
+ "XML",
+ "Naver",
+ "DuckDuckGo",
+ "Apple",
+ "store",
+ "homedepot",
+ "datamining",
+ "ml",
+ "learning",
+ "scrape"
+ ],
+ "homepage": "https://github.com/serpapi/serpapi-php",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Alaa Abdulridha",
+ "role": "Software Engineer",
+ "email": "alaa@serpapi.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/serpapi/serpapi-php/issues",
+ "source": "https://github.com/serpapi/serpapi-php",
+ "docs": "https://serpapi.com/integrations/php"
+ },
+ "require": {
+ "php": ">=7.2",
+ "ext-curl": "*",
+ "ext-json": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.52 || ^9.6 || ^10.5 || ^11.5 || ^12.5 || ^13.0",
+ "squizlabs/php_codesniffer": "^3.10"
+ },
+ "autoload": {
+ "psr-4": {
+ "SerpApi\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "SerpApi\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "phpunit -c phpunit.xml",
+ "lint": "phpcs"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..7ba24af
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,26 @@
+
+
+ PSR-12 with 2-space indentation
+
+ src
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..74bd3ca
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ tests/
+
+
+
+
diff --git a/src/Client.php b/src/Client.php
new file mode 100644
index 0000000..2d4b034
--- /dev/null
+++ b/src/Client.php
@@ -0,0 +1,310 @@
+apiKey = $apiKey;
+ $this->engine = $engine;
+ $this->timeout = $timeout;
+ }
+
+ /**
+ * Set the SerpApi API key.
+ *
+ * @param string $apiKey
+ * @throws SerpApiException
+ */
+ public function setApiKey(string $apiKey): void
+ {
+ if (empty($apiKey)) {
+ throw new SerpApiException('api_key must have a value');
+ }
+
+ $this->apiKey = $apiKey;
+ }
+
+ /**
+ * Get the current API key.
+ */
+ public function getApiKey(): string
+ {
+ return $this->apiKey;
+ }
+
+ /**
+ * Get the current engine.
+ */
+ public function getEngine(): string
+ {
+ return $this->engine;
+ }
+
+ /**
+ * Run a search and return decoded JSON.
+ *
+ * @param array $params
+ * @throws SerpApiException
+ */
+ public function search(array $params = []): object
+ {
+ return $this->get('/search', 'json', $params);
+ }
+
+ /**
+ * Run a search and return raw HTML.
+ *
+ * @param array $params
+ * @throws SerpApiException
+ */
+ public function html(array $params = []): string
+ {
+ return $this->get('/search', 'html', $params);
+ }
+
+ /**
+ * Get account information using Account API.
+ *
+ * @throws SerpApiException
+ */
+ public function account(?string $apiKey = null): object
+ {
+ $params = empty($apiKey) ? [] : ['api_key' => $apiKey];
+ return $this->get('/account', 'json', $params);
+ }
+
+ /**
+ * Get locations using Location API.
+ *
+ * @param array $params
+ * @return array
+ * @throws SerpApiException
+ */
+ public function location(array $params = []): array
+ {
+ return $this->get('/locations.json', 'json', $params);
+ }
+
+ /**
+ * Retrieve search result from the Search Archive API.
+ *
+ * @return object|string
+ * @throws SerpApiException
+ */
+ public function searchArchive(string $searchId, string $format = 'json')
+ {
+ if (empty($searchId)) {
+ throw new SerpApiException('search_id must be present');
+ }
+
+ if (!in_array($format, ['json', 'html'], true)) {
+ throw new SerpApiException('format must be json or html');
+ }
+
+ $safeSearchId = rawurlencode($searchId);
+ return $this->get("/searches/{$safeSearchId}.{$format}", $format, []);
+ }
+
+ /**
+ * @param array $params
+ * @return object|array|string
+ * @throws SerpApiException
+ */
+ private function get(string $endpoint, string $format = 'json', array $params = [])
+ {
+ if (!in_array($format, ['json', 'html'], true)) {
+ throw new SerpApiException("Unsupported format '$format'. Expected 'html' or 'json'.");
+ }
+
+ $apiKey = $params['api_key'] ?? $this->apiKey;
+
+ $requiresKey = strpos($endpoint, '/locations') !== 0;
+ if ($requiresKey && empty($apiKey)) {
+ throw new SerpApiException('api_key must be present');
+ }
+
+ $defaultQuery = [
+ 'engine' => $this->engine,
+ 'source' => 'php',
+ ];
+
+ if (!empty($apiKey)) {
+ $defaultQuery['api_key'] = $apiKey;
+ }
+
+ $query = array_merge($defaultQuery, $params);
+ $query['output'] = $format;
+
+ $url = self::BASE_URL . $endpoint . '?' . http_build_query($query);
+
+ $requestResult = $this->request($url);
+ $response = $requestResult['response'];
+ $httpCode = $requestResult['http_code'];
+ $curlError = $requestResult['curl_error'];
+
+ if ($response === false) {
+ throw new SerpApiException('cURL error: ' . $curlError);
+ }
+
+ if ($format === 'html') {
+ if ($httpCode === 200) {
+ return $response;
+ }
+
+ $this->raiseHttpError($httpCode, $endpoint, $query, null, null, 'html');
+ }
+
+ $decoded = json_decode($response);
+ if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
+ $this->raiseParserError($httpCode, $endpoint, $query);
+ }
+
+ $serpApiError = (is_object($decoded) && isset($decoded->error)) ? $decoded->error : null;
+ $searchId = (is_object($decoded) && isset($decoded->search_metadata->id))
+ ? (string) $decoded->search_metadata->id
+ : null;
+
+ if ($httpCode === 200) {
+ if ($serpApiError !== null) {
+ $this->raiseHttpError($httpCode, $endpoint, $query, $serpApiError, $searchId, 'json');
+ }
+
+ return $decoded;
+ }
+
+ $this->raiseHttpError($httpCode, $endpoint, $query, $serpApiError, $searchId, 'json');
+ }
+
+ /**
+ * @return array{response: string|false, http_code: int, curl_error: string}
+ * @throws SerpApiException
+ */
+ private function request(string $url): array
+ {
+ $ch = curl_init();
+ if ($ch === false) {
+ throw new SerpApiException('Failed to initialize cURL handle');
+ }
+
+ try {
+ $isConfigured = curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_USERAGENT => 'serpapi-php/' . self::VERSION,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => $this->timeout,
+ ]);
+
+ if ($isConfigured === false) {
+ throw new SerpApiException('Failed to configure cURL options: ' . curl_error($ch));
+ }
+
+ $response = curl_exec($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $curlError = curl_error($ch);
+
+ return [
+ 'response' => $response,
+ 'http_code' => $httpCode,
+ 'curl_error' => $curlError,
+ ];
+ } finally {
+ if (PHP_VERSION_ID < 80500) {
+ curl_close($ch);
+ }
+
+ $ch = null;
+ }
+ }
+
+ /**
+ * @param array $searchParams
+ * @return never
+ * @throws SerpApiException
+ */
+ private function raiseHttpError(
+ int $responseStatus,
+ string $endpoint,
+ array $searchParams,
+ ?string $serpApiError = null,
+ ?string $searchId = null,
+ string $decoder = 'json'
+ ): void {
+ $message = "HTTP request failed with status: {$responseStatus}";
+ if ($serpApiError !== null) {
+ $message .= " error: {$serpApiError}";
+ }
+ $message .= ' from url: ' . self::BASE_URL . $endpoint;
+ $sanitizedSearchParams = $this->sanitizeSearchParams($searchParams);
+
+ throw new SerpApiException(
+ $message,
+ $serpApiError,
+ $sanitizedSearchParams,
+ $responseStatus,
+ $searchId,
+ $decoder
+ );
+ }
+
+ /**
+ * @param array $searchParams
+ * @return never
+ * @throws SerpApiException
+ */
+ private function raiseParserError(
+ int $responseStatus,
+ string $endpoint,
+ array $searchParams
+ ): void {
+ $sanitizedSearchParams = $this->sanitizeSearchParams($searchParams);
+
+ throw new SerpApiException(
+ 'JSON parse error: ' . json_last_error_msg() . ' on get url: ' . self::BASE_URL . $endpoint,
+ null,
+ $sanitizedSearchParams,
+ $responseStatus,
+ null,
+ 'json'
+ );
+ }
+
+ /**
+ * @param array $searchParams
+ * @param array $keysToRemove
+ * @return array
+ */
+ private function sanitizeSearchParams(array $searchParams, array $keysToRemove = ['api_key']): array
+ {
+ foreach ($keysToRemove as $key) {
+ unset($searchParams[$key]);
+ }
+
+ return $searchParams;
+ }
+}
diff --git a/src/SerpApiException.php b/src/SerpApiException.php
new file mode 100644
index 0000000..d3757b0
--- /dev/null
+++ b/src/SerpApiException.php
@@ -0,0 +1,87 @@
+|null */
+ private $searchParams;
+
+ /** @var int|null */
+ private $responseStatus;
+
+ /** @var string|null */
+ private $searchId;
+
+ /** @var string|null */
+ private $decoder;
+
+ /**
+ * @param array|null $searchParams
+ */
+ public function __construct(
+ string $message = '',
+ ?string $serpApiError = null,
+ ?array $searchParams = null,
+ ?int $responseStatus = null,
+ ?string $searchId = null,
+ ?string $decoder = null,
+ ?\Throwable $previous = null
+ ) {
+ parent::__construct($message, 0, $previous);
+
+ $this->serpApiError = $serpApiError;
+ $this->searchParams = $searchParams;
+ $this->responseStatus = $responseStatus;
+ $this->searchId = $searchId;
+ $this->decoder = $decoder;
+ }
+
+ public function getSerpApiError(): ?string
+ {
+ return $this->serpApiError;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getSearchParams(): ?array
+ {
+ return $this->searchParams;
+ }
+
+ public function getResponseStatus(): ?int
+ {
+ return $this->responseStatus;
+ }
+
+ public function getSearchId(): ?string
+ {
+ return $this->searchId;
+ }
+
+ public function getDecoder(): ?string
+ {
+ return $this->decoder;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return array_filter([
+ 'message' => $this->getMessage(),
+ 'serpApiError' => $this->serpApiError,
+ 'searchParams' => $this->searchParams,
+ 'responseStatus' => $this->responseStatus,
+ 'searchId' => $this->searchId,
+ 'decoder' => $this->decoder,
+ ], static function ($value) {
+ return $value !== null;
+ });
+ }
+}
diff --git a/tests/ClientIntegrationTest.php b/tests/ClientIntegrationTest.php
new file mode 100644
index 0000000..2acd6db
--- /dev/null
+++ b/tests/ClientIntegrationTest.php
@@ -0,0 +1,58 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'q' => 'Coffee',
+ 'location' => 'Austin,Texas',
+ ];
+ }
+
+ public function testAccount()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->account();
+ $this->assertEquals($client->getApiKey(), $response->api_key);
+ }
+
+ public function testHtml()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->html($this->searchParams);
+ $this->assertGreaterThan(10000, strlen($response));
+ }
+
+ public function testSearch()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testLocation()
+ {
+ $client = $this->serpApiClient();
+ $location_list = $client->location(['q' => 'Austin', 'limit' => 3]);
+ $this->assertCount(3, $location_list);
+ $this->assertStringContainsString('Austin', $location_list[0]->name);
+ $this->assertGreaterThan(0, $location_list[0]->google_id);
+ }
+
+ public function testSearchArchive()
+ {
+ $client = $this->serpApiClient();
+ $result = $client->search($this->searchParams);
+ $archived_result = $client->searchArchive($result->search_metadata->id);
+ $this->assertEquals($result->search_metadata->id, $archived_result->search_metadata->id);
+ }
+}
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
new file mode 100644
index 0000000..c2f3920
--- /dev/null
+++ b/tests/ClientTest.php
@@ -0,0 +1,74 @@
+expectException(SerpApiException::class);
+ $this->expectExceptionMessage('api_key must be present');
+ $client = new Client();
+ $client->search(['q' => 'Coffee']);
+ }
+
+ public function testThrowsWhenEngineEmpty()
+ {
+ $this->expectException(SerpApiException::class);
+ $this->expectExceptionMessage('engine must be present');
+ new Client('test_key', '');
+ }
+
+ public function testThrowsWhenApiKeyInvalid()
+ {
+ $this->expectException(SerpApiException::class);
+ $this->expectExceptionMessageMatches('/Invalid API key/i');
+ $client = new Client('not_valid_key');
+ $client->search(['q' => 'Coffee']);
+ }
+
+ public function testSearchArchiveThrowsWhenIdEmpty()
+ {
+ $this->expectException(SerpApiException::class);
+ $this->expectExceptionMessage('search_id must be present');
+ $client = new Client('test_key');
+ $client->searchArchive('');
+ }
+
+ public function testSearchArchiveThrowsWhenFormatInvalid()
+ {
+ $this->expectException(SerpApiException::class);
+ $this->expectExceptionMessage('format must be json or html');
+ $client = new Client('test_key');
+ $client->searchArchive('abc', 'xml');
+ }
+
+ public function testSetApiKeyThrowsWhenEmpty()
+ {
+ $this->expectException(SerpApiException::class);
+ $this->expectExceptionMessage('api_key must have a value');
+ $client = new Client('test_key');
+ $client->setApiKey('');
+ }
+
+ public function testSetApiKeyUpdatesValue()
+ {
+ $client = new Client('initial_key');
+ $client->setApiKey('updated_key');
+ $this->assertEquals('updated_key', $client->getApiKey());
+ }
+
+ public function testGetEngineReturnsEngine()
+ {
+ $client = new Client('test_key', 'bing');
+ $this->assertEquals('bing', $client->getEngine());
+ }
+}
diff --git a/tests/EnginesTest.php b/tests/EnginesTest.php
new file mode 100644
index 0000000..1caf5f5
--- /dev/null
+++ b/tests/EnginesTest.php
@@ -0,0 +1,75 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'q' => 'Coffee',
+ 'location' => 'Austin,Texas',
+ ];
+ }
+
+ public function testBingSearch()
+ {
+ $client = $this->serpApiClient('bing');
+ $response = $client->search($this->searchParams);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testBaiduSearch()
+ {
+ $client = $this->serpApiClient('baidu');
+ $response = $client->search($this->searchParams);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testYahooSearch()
+ {
+ $client = $this->serpApiClient('yahoo');
+ $response = $client->search(['p' => 'Coffee']);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testYandexSearch()
+ {
+ $client = $this->serpApiClient('yandex');
+ $response = $client->search(['text' => 'Coffee']);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testEbaySearch()
+ {
+ $client = $this->serpApiClient('ebay');
+ $response = $client->search([
+ '_nkw' => 'Coffee',
+ 'no_cache' => true,
+ ]);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testYoutubeSearch()
+ {
+ $client = $this->serpApiClient('youtube');
+ $response = $client->search(['search_query' => 'Coffee']);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'video_results');
+ $this->assertNotEmpty($response->video_results);
+ }
+}
diff --git a/tests/ExampleSearchAppleAppStoreTest.php b/tests/ExampleSearchAppleAppStoreTest.php
new file mode 100644
index 0000000..6d14cfd
--- /dev/null
+++ b/tests/ExampleSearchAppleAppStoreTest.php
@@ -0,0 +1,29 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'apple_app_store',
+ 'term' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'organic_results',
+ 'Error on `apple_app_store` engine: no `organic_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchBaiduTest.php b/tests/ExampleSearchBaiduTest.php
new file mode 100644
index 0000000..9837e19
--- /dev/null
+++ b/tests/ExampleSearchBaiduTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'baidu',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'organic_results', 'Error on `baidu` engine: no `organic_results`');
+ }
+}
diff --git a/tests/ExampleSearchBingTest.php b/tests/ExampleSearchBingTest.php
new file mode 100644
index 0000000..2316c0d
--- /dev/null
+++ b/tests/ExampleSearchBingTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'bing',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'organic_results', 'Error on `bing` engine: no `organic_results`');
+ }
+}
diff --git a/tests/ExampleSearchDuckduckgoTest.php b/tests/ExampleSearchDuckduckgoTest.php
new file mode 100644
index 0000000..5fe28fe
--- /dev/null
+++ b/tests/ExampleSearchDuckduckgoTest.php
@@ -0,0 +1,29 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'duckduckgo',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'organic_results',
+ 'Error on `duckduckgo` engine: no `organic_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchEbayTest.php b/tests/ExampleSearchEbayTest.php
new file mode 100644
index 0000000..6a4fc48
--- /dev/null
+++ b/tests/ExampleSearchEbayTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'ebay',
+ '_nkw' => 'water',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'organic_results', 'Error on `ebay` engine: no `organic_results`');
+ }
+}
diff --git a/tests/ExampleSearchGoogleAutocompleteTest.php b/tests/ExampleSearchGoogleAutocompleteTest.php
new file mode 100644
index 0000000..87292a1
--- /dev/null
+++ b/tests/ExampleSearchGoogleAutocompleteTest.php
@@ -0,0 +1,29 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_autocomplete',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'suggestions',
+ 'Error on `google_autocomplete` engine: no `suggestions`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchGoogleEventsTest.php b/tests/ExampleSearchGoogleEventsTest.php
new file mode 100644
index 0000000..b6bb5ad
--- /dev/null
+++ b/tests/ExampleSearchGoogleEventsTest.php
@@ -0,0 +1,45 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_events',
+ 'q' => 'Events in Austin',
+ 'location' => 'Austin, Texas, United States',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+
+ // Temporary fix for google_events engine being unavailable
+ try {
+ $response = $client->search($this->searchParams);
+ } catch (SerpApiException $e) {
+ $this->markTestSkipped(
+ 'google_events is currently unavailable: ' . ($e->getSerpApiError() ?? $e->getMessage())
+ );
+ }
+
+ if (!property_exists($response, 'events_results')) {
+ $this->markTestSkipped('google_events returned no events_results');
+ }
+
+ $this->assertResponseHasProperty(
+ $response,
+ 'events_results',
+ 'Error on `google_events` engine: no `events_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchGoogleJobsTest.php b/tests/ExampleSearchGoogleJobsTest.php
new file mode 100644
index 0000000..16f9494
--- /dev/null
+++ b/tests/ExampleSearchGoogleJobsTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_jobs',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'jobs_results', 'Error on `google_jobs` engine: no `jobs_results`');
+ }
+}
diff --git a/tests/ExampleSearchGoogleLensTest.php b/tests/ExampleSearchGoogleLensTest.php
new file mode 100644
index 0000000..f31f27a
--- /dev/null
+++ b/tests/ExampleSearchGoogleLensTest.php
@@ -0,0 +1,27 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_lens',
+ 'url' => 'https://i.imgur.com/5bGzZi7.jpg',
+ 'gl' => 'us',
+ 'hl' => 'en',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'visual_matches', 'Error on `google_lens` engine: no `visual_matches`');
+ }
+}
diff --git a/tests/ExampleSearchGoogleLocalServicesTest.php b/tests/ExampleSearchGoogleLocalServicesTest.php
new file mode 100644
index 0000000..8e6b613
--- /dev/null
+++ b/tests/ExampleSearchGoogleLocalServicesTest.php
@@ -0,0 +1,26 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_local_services',
+ 'q' => 'electrician',
+ 'data_cid' => '6745062158417646970',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'local_ads', 'Error on `google_local_services` engine: no `local_ads`');
+ }
+}
diff --git a/tests/ExampleSearchGoogleMapsTest.php b/tests/ExampleSearchGoogleMapsTest.php
new file mode 100644
index 0000000..04220ba
--- /dev/null
+++ b/tests/ExampleSearchGoogleMapsTest.php
@@ -0,0 +1,27 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_maps',
+ 'q' => 'pizza',
+ 'll' => '@40.7455096,-74.0083012,15.1z',
+ 'type' => 'search',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'local_results', 'Error on `google_maps` engine: no `local_results`');
+ }
+}
diff --git a/tests/ExampleSearchGooglePlayTest.php b/tests/ExampleSearchGooglePlayTest.php
new file mode 100644
index 0000000..36b693e
--- /dev/null
+++ b/tests/ExampleSearchGooglePlayTest.php
@@ -0,0 +1,30 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_play',
+ 'q' => 'kite',
+ 'store' => 'apps',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'organic_results',
+ 'Error on `google_play` engine: no `organic_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchGoogleScholarTest.php b/tests/ExampleSearchGoogleScholarTest.php
new file mode 100644
index 0000000..bcc0f6f
--- /dev/null
+++ b/tests/ExampleSearchGoogleScholarTest.php
@@ -0,0 +1,29 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_scholar',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'organic_results',
+ 'Error on `google_scholar` engine: no `organic_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchGoogleShoppingTest.php b/tests/ExampleSearchGoogleShoppingTest.php
new file mode 100644
index 0000000..07b3687
--- /dev/null
+++ b/tests/ExampleSearchGoogleShoppingTest.php
@@ -0,0 +1,29 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google_shopping',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty(
+ $response,
+ 'shopping_results',
+ 'Error on `google_shopping` engine: no `shopping_results`'
+ );
+ }
+}
diff --git a/tests/ExampleSearchGoogleTest.php b/tests/ExampleSearchGoogleTest.php
new file mode 100644
index 0000000..338f7bb
--- /dev/null
+++ b/tests/ExampleSearchGoogleTest.php
@@ -0,0 +1,26 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'google',
+ 'tbm' => 'isch',
+ 'q' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'images_results', 'Error on `google` engine: no `images_results`');
+ }
+}
diff --git a/tests/ExampleSearchHomeDepotTest.php b/tests/ExampleSearchHomeDepotTest.php
new file mode 100644
index 0000000..47b8894
--- /dev/null
+++ b/tests/ExampleSearchHomeDepotTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'home_depot',
+ 'q' => 'table',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'products', 'Error on `home_depot` engine: no `products`');
+ }
+}
diff --git a/tests/ExampleSearchNaverTest.php b/tests/ExampleSearchNaverTest.php
new file mode 100644
index 0000000..6e54130
--- /dev/null
+++ b/tests/ExampleSearchNaverTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'naver',
+ 'query' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'ads_results', 'Error on `naver` engine: no `ads_results`');
+ }
+}
diff --git a/tests/ExampleSearchWalmartTest.php b/tests/ExampleSearchWalmartTest.php
new file mode 100644
index 0000000..867883b
--- /dev/null
+++ b/tests/ExampleSearchWalmartTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'walmart',
+ 'query' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'organic_results', 'Error on `walmart` engine: no `organic_results`');
+ }
+}
diff --git a/tests/ExampleSearchYahooTest.php b/tests/ExampleSearchYahooTest.php
new file mode 100644
index 0000000..f082c40
--- /dev/null
+++ b/tests/ExampleSearchYahooTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'yahoo',
+ 'p' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'organic_results', 'Error on `yahoo` engine: no `organic_results`');
+ }
+}
diff --git a/tests/ExampleSearchYoutubeTest.php b/tests/ExampleSearchYoutubeTest.php
new file mode 100644
index 0000000..3094ecd
--- /dev/null
+++ b/tests/ExampleSearchYoutubeTest.php
@@ -0,0 +1,25 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'engine' => 'youtube',
+ 'search_query' => 'coffee',
+ ];
+ }
+
+ public function testResultExists()
+ {
+ $client = $this->serpApiClient();
+ $response = $client->search($this->searchParams);
+ $this->assertResponseHasProperty($response, 'video_results', 'Error on `youtube` engine: no `video_results`');
+ }
+}
diff --git a/tests/GoogleSearchTest.php b/tests/GoogleSearchTest.php
new file mode 100644
index 0000000..287026c
--- /dev/null
+++ b/tests/GoogleSearchTest.php
@@ -0,0 +1,58 @@
+ */
+ private $searchParams;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->searchParams = [
+ 'q' => 'Coffee',
+ 'location' => 'Austin,Texas',
+ ];
+ }
+
+ public function testGoogleSearchReturnsOrganicResults()
+ {
+ $client = $this->serpApiClient('google');
+ $response = $client->search($this->searchParams);
+ $this->assertEquals('Success', $response->search_metadata->status);
+ $this->assertResponseHasProperty($response, 'organic_results');
+ $this->assertNotEmpty($response->organic_results);
+ }
+
+ public function testGoogleHtmlReturnsHtmlPayload()
+ {
+ $client = $this->serpApiClient('google');
+ $response = $client->html($this->searchParams);
+ $this->assertGreaterThan(10000, strlen($response));
+ }
+
+ public function testGoogleAccountReturnsApiKey()
+ {
+ $client = $this->serpApiClient('google');
+ $info = $client->account();
+ $this->assertEquals($client->getApiKey(), $info->api_key);
+ }
+
+ public function testGoogleLocationReturnsResults()
+ {
+ $client = $this->serpApiClient('google');
+ $location_list = $client->location(['q' => 'Austin', 'limit' => 3]);
+ $this->assertCount(3, $location_list);
+ $this->assertStringContainsString('Austin', $location_list[0]->name);
+ $this->assertGreaterThan(0, $location_list[0]->google_id);
+ }
+
+ public function testGoogleSearchArchiveReturnsSameId()
+ {
+ $client = $this->serpApiClient('google');
+ $result = $client->search($this->searchParams);
+ $archived_result = $client->searchArchive($result->search_metadata->id);
+ $this->assertEquals($result->search_metadata->id, $archived_result->search_metadata->id);
+ }
+}
diff --git a/tests/SerpApiTestCase.php b/tests/SerpApiTestCase.php
new file mode 100644
index 0000000..5481a48
--- /dev/null
+++ b/tests/SerpApiTestCase.php
@@ -0,0 +1,64 @@
+resolveApiKey();
+
+ if ($resolved === null && $this->requiresApiKey()) {
+ $this->markTestSkipped('API_KEY is not set');
+ return;
+ }
+
+ $this->apiKey = $resolved;
+ }
+
+ protected function requiresApiKey(): bool
+ {
+ return true;
+ }
+
+ protected function resolveApiKey(): ?string
+ {
+ $env = $_ENV['API_KEY'] ?? null;
+
+ if (!empty($env)) {
+ return $env;
+ }
+
+ $value = getenv('API_KEY');
+ if (!empty($value)) {
+ return $value;
+ }
+
+ return null;
+ }
+
+ protected function serpApiClient(string $engine = 'google'): Client
+ {
+ return new Client($this->apiKey ?? '', $engine);
+ }
+
+ protected function assertResponseHasProperty(object $response, string $property, string $message = ''): void
+ {
+ if (method_exists($this, 'assertObjectHasProperty')) {
+ $this->assertObjectHasProperty($property, $response, $message);
+ return;
+ }
+
+ $this->assertTrue(
+ property_exists($response, $property),
+ $message ?: "Failed asserting that object has property '{$property}'"
+ );
+ }
+}