Skip to content

Instantly share code, notes, and snippets.

@aertmann
Last active February 12, 2018 21:30
Show Gist options
  • Save aertmann/8a09204e5a051d0641f8 to your computer and use it in GitHub Desktop.
Save aertmann/8a09204e5a051d0641f8 to your computer and use it in GitHub Desktop.
Donation plugin for Neos using form framework and custom payment gateway integration (QuickPay) – Included in https://speakerdeck.com/aertmann/tasty-recipes-for-every-day-neos

Using the TYPO3.Form framework to create a form and a custom package to handle payment gateway callback

Plugin creates the form using a custom form factory

Form displays a confirmation page and handles validation and if passed redirects to the payment gateway with a custom form listener that stores the form information

The plugin handles the returning customer

A custom controller is used to capture the payment gateways callback and stores the payment information and processes the order through a signal

prototype(Acme.Donate:Donate) < prototype(TYPO3.Neos:Plugin) {
package = 'Venstre.VenstreDk'
controller = 'Donate'
node = ${node}
}
<?php
namespace Acme\Donate\Form\Finishers;
use TYPO3\Flow\Annotations as Flow;
class DonateFinisher extends \TYPO3\Form\Core\Model\AbstractFinisher {
/**
* @Flow\Inject
* @var \Acme\Donate\Domain\Repository\DonationRepository
*/
protected $donationRepository;
/**
* @Flow\Inject
* @var \TYPO3\Flow\Session\SessionInterface
*/
protected $session;
/**
* @Flow\Inject
* @var \TYPO3\Flow\Persistence\PersistenceManagerInterface
*/
protected $persistenceManager;
/**
* Executes this finisher
* @see AbstractFinisher::execute()
*
* @return void
* @throws \TYPO3\Form\Exception\FinisherException
* @Flow\Session(autoStart = TRUE)
*/
protected function executeInternal() {
$values = $this->finisherContext->getFormValues();
if (!isset($values['price'])) {
$values['price'] = intval($values['priceOther']);
}
if (intval($values['price']) > 10) {
$donation = new \Venstre\VenstreDk\Domain\Model\Donation();
$donation->setName($values['name'])
->setStreet($values['street'])
->setCity($values['city'])
->setZip($values['zip'])
->setPhone($values['phone'])
->setCellphone($values['cellphone'])
->setEmail($values['email']);
$this->donationRepository->add($donation);
$this->persistenceManager->persistAll();
$this->session->putData('donation', $donation->getId());
$this->session->putData('donationPrice', $values['price']);
}
}
}
<?php
namespace Acme\Donate\Form;
use TYPO3\Flow\Annotations as Flow;
use TYPO3\Form\Core\Model\FormDefinition;
use \TYPO3\Flow\Validation\Validator as Validator;
use \Acme\Donate\Form\Finishers as DonateFinisher;
use \TYPO3\Form\Finishers as TYPO3Finisher;
use \TYPO3\Form\Factory as Factory;
/**
* Class DonateFormFactory
*
* @package Acme\Donate\Form
*/
class DonateFormFactory extends Factory\AbstractFormFactory {
/**
* @param array $factorySpecificConfiguration
* @param string $presetName
* @return \TYPO3\Form\Core\Model\FormDefinition
*/
public function build(array $factorySpecificConfiguration, $presetName) {
$formConfiguration = $this->getPresetConfiguration($presetName);
$form = new FormDefinition('donate', $formConfiguration);
$form->setRenderingOption('submitButtonLabel', 'To payment');
$userinformationPage = $form->createPage('page1');
$sectionPrice = $userinformationPage->createElement('sectionPrice', 'TYPO3.Form:Section');
$price = $sectionPrice->createElement('price', 'TYPO3.Form:SingleSelectRadiobuttons');
$price->setProperty('required', 'required');
$price->setProperty('options', $priceOptions);
$price->setProperty('optionSets', $priceOptionSets);
$price->setLabel('Amount');
$sectionInfo = $userinformationPage->createElement('sectionInfo', 'TYPO3.Form:Section');
$sectionInfo->setProperty('elementClassAttribute', 'hidden');
$name = $sectionInfo->createElement('name', 'TYPO3.Form:SingleLineText');
$name->setLabel('Name');
$name->setProperty('required', 'required');
$name->addValidator(new Validator\NotEmptyValidator());
$name->addValidator(new Validator\LabelValidator());
$name->addValidator(new Validator\TextValidator());
$name->addValidator(new Validator\StringLengthValidator(array('minimum' => 5, 'maximum' => 100)));
$street = $sectionInfo->createElement('street', 'TYPO3.Form:SingleLineText');
$street->setLabel('Address');
$street->setProperty('required', 'required');
$street->addValidator(new Validator\NotEmptyValidator());
$street->addValidator(new Validator\LabelValidator());
$street->addValidator(new Validator\TextValidator());
$street->addValidator(new Validator\StringLengthValidator(array('minimum' => 2, 'maximum' => 100)));
$zip = $sectionInfo->createElement('zip', 'TYPO3.Form:SingleLineText');
$zip->setLabel('Zipcode');
$zip->setProperty('required', 'required');
$zip->setProperty('elementType', 'tel');
$zip->addValidator(new Validator\NotEmptyValidator());
$zip->addValidator(new Validator\LabelValidator());
$zip->addValidator(new Validator\TextValidator());
$zip->addValidator(new Validator\StringLengthValidator(array('minimum' => 1, 'maximum' => 10)));
$city = $sectionInfo->createElement('city', 'TYPO3.Form:SingleLineText');
$city->setLabel('City');
$city->setProperty('required', 'required');
$city->addValidator(new Validator\NotEmptyValidator());
$city->addValidator(new Validator\LabelValidator());
$city->addValidator(new Validator\TextValidator());
$city->addValidator(new Validator\StringLengthValidator(array('minimum' => 2, 'maximum' => 100)));
$cellphone = $sectionInfo->createElement('cellphone', 'TYPO3.Form:SingleLineText');
$cellphone->setLabel('Mobile');
$cellphone->setProperty('elementType', 'tel');
$cellphone->addValidator(new Validator\LabelValidator());
$cellphone->addValidator(new Validator\TextValidator());
$cellphone->addValidator(new Validator\StringLengthValidator(array('minimum' => 0, 'maximum' => 14)));
$phone = $sectionInfo->createElement('phone', 'TYPO3.Form:SingleLineText');
$phone->setLabel('Telephone');
$phone->setProperty('elementType', 'tel');
$phone->addValidator(new Validator\LabelValidator());
$phone->addValidator(new Validator\TextValidator());
$phone->addValidator(new Validator\StringLengthValidator(array('minimum' => 0, 'maximum' => 14)));
$email = $sectionInfo->createElement('email', 'TYPO3.Form:SingleLineText');
$email->setLabel('Email');
$email->setProperty('required', 'required');
$email->setProperty('elementType', 'email');
$email->addValidator(new Validator\NotEmptyValidator());
$email->addValidator(new Validator\EmailAddressValidator());
$form->createPage('confirmation', 'TYPO3.Form:PreviewPage');
$donateFinisher = new DonateFinisher\DonateFinisher();
$form->addFinisher($donateFinisher);
$redirectFinisher = new TYPO3Finisher\RedirectFinisher();
$redirectFinisher->setOptions(array(
'action' => 'show',
'controller' => 'Frontend\Node',
'package' => 'TYPO3.Neos',
'format' => 'html',
'arguments' => array(
'node' => $factorySpecificConfiguration['documentNode'],
$factorySpecificConfiguration['argumentNamespace'] => array('@action' => 'paymentForm')
)
));
$form->addFinisher($redirectFinisher);
return $form;
}
}
<?php
namespace Acme\Donate\Listeners\Donate;
use TYPO3\Flow\Annotations as Flow;
use MOC\Payment\Gateway\GatewayInterface;
use TYPO3\Flow\Object\ObjectManagerInterface;
class DonationTransactionPostProcessor {
/**
* @Flow\Inject
* @var \Acme\Donate\Domain\Repository\DonationRepository
*/
protected $donationRepository;
/**
* @Flow\Inject
* @var \TYPO3\Flow\Log\SystemLoggerInterface
*/
protected $systemLogger;
/**
* @Flow\Inject
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @var array
*/
protected $settings;
/**
* @param array $settings
* @return void
*/
public function injectSettings(array $settings) {
$this->settings = $settings;
}
/**
* Listens to processed transactions.
*
* @param \MOC\Payment\Domain\Model\Transaction $transaction
* @param array $transactionData
* @param GatewayInterface $gateway
* @return void
* @Flow\Signal
* @throws \Exception
*/
public function postProcess(\MOC\Payment\Domain\Model\Transaction $transaction, array $transactionData, GatewayInterface $gateway) {
/** @var \Acme\Donate\Domain\Model\Donation $donor */
$donor = $this->donationRepository->findByIdentifier($transaction->getOrderNumber());
if (!$donor) {
return;
}
$donor->setTransaction($transaction);
$this->donationRepository->update($donor);
$validator = new \TYPO3\Flow\Validation\Validator\EmailAddressValidator();
if ($validator->validate($this->settings['donations']['newDonationEmail'])->hasErrors() === TRUE) {
$exception = new \Exception(sprintf('Could not send info about the new donation "%s" - missing email.', $transaction->getOrderNumber()));
$this->systemLogger->logException($exception, $transactionData);
throw $exception;
}
$amount = intval($transaction->getAmount()) / 100;
// send email to the new donor
$viewReceipt = $this->getMailTemplate('resource://Acme.Donate/Private/Templates/Donate/EmailReceipt.html');
$viewReceipt->assignMultiple(array(
'user' => $donor,
'amount' => $amount
));
$mailReceipt = new \TYPO3\SwiftMailer\Message();
$mailReceipt
->setFrom(array($this->settings['donations']['newDonationEmail'] => 'Acme Donations'))
->setTo(array($donor->getEmail() => $donor->getName()))
->setSubject('Donation to Acme')
->setReplyTo($this->settings['donations']['newDonationEmail'])
->setBody($viewReceipt->render(), 'text/plain')
->send();
// send email about the new donor
$viewnewDonor = $this->getMailTemplate('resource://Acme.Donate/Private/Templates/Donate/EmailNewDonation.html');
$viewnewDonor->assignMultiple(array(
'user' => $donor,
'amount' => $amount
));
$mailNewDonor = new \TYPO3\SwiftMailer\Message();
$mailNewDonor
->setFrom(array($this->settings['donations']['newDonationEmail'] => 'Acme donations'))
->setTo(array($this->settings['donations']['newDonationEmail'] => ''))
->setSubject('New donation')
->setReplyTo($this->settings['donations']['newDonationEmail'])
->setBody($viewnewDonor->render(), 'text/plain')
->send();
}
/**
* @param string $templateFile
* @return \TYPO3\Fluid\View\StandaloneView
*/
protected function getMailTemplate($templateFile) {
/** @var \TYPO3\Fluid\View\StandaloneView $view */
$view = $this->objectManager->get('\TYPO3\Fluid\View\StandaloneView');
$view->setLayoutRootPath('resource://Acme.Donate/Private/Layouts');
$view->setPartialRootPath('resource://Acme.Donate/Private/Partials');
$view->setFormat('html');
$view->setTemplatePathAndFilename($templateFile);
return $view;
}
}
<h3>Thank you for your kind donation</h3>
<p>Payment has been completed.</p>
<table>
<tr>
<th>Donation:</th>
<td>{amount} DKK</td>
</tr>
<tr>
<th>Order number:</th>
<td>{user.transaction.orderNumber}</td>
</tr>
<tr>
<th>Name:</th>
<td>{user.name}</td>
</tr>
<tr>
<th>Address:</th>
<td>{user.street}</td>
</tr>
<tr>
<th>Zip/City:</th>
<td>{user.zip} {user.city}</td>
</tr>
<tr>
<th>Telephone:</th>
<td>{user.phone}</td>
</tr>
<tr>
<th>Mobile:</th>
<td>{user.cellPhone}</td>
</tr>
<tr>
<th>Email:</th>
<td>{user.email}</td>
</tr>
</table>
{namespace form=TYPO3\Form\ViewHelpers}
<form:render factoryClass="Acme\Donate\Form\DonateFormFactory" presetName="donate" overrideConfiguration="{node: node, argumentNamespace: argumentNamespace, documentNode: documentNode}" />
'Venstre.VenstreDk:Donate':
superTypes:
- 'TYPO3.Neos:Plugin'
<?php
namespace Acme\Donate;
use TYPO3\Flow\Package\Package as BasePackage;
class Package extends BasePackage {
/**
* Invokes custom PHP code directly after the package manager has been initialized.
*
* @param \TYPO3\Flow\Core\Bootstrap $bootstrap The current bootstrap
* @return void
*/
public function boot(\TYPO3\Flow\Core\Bootstrap $bootstrap) {
$dispatcher = $bootstrap->getSignalSlotDispatcher();
$dispatcher->connect('MOC\Payment\Gateway\AbstractGateway', 'transactionProcessed', 'Acme\Demo\Listeners\Donate\DonationTransactionPostProcessor', 'postProcess');
}
}
<h3>Payment cancelled...</h3>
<f:link.action action="paymentForm">Try again</f:link.action>
<br /><br />
<f:link.action action="cancel">Cancel</f:link.action>
<h3>Payment</h3>
<p>You'll now be redirected automatically to the payment gateway – if not click here:</p>
<form action="{action}" method="post" name="payment" id="payment">
<f:for each="{fields}" as="value" key="name">
<input type="hidden" name="{name}" id="{name}" value="{value}" />
</f:for>
<input type="submit" value="Go to payment">
</form>
<script>
document.forms['payment'].submit();
</script>
{namespace form=TYPO3\Form\ViewHelpers}
{namespace media=TYPO3\Media\ViewHelpers}
{namespace donate=Acme\Donate\ViewHelpers}
<fieldset>
<f:if condition="{page.label}">
<legend>{page.label}</legend>
</f:if>
<f:alias map="{formValues: '{donate:form.formValues()}'}">
<table>
<tr>
<th style="width: 170px">Label</th>
<th>Value</th>
</tr>
<tr><th>Donation</th><td>{formValues.price}</td></tr>
<form:renderValues renderable="{page.rootForm}">
<tr class="{formValue.element.identifier}">
<th>{formValue.element.label}</th>
<td>
<f:if condition="{formValue.value}">
<f:then>
<f:if condition="{0: formValue.element.type} == {0: 'TYPO3.Form:ImageUpload'}">
<f:then>
<a href="{media:uri.image(image: formValue.value, maximumWidth: 600)}" rel="lightbox">
<media:image image="{formValue.value}" maximumWidth="100" alt="{formValue.processedValue}" />
</a>
</f:then>
<f:else>
<f:if condition="{formValue.isMultiValue}">
<f:then>
<ul>
<f:for each="{formValue.processedValue}" as="value">
<li>{value}</li>
</f:for>
</ul>
</f:then>
<f:else>
{formValue.processedValue}
</f:else>
</f:if>
</f:else>
</f:if>
</f:then>
<f:else>
-
</f:else>
</f:if>
</td>
</tr>
</form:renderValues>
</table>
</f:alias>
</fieldset>
TYPO3:
Form:
yamlPersistenceManager:
savePath: 'resource://Acme.Demo/Private/Form/'
presets:
default:
formElementTypes:
'TYPO3.Form:SingleLineText':
renderingOptions:
templatePathPattern: 'resource://Acme.Demo/Private/Templates/Form/{@type}.html'
donate:
parentPreset: 'default'
formElementTypes:
'TYPO3.Form:PreviewPage':
renderingOptions:
templatePathPattern: 'resource://Acme.Demo/Private/Templates/Form/{@type}.html'
<f:layout name="TYPO3.Form:Field" />
<f:section name="field">
<f:form.textfield
type="{f:if(condition: element.properties.elementType, then: element.properties.elementType, else: 'text')}"
property="{element.identifier}"
id="{element.uniqueIdentifier}"
class="{element.properties.elementClassAttribute}"
placeholder="{element.properties.placeholder}"
errorClass="{element.properties.elementErrorClassAttribute}"
required="{element.properties.required}"
additionalAttributes="{element.properties.additionalAttributes}"
/>
</f:section>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment