Pull to refresh

JSON API Мой Склад, самообучение

Reading time 11 min
Views 27K
START UPDATE 2019-11-18
Заметил, что статья до сих пор для кого то служит источником информации.
Я сделал рефакторинг что бы сделать код более прямолинейным. По пути поправил пару досадных багов.
FINISH UPDATE 2019-11-18

Есть такой способ самообучения — как выполнение тестовых заданий. Его преимущество в том что объём задания конечен, сроки ограничены. Это не позволяет тянуть резину до бесконечности или самозабвенно вырисовывать завихрения и завитушки архитектурных изысков.

Приятный бонус такой тренировки скиллов заключается в знакомстве с новыми технологиями и бизнес процессами, которое кроме всего прочего востребованы в реальных задачах.

На этот раз надо было сделать страничку для формирования заказа покупателя в сервисе «Мой склад». Для меня это как полёт на Луну: в веб разработке я чуть меньше чем новичок, с фронтэндом знаком только по наслышке, а тут целую страницу надо разработать, ох ты Йожик!
Любая критика и советы приветствуются.

В коментах очень много ругательств, моё решение настолько ужасно, что для него сделали рефакторинг во что то приличное:
michael_vostrikov
От нечего делать сделал небольшой рефакторинг этого задания (хотя там много чего еще можно поменять), не столько для вас, сколько для тех, кто потом найдет в поиске эту статью:
коммиты, разметка, отправка формы.

Поехали!


Первым делом конечно гуглить, нагуглилась только ссылка на документацию, туториалов, примеров — ноль.

Ещё нагуглилось: «JSON API доступен для подписчиков на всех тарифах, кроме Бесплатного» уупс! Платного мне конечно ни кто не дал, покупать не камильфо, но я подумал что если дали такое задание, то наверное на Бесплатном что то там функционирует и продолжил работу.

И конечно нагуглилось «moysklad-client — npm — JavaScript клиент для комфортной работы с API сервиса МойСклад», но я с JS исключительно на «Вы», и по условиям задания, написать надо на PHP. Так что даже разбираться не стал, что там на JS можно делать.

Первое


Первое что надо сделать, это познакомиться с документацией. Познакомился.
Второе — составить план. Составил.
План, начало.
Действие первое — авторизация.
Действие второе — показать список Номенклатур.
Действие третье — добавить Заказ покупателя.
Действие четвёртое — добавить Позиции в Заказ покупателя.
Цель достигнута, конец плана.

Авторизация


Я видел код в котором для общения с API использовался cUrl. Я не знаю что такое cUrl, я не знаю как принято общаться с API, но если есть код который можно скопипастить, то проверить его пригодность не сложно. Скопировал вставил, обработал напильником — получилось.

Не буду утомлять вас интимными подробностями о дружбе напильника с копипастой, вот работающий код:

function setupCurl($apiSettings)
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, false);

    $userName = $apiSettings[MOYSKLAD_USERNAME];
    $userPassword = $apiSettings[MOYSKLAD_PASSWORD];
    curl_setopt($curl, CURLOPT_USERPWD, "$userName:$userPassword");
    curl_setopt($curl, CURLOPT_USERAGENT, $apiSettings[MOYSKLAD_USER_AGENT]);
    return $curl;
}

Параметры curl:

  • RETURNTRANSFER — не только отправляем запрос, но и записываем ответ;
  • USERPWD — реквизиты аутентификации;

Остальные опции не знаю зачем нужны, тупо копипаста.
Не факт, что заголовки авторизации надо отправлять в каждом запросе, хотя не факт, что они не сбросились после первой же отправки… кто подскажет?

Итак, это была инициализация объекта curl для обмена сообщениями с сервером API.

Использование:

function curlExec($curlObject)
{

    $response = curl_exec($curlObject);

    $curlErrorNumber = curl_errno($curlObject);
    if ($curlErrorNumber) {
        throw new Exception(curl_error($curlObject));
    }

    return $response;
}


Чистая копипаста, не спрашивайте меня почему так.

Показать список Номенклатур


Одних номенклатур оказалось мало, для Заказа покупателя, надо указать юридическое лицо Поставщика и контрагента Покупателя. У владельца учётки «Мой склад» может быть несколько юридических лиц, контрагентов — ясно понятно 100500, но конкретный Заказ покупателя, это заказ конкретного Контрагента в адрес конкретного Юридического лица.

Поэтому с номенклатурами обождём, займёмся сторонами «договора» — сделки.

Юридические лица

$curl = setCurl(
    $curl,
    $apiSettings[MOYSKLAD_API_URL] . $apiSettings[MOYSKLAD_GET_JURIDICAL_PERSON],
    $apiSettings[MOYSKLAD_GET_JURIDICAL_PERSON_METHOD]);

