На ((http://culttt.com/2014/05/28/handle-money-currency-web-applications/==прошлой неделе)) я говорил о возможных подводных камнях при работе с деньгами и валютами в ваших приложениях. Есть много вещей, которые стоит учитывать при работе с деньгами в приложении, и множество хороших практик, которые должны непременно использоваться в вашем коде. В этой статье я покажу вам, что надо делать для абстрагирования многих из этих хороших практик в PHP-пакете для работы с деньгами. Абстрагируя этот код в его собственный пакет, мы можем подключить его к любому PHP-приложению, которому требуется работать с деньгами. Это значит, что мы не должны думать обо всех деталях реализации в каждом приложении. Будем считать, что вы уже создали структуру пакета, которая соответствует стандарту PSR-4, описанному мной в ((http://culttt.com/2014/05/07/create-psr-4-php-package/==этой статье)). ==Работа с валютами== Как я говорил на прошлой неделе, управление различными валютами - чрезвычайно важный аспект при создании приложения, поддерживающего международную торговлю. В мире существует много различных типов валют, и для каждого типа нам требуется множество данных. К счастью, щедрый мир Open Source уже исследовал и предоставил список мета данных, которые мы можем использовать не изобретая велосипед. Это и есть прелесть Open Source. Создайте новую папку %%(t)config%% в вашей папке %%(t)src%%. Затем скопируйте два json-файла из репозитория ((https://github.com/RubyMoney/money/blob/master/config==RubyMoney)) на GitHub. Пакет, который мы создадим, будет бессовестным портом RubyMoney на PHP. Эти два json-файла служат постоянным хранилищем всех различных типов мировых валют. Далее нам надо создать объект для работы с этими типами валют. Создайте новый файл %%(t)Currency.php%% (Валюта) в папке %%(t)src%% и задайте свойства класса для каждого ключа json-конфигурации: %% priority = $currencies[$name]['priority']; $this->iso_code = $currencies[$name]['iso_code']; $this->name = $currencies[$name]['name']; $this->symbol = $currencies[$name]['symbol']; $this->alternate_symbols = $currencies[$name]['alternate_symbols']; $this->subunit = $currencies[$name]['subunit']; $this->subunit_to_unit = $currencies[$name]['subunit_to_unit']; $this->symbol_first = $currencies[$name]['symbol_first']; $this->html_entity = $currencies[$name]['html_entity']; $this->decimal_mark = $currencies[$name]['decimal_mark']; $this->thousands_separator = $currencies[$name]['thousands_separator']; $this->iso_numeric = $currencies[$name]['iso_numeric']; } %% В этом методе мы принимаем название валюты в качестве аргумента и затем конвертируем его в строку в нижнем регистре. Далее мы берём содержимое двух json-файлов и объединяем их в массив, с которым будем работать. Если переданное название валюты не совпадает с одним из находящихся в мета данных в json-файлах, мы можем просто выдавать исключение. Если же валюта найдена в мета данных, мы можем заполнить все свойства класса. Как и в прошлых статьях про пакеты, создайте новую папку %%(t)Exception%% и скопируйте следующий изменённый класс исключения: %% priority = $currencies[$name]['priority']; $this->iso_code = $currencies[$name]['iso_code']; $this->name = $currencies[$name]['name']; $this->symbol = $currencies[$name]['symbol']; $this->alternate_symbols = $currencies[$name]['alternate_symbols']; $this->subunit = $currencies[$name]['subunit']; $this->subunit_to_unit = $currencies[$name]['subunit_to_unit']; $this->symbol_first = $currencies[$name]['symbol_first']; $this->html_entity = $currencies[$name]['html_entity']; $this->decimal_mark = $currencies[$name]['decimal_mark']; $this->thousands_separator = $currencies[$name]['thousands_separator']; $this->iso_numeric = $currencies[$name]['iso_numeric']; } /** * Создание нового объекта Currency * * @param string $name * @return PhilipBrown\Money\Currency */ public static function init($name) { return new Currency($name); } /** * Get Priority * * @return string */ public function getPriority() { return $this->priority; } /** * Get ISO Code * * @return string */ public function getIsoCode() { return $this->iso_code; } /** * Get Name * * @return string */ public function getName() { return $this->name; } /** * Get Symbol * * @return string */ public function getSymbol() { return $this->symbol; } /** * Get Alternate Symbols * * @return string */ public function getAlternateSymbols() { return $this->alternate_symbols; } /** * Get Subunit * * @return string */ public function getSubunit() { return $this->subunit; } /** * Get Subunit to unit * * @return string */ public function getSubunitToUnit() { return $this->subunit_to_unit; } /** * Get Symbol First * * @return string */ public function getSymbolFirst() { return $this->symbol_first; } /** * Get HTML Entity * * @return string */ public function getHtmlEntity() { return $this->html_entity; } /** * Get Decimal Mark * * @return string */ public function getDecimalMark() { return $this->decimal_mark; } /** * Get Thousands Seperator * * @return string */ public function getThousandsSeperator() { return $this->thousands_separator; } /** * Get ISO Numberic * * @return string */ public function getIsoNumeric() { return $this->iso_numeric; } /** * @return string */ public function __toString() { return $this->getName(); } } %% ==Работа с деньгами== Теперь, когда мы можем представлять валюты как объекты в пакете, нам также нужна возможность представлять денежные величины как объекты. Создайте новый класс %%(t)Money.php%% в папке %%(t)src%%: %% fractional = $fractional; $this->currency = $currency; } %% Снова, как и с объектом %%Currency%%, я добавлю немного синтаксического сахара с помощью статического метода %%init()%%: %% /** * Статическая функция для создания нового экземпляра Money. * * @param int $value * @param string $currency * @return PhilipBrown\Money\Money */ public static function init($value, $currency) { return new Money($value, new Currency($currency)); } %% И также как в объекте %%Currency%% я добавлю два getter-метода для доступа к защищённым свойствам класса: %% /** * Получение дробного значения объекта * * @return int */ public function getCentsParameter() { return $this->fractional; } /** * Получение объекта Currency * * @return PhilipBrown\Money\Currency */ public function getCurrencyParameter() { return $this->currency; } %% Ещё я добавлю волшебный метод %%__get()%%, чтобы свойства класса %%cents%% и %%currency%% автоматически вызывали два следующих метода: %% /** * Волшебный метод для динамического получения параметров объекта * * @return mixed */ public function __get($param) { $method = 'get'.ucfirst($param).'Parameter'; if(method_exists($this, $method)) return $this->{$method}(); } %% Как я написал в статье ((http://culttt.com/2014/04/16/php-magic-methods/==Что такое волшебные методы PHP)), этот волшебный метод будет вызываться, когда вы попытаетесь получить доступ к свойствам класса, которые не открыты (public). В этом случае метод %%__get()%% будет принимать в качестве аргумента требуемое свойство и проверять, определён ли соответствующий getter-метод. ==Важность равенства== При работе с деньгами в приложении чрезвычайно важно равенство. Например, будет очень плохо, если вы сможете сложить две величины с разными валютами, потому что это приведёт к бухгалтерскому кошмару. Чтобы убедиться в том, что два объекта %%Money%% имеют одинаковую валюту, мы можем добавить метод для проверки: %% /** * Проверка кода Iso для оценки равенства валют * * @param PhilipBrown\Money\Money * @return bool */ public function isSameCurrency(Money $money) { return $this->currency->getIsoCode() == $money->currency->getIsoCode(); } %% Как я писал в статье ((http://culttt.com/2014/04/30/difference-entities-value-objects/==В чём различия между Сущностями и Объектами-значениями)), объекты-значения основывают равенство на атрибутах объекта, а не их идентичности. Мы можем добавить метод для проверки равенства указанного экземпляра %%Money%% и текущего экземпляра %%Money%%: %% /** * Проверка равенства двух объектов Money. * Сначала проверяется валюта, затем величина. * * @param PhilipBrown\Money\Money * @return bool */ public function equals(Money $money) { return $this->isSameCurrency($money) && $this->cents == $money->cents; } %% Важно проверить совпадение и валют и величин, чтобы мы могли утверждать, что два объекта %%Money%% равны. ==Выполнение вычислений над объектами Money== В коммерческом приложении вам неизбежно потребуется выполнять вычисления над объектами %%Money%%. Возможно самым распространённым случаем будет сложение стоимости двух продуктов: %% /** * Сложение величин двух объектов Money и возвращение нового объекта Money. * * @param PhilipBrown\Money\Money $money * @return PhilipBrown\Money\Money */ public function add(Money $money) { if($this->isSameCurrency($money)) { return Money::init($this->cents + $money->cents, $this->currency->getIsoCode()); } throw new InvalidCurrencyException("You can't add two Money objects with different currencies"); } %% Как я уже упоминал, перед сложением нам надо проверить, что валюты обоих объектов %%Money%% совпадают. Если валюты объектов совпадают, мы можем взять величину текущего объекта и величину объекта, который мы хотим добавить, а также валюту для создания нового объекта %%Money%%. Помните, объекты-значения неизменны и поэтому, если вы хотите изменить величину объекта-значения, вам надо уничтожить текущий объект и создать новый. Наконец, если валюты объектов не совпадают, мы можем передать исключение, так как это действительно плохая ситуация. Метод %%subtract()%% почти идентичен предыдущему, только он вычитает величины вместо их сложения: %% /** * Вычитание величины одного объекта Money из другого и возвращение нового объекта * * @param PhilipBrown\Money\Money $money * @return PhilipBrown\Money\Money */ public function subtract(Money $money) { if($this->isSameCurrency($money)) { return Money::init($this->cents - $money->cents, $this->currency->getIsoCode()); } throw new InvalidCurrencyException("You can't subtract two Money objects with different currencies"); } %% Умножение и деление объектов %%Money%% немного сложнее, так как мы можем столкнуться с ситуациями, когда нам надо округлять величину до целого числа. Функция PHP [[php:round round()]] может нам в этом помочь. Для умножения объекта %%Money%% я буду использовать этот метод: %% /** * Умножение объекта Money и возвращение нового объекта * * @param int $number * @return PhilipBrown\Money\Money */ public function multiply($number) { return Money::init((int) round($this->cents * $number, 0, PHP_ROUND_HALF_EVEN), $this->currency->getIsoCode()); } %% А для деления объекта %%Money%% я буду использовать этот метод: %% /** * Деление объекта Money и возвращение нового объекта * * @param int $number * @return PhilipBrown\Money\Money */ public function divide($number) { return Money::init((int) round($this->cents / $number, 0, PHP_ROUND_HALF_EVEN), $this->currency->getIsoCode()); } %% Заметьте, в обоих методах я передаю в функцию %%round()%% значение точности %%0%% и режим %%PHP_ROUND_HALF_EVEN%%. Вы должны убедиться в том, что везде в вашем приложении используется выбранный способ округления. ==Работа с этим пакетом== Для создания нового объекта %%Money%% вы либо создаёте экземпляр как обычно, либо используете удобный статический метод %%init()%%. %% // Создание нового объекта Money, представляющего $5 USD $m = Money::init(500, 'USD'); $m = new Money(500, 'USD'); %% Для доступа к величине объекта %%Money%% вы можете просто запросить свойство %%cents%%. Для получения валюты объекта вы можете запросить свойство %%currency%%: %% $m->cents; // 500 $m->currency; // United States Dollar %% Равенство важно для работы со множеством различных типов валют. У вас не должно быть возможности слепо складывать две разные валюты без какого-либо процесса обмена: %% $m = Money::init(500, 'USD'); $m->isSameCurrency(Money::init(500, 'GBP')); // false %% Объект-значение - объект, представляющий сущность, равенство которой не основано на идентичности: то есть два объекта-значения равны, когда они имеют одинаковые значения, это не обязательно должны быть одинаковые объекты: %% $one = Money::init(500, 'USD'); $two = Money::init(500, 'USD'); $three = Money::init(501, 'USD'); $one->equals($two); // true $one->equals($three); // false %% Вам неизбежно понадобится складывать, вычитать, умножать и делить денежные величины в вашем приложении: %% $one = Money::init(500, 'USD'); $two = Money::init(500, 'USD'); $three = $one->add($two); $three->cents // 1000 %% Повторюсь, у вас не должно быть возможности складывать величины с разными валютами без какого-либо процесса обмена: %% $one = Money::init(500, 'USD'); $two = Money::init(500, 'GBP'); $three = $one->add($two); // Money\Exception\InvalidCurrencyException %% ==Заключение== В этой статье мы создали пакет, который абстрагирует множество головной боли при работе с деньгами. Мы создали объекты, которые представляют деньги и валюты, и мы реализовали некоторые правила того, как различные величины могут рассчитываться, храниться и сравниваться в вашем приложении. Использование этого пакета поможет избежать множества проблем и подводных камней, с которыми вы можете столкнуться при работе с деньгами и валютами в вашем приложении. Однако, как я писал в статье ((http://culttt.com/2014/05/28/handle-money-currency-web-applications/==Как обрабатывать деньги и валюты в веб-приложениях)), есть ещё ряд вещей, о которых мы должны думать при работе с деньгами в приложениях. Если вы захотите использовать этот пакет в одном из ваших проектов, он доступен на ((https://github.com/philipbrown/money==GitHub)). Загляните на ((https://github.com/RubyMoney/money==RubyMoney)) и ((http://verraes.net/2011/04/fowler-money-pattern-in-php/==Mathias Verraes)) для вдохновения. На следующей неделе я покажу вам, как создать пакет, который может использовать этот пакет Money для абстрагирования еще большего количества головной боли при работе с деньгами в вашем приложении. Наслаивая эти небольшие концентрированные пакеты, мы можем создать прочный фундамент для построения высококачественных сайтов и приложений в области электронной коммерции.