paypal/sdk-core-php
Core PHP library for PayPal SDKs, providing shared utilities, HTTP client abstractions, credential handling, and common infrastructure used by PayPal REST API integrations to build payments, orders, and related API calls in PHP applications.
What this package actually is. paypal/sdk-core-php is the shared foundation underneath PayPal's classic SDKs (paypal/merchant-sdk-php, adaptivepayments-sdk-php, adaptiveaccounts-sdk-php, permissions-sdk-php, invoice-sdk-php). It ships the config loader, credential model, HTTP transport, XML serialization envelope, logging, and the IPN validator. You rarely install it deliberately — it arrives as a transitive dependency — but two things in it are legitimately useful on their own: PPIPNMessage and PPAPIService.
Minimal steps
composer require paypal/sdk-core-php
sdk_config.ini (see below) and point the SDK at it.PPCredentialManager.Look here first, in this order
| Class | Why you care |
|---|---|
PPConfigManager |
Singleton over sdk_config.ini; getInstance()->get('mode'), get('acct1.UserName') |
PPCredentialManager |
Turns config keys into PPSignatureCredential / PPCertificateCredential objects |
PPAPIService |
The request dispatcher every concrete SDK service delegates to |
PPConnection / PPHttpConnection |
The cURL layer — your mock seam and your timeout/proxy seam |
PPXmlMessage |
Base envelope: toXMLString(), __toString(), nested-object marshalling |
PPIPNMessage |
Standalone IPN post-back validator |
PPLoggingManager |
File logger driven by the log.* config keys |
First use case — IPN validation without the merchant SDK. This is the highest-value direct use, and it fits a Laravel webhook controller cleanly:
use PPIPNMessage;
use PPConfigManager;
Route::post('/webhooks/paypal', function (Request $request) {
PPConfigManager::getInstance()->setConfigFile(base_path('sdk_config.ini'));
$ipn = new PPIPNMessage($request->getContent(), 'sandbox');
if (! $ipn->validate()) {
abort(400);
}
// $ipn->getData() / $ipn->getData('txn_id')
return response('OK');
});
Config file shape (copy from the repo's sdk_config.ini sample and edit):
[Sandbox]
acct1.UserName = facilitator_api1.example.com
acct1.Password = XXXX
acct1.Signature = XXXX
acct1.AppId = APP-80W284485P519543T
acct1.ServiceName = PayPalAPI
http.ConnectionTimeOut = 30
http.Retry = 1
log.LogEnabled = true
log.FileName = /absolute/path/to/storage/logs/paypal.log
log.LogLevel = INFO
[Live]
; same keys, live credentials
Config-driven mode switching. Keep one sdk_config.ini with [Sandbox] and [Live] sections, and select via PPConfigManager::getInstance()->get('mode'). In Laravel, bind the resolved mode from config('services.paypal.mode') in a service provider rather than hardcoding the 'sandbox' argument at call sites.
Credential construction. Never hand-build credentials; let the manager do it so certificate vs. signature is decided by which keys exist:
$creds = PPCredentialManager::getInstance()->getCredentials(['acct1']);
// returns PPSignatureCredential or PPCertificateCredential
Third-party / subject authorization. For calls on behalf of a merchant, attach a PPSubjectAuthorization to the credential:
$auth = new PPSubjectAuthorization();
$auth->setSubject('[email protected]');
$creds->setThirdPartyAuthorization($auth);
PPThirdPartyAuthorization is the abstract parent — you can subclass it if you need token-based delegation instead of a plain subject.
Dispatching a call through a classic SDK. Concrete services (e.g. PayPalAPIInterfaceServiceService) are thin: they build a PPXmlMessage subclass and pass it to PPAPIService::makeRequest($request, $serviceName, $handlers, $apiName). Pattern is always the same:
$service = new PayPalAPIInterfaceServiceService($config);
$request = new SetExpressCheckoutRequestType();
// ... populate ...
$response = $service->SetExpressCheckout($request);
if (strtoupper($response->Ack) !== 'SUCCESS') { /* handle */ }
Custom endpoints / operations not covered by a shipped SDK. Subclass PPXmlMessage for the request, mirror the response shape into a plain class, then call PPAPIService directly with your own $apiName. This is how the community added operations PayPal never published a wrapper for.
Transport substitution. For tests, swap PPHttpConnection for a subclass whose execute($data) returns a canned XML body. Everything above it (PPAPIService, response hydration) is unaware, so you get full round-trip coverage without network calls.
Logging integration. PPLoggingManager::getInstance(__CLASS__) gives you info(), warn(), error(), fine(), filtered by log.LogLevel. In Laravel, point log.FileName at storage_path('logs/paypal.log') and let logrotate handle it; don't try to bridge it into Monolog, the singleton doesn't accept a PSR-3 handler.
Retry and proxy. http.Retry, http.ConnectionTimeOut, and the http.Proxy.* keys are read per-request from PPConfigManager. If you need per-tenant proxies, override the config file path in a middleware before the singleton is first resolved — after that it's cached for the process.
Global namespace, no PSR-4 root. The classes live in the global namespace (PPAPIService, PPConnection, PPIpNMessage…). Anything importing them pollutes your autoload map with unprefixed names. Only load this package in the process that actually needs it, and never use a PP* class in a file that also ships a same-named helper.
Deprecation status is real. PayPal has sunset these classic NVP/SOAP SDKs; there has been no meaningful release in years. For new work, prefer the REST APIs and the current Checkout SDKs. Treat this package as a maintenance liability: it is fine for existing merchant/adaptive integrations and IPN validation, and a bad choice for greenfield. The zero stars / zero direct dependents is a signal that virtually nobody consumes it as a first-class dependency — it lives only downstream.
PHP 8 friction. Expect breakage on modern PHP:
resource fail when CurlHandle objects are returned.<...> nodes emits notices.each(), create_function(), and reflection patterns that are gone in 8.x.sdk_config.ini resolution is the #1 source of "why is it using sandbox?" The default path is derived from the package's own directory, not your project root. A copy that's one directory off silently falls back to defaults. Always verify:
PPConfigManager::getInstance()->setConfigFile(base_path('sdk_config.ini'));
$pcm = PPConfigManager::getInstance();
dd($pcm->get('mode'), $pcm->get('acct1.UserName'));
The singleton caches on first read, so call setConfigFile() before any credential or connection object is constructed.
Empty-string vs. missing keys. PPCredentialManager decides signature-vs-certificate by key presence. An acct1.CertPath = line with no value counts as present in some parser paths and produces a PPInvalidCredentialException that reads like a path error. Delete unused keys entirely rather than blanking them.
Errors are exceptions, not return codes. PPConnectionException (network/HTTP), PPInvalidCredentialException, and PPTransformerException (XML parse) are the three to catch. PPConnectionException carries getUrl() and getData() — log both, because PayPal's SOAP faults come back in the body with a 200 or 500 status depending on transport error, and the message alone is uninformative.
TLS. Current PayPal requires TLS 1.2+. Very old versions of the transport set CURLOPT_SSLVERSION in ways that can pin you below it. If you see handshake failures on a box with a modern OpenSSL, override PPHttpConnection and explicitly set CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2.
Response objects are dumb containers. $response->Ack is a string you must strtoupper() and compare to 'SUCCESS' / 'SUCCESSWITHWARNING'. SUCCESSWITHWARNING still means the operation succeeded — treating it as failure is a classic bug. Field names are case-sensitive XML node names, so a null where you expect data usually means the camelCase assumption was wrong.
Retries are not idempotent-safe. http.Retry > 1 will re-POST DoCapture/DoAuthorization on timeout, which can double-charge or double-capture. Leave retry at 0 or 1 and implement idempotency at the application layer with your own request keys.
IPN specifics. PPIPNMessage::validate() posts the raw body back to PayPal — it needs outbound HTTPS and will fail silently-ish behind a restrictive egress firewall (you'll get false, not an exception). Also, the constructor's second argument takes the mode string, not a boolean; passing true puts you in an undefined config section. Read the raw body from $request->getContent(), not from $_POST — re-encoding loses the original ordering and the signature check is byte-sensitive.
Extension points that actually hold up: subclass PPConnection/PPHttpConnection (transport, retries, TLS, mocking), subclass PPXmlMessage (new request envelopes), and subclass PPThirdPartyAuthorization (auth schemes). Avoid monkey-patching PPAPIService internals — the private $config/$connection fields and their initialization order vary between 2.x point releases.
Where to look when something breaks. Turn log.LogLevel = FINE and read the raw request/response XML in the log file; it contains the un-escaped SOAP envelope, which is the fastest way to confirm whether a bad field or a bad credential is at fault. Set it back to INFO afterwards — FINE dumps credentials in cleartext.
How can I help you explore Laravel packages today?