Note: this repository is not published. It was a WIP years ago, and isn’t used. For the time being, I leave the code here for anyone to make use of. I may release this one day.
Find a file
Mike Rockett e0c18bb2dc
wip
2022-05-07 14:13:58 +02:00
config wip 2022-05-01 12:45:31 +02:00
src wip 2022-05-07 14:13:58 +02:00
tests wip 2022-05-07 08:38:12 +02:00
.editorconfig init 2022-04-30 20:34:04 +02:00
.gitattributes init 2022-04-30 20:34:04 +02:00
.gitignore init 2022-04-30 20:34:04 +02:00
composer.json wip 2022-05-07 08:38:12 +02:00
composer.lock wip 2022-05-01 13:43:57 +02:00
phpunit.xml init 2022-04-30 20:34:04 +02:00
README.md wip 2022-05-07 14:13:58 +02:00

Payfast for Laravel

forthebadge forthebadge

A PayFast integration for the Laravel framework.

This package requires at least Laravel 9 running on PHP 8.1+.



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, well 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 PayFasts 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, lets 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 isnt 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, well impleent Onsite payments in this package.

Before we get to the trigger that initiates a payment, lets 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, heres an example with all possible values provided (with comments to indicate whats required and whats 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… Its really quite simple: Laravel encourages expressive and self-descriptive development thats 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 custom id for the <form/> element. This can also be changed by adjusting defaultFormId in the package configuration. By default, the forms ID is payfastPaymentForm.
  • ?Button $button = null Provide a custom submit button for the form, per the example below. This can also be changed by adjusting paymentButtonText and paymentButtonAttributes in the package configuration, which defaults to Pay and [], 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 its 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 youd like to use this approach, you can call the initiatePayment method instead of creating a form, and then return a redirect response to the customers 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, youll 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 ItnRequest into 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? Thats precisely whats 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 Payment object 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 arent looking.

Yes, you read that correctly. And thats why you dont 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 thats 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 doesnt receive an HTTP 200 OK response from you, then theyll send the request again, which you dont really want.

Option 2: Package-provided Handler

If youd 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\ItnReceived event 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 youd 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],
];