$persons = getJuridicalPerson($curl);

function setCurl(&$curlObject, $uri, $method)
{
    curl_setopt($curlObject, CURLOPT_URL, $uri);

    curl_setopt($curlObject, CURLOPT_HTTPGET, true);
    switch ($method) {
        case MOYSKLAD_METHOD_GET:
            break;
        case MOYSKLAD_METHOD_POST:
            curl_setopt($curlObject, CURLOPT_POST, true);
            break;
        case MOYSKLAD_METHOD_PUT:
            curl_setopt($curlObject, CURLOPT_PUT, true);
            break;
    }

    return $curlObject;
}

function getJuridicalPerson($curlObject)
{
    $response = curlExec($curlObject);
    $data = json_decode($response, true);
    $result = $data['rows'];
    return $result;
}

Извиняюсь за ужасные названия констант, но мне с такими спокойней, точно ни с чем не перепутаю. Да я знаю что у case (switch) есть ветка default, но мне спокойней вбетонировать в код значение по умолчанию и не надеяться на превратности судьбы с case.

У каждой команды API свой адрес и свой метод, setCurl — устанавливает адрес и метод.

Для получения списка юридических лиц устанавливаем соответствующий адрес и метод ( адрес и метод задаются в настройках, настройки подгружаются методом function getSettings(){ $apiConfig = include('moysklad_curl_details.php'); return $apiConfig;} ).

После этого методом getJuridicalPerson исполняем curl, получаем ответ в JSON, из ответа забираем только массив 'rows'. Получили, сохранили, отложили.

С Контрагентами поступаем аналогично: setCurl => getCounterparty, Номенклатуры по тому же алгоритму: setCurl => getNomenclature.

Если бы это было не тестовое на два вечера после работы, а на два дня безработного специалиста, то можно было бы это автоматизировать, но это было тестовое в стиле — «лишь бы работало», поэтому я не стал изгаляться.

Для меня цель тестового была в том что бы пригубить и попробовать на вкус JSON API, рисовать красоту — цели не было.

Данные получили — это вообще не вопрос, дело дурацкое — дело не хитрое, интересней было как то это вывести на страничку, а потом со странички забрать, вот это была задачка.

Фронтэнд


Не знаю как правильно, я сделал так:

echo '<form action="#" onsubmit="return false;" id="orderForm"  ><p>Доступные юридические лица:<br />';
foreach ($persons as $key => $person) {
    $personId = $person['id'];
    echo '<label for="' . $personId . '">' . $person['name'] . '</label><input type="radio" data-organization-type="1" id="' . $personId . '" name="organization"><br />';
}
echo 'Доступные контрагенты:<br />';
foreach ($counterparty as $key => $person) {
    $personId = $person['id'];
    echo '<label for="' . $personId . '">' . $person['name'] . '</label><input type="radio" data-counterparty-type="1" id="' . $personId . '" name="counterparty"><br />';
}
echo 'Номенклатура товаров:<br />';
foreach ($nomenclature as $key => $position) {
    $positionId = $position['id'];
    echo '<label for="' . $positionId . '">' . $position['name'] . ', количество для заказа => </label><input type="text" id="' . $positionId . '" data-position-type="1"><br />';
}
echo '
<input type="submit" name="Сформировать заказ покупателя" onclick="sendOrder();"><br /></p></form>'

Общий алгоритм:

  1. пишем название раздела
  2. пишем название позиции,
  3. пишем тег input, в атрибут id пишем идентификатор полученный из API,
  4. пишем соответствующий атрибут data-organization-type / data-counterparty-type / data-position-type, поскольку для получения атрибута надо присвоить значение, то присваиваем

(вообще конечно можно использовать атрибуты без значений, но я не разобрался как).

По клику на кнопку «Сформировать заказ покупателя», форма не отправляется — «return false;», но вызывается функция — «sendOrder();».

Отправить заказ


