Repository navigation
Bring the PHP client to parity with serpapi-ruby - #3
Merged
Merged
Conversation
This was referenced Aug 16, 2026
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 assembly moves into buildQuery(), and request() becomes a protected seam so tests can observe outgoing requests through a recording double instead of reflecting into private methods or hitting the network. Co-authored-by: Victor Benarbia <victor@serpapi.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. Configuration keys are honored in either array, rather than silently dropped. Per-call parameters take precedence, a null value drops a default for one call, and a per-call `timeout` applies to that request instead of being sent to the API. The effective values are exposed via getParams() and getTimeout(). The existing positional signature is unchanged. Co-authored-by: Victor Benarbia <victor@serpapi.com>
var_dump() on a client printed the API key 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(), print_r() and debuggers, plus an inspect() method for parity. Keys of 8 characters or fewer are replaced entirely rather than partially revealed. var_export() and (array) casts read properties directly and cannot be hooked; this is called out in the docblock. Co-authored-by: Victor Benarbia <victor@serpapi.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, five warmed-up requests drop from ~300ms to ~100ms. 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. Holding a handle brings lifecycle concerns that are handled explicitly: - a clone opens its own connection instead of sharing the original's, which on PHP 7 it would otherwise close when destroyed - the handle is left out of serialization, since serializing a CurlHandle throws on PHP 8 and would break clients stored on queued jobs - a stale handle (closed, or deserialized on PHP 7.2/7.3) is replaced - a per-call `persistent` is rejected rather than silently ignored Co-authored-by: Victor Benarbia <victor@serpapi.com>
search() keeps returning stdClass. Error and search id extraction now reads both decoded shapes. Co-authored-by: Victor Benarbia <victor@serpapi.com>
Ports the PR's engine examples to the master test style and adds google_ai_mode. Compared to the original PR: - amazon searches with `k`, its query parameter, instead of `q` - google_videos asserts `video_results`, which the engine returns - google_images drops the redundant `tbm=isch` - google_flights matches the Ruby spec (LAX to AUS) and accepts either `best_flights` or `other_flights` - google_immersive_product and google_product take a page_token and a product_id from a live Google Shopping search instead of hardcoding values that go stale, and skip when none is returned - google_ai_overview bypasses the cache when fetching its page_token, which expires a minute after the search Co-authored-by: Victor Benarbia <victor@serpapi.com>
demo.php runs a basic search, demo_suggest.php reuses default search parameters, demo_async.php submits a batch with async and collects it through the Search Archive API, and demo_persistent.php benchmarks connection reuse. Compared to the original PR: - the async demo stops polling on Success, fails fast on an archived error, and passes `async` per call so it is not also sent with every Search Archive request - the benchmark warms up both clients before timing them - `demo` is declared .PHONY; otherwise make sees the demo/ directory and reports the target as up to date without running anything - demo/ is linted with phpcs Co-authored-by: Victor Benarbia <victor@serpapi.com>
Analyses src and demo at level 6, from `composer analyse` or `make analyse`. CI runs it once, in the lint job, rather than across the PHP matrix. searchArchive() now declares that the json format returns an object, so callers need not narrow object|string themselves. Co-authored-by: Victor Benarbia <victor@serpapi.com>
Covers searchArray(), per-call timeouts, default search parameters, persistent connections, API key masking, the demo scripts and static analysis, and adds a section for each engine with a new example. The snippet helper now matches assertions split over several lines, as master formats them. Google Autocomplete, Google Shopping and Google Events previously fell back to printing organic_results. Co-authored-by: Victor Benarbia <victor@serpapi.com>
strzibny
force-pushed
the
feature/ruby-parity
branch
from
October 21, 2026 12:20
92a4a26 to
e057e0d
Compare
Contributor
|
@jvmvik I rebased this onto Aligned with master
Fixes
Additions
PHPUnit and PHPCS pass at every commit, and PHPStan level 6 is clean. |
Running the examples with an API key showed four failures and one skip,
all inherited from the serpapi-ruby specs:
- google_light_search is not a SerpApi engine ("Unsupported
`google_light_search` search engine"); remove the example
- google_finance no longer returns `markets` for a ticker; assert
`summary`, which it does return
- google_reverse_image found nothing for the old sample image; use the
one from the current API docs, which returns `image_results`
- google_product found nothing for any product_id taken from Google
Shopping; take the shopping result's page_token instead, the other
documented way to identify a product
- google_ai_overview always skipped: Google now shows the overview
inline for "what is coffee". Use the query from the API docs, which
still returns a page_token
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