clientId = getPayPalClientId(); $this->clientSecret = getPayPalClientSecret(); $this->baseUrl = getPayPalBaseUrl(); } private function httpRequest($method, $url, $headers = [], $body = null, $basicAuth = null) { $method = strtoupper($method); $verifySsl = defined('PAYPAL_VERIFY_SSL') ? (bool)PAYPAL_VERIFY_SSL : true; // Prefer cURL when available if (function_exists('curl_init')) { $ch = curl_init(); $opts = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => $verifySsl, CURLOPT_SSL_VERIFYHOST => $verifySsl ? 2 : 0, CURLOPT_TIMEOUT => 30, ]; if ($basicAuth) { $opts[CURLOPT_USERPWD] = $basicAuth; } if ($body !== null) { $opts[CURLOPT_POSTFIELDS] = $body; } curl_setopt_array($ch, $opts); $responseBody = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { throw new Exception("cURL Error: $error"); } return [$httpCode, $responseBody]; } // Fallback to file_get_contents if cURL isn't enabled $headerLines = $headers; if ($basicAuth) { $headerLines[] = 'Authorization: Basic ' . base64_encode($basicAuth); } $context = stream_context_create([ 'http' => [ 'method' => $method, 'header' => implode("\r\n", $headerLines), 'content' => $body ?? '', 'ignore_errors' => true, 'timeout' => 30, ], 'ssl' => [ 'verify_peer' => $verifySsl, 'verify_peer_name' => $verifySsl, ], ]); $responseBody = @file_get_contents($url, false, $context); $httpCode = 0; if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#', $h, $m)) { $httpCode = intval($m[1]); break; } } } if ($responseBody === false) { $lastError = error_get_last(); $msg = $lastError['message'] ?? 'HTTP request failed'; throw new Exception($msg); } return [$httpCode, $responseBody]; } /** * Get OAuth 2.0 access token from PayPal */ public function getAccessToken() { if ($this->accessToken) { return $this->accessToken; } [$httpCode, $response] = $this->httpRequest( 'POST', $this->baseUrl . '/v1/oauth2/token', [ 'Accept: application/json', 'Accept-Language: en_US', 'Content-Type: application/x-www-form-urlencoded', ], 'grant_type=client_credentials', $this->clientId . ':' . $this->clientSecret ); $data = json_decode($response, true); if ($httpCode !== 200 || !isset($data['access_token'])) { $errorMsg = isset($data['error_description']) ? $data['error_description'] : 'Failed to get access token'; throw new Exception("PayPal Auth Error: $errorMsg"); } $this->accessToken = $data['access_token']; return $this->accessToken; } /** * Create a PayPal order * * @param float $amount The order amount * @param string $currency Currency code (e.g., 'USD', 'CAD') * @param string $description Order description * @param string $customId Your internal reference ID (e.g., TRXnum) * @return array Order data including order ID */ public function createOrder($amount, $currency, $description = '', $customId = '') { $token = $this->getAccessToken(); $orderData = [ 'intent' => 'CAPTURE', 'purchase_units' => [ [ 'amount' => [ 'currency_code' => $currency, 'value' => number_format($amount, 2, '.', '') ], 'description' => $description, 'custom_id' => $customId // This is like the 'custom' field in old PayPal ] ], 'application_context' => [ 'brand_name' => 'Hot Lunches Online', 'landing_page' => 'NO_PREFERENCE', 'user_action' => 'PAY_NOW', 'return_url' => $this->getCurrentUrl() . $this->appendParam('paypal=success'), 'cancel_url' => $this->getCurrentUrl() . $this->appendParam('paypal=cancel') ] ]; [$httpCode, $response] = $this->httpRequest( 'POST', $this->baseUrl . '/v2/checkout/orders', [ 'Content-Type: application/json', 'Authorization: Bearer ' . $token, 'PayPal-Request-Id: ' . uniqid('order_', true), ], json_encode($orderData) ); $data = json_decode($response, true); if ($httpCode !== 201 || !isset($data['id'])) { $errorMsg = isset($data['message']) ? $data['message'] : 'Failed to create order'; if (isset($data['details'])) { $errorMsg .= ': ' . json_encode($data['details']); } throw new Exception("PayPal Create Order Error: $errorMsg"); } return $data; } /** * Capture a PayPal order (complete the payment) * * @param string $orderId PayPal order ID * @return array Capture result including transaction details */ public function captureOrder($orderId) { $token = $this->getAccessToken(); [$httpCode, $response] = $this->httpRequest( 'POST', $this->baseUrl . '/v2/checkout/orders/' . $orderId . '/capture', [ 'Content-Type: application/json', 'Authorization: Bearer ' . $token, 'PayPal-Request-Id: ' . uniqid('capture_', true), ], '{}' ); $data = json_decode($response, true); if ($httpCode !== 201 || !isset($data['status'])) { $errorMsg = isset($data['message']) ? $data['message'] : 'Failed to capture order'; if (isset($data['details'])) { $errorMsg .= ': ' . json_encode($data['details']); } throw new Exception("PayPal Capture Error: $errorMsg"); } return $data; } /** * Get order details * * @param string $orderId PayPal order ID * @return array Order details */ public function getOrder($orderId) { $token = $this->getAccessToken(); [$httpCode, $response] = $this->httpRequest( 'GET', $this->baseUrl . '/v2/checkout/orders/' . $orderId, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $token, ] ); return json_decode($response, true); } /** * Get current page URL (for return/cancel URLs) — includes the path and * query string, not just the host, so PayPal redirects the buyer back * to the same page/view they paid from (the one that can capture the * order, e.g. ?action=account-deposit), not the site root. */ private function getCurrentUrl() { $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $uri = $_SERVER['REQUEST_URI'] ?? '/'; return $protocol . '://' . $host . $uri; } /** * Append a query param to a URL that may already have a query string. */ private function appendParam($param) { $uri = $_SERVER['REQUEST_URI'] ?? '/'; return (strpos($uri, '?') !== false ? '&' : '?') . $param; } }