echo '
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
function sendOrder(){
    var $text_field = $('#orderForm :input:text');
    
    var position = {};    
    $text_field.each(function() {        
        var this_val = $(this).val();    
        var may_assign = this_val>0 || this_val !="";
        var is_it_position = $(this).data('position-type');    
        
        if ( may_assign && is_it_position > 0){
            position[this.id] = this_val;
        }
    });
    
    var $radio_field = $('#orderForm :input:radio:checked');

    var counterparty = {};
    var organization = {};
    $radio_field.each(function() {
        var this_val = $(this).val();
        
        var is_it_counterparty = $(this).data('counterparty-type');
        var is_it_organization = $(this).data('organization-type');
        
        var may_assign = this_val>0 || this_val !="";
        
        if ( may_assign && is_it_counterparty > 0){
            counterparty[this.id] = this_val;
        }
        if ( may_assign && is_it_organization > 0){
            organization[this.id] = this_val;
        }    
    });

C JS мне кажется всё более чем прозрачно:

  1. $('#orderForm :input:text'); — выбрали все теги input с типом text внутри тега с идентификатором orderForm
  2. $text_field.each — для каждого элемента выполняем анонимную функцию
  3. var this_val = $(this).val(); — сохранили значение
  4. var may_assign = this_val>0 || this_val !=""; — вычислили что значение не пустое
  5. var is_it_position = $(this).data('position-type'); — вычислили что атрибут data-position-type установлен
  6. if ( may_assign && is_it_position > 0){ position[this.id] = this_val; } — если значение не пустое и этот input соответствует позиции, то добавляем в массив позиций соответствующий элемент, идентификатор в качестве индекса гарантирует уникальность.

Проделываем такую же акробатику с юридическими лицами и контр агентами, с тем отличием что для анализа выбираем все input с типом «radio» в состоянии «checked»:

$('#orderForm :input:radio:checked'), и кроме того значение элемента input нам не требуется, нам просто надо знать кого ( одного ) из всего списка выбрал наш Покупатель.

Теперь когда данные для отправки в обработку готовы, надо сформировать запрос:

    var postData = JSON.stringify({position : position, counterparty : counterparty , organization : organization});
    console.log(postData);
    $.ajax({
        type: "POST",
        url: "moyskald_add_order.php",        
        data: postData,
        contentType: "application/json; charset=utf-8",
        dataType: "text",
        timeout: 10000,        
        error: function(){
            alert("сбой добавления заказа");        
        },
        success: function(data){alert(data);},
        failure: function(errMsg) {
            alert(errMsg);
        }        
    });

К этой копипасте мне добавить не чего, метод отправки — «POST», обработка будет выполнена — «moyskald_add_order.php», данные уходят в формате -«application/json; charset=utf-8», приходят в формате — «text».

Данные вообще не нужны, хотя конечно хороший тон это сообщить пользователю «Ваш заказ принят» или «Сбой добавления заказа», и ладно.

Едем дальше, следующий пункт прибытия — «Обработка».

Обработка


С обработкой вышла осечка. Я ламер-эникейщик и для разработки использую XAMPP (под Win10), который как то раз настроил и забыл. И вот что то там такое настроено, что я GET запросы в PHP-скрипт получаю как нормальный человек, а POST-запросы, только через одно место.

Но GET — не камильфо, потому что GET кэшуруется, а у нас не факт что ответ сервера будет точно таким как в прошлый раз, особенно когда ты ведёшь разработку, и серверная логика меняется каждые пять минут.

Поэтому пришлось смириться с использованием чёрной магии в виде file_get_contents(«php://input»), потому что, что бы я ни делал, но var_export($POST) стабильно выдавал «array()». Между прочим, буду благодарен за серию пинков в верном направлении.

А дальше всё просто:

$data = json_decode($rawData, true);

$rawPosition = $data['position'];
$rawCounterparty = $data['counterparty'];
$rawOrganization = $data['organization'];

Разобрали входные данные.

Дёрнули Юридическое лицо и контрагента:

const FIRST_INDEX = 0;
$counterpartyId = $counterpartyIdCollection[FIRST_INDEX];
$organizationId = $organizationIdCollection[FIRST_INDEX];

Сформировали поля запроса:

$textAddCustomerOrder = '
{
  "name": "' . time() . '",
  "organization": {
    "meta": {
      "href": "https://online.moysklad.ru/api/remap/1.1/entity/organization/' . $organizationId . '",
      "type": "organization",
      "mediaType": "application/json"
    }
  },
  "agent": {
    "meta": {
      "href": "https://online.moysklad.ru/api/remap/1.1/entity/counterparty/' . $counterpartyId . '",
      "type": "counterparty",
      "mediaType": "application/json"
    }
  }
}
';

$apiSettings = getSettings();
$curl = setupCurl($apiSettings);

$curl = setCurl(
    $curl,
    $apiSettings[MOYSKLAD_API_URL] . $apiSettings[MOYSKLAD_ADD_CUSTOMER_ORDER],
    $apiSettings[MOYSKLAD_ADD_CUSTOMER_ORDER_METHOD]);

curl_setopt($curl, CURLOPT_POSTFIELDS, $textAddCustomerOrder);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'Content-Length: ' . strlen($textAddCustomerOrder))
);

Тут получилось схалявить и зафигачить JSON без json_encode, тупо вклеить нужные идентификатору, в нужные места. Обязательно делаем POSTFIELDS — $textAddCustomerOrder, HTTPHEADER — 'Content-Length: '. strlen($textAddCustomerOrder).

