Magento 2: Как вернуть объект JSON из API?


8

Я пытаюсь вернуть объект JSON из одной из моих REST-моделей, что-то вроде этого:

{
    "settings": {
        "set1" : 2,
        "set2" : "key1" 
    },
    "extra": {
        "e1'" : {
            "e2'": true 
        }
    }
}

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

<?php

namespace AppFactory\Core\Api;

/**
 * @api
 */

interface SettingsInterface
{


    /**
     * @return object
     */
    public function get();
}

Класс объекта вернется

{
  "message": "Class object does not exist",

при вызове API. Доступные типы примитивов int, number и array для меня не подойдут. Я не хочу создавать класс для каждого возвращаемого сложного типа. Как я могу это сделать?

Спасибо.


Данные json являются строками для php, поэтому сделайте их строковыми
Мохаммад Муджассам

@MohammadMujassam возвращенная строка в DocBlock будет сделать Magento преобразовать объект в строку вывода Ускользающей «с обратными косыми и окружающей всем объектом». Я пролистал эту статью maxchadwick.xyz/blog/… и он предполагает, что нет другого способа вернуть объект, кроме как создать для него модель данных, но я просто хочу убедиться, что это единственный способ и нет другие способы.
Yehia A.Salam

да, определенно, так и будет.
Мохаммад Муджассам

Ответы:


17

Я предполагаю, что AppFactory\Core\Api\SettingInterface::get()это конечная точка REST. В этом случае в комментариях phpdoc вы должны определить, что это будет возвращать. Обработчик Magento REST примет это значение и обработает его, чтобы удалить все ненужные данные. То, что осталось, будет закодировано в JSON, поэтому в javascript вы можете получить его как уже правильный JS-хэш, а не как json-кодированную строку.

Хитрость этих конечных точек заключается в том, что вам нужно очень точно определить, что вы вернете. Magento не сможет обрабатывать что-то такое общее, как «массив», где вы будете устанавливать все, что вам нравится.

В вашем случае, чтобы не пытаться играть с массивом строк, будет проще создать интерфейс, который будет возвращать ваша конечная точка.

 <?php

 namespace AppFactory\Core\Api;

 /**
  * @api
  */

 interface SettingsInterface
 {


     /**
      * @return Data\SettingsInterface
      */
     public function get();
 }

Теперь, когда вы возвращаете экземпляр объекта, реализующего этот интерфейс, Magento будет читать его phpdocs и обрабатывать их возвращаемые значения. Теперь создайте файл AppFactory\Core\Api\Data\SettingsInterfaceследующим образом

<?php

namespace AppFactory\Core\Api\Data;

interface SettingsInterface
{
    /**
    * @return int[]
    **/
    public function getSettings();

    /**
    * @return string[]
    **/
    public function getExtra();
}

Теперь, когда вы создадите реальный класс, который будет реализовывать эти 2 метода get, и вы вернете его, AppFactory\Core\Api\SettingsInterface::get()magento вернет что-то вроде

{
    "settings": [1, 2, 5],
    "extra": ["my","array","of","strings"]
}

Если вы хотите другой уровень, вам нужно создать другой интерфейс, который сохранит settingsструктуру и добавит его в качестве возвращаемого значения для AppFactory\Core\Api\Data\SettingsInterface::getSettings().

Если вам нужно что-то, что будет динамичным, и вы не хотите или не можете подготовить этот интерфейс структуры, тогда вы можете попробовать установить json-закодированную строку и место @return stringдля любого из полей. Таким образом, однако, вы должны будете убедиться, что вручную расшифровали эту строку после получения ответа, так как тогда ваш ответ будет выглядеть так:

{
    "settings": [1, 2, 5],
    "extra": "{\"test\":\"string\",\"value\":8}"
}

и для того, чтобы использовать, response.extra.testвам придется сначала сделать response.extra = JSON.parse(response.extra);вручную


спасибо за подробное объяснение, декодирование строки на стороне клиента не кажется естественным, и написание всех классов для представления каждого элемента является кошмаром, есть ли четвертая возможность просто вернуть объект json без необходимости писать классы или возвращать строка, может быть, обратная смешанная аннотация, хотя я пробовал, но, к сожалению, не сработало. Похоже, я в конечном итоге заключу заключительный json в массив, например, массив ($ json_object), это сделает свое дело, но также не выглядит естественным, так как мне нужно подобрать первый элемент в массиве со стороны клиента
Yehia A.Salam

Вы можете создать обычное действие контроллера, которое будет возвращать строку json и устанавливать заголовок text / json или application / json, и оно будет декодировано на стороне браузера. Если вы хотите использовать остальные API, то я не нашел ничего, что могло бы обойти эту пост-обработку.
Зефирин

да, похоже, magento должен каким-то образом поддерживать это и ослаблять, не применяя этот тип проверки для типа возвращаемого значения
Yehia A.Salam

@ YehiaA.Salam Я что-то проверял. У меня нет простого способа проверить это, но я стараюсь использовать «смешанный» в качестве возврата методов AppFactory\Core\Api\DataSettingsInterface. Если это работает, вам нужно только сделать первый уровень ответа.
Зефирин

Очень полезный ответ
Pandurang

5

Я также столкнулся с этой проблемой, и в качестве альтернативы предложенному @Zefiryn решению я обошел ее, заключив возвращаемые данные в массив (или два). Пожалуйста, рассмотрите пример ниже.

/**
 * My function
 *
 * @return
 */
public function myFunction()
{
  $searchCriteria = $this->_searchCriteriaBuilder->addFilter('is_filterable_in_grid',true,'eq')->create();
  $productAttributes = $this->_productAttributeRepository->getList($searchCriteria)->getItems();

  $productAttributesArray = [];
  foreach ($productAttributes as $attribute) {
    $productAttributesArray[$attribute->getAttributeCode()] = $this->convertAttributeToArray($attribute);
  }

  return [[
          "attributes"=>$productAttributesArray,
          "another_thing"=>["another_thing_2"=>"two"]
        ]];
}

private function convertAttributeToArray($attribute) {
  return [
    "id" => $attribute->getAttributeId(),
    "code" => $attribute->getAttributeCode(),
    "type" => $attribute->getBackendType(),
    "name" => $attribute->getStoreLabel(),
    "options" => $attribute->getSource()->getAllOptions(false)
  ];
}

Благодаря тому, что Magento 2 допускает массивы смешанного содержимого в качестве возвращаемых значений, более сложные структуры данных могут быть встроены в другие массивы. В приведенном выше примере получен следующий ответ JSON (усеченный для удобства чтения).

[
{
    "attributes": {
        "special_price": {
            "id": "78",
            "code": "special_price",
            "type": "decimal",
            "name": "Special Price",
            "options": []
        },
        "cost": {
            "id": "81",
            "code": "cost",
            "type": "decimal",
            "name": "Cost",
            "options": []
        },
    "another_thing": {
        "another_thing_2": "two"
    }
}
]

Включение его в один слой удаляет ключи массива, а без включения его в какой-либо массив приводит к ошибке.

Понятно, что все это не идеально, но этот подход позволяет мне в определенной степени контролировать согласованность возвращаемой структуры данных (ожидаемой структуры и типов данных). Если вы также управляете написанием клиентской библиотеки, может быть реализован перехватчик для удаления внешнего массива перед его возвратом в приложение.


1

Для Magento 2.3.1, если вам нужно обойти сериализацию массива, вы можете проверить этот файл, чтобы обновить логику ядра. Я думаю, что это хорошая отправная точка. Но, сделав это, вы наверняка нарушите совместимость Soap.

Более того, в Magento 2.1.X у вас нет этой проблемы, если вы указали в качестве типа возврата anyType.

Ссылка на Github: https://github.com/magento/magento2/blob/2.3-develop/lib/internal/Magento/Framework/Reflection/TypeCaster.php

Ссылка на изменение изменений: https://github.com/magento/magento2/commit/6ba399cdaea5babb373a35e88131a8cbd041b0de#diff-53855cf24455a74e11a998ac1a871bb8

продавец / Magento / рамки / Отражение / TypeCaster.php: 42

     /**
     * Type caster does not complicated arrays according to restrictions in JSON/SOAP API
     * but interface and class implementations should be processed as is.
     * Function `class_exists()` is called to do not break code which return an array instead
     * interface implementation.
     */
    if (is_array($value) && !interface_exists($type) && !class_exists($type)) {
        return $this->serializer->serialize($value);
    }

И заменить на:

     /**
     * Type caster does not complicated arrays according to restrictions in JSON/SOAP API
     * but interface and class implementations should be processed as is.
     * Function `class_exists()` is called to do not break code which return an array instead
     * interface implementation.
     */
    if (is_array($value) && !interface_exists($type) && !class_exists($type)) {
        return $value;
    }

1

Я знаю, что этот вопрос довольно старый, но есть одно довольно простое решение для этого:

Вам нужно либо заменить Json-Renderer, Magento\Framework\Webapi\Rest\Response\Renderer\Jsonлибо написать плагин для него.

Вот небольшой пример плагина:

В вашей di.xml

<type name="Magento\Framework\Webapi\Rest\Response\Renderer\Json">
    <plugin name="namespace_module_renderer_json_plugin" type="Namespace\Module\Plugin\Webapi\RestResponse\JsonPlugin" sortOrder="100" disabled="false" />
</type>

В вашем новом плагин-классе Namespace\Module\Plugin\Webapi\RestResponse\JsonPlugin

<?php
namespace Namespace\Module\Plugin\Webapi\RestResponse;

use Magento\Framework\Webapi\Rest\Request;
use Magento\Framework\Webapi\Rest\Response\Renderer\Json;

class JsonPlugin
{

    /** @var Request */
    private $request;

    /**
     * JsonPlugin constructor.
     * @param Request $request
     */
    public function __construct(
        Request $request
    )
    {
        $this->request = $request;
    }

    /**
     * @param Json $jsonRenderer
     * @param callable $proceed
     * @param $data
     * @return mixed
     */
    public function aroundRender(Json $jsonRenderer, callable $proceed, $data)
    {
        if ($this->request->getPathInfo() == "/V1/my/rest-route" && $this->isJson($data)) {
            return $data;
        }
        return $proceed($data);
    }

    /**
    * @param $data
    * @return bool
    */
    private function isJson($data)
    {
       if (!is_string($data)) {
       return false;
    }
    json_decode($data);
    return (json_last_error() == JSON_ERROR_NONE);
}

}

Что здесь происходит:

  • Если rest-route является «/ V1 / my / rest-route», то используется новый метод рендеринга, который просто означает, что данные не кодируются.
  • Дополнительный метод проверки используется для оценки, действительно ли строка является json-объектом. В противном случае (например, если ответ является ошибкой 401, это приведет к внутренней ошибке и вернет неправильный код состояния)
  • Таким образом, в вашем rest-методе вы можете вернуть json-строку, которая не будет изменена.

Конечно, вы также можете написать свой собственный Renderer, который, например, обрабатывает массив.


0

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

Magento делает что-то странное в этом выходном процессоре службы web api, который находится в Magento \ Framework \ Webapi \ ServiceOutputProcessor. В этом классе есть метод с именем convertValue (); что является причиной для скобок [].

Лучшее решение для меня, чтобы решить эту проблему, было создать плагин вокруг, чтобы преодолеть это условие if в convertValue (); метод, в котором они проверяют, является ли $ data массивом, и делают с ним странные вещи.

Вот мой пример кода плагина: я думаю, что каждый знает, как создать базовый модуль Magento 2, поэтому я публикую здесь только код самого плагина.

  • Создать папку плагинов

  • Создайте класс Vendor \ ModuleName \ Plugin \ ServiceOutputProcessorPlugin.php

<?php

namespace Vendor\ModuleName\Plugin;

use Magento\Framework\Webapi\ServiceOutputProcessor;

class ServiceOutputProcessorPlugin
{
    public function aroundConvertValue(ServiceOutputProcessor $subject, callable $proceed, $data, $type)
    {
        if ($type == 'array') {
            return $data;
        }
        return $proceed($data, $type);
    }
}
  • Создайте объявление плагина в Vendor \ ModuleName \ etc \ di.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Webapi\ServiceOutputProcessor">
        <plugin name="vendor_modulenameplugin" type="Vendor\ModuleName\Plugin\ServiceOutputProcessorPlugin"/>
    </type>
</config>

Это должно решить проблему вывода массива JSON в веб-API

Надеюсь это поможет

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.