/** * @override * @see _P::execute() * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { // 2016-06-05 // @see urldecode() здесь вызывать уже не надо, проверял. /** @var string $redirectUrl */ $redirectUrl = df_request($this->redirectUrlKey()) ?: df_url(); /** * 2016-12-02 * Если адрес для перенаправления покупателя передётся в адресе возврата, * то адрес для перенаправления там закодирован посредством @see base64_encode() * @see \Dfe\BlackbaudNetCommunity\Url::get() */ if (!df_starts_with($redirectUrl, 'http')) { $redirectUrl = base64_decode($redirectUrl); } try { /** @var Session|DfSession $s */ $s = df_customer_session(); if (!$this->mc()) { /** * 2016-12-01 * Учётная запись покупателя отсутствует в Magento, * и в то же время информации провайдера SSO недостаточно * для автоматической регистрации покупателя в Magento * (случай Blackbaud NetCommunity). * Перенаправляем покупателя на стандартную страницу регистрации Magento, * где часть полей будет уже заполнена данными от провайдера SSO, * а пароль будет либо скрытым, либо необязательным полем. * После регистрации свежесозданная учётная запись будет привязана * к учётной записи покупателя в провайдере SSO. */ $redirectUrl = df_customer_url()->getRegisterUrl(); } else { /** * 2015-10-08 * По аналогии с @see \Magento\Customer\Controller\Account\LoginPost::execute() * https://github.com/magento/magento2/blob/1.0.0-beta4/app/code/Magento/Customer/Controller/Account/LoginPost.php#L84-L85 */ $s->setCustomerDataAsLoggedIn($this->mc()->getDataModel()); $s->regenerateId(); /** * По аналогии с @see \Magento\Customer\Model\Account\Redirect::updateLastCustomerId() * Напрямую тот метод вызвать не можем, потому что он protected, * а использовать весь класс @see \Magento\Customer\Model\Account\Redirect пробовал, * но оказалось неудобно из-за слишком сложной процедуры перенаправлений. */ if ($s->getLastCustomerId() != $s->getId()) { $s->unsBeforeAuthUrl()->setLastCustomerId($s->getId()); } } } catch (\Exception $e) { df_message_error($e); } $this->postProcess(); return $this->resultRedirectFactory->create()->setUrl($redirectUrl); }
/** * @override * @param string $value * @return bool */ public function isValid($value) { $this->prepareValidation($value); /** * Думаю, правильно будет конвертировать строки типа «09» в целые числа без сбоев. * Обратите внимание, что * 9 === (int)'09'. * * Обратите также внимание, что если строка равна '0', * то нам применять @see ltrim нельзя, потому что иначе получим пустую строку. * * 2015-01-23 * Раньше код был таким: if ('0' !== $value) { $value = ltrim($value, '0'); } return strval($value) === strval(intval($value)); это приводило к неправильной работе метода для значения «0.0» (вещественное число), * потому что ltrim(0.0, '0') возвращает пустую строку. * Предыдущая версия кода была написала 2014-08-30 * (хотя и версии до неё были тоже дефектными, просто там дефекты были другие). */ /** @var string $valueAsString */ $valueAsString = is_string($value) && '0' !== $value && !df_starts_with($value, '0.') ? ltrim($value, '0') : strval($value); return $valueAsString === strval((int) $value); }
/** * 2016-07-28 * @override * @see ObserverInterface::execute() * @used-by \Magento\Framework\Event\Invoker\InvokerDefault::_callObserverMethod() * @param O $o * @return void */ public function execute(O $o) { /** @var Provider $provider */ $provider = $o[Plugin::PROVIDER]; /** @var ISearchResult|ApiSearchResult|UiSearchResult|OrderGC|InvoiceGC|CreditmemoGC $result */ $result = $o[Plugin::RESULT]; if (in_array($provider->getName(), ['sales_order_grid_data_source'])) { /** * 2016-07-28 * https://github.com/magento/magento2/blob/2.1.0/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/SearchResult.php#L37-L40 * @see \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult::$document * * Структура документа описана здесь: https://mage2.pro/t/1908 */ /** @var string $cacheKey */ $cacheKey = __METHOD__; /** @var string $prop */ $prop = 'payment_method'; df_map(function (Document $item) use($cacheKey, $prop) { /** @var string|null $methodCode */ $methodCode = $item[$prop]; if ($methodCode && df_starts_with($methodCode, 'dfe_')) { /** @var int $id */ $id = $item['entity_id']; /** * 2016-07-29 * Эта операция очень ресурсоёмка: * для каждой строки таблицы заказов она делает кучу запросов к базе данных. * Поэтому кэшируем результаты в постоянном кэше. */ $item[$prop] = df_cache_get_simple([$cacheKey, $id], function () use($id) { /** @var Method $method */ $method = df_order($id)->getPayment()->getMethodInstance(); return $method->titleDetailed(); }); } }, $result); } }
/** * 2016-10-06 * When browsing the default soap url that should return xml soap services, * instead there is an exception with the following: * «The service interface name "Df\Payment\PlaceOrder" is invalid» * https://code.dmitry-fedyuk.com/m2e/stripe/issues/7 * * Magento 2 накладывает ограничения на имена классов-вебсервисов: * https://github.com/magento/magento2/blob/2.1.1/app/code/Magento/Webapi/Model/ServiceMetadata.php#L188-L230 * Однако, как я понял, моего веб-сервиса @see \Df\Payment\PlaceOrder эти ограничения касаются * только в сценарии генерации документа WSDL /soap/default?wsdl_list=1 * Мой веб-сервис предназначен исключительно для моих платёжных модулей, и, * будь моя воля, я бы вообще не включал его в документ WSDL. * Однако, как я понял, избежать включения веб-сервиса в документ WSDL не так-то просто. * Но и менять моё короткое имя Df\Payment\PlaceOrder на имя типа Df\Payment\API\PlaceOrderInterface * мне не хочется: это имя используется каждым моим платёжным модулем, * и мне удобнее иметь для себя свои имена. * Поэтому я и написал этот плагин: чтобы возвращать ядру имя своего сервиса * (и других моих сервисов, если они потом будут), обходя ограничения ядра на имена классов сервисов. * * @see \Magento\Webapi\Model\ServiceMetadata::getServiceName() * https://github.com/magento/magento2/blob/2.1.1/app/code/Magento/Webapi/Model/ServiceMetadata.php#L188-L230 * * @param Sb $sb * @param \Closure $proceed * @param string $interfaceName * @param string $version * @param bool $preserveVersion Should version be preserved during interface name conversion into service name * @return string */ public function aroundGetServiceName(Sb $sb, \Closure $proceed, $interfaceName, $version, $preserveVersion = true) { return df_starts_with($interfaceName, 'Df\\') ? lcfirst(implode(df_explode_class($interfaceName))) . (!$preserveVersion ? '' : $version) : $proceed($interfaceName, $version, $preserveVersion); }
/** * 2015-12-13 * Отличия от модифицируемого метода * @see \Magento\Framework\Data\Form\Element\AbstractElement::getLabelHtml(): * 1) Добавляем свои классы для Font Awesome. * 2) При использовании Font Awesome не добавляем исходную подпись * (значением которой является класс Font Awesome) * и выводим, по сути, пустые теги <label><span></span></label>. * 3) Добавляем атрибут title. * 2015-12-28 * 4) Добавляем класс, соответствующий типу элемента. * * Пример использования Font Awesome: https://github.com/mage2pro/core/tree/7cb37ab2c4d728bc20d29ca3c7c643e551f6eb0a/Framework/Data/Form/Element/Font.php#L40 * * @see \Df\Framework\Form\Element\Font::onFormInitialized() * @see \Magento\Framework\Data\Form\Element\AbstractElement::getLabelHtml() * @param Sb|E $sb * @param \Closure $proceed * @param string|null $idSuffix * @return string */ public function aroundGetLabelHtml(Sb $sb, \Closure $proceed, $idSuffix = '') { /** @var string|null|Phrase $label */ $label = $sb->getLabel(); /** @var string $result */ if (is_null($label)) { $result = ''; } else { $label = (string) $label; /** * 2015-12-25 * @see \Magento\Framework\Data\Form\Element\Multiline::getLabelHtml() * имеет другое значение по-умолчанию параметра $idSuffix: * public function getLabelHtml($suffix = 0) * https://github.com/magento/magento2/blob/2.0.0/lib/internal/Magento/Framework/Data/Form/Element/Multiline.php#L59 */ if ('' === $idSuffix && $sb instanceof Multiline) { $idSuffix = 0; } /** @var bool $isFontAwesome */ $isFontAwesome = df_starts_with($label, 'fa-'); /** @var string[] $classA */ $classA = ['label', 'admin__field-label', 'df-element-' . $sb->getType()]; if ($isFontAwesome) { $classA[] = 'fa'; $classA[] = $label; $label = ''; } /** @var array(string => string) $params */ $params = ['class' => df_cc_s($classA), 'for' => $sb->getHtmlId() . $idSuffix, 'data-ui-id' => E::uidSt($sb, 'label')]; /** @var string $title */ $title = (string) $sb->getTitle(); if ($title !== $label) { $params['title'] = $title; } $result = df_tag('label', $params, df_tag('span', [], $label)) . "\n"; } return $result; }
/** * @param string $resource * @return \Magento\Framework\View\Asset\File */ function df_asset_create($resource) { return !df_starts_with($resource, 'http') && !df_starts_with($resource, '//') ? df_asset()->createAsset($resource) : df_asset()->createRemoteAsset($resource, dfa(['css' => 'text/css', 'js' => 'application/javascript'], df_file_ext($resource))); }
/** * Простой, неполный, но практически адекватный для моих ситуаций * способ опредилелить, является ли строка регулярным выражением. * @param string $text * @return string */ public function isRegex($text) { return df_starts_with($text, '#'); }
/** * 2016-08-19 * @see json_decode() спокойно принимает не только строки, но и числа, а также true. * Наша функция возвращает true, если аргумент является именно строкой. * @param mixed $v * @return bool */ function df_check_json_complex($v) { return is_string($v) && df_starts_with($v, '{') && df_check_json($v); }
/** * 2016-09-01 * Вообще говоря, заголовок у XML необязателен, * но моя функция @see df_xml_prettify() его добавляет, * поэтому меня пока данный алгоритм устраивает. * Более качественный алгоритм будет более ресурсоёмким: нам надо будет разбирать весь XML. * @param mixed $v * @return bool */ function df_check_xml($v) { return is_string($v) && df_starts_with($v, '<?xml'); }
/** * 2016-06-06 * Цель плагина — устранение дефекта ядра, который проявляется в том, * что непосредственно после авторизации посетителя через какой-либо сторонний сервис * (в моих случаях: Facebook, Amazon) имя покупателя не отображается в шапке. * Это блок @see \Magento\Customer\Block\Account\Customer не имеет атрибута «cacheable»: * https://github.com/magento/magento2/blob/2.1.0-rc1/app/design/frontend/Magento/luma/Magento_Customer/layout/default.xml#L11 * * Это разумно, потому что этот блок отображается на всех страницах витрины, * и он используется не только для авторизованных посетителей, но и для анонимных, * и если он будет «cacheable», то тогда полностраничное кэширование не будет работать вовсе. * * Однако из-за отсутствия атрибута «cacheable» система считает, * что она может кэшировать страницу целиком: * @see \Magento\Framework\View\Layout::isCacheable() * https://github.com/magento/magento2/blob/2.1.0-rc1/lib/internal/Magento/Framework/View/Layout.php#L1073-L1083 public function isCacheable() { $this->build(); $cacheableXml = !(bool)count($this->getXml()->xpath('//' . Element::TYPE_BLOCK . '[@cacheable="false"]')); return $this->cacheable && $cacheableXml; } * Это, в принципе, ещё тоже само по себе не смертельно, ведь блок работает через AJAX, * и по хорошему вполне бы мог корректно подгружать имя посетителя асинхронно * даже при полностью закэшированной странице. * * Однако коварный метод @see \Magento\PageCache\Model\Layout\LayoutPlugin::afterGenerateXml() * https://github.com/magento/magento2/blob/2.1.0-rc1/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php#L37-L51 * видит, что isCacheable() вернуло true, и устанавливает заголовок «Сache-Сontrol: public»: *«Set appropriate Cache-Control headers. We have to set public headers in order to tell Varnish and Builtin app that page should be cached»: public function afterGenerateXml(\Magento\Framework\View\Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { $this->response->setPublicHeaders($this->config->getTtl()); } return $result; } * * Непосвящённому программисту может быть ещё неочевидно, что здесь такого особенного. * Однако затем в дело вступает метод @see \Magento\Framework\App\PageCache\Kernel::process(): * https://github.com/magento/magento2/blob/2.1.0-rc1/lib/internal/Magento/Framework/App/PageCache/Kernel.php#L65-L90 * Он видит, что заголовок «Сache-Сontrol» начинается с «Сache-Сontrol: public»: * if (preg_match('/public.*s-maxage=(\d+)/', $response->getHeader('Cache-Control')->getFieldValue(), $matches)) * ... и грохает все куки вызовом функции @see header_remove() $response->clearHeader('Set-Cookie'); if (!headers_sent()) { header_remove('Set-Cookie'); } * Тут уже ясно, что наступает пипец, но может быть ещё неочевидно, какой именно. * Пипец же в том, что в числе прочих грохается кука * @see \Magento\Customer\Model\Customer\NotificationStorage::UPDATE_CUSTOMER_SESSION * https://github.com/magento/magento2/blob/2.1.0-rc1/app/code/Magento/Customer/Model/Customer/NotificationStorage.php#L12 * * Эта кука ранее была установлена методом * @see \Magento\Customer\Model\Plugin\CustomerNotification::beforeDispatch(): * https://github.com/magento/magento2/blob/2.1.0-rc1/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php#L70-L97 * if ($this->state->getAreaCode() == Area::AREA_FRONTEND && $this->notificationStorage->isExists( NotificationStorage::UPDATE_CUSTOMER_SESSION, $this->session->getCustomerId() )) { ... $publicCookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata(); $publicCookieMetadata->setDurationOneYear(); $publicCookieMetadata->setPath('/'); $publicCookieMetadata->setHttpOnly(false); $this->cookieManager->setPublicCookie( NotificationStorage::UPDATE_CUSTOMER_SESSION, $this->session->getCustomerId(), $publicCookieMetadata ); * В свою очередь, в notification storage ключ UPDATE_CUSTOMER_SESSION устанавливается * при сохранении покупателя: * @see \Magento\Customer\Model\ResourceModel\Customer::_afterSave() protected function _afterSave(\Magento\Framework\DataObject $customer) { $this->getNotificationStorage()->add( NotificationStorage::UPDATE_CUSTOMER_SESSION, $customer->getId() ); return parent::_afterSave($customer); } * При авторизации покупателя через внешнюю систему мы как раз и делаем сохранение покупателя: * ведь мы получаем данные покупателя из внешней системы и их надо сохранить в Magento: * @see \Df\Sso\CustomerReturn::customer() * https://github.com/mage2pro/core/blob/4cd771d1/Customer/External/ReturnT.php?ts=4#L191 * * Итак, куки грохаются, ключ «update_customer_session» из кук пропадает. * Что теперь происходит в браузере? Смотрим: updateSession = $.cookieStorage.get('update_customer_session'); if (updateSession) { mageStorage.post( options.updateSessionUrl, JSON.stringify({ 'customer_id': updateSession, 'form_key': window.FORM_KEY }) ).done( function() { $.cookieStorage .setConf({path: '/', expires: -1}) .set('update_customer_session', null) ; } ); } * Вот именно здесь браузер должен поддягивать свежую информацию о покупателе. * Но мы этого удовольствия лишены, потому что куки-то грохнуты. * Вот для исправления этой ситуации и предназначен мой метод. * @see \Magento\Framework\View\Layout::isCacheable() * * 2016-06-06 * df_cookie_m()->getCookie(NotificationStorage::UPDATE_CUSTOMER_SESSION) * здесь нихуя не работает, потому что * @see \Magento\Framework\Stdlib\Cookie\PhpCookieReader::getCookie() * тупо смотрит в $_COOKIE (куки прошлого сеанса), * но не смотрит те новые куки, которые мы установили в этом сеансе. * * @param Sb $sb * @param int|void $result * @return int|void */ public function afterIsCacheable(Sb $sb, $result) { return $result && !dfc($this, function () { return df_find(function ($h) { return df_starts_with($h, 'Set-Cookie: update_customer_session') || df_starts_with($h, 'Set-Cookie: ' . self::NEED_UPDATE_CUSTOMER_DATA); }, headers_list()); }); }
/** * 2016-03-08 * Добавляет к строке $s приставку $head, * если она в этой строке отсутствует. * @param string $s * @param string $head * @return string */ function df_prepend($s, $head) { return df_starts_with($s, $head) ? $s : $head . $s; }
/** * 2015-12-11 * $element->getClass() может вернуть строку вида: * «df-google-font df-name-family select admin__control-select». * Оставляем в ней только наши классы: чьи имена начинаются с df-. * Системные классы мы контейнеру не присваиваем, * потому что для классов типа .admin__control-select * в ядре присутствуют правила CSS, которые считают элементы с этими классами * элементами управления, а не контейнерами, и корёжат нам вёрстку. * @param AE|Element $e * @return string */ public static function getClassDfOnly(AE $e) { return df_cc_s(array_filter(df_trim(explode(' ', $e->getClass())), function ($class) { return df_starts_with($class, 'df-'); })); }