отправляем запрос на обработку на сервер API: $customerOrderId = setCustomerOrder($curl), в ответе забираем 'id'.

Заказ добавлен.

Добавить Позиции в Заказ покупателя


С этим пунктом Плана, ни каких проблем, кроме необходимости использования json_encode для форматирования текста запроса и floatval для количества товара в позиции. Без floatval сервер API выдаёт ошибку формата для поля «quantity» (и «reserve» соответственно).

$isPositionArray = is_array($rawPosition);

$orderPositions= array();
if ($isPositionArray) {
    foreach ($rawPosition as $id => $quantity) {

        $positionQuantity=floatval($quantity);

        $orderPositions[] =
            [
                "quantity" =>$positionQuantity,
                "price"=>0,
                "discount"=>0,
                "vat"=>0,
                "assortment" =>[
                    "meta"=>[
                        "href"=>"https://online.moysklad.ru/api/remap/1.1/entity/product/$id",
                        "type"=>"product",
                        "mediaType"=>"application/json"
                    ]
                ],
                "reserve"=>$positionQuantity,
            ];
    }
}

Ремарка: foreach ($rawPosition as $id => $quantity), в ключах массива записаны идентификаторы позиций, в значениях элементов массива — количество для заказа.

Мой фэншуй требует создания каждого массива по отдельности и добавления всех «элементарных» массивов в общую кучу, но… мне кажется это будет излишний фанатизм.

Не уверен что в заказе покупателя надо выставлять резерв, но мне кажется это больше бизнес требование, чем механика работы с API.

С ценой смешно вышло, в задании требовалось вывести список номенклатур, для меня это просто перечень наименований и соответствующих ТТХ, в которые цена не входит, но если подумать, то цена это самое первое ТТХ в подобном приложении, но у меня в форме заказа цены нет, поэтому — проехали.

С отправкой API запроса теперь мне кажется всё предельно ясно:

$jsonResponse = 'empty';
$isContainPosition = count($orderPositions)>0;
if($isContainPosition ){
    $jsonOrderPositions= json_encode($orderPositions);

    $curl = setupCurl($apiSettings);

    $curl = setCurl(
        $curl,
        $apiSettings[MOYSKLAD_API_URL]
        . $apiSettings[MOYSKLAD_ADD_ORDER_POSITION_PREFIX]
        . $customerOrderId
        . $apiSettings[MOYSKLAD_ADD_ORDER_POSITION_SUFFIX],
        $apiSettings[MOYSKLAD_ADD_ORDER_POSITION_METHOD]);

    curl_setopt($curl, CURLOPT_POSTFIELDS, $jsonOrderPositions);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
            'Content-Type: application/json',
            'Content-Length: ' . strlen($jsonOrderPositions))
    );

    $jsonResponse = setCustomerOrderPosition($curl);
}

Адрес-команда запроса «вычисляется» несколько странным образом:
$apiSettings[MOYSKLAD_API_URL]
        . $apiSettings[MOYSKLAD_ADD_ORDER_POSITION_PREFIX]
        . $customerOrderId
        . $apiSettings[MOYSKLAD_ADD_ORDER_POSITION_SUFFIX]

По документации должно быть :"/entity/customerorder/{id}/positions".

Типа даже нативная PHP строка, бери да прямо так и пиши — {id} подставиться само, но я не могу допустить хардкода в отношении записи команды API, нет конечно за время написания тестового команда не поменяется, но феншуй требует такие глобальные вещи выносить в константы, аминь. Хотя когда я смотрю на исходники всяких open source фреймворков, я вижу что хардкод там сплошь и рядом, но это делают они, а это делаю я, и я так не делаю.

Эпилог


Вот собственно и всё. API, на Бесплатном тарифе, обрабатывает запросы в последнюю очередь, поэтому иногда таймаута в 10 секунд не достаточно. В остальном работает стабильно. Я проверял.
Но наша цель конечно была не в этом, в результате выполнения задания, мы научились работать с JSON Web API и научились мутить фронтэнд с выводом информации в форму и передачей пользовательского ввода для обработки в серверный скрипт.
Ура :)

PostScriptum


Перед написанием статьи я чуть плотнее погуглил на тему «php json api мой склад» и нашёл массу реализаций, но не для JSON, а для XML. Из десятка найденных, парочка похожа на реально рабочие, но это не актуально потому что XML API грозятся отключить с 31 марта 2017.

А может всё будет так же как с «JSON API доступен для подписчиков на всех тарифах, кроме Бесплатного».

Конструктивная критика и ссылки на best practics ПРИВЕТСТВУЮТСЯ! мне не стыдно быть колхозником, но не хорошо бы если лучше?

Ссылки


  1. Git
Tags:
Hubs:
-2
Comments 45
Comments Comments 45

Articles