| config | ||
| src | ||
| tests | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| composer.json | ||
| composer.lock | ||
| phpunit.xml | ||
| README.md | ||
Payfast for Laravel
A PayFast integration for the Laravel framework.
This package requires at least Laravel 9 running on PHP 8.1+.
- Package Installation
- Service Instantiation
- Initiating Payments
- Setting up Instant Transaction Notification (ITN) Webhooks
Package Installation
$ composer require rockett/payfast
Service Instantiation
Payfast for Laravel has three different ways in which you can instantiate the service class, which is the central class that does all the heavy lifting: direct-access, dependency injection, and the facade. Each of these options is described in detail below:
Option 1: Direct Access (Service Instantiation)
use Rockett\Payfast\Services\PayfastService;
$payfast = PayfastService::new(); // static constructor
$payfast = new PayfastService; // instance constructor
$payfast->createPaymentForm(/* Payment instance */);
To create a new instance of the service class, you can call the static constructor, or the instance constructor. This gives you a brand new service class, ready to go.
💡 For the rest of this readme, we’ll use the static constructor or dependency injection (discussed below), which is encouraged for fluent interfaces.
By default, it will be configured based on the contents of the package configuration file, whether or not you have published it. You can override all or parts of the package configuration by providing a custom configuration to the the new static constructor:
$service = PayfastService::new(
merchantId: '123123123',
merchantKey: 'ABCXYZ987',
);
You can learn more about configuring the package here.
Option 2: Dependency Injection
use Rockett\Payfast\Contracts\PayfastFactory;
class InitiatePayment
{
public function __invoke(PayfastFactory $payfast): string
{
$form = $payfast->createPaymentForm(/* Payment instance */);
}
}
To use dependency injection, you need to use the PayfastFactory contract, which will resolve the PayfastService singleton from the Service Container. This singleton is scoped to ensure support for Laravel Octane.
To reconfigure this instance, you may call mergeConfig on the new service instance:
$payfast->mergeConfig(
merchantId: '123123123',
merchantKey: 'ABCXYZ987',
);
You do not need to call this method at any specific point in time, but you must call it before you interact with PayFast’s servers.
Option 3: Facade
use Rockett\Payfast\Facades\Payfast;
$form = Payfast::createPaymentForm(/* Payment instance */);
Similar to dependency injection, using the Facade will give you an instance of the Payfast service singleton. The Facade resolves to the PayfastFactory contract which, in turn, provides you with the singleton.
To change the configuration, you may call the mergeConfig method, just as you would with dependency injection.
With that out of the way, let’s talk about initiating payments.
Initiating Payments
Whilst PayFast only documents one way to initiate a payment (that is, through an HTML form submission), this package offers you two. In addition to an HTML form, you can also do a server-side redirect. Whilst this approach isn’t documented anywhere, it is fully supported, and PayFast does not discourage its use.
PayFast does also have Onsite payments, currently in beta. However, this package is out of scope for the feature as it is both not production ready, and does not yet work in the sandbox. As soon as this is not the case, we’ll impleent Onsite payments in this package.
Before we get to the trigger that initiates a payment, let’s first create a Payment object, which holds all the essential information about the transaction, ready for transmission to PayFast.
Creating a Payment object
To create a new payment object, you need only construct it, like so:
use Rockett\Payfast\DataObjects\{Payment, TransactionDetails};
$payment = new Payment(
transaction: new TransactionDetails(
amount: 100.00,
itemName: 'My Awesome Product'
),
);
This example contains the bare minimum needed in order to create a payment object. That is, it contains an amount and an item name.
The Payment class accepts all the arguments accepted by PayFast, grouped together into neat little sub-classes that separate concerns. Namely, these are:
TransactionDetails– Attributes relating to the transaction itself, specifically regarding things that identify the transaction and define the amount to charge the customer.Merchant– Attributes relating to the merchant account that the transaction is being done on. This only needs to be provided if the default merchant, as defined in the package configuration.Customer– Attributes relating to the customer doing the transaction, including their first name, last name, and email address. Defining the customer is not required, but is recommended.TransactionOptions– Attributes relating to the options of the transaction. This is where a payment method can be enforced, and email confirmation can be turned on by providing an email address.Subscription– Attributes relating to a recurring or tokenized subscription.
Without boring you with a table, here’s an example with all possible values provided (with comments to indicate what’s required and what’s not):
use Rockett\Payfast\DataObjects\{
Payment,
TransactionDetails,
Merchant,
Customer,
TransactionOptions,
RecurringSubscription,
TokenizedSubscription
};
$payment = new Payment(
// required
transaction: new TransactionDetails(
amount: 2.00, // required float
itemName: 'Test Item', // required string
itemDescription: 'This is a test item.', // optional string
merchantPaymentIdentifier: (string) $uuid = str()->uuid(), // optional string
customIntegers: [1, 2, 3, 4, 5], // optional array of integers (up to 5)
customStrings: ['a', 'b', 'c', 'd', 'e'], // optional array of strings (up to 5)
),
// optional
merchant: new Merchant(
id: '101010', // required string
key: 'ABC123', // required string
passphrase: '987XYZ', // nullable string (but required when passing in a subscription below)
returnUrl: "https://example-company.co.za/payments/some-id/complete", // optional string
cancelUrl: "https://example-company.co.za/payments/some-id/cancel", // optional string
notifyUrl: "https://example-company.co.za/payments/some-id/itn", // optional string
),
// optional
customer: new Customer(
firstName: 'Joe', // required string
lastName: 'Soap', // optional string
emailAddress: 'joe@soap.co.za', // optional string
cellNumber: '0110220333', // optional string
),
// optional
options: new TransactionOptions(
paymentMethod: PaymentMethod::creditCard, // required PaymentMethod
confirmationAddress: 'payments@example-company.co.za', // optional string (turns on email_confirmation when set)
),
// optional
subscription: new RecurringSubscription(
cycleFrequency: SubscriptionCycleFrequency::monthly, // required SubscriptionCycleFrequency
cycles: 0, // required integer
cycleStartDate: today(), // optional Carbon\CarbonInterface
futureRecurringAmount: 1.00, // optional float
notifyEmail: true, // optional boolean
notifyWebhook: true, // optional boolean
notifyBuyer: true, // optional boolean
),
// OR
subscription: new TokenizedSubscription, // takes no parameters
);
You might be wondering why some of the names of the parameters are different… It’s really quite simple: Laravel encourages expressive and self-descriptive development that’s easy to understand. The parameter names were chosen based on this basic principle.
Creating a Payment Form
Now that we have a Payment object, we can generate a payment form that contains hidden data fields and a form-submission button that will redirect the customer to the PayFast payment page.
To do this, the PayfastService has a method called createPaymentForm that accepts, amoung other things, the payment object:
$form = $payfast->createPaymentForm($payment);
This will generate a full form that posts to the PayFast process endpoint on either the production site or the sandbox, based on whether or not testMode is enabled in the configuration.
Note: The signature is generated and included in the form automatically. If you have set a passphrase (either in the package configuration, or in a Merchant object), it will be included when generating the signature.
You can now use the form on your site by passing it to a view, or using it as a string response to a request:
Pass it to a view:
use Illuminate\Contracts\View\View;
use Rockett\Payfast\Contracts\PayfastFactory;
class InitiatePayment
{
public function __invoke(PayfastFactory $payfast): View
{
// $payment = ...
$form = $payfast->createPaymentForm($payment);
return view('forms.payment', ['form' => $form]);
}
}
Returning it as a string:
use Rockett\Payfast\Contracts\PayfastFactory;
class InitiatePayment
{
public function __invoke(PayfastFactory $payfast): string
{
// $payment = ...
return $payfast->createPaymentForm($payment);
}
}
Form Options
The createPaymentForm method accepts two additional parameters that you can use to customize the form:
?string $formId = null– Provide a customidfor the<form/>element. This can also be changed by adjustingdefaultFormIdin the package configuration. By default, the form’s ID ispayfastPaymentForm.?Button $button = null– Provide a custom submit button for the form, per the example below. This can also be changed by adjustingpaymentButtonTextandpaymentButtonAttributesin the package configuration, which defaults toPayand[], respectively.
Create a Custom button
use Rockett\Payfast\DataObjects\Button;
$displayAmount = 'R100.00';
$button = new Button(
text: "Pay $displayAmount",
attributes: ['class' => 'px-4 h-2 bg-blue-500 shadow text-white flex items-center']
);
Note: If you want to include built-in Tailwind classes, you should add the relevant file(s) to the content array in your tailwind.config.js file:
module.exports = {
content: [
'./path/to/file.php'
// …
]
// …
]
This is somewhat anti-pattern, however, and so it’s recommended to use a custom class that your main CSS file defines instead, for example:
$button = new Button(
attributes: ['class' => 'blue-button']
);
.blue-button {
@apply px-4 h-2 bg-blue-500 shadow text-white flex items-center;
}
Initiating a Server-Side Redirect
As noted earlier, this package also provides you with the ability to initiate a payment using a server-side redirect.
This approach is fully supported by PayFast, and they do not discourage its use. It is safe, if not safer, than directing the customer to a payment form using the browser alone.
If you’d like to use this approach, you can call the initiatePayment method instead of creating a form, and then return a redirect response to the customer’s browser, like so:
use Illuminate\Http\RedirectResponse;
use Rockett\Payfast\Contracts\PayfastFactory;
class InitiatePayment
{
public function __invoke(PayfastFactory $payfast): RedirectResponse
{
// $payment = ...
$pendingPayment = $payfast->initiatePayment($payment);
// If needed, access the redirect URL and/or
// payment UUID before redirecting the customer:
$url = $pendingPayment->paymentUrl;
$uuid = $pendingPayment->uuid;
return $pendingPayment->redirect();
}
}
The paymentUrl property is a string that contains the full URL to redirect the customer to, and the uuid property contains an instance of Ramsey\Uuid\UuidInterface, which is required by illuminate/support. Most, if not all, of the time, you’ll only ever interact with the UUID as a string, but you can do other things with it, too.
Setting up Instant Transaction Notification (ITN) Webhooks
Payfast for Laravel offers several methods for accepting ITNs from PayFast. These ITNs let you know when payments are confirmed or cancelled, and provide additional information pertaining to the payments themselves.
In addition, the package automatically verifies webhook requests so that you need not worry about it at all.
The two primary ways methods to accept ITNs are:
- Injection of
ItnRequestinto your controller, using your own routes - Package-provided routes with event-based handling
Option 1: Your own Handler
This approach is enough for most developers’ needs, and involves setting up a route and corresponding controller to handle the requests.
use App\Http\Controllers\ItnWebHook;
Route::post('payments/itn', ItnWebHook::class);
use Rockett\Payfast\DataObjects\Payment;
use Rockett\Payfast\Enums\PaymentStatus;
use Rockett\Payfast\Requests\ItnRequest;
class ItnWebHook
{
public function __invoke(ItnRequest $request): void
{
match ($request->payment->status) {
PaymentStatus::complete => $this->handlePaymentCompletion($request->payment),
PaymentStatus::cancelled => $this->handlePaymentCancellation($request->payment),
};
}
protected function handlePaymentCompletion(Payment $payment): void
{
// Mark payment as paid in your database, send a receipt, etc.
}
protected function handlePaymentCancellation(Payment $payment): void
{
// Mark payment as cancelled, send notification, etc.
}
}
Wait, why is this so easy? 👀
Remember that Black Magic tag at the top of this file? That’s precisely what’s going on here.
The ItnRequest behaves very much like a FormRequest and validates every parameter sent by PayFast to your webhook, except that it also –
- ensures that the request actually came from PayFast,
- verifies the signature of the incoming payload,
- asks PayFast to confirm that all is in order,
- hydrates a fresh
Paymentobject that you can use to update your database, or do whatever else needs doing, AND - sends an HTTP 200 OK response to PayFast while you aren’t looking.
Yes, you read that correctly. And that’s why you don’t need to return your own response in your controller. All you need to do is react to a confirmed or cancelled payment by handling the actual business logic that matters to you.
The reason the package sends an HTTP response early is to ensure that PayFast is immediately informed that your webhook received the request, because that’s all it cares about. This means that if your business logic fails somewhere along the line (like, your disk could be full, or your database engine could go offline), then you can deal with it without accidentally failing the request. If the request fails, and PayFast doesn’t receive an HTTP 200 OK response from you, then they’ll send the request again, which you don’t really want.
Option 2: Package-provided Handler
If you’d like to skip your own route and controller, you can use those provided by the package.
use Illuminate\Support\Facades\Route;
Route::forPayfast(); // or ::payfast()
This will register the following route:
| URI | Handler |
|---|---|
/payfast/webhook/itn |
Rockett\Payfast\Handlers\ItnWebhook::class |
The ItnWebhook handler does two things:
- Injects an
ItnRequest(much like you do for your own handler) - Dispatches a
Rockett\Payfast\Events\ItnReceivedevent that your app can listen to to handle the payment.
This provides flexibility by allowing you to react to the event immediately, or by using a queued listener.
The ItnReceived Event
This event simply contains an instance of ItnRequest.
Customizing the Route
If you’d like to specify your own route URI and name, you can pass the relevant arguments to the forPayfast method:
Route::forPayfast(
uri: 'payments/process',
name: 'process-payment',
);
Listening to the Event
To listen to the ItnReceived event, first create an event listener:
use Rockett\Payfast\Events\ItnReceived;
class ProcessPayment
{
public function handle(ItnReceived $event): void
{
// Process the payment…
}
}
Then, register it in your EventServiceProvider, or using whatever registration method your application uses.
use App\Listeners\ProcessPayment;
use Rockett\Payfast\Events\ItnReceived;
protected $listen = [
ItnReceived::class => [ProcessPayment::class],
];