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 + +[![Packagist Version](https://img.shields.io/packagist/v/serpapi/serpapi-php.svg?label=packagist)](https://packagist.org/packages/serpapi/serpapi-php) +[![PHP](https://img.shields.io/badge/php-%3E%3D7.2-brightgreen.svg)](https://www.php.net) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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 + +[![Packagist Version](https://img.shields.io/packagist/v/serpapi/serpapi-php.svg?label=packagist)](https://packagist.org/packages/serpapi/serpapi-php) +[![PHP](https://img.shields.io/badge/php-%3E%3D7.2-brightgreen.svg)](https://www.php.net) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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}'" + ); + } +}