src/NetglueMoney/Form/MoneyFieldset.php
<?php
declare(strict_types=1);
namespace NetglueMoney\Form;
use Locale;
use NetglueMoney\Hydrator\MoneyHydrator;
use NetglueMoney\Money\Money;
use NetglueMoney\Validator\CurrencyCode;
use NumberFormatter;
use Throwable;
use Zend\Filter\StringToUpper;
use Zend\Filter\StringTrim;
use Zend\Form\Element as ZendElement;
use Zend\Form\ElementInterface;
use Zend\Form\Fieldset;
use Zend\I18n\Filter\NumberParse;
use Zend\I18n\Validator\IsFloat;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Validator\GreaterThan;
use Zend\Validator\LessThan;
class MoneyFieldset extends Fieldset implements InputFilterProviderInterface
{
/**
* Locale string used for interpreting inputted numbers
* @var string
*/
protected $locale;
/**
* Money instances are all we want
* @var string
*/
protected $allowedObjectBindingClass = Money::class;
/**
* Currency Code Element Specification
* @var array
*/
protected $currencyElementSpec = [
'name' => 'currency',
'type' => ZendElement\Text::class,
'options' => [
],
'attributes' => [
'maxlength' => 3,
'required' => true,
'placeholder' => 'XXX',
],
];
/**
* Amount Element Specification
* @var array
*/
protected $amountElementSpec = [
'name' => 'amount',
'type' => Element\Money::class,
'options' => [
],
'attributes' => [
'required' => true,
'placeholder' => '0.00',
],
];
/**
* Options used to seed the GreaterThan validator if required
* @var array
*/
private $minimumOptions = [];
/**
* Options used to seed the LessThan validator if required
* @var array
*/
private $maximumOptions = [];
/**
* @param null|int|string $name Optional name for the element
* @param array $options Optional options for the element
*/
public function __construct($name = null, $options = [])
{
parent::__construct($name, $options);
/**
* Use specific hydrator that converts a money object to an array
* with the keys 'amount', 'currency' and returns a new money
* instance given an array with these keys
*/
$this->setHydrator(new MoneyHydrator);
}
/**
* Init
* @return void
*/
public function init() : void
{
$this->initialiseElements();
$code = $this->getDefaultCurrencyCode();
if ($code) {
$this->get('currency')->setValue($code);
}
}
/**
* Adds the required elements if they do not already exist
* @return void
*/
private function initialiseElements() : void
{
if (! $this->has('currency')) {
$this->add($this->getCurrencyElementSpec());
}
if (! $this->has('amount')) {
$this->add($this->getAmountElementSpec());
}
}
/**
* Get input spec
* @return array
*/
public function getInputFilterSpecification() : array
{
$required = true;
if ($this->hasAttribute('required')) {
$required = $this->getAttribute('required');
}
$amountValidators = [
[
'name' => IsFloat::class,
'options' => [
'locale' => $this->getLocale(),
],
],
];
if (count($this->minimumOptions)) {
$spec = [
'name' => GreaterThan::class,
'options' => [
'min' => $this->minimumOptions['min'],
'inclusive' => $this->minimumOptions['inclusive'],
],
];
if (! empty($this->minimumOptions['message'])) {
$spec['options']['messages'] = [
GreaterThan::NOT_GREATER => $this->minimumOptions['message'],
GreaterThan::NOT_GREATER_INCLUSIVE => $this->minimumOptions['message'],
];
}
$amountValidators[] = $spec;
}
if (count($this->maximumOptions)) {
$spec = [
'name' => LessThan::class,
'options' => [
'max' => $this->maximumOptions['max'],
'inclusive' => $this->maximumOptions['inclusive'],
],
];
if (! empty($this->maximumOptions['message'])) {
$spec['options']['messages'] = [
LessThan::NOT_LESS => $this->maximumOptions['message'],
LessThan::NOT_LESS_INCLUSIVE => $this->maximumOptions['message'],
];
}
$amountValidators[] = $spec;
}
return [
'currency' => [
'required' => $required,
'filters' => [
['name' => StringTrim::class],
['name' => StringToUpper::class],
],
'validators' => [
['name' => CurrencyCode::class],
],
],
'amount' => [
'required' => $required,
'filters' => [
['name' => StringTrim::class],
[
'name' => NumberParse::class,
'options' => [
'style' => NumberFormatter::DECIMAL,
'type' => NumberFormatter::TYPE_DOUBLE,
'locale' => $this->getLocale(),
],
],
],
'validators' => $amountValidators,
],
];
}
/**
* Set the given money object as the bound object, and populate the form fields with the values
* @param Money $money
*/
public function setMoney(Money $money) : void
{
$this->setObject($money);
$this->initialiseElements();
$this->populateValues($this->extract());
}
/**
* Try to return a money object based on current values if possible
*/
public function getMoney() :? Money
{
$object = $this->getObject();
if ($object instanceof Money) {
return $object;
}
try {
$money = $this->getHydrator()->hydrate([
'amount' => $this->getAmountElement()->getValue(),
'currency' => $this->getCurrencyElement()->getValue(),
], null);
return $money instanceof Money ? $money : null;
} catch (Throwable $error) {
return null;
}
}
public function getCurrencyElement() : ElementInterface
{
$this->initialiseElements();
return $this->get('currency');
}
public function getAmountElement() : ElementInterface
{
$this->initialiseElements();
return $this->get('amount');
}
/**
* Return currency element specification
* @return array
*/
public function getCurrencyElementSpec() : array
{
return $this->currencyElementSpec;
}
/**
* Set the currency element specification
* @param array $spec
*/
public function setCurrencyElementSpec(array $spec) : void
{
$this->currencyElementSpec = $spec;
}
/**
* Return amount element specification
* @return array
*/
public function getAmountElementSpec() : array
{
return $this->amountElementSpec;
}
/**
* Set amount element specification
* @param array $spec
*/
public function setAmountElementSpec(array $spec) : void
{
$this->amountElementSpec = $spec;
}
public function setLocale(?string $locale = null) : void
{
$this->locale = $locale;
}
public function getLocale() : string
{
if (null === $this->locale) {
return Locale::getDefault();
}
return $this->locale;
}
public function setDefaultCurrencyCode(string $code) : void
{
$this->options['default_currency'] = $code;
$this->currencyElementSpec['attributes']['value'] = $code;
}
public function getDefaultCurrencyCode() :? string
{
return $this->options['default_currency'] ?? null;
}
/**
* Set a minimum amount with an optional error message
* @param float|int $min
* @param bool $inclusive
* @param string $message
*/
public function setMinimumAmount($min, bool $inclusive = false, ?string $message = null) : void
{
$this->minimumOptions = [
'min' => $min,
'inclusive' => $inclusive,
'message' => $message,
];
}
/**
* Set a maximum amount with an optional error message
* @param float|int $max
* @param bool $inclusive
* @param string $message
*/
public function setMaximumAmount($max, bool $inclusive = false, ?string $message = null) : void
{
$this->maximumOptions = [
'max' => $max,
'inclusive' => $inclusive,
'message' => $message,
];
}
public function allowValueBinding() : bool
{
return true;
}
}