Bring the PHP client to parity with serpapi-ruby - #3
Open
jvmvik wants to merge 9 commits into
Open
Conversation
The client sent `source=php`, which made it impossible to tell PHP client versions apart in SerpApi usage statistics. Report `serpapi-php:<version>` instead, matching the `serpapi-ruby:<version>` convention. Query string assembly moves into a private `query()` method so it can be covered without issuing an HTTP request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The constructor only accepted api_key, engine and timeout, so parameters like location, hl, gl or no_cache had to be repeated on every search call. The Ruby client takes a hash where any unrecognized key becomes a default search parameter. Add a fourth positional argument for default parameters, and accept a configuration array as the first argument for parity with the Ruby client. Per-call parameters still take precedence, and null values are dropped from the query rather than sent empty. Expose the effective defaults via get_params(), alongside a get_timeout() accessor. The existing positional signature is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
var_dump() on a client printed the api_key property in clear text, so a key could end up in logs or a bug report. The Ruby client masks it in inspect. Add __debugInfo() to mask the key for var_dump and debuggers, plus an inspect() method for parity. Keys of 8 characters or fewer are replaced entirely rather than partially revealed. print_r() and var_export() read properties directly and cannot be hooked; this is called out in the docblock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every request created and destroyed its own cURL handle, paying for DNS, TCP and TLS setup each time. The Ruby client keeps a persistent socket and reports roughly twice the throughput because of it. Keep one cURL handle per client and reuse it, so curl holds the connection open between requests. Measured against the locations endpoint, follow-up requests drop from ~110ms to ~22ms. Persistent mode is on by default, matching the Ruby client, and can be turned off with `['persistent' => false]`. close() releases the connection and the client reconnects on the next request; the destructor closes it too, so existing code needs no change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Responses were always decoded to stdClass, with no way to get arrays. The Ruby client exposes symbolize_names for the equivalent choice. Add an `assoc` option, settable on the client or per call. Error and search_id extraction now reads either shape through a small dig() helper, so exceptions carry the same context in both modes. The default stays stdClass to keep existing code working, which is why this is opt-in rather than mirroring Ruby's default. search() and account() lose their `object` return type declaration since they can now return an array; the docblocks carry the union type instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite covered 20 engines against the Ruby client's 38. Add examples for amazon, yelp, yandex, google_news, google_news_light, google_images, google_images_light, google_light, google_light_search, google_finance, google_flights, google_hotels, google_patents, google_product, google_reverse_image, google_trends, google_videos, google_immersive_product and google_ai_overview. Two of these are written differently from their Ruby counterparts: - google_flights and google_hotels compute check-in and departure dates relative to today. The Ruby specs hardcode 2025 dates that have since passed. - google_ai_overview reads page_token from a live Google search instead of a hardcoded token, and skips when Google returns no overview. The Ruby spec pins an expired token. ClientIntegrationTest duplicated GoogleSearchTest assertion for assertion, so it is removed. These require an API key to run and were not executed here; only the key-free unit tests were. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Ruby client ships a demo directory used as out-of-box testing; the PHP
client had no runnable examples outside the test suite.
Add four scripts and a `make demo` target:
demo.php basic search
demo_suggest.php autocomplete, showing client-level default parameters
demo_async.php non-blocking batch submission collected via the
Search Archive API
demo_persistent.php connection reuse benchmark, needs no API key
demo_persistent.php measures a 2.6x speedup locally, in line with the Ruby
client's reported figure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI ran the test suite but nothing checked types, despite the codebase already carrying full @PARAM and @return annotations. The Ruby client gates its build on rubocop. Add PHPStan at level 6 over src/ and demo/, wired into `make analyse`, `composer run-script analyse` and the workflow. Analysis is skipped on the 7.2 and 7.3 matrix entries, which predate PHPStan 2. It caught one real problem: demo_async.php read ->search_metadata off a value that search() can also return as an array, which would have surfaced as a fatal error rather than a clear message. The demo now validates the response shape first. Test files are left out. Level 6 wants a return type on every test method, which is churn across 40 files for little benefit. The cURL handle type is excluded, with a comment: it is a resource on PHP 7 and a CurlHandle on PHP 8, and PHPStan only ever sees one version. No code style fixer is added; the codebase uses 2-space indentation, so PSR-12 enforcement would rewrite every file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test suite, demos and README used API_KEY, while the Ruby client and SerpApi's own documentation use SERPAPI_KEY. Anyone working across both libraries had to keep two names for the same secret. SERPAPI_KEY is now the documented name everywhere. The test helper still falls back to API_KEY so existing local setups keep working, and the workflow reads `secrets.SERPAPI_KEY || secrets.API_KEY` so CI does not break before the repository secret is renamed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the PHP client in line with serpapi-ruby v1.0.3, after a feature-by-feature comparison of the two libraries. One commit per issue, all backwards compatible.
Branched from and targeting
Init.What changed
a1f8242sourcereportedphp; nowserpapi-php:<version>, so PHP client versions are distinguishable in usage statsced70fac47220bvar_dump($client)printed the API key in clear text; now masked via__debugInfo(), plusinspect()08848d6c529f01stdClass; newassocoption, the counterpart of Ruby'ssymbolize_names88f82c79d44a1cdemo/(basic, autocomplete, async, persistent benchmark) +make demo6dddb28make analyseand CI92a4a26API_KEYrenamed toSERPAPI_KEYto match the Ruby client and SerpApi's docsVerification
src/anddemo/: clean.ClientQueryTest,ClientDefaultParamsTest,ClientInspectTest,ClientPersistentTest,ClientAssocTest.assocmode were verified live against the locations endpoint, which needs no API key.For the reviewer
The 19 new engine tests have never been executed — no SerpApi key was available in the environment where this was written. They are syntax-checked and follow the existing verified pattern, but this PR's CI run is their first real execution. Please check that job before merging.
Two of them deliberately differ from their Ruby counterparts:
google_flights/google_hotelscompute dates relative to today. The Ruby specs hardcode 2025 dates that have since passed.google_ai_overviewreadspage_tokenfrom a live Google search and skips when there is no overview. The Ruby spec pins a token that has expired.One genuine API change:
search()andaccount()lost their: objectreturn declarations, since they can now return arrays. The union type moved to the docblock. Everything else is additive — the positional constructor still works, andassocdefaults tofalseso responses staystdClass. That last choice deliberately does not match Ruby's default, which would have broken every existing caller.PHPStan caught a real bug in this PR's own
demo_async.php: it read->search_metadataoff a valuesearch()can return as an array, which would have been a fatal error rather than a clear message.Two exclusions, both documented in
phpstan.neon:tests/is out of scope — level 6 wants a return type on every method across 40+ files for little benefit.property.unusedTypeexclusion for the cURL handle, genuinelyresourceon PHP 7 andCurlHandleon PHP 8; PHPStan only analyses one version at a time.No code style fixer was added. The codebase uses 2-space indentation, so PSR-12 enforcement would rewrite every file — that is a call for the maintainers, not a side effect of this PR.
Not included
The README documents none of the new API.
close(),get_params(),assoc,persistentand the array constructor are currently undiscoverable.README.mdis generated fromREADME.md.erbviamake readme, which needserb. This was outside the agreed scope; it is the obvious follow-up, along with the missingCHANGELOG.mdandCONTRIBUTING.md.Repository secret: the workflow reads
secrets.SERPAPI_KEY || secrets.API_KEY, so CI keeps working either way. Renaming the secret toSERPAPI_KEYlets that fallback be dropped later.Also worth noting three places where this library is already ahead of Ruby and the behaviour should probably be backported: exception payloads strip
api_key, parse errors omit the response body, andsearch_archiveURL-encodes the search ID.🤖 Generated with Claude Code