hakito / cakephp-paypal-checkout
PayPalCheckout plugin for CakePHP
Requires
- php: >=8.1
- cakephp/cakephp: ^5.0
- guzzlehttp/guzzle: ^7.4.5
Requires (Dev)
- phpunit/phpunit: ^10.1
README
This plugin can be used for the backend part of the PayPal Checkout API.
Installation
You can install this plugin into your CakePHP application using composer.
The recommended way to install composer packages is:
composer require hakito/paypal-checkout
Load the plugin
In your plugins.php add
'PayPalCheckout' => [
'routes' => true
],
Configuration
In your app.php you have to setup the api credentials:
'PayPalCheckout' => [
//'Mode' => 'sandbox', // optional set the current mode
'ClientId' => 'CLIENT ID',
'ClientSecret' => 'CLIENT SECRET'
],
For some basic logging you can add this to the Log
section:
'PayPalCheckout' => [
'className' => FileLog::class,
'path' => LOGS,
'file' => 'PayPalCheckout',
'scopes' => ['PayPalCheckout'],
'levels' => ['warning', 'error', 'critical', 'alert', 'emergency', 'info'],
]
Usage
Implement the client side according to the api docs. Example
<?php
use Cake\Core\Configure;
?>
<script src="https://www.paypal.com/sdk/js?client-id=<?= Configure::read('PayPalCheckout.ClientId') ?>¤cy=EUR&locale=de_AT"></script>
<script lang="javascript" type="text/javascript">
window.paypal
.Buttons({
style: {
layout: 'horizontal',
label: 'buynow'
},
async createOrder() {
try {
const response = await fetch("/paypal-checkout/Orders/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
if (orderData.abort) // example for handling your own data from the controller
{
if (orderData.abort == 'already paid')
{
window.location.reload();
}
else
throw new Error(orderData.abort);
}
if (orderData.id) {
return orderData.id;
} else {
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
}
} catch (error) {
console.error(error);
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
},
async onApprove(data, actions) {
try {
const response = await fetch(`/paypal-checkout/Orders/capture/${data.orderID}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const orderData = await response.json();
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const errorDetail = orderData?.details?.[0];
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (errorDetail) {
// (2) Other non-recoverable errors -> Show a failure message
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
} else if (!orderData.purchase_units) {
throw new Error(JSON.stringify(orderData));
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
console.log('redirecting');
actions.redirect(orderData.redirectSuccessUrl);
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, your transaction could not be processed...<br><br>${error}`,
);
}
},
})
.render("#paypal-button-container");
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;
}
</script>
Event handling
You have to handle two events in your application:
EventManager::instance()
->on('PayPalCheckout.CreateOrder', OrdersController::createPayPalOrder(...))
->on('PayPalCheckout.CaptureOrder', PaymentCallbacks::capturePayPalOrder(...));
CreateOrder events
Fired when a request for payment is built. You can setup the ordered items and amounts here.
The function has to return an array with the order data. Either build the array on your own or use the OrderBuilder.
public static function createPayPalOrder(Event $event)
{
// Optional you can also send back custom data and handle it on the client side
if (ORDER_ALREADY_AID)
return ['abort' => ['already paid']];
$purchaseUnitBuilder = new PurchaseUnitBuilder(new AmountBuilder('USD', '123.56'));
$purchaseUnitBuilder->ReferenceId('YOUR ORDER'); // optionally set your order id here
return (new OrderBuilder())
->add($purchaseUnitBuilder)
->Build();
}
CaptureOrder event
Fired when the payment has been completed.
public static function capturePayPalOrder(Event $event, $args)
{
$data = $event->getData('body');
if ($data->status != 'COMPLETED')
{
Log::error("Captured PayPal checkout payment is not COMPLETED but $data->status");
return;
}
$purchaseUnit = $data->purchase_units[0];
$orderId = $purchaseUnit->reference_id;
// Capture ID for issuing refunds
$captureId = $purchaseUnit->payments->captures[0]->id;
// DO YOUR PAYMENT HANDLING HERE ($orderId, $captureId);
return ['redirectSuccessUrl' => 'http://payment-success.example.com'];
}
Refund a payment
You can do a full refund of the payment in a controller.
$this->loadComponent('PayPalCheckout.Checkout', Configure::read('PayPalCheckout'));
$this->Checkout->refundPayment('CAPTURE_ID_OF_YOUR_ORDER');