Chciałbym podać nieco więcej szczegółów oprócz doskonałej odpowiedzi @ryanF.
Chciałbym podsumować powody dodania repozytorium dla encji niestandardowych, podać przykłady, jak to zrobić, a także wyjaśnić, jak udostępnić te metody repozytorium jako część interfejsu API sieci Web.
Oświadczenie: opisuję pragmatyczne podejście do tego, jak to zrobić w przypadku modułów stron trzecich - zespoły podstawowe mają własne standardy, których przestrzegają (lub nie).
Ogólnie rzecz biorąc, celem repozytorium jest ukrycie logiki związanej z pamięcią masową.
Klient repozytorium nie powinien dbać o to, czy zwrócony obiekt jest przechowywany w pamięci w tablicy, czy jest pobierany z bazy danych MySQL, pobierany ze zdalnego interfejsu API czy z pliku.
Zakładam, że główny zespół Magento to zrobił, aby mogli zmienić lub wymienić ORM w przyszłości. W Magento ORM składa się obecnie z modeli, modeli zasobów i kolekcji.
Jeśli moduł innej firmy korzysta tylko z repozytoriów, Magento może zmienić sposób i miejsce przechowywania danych, a moduł będzie kontynuował pracę, pomimo tych głębokich zmian.
Repozytoria mają na ogół metod, takich jak findById()
, findByName()
, put()
lub remove()
.
W Magento te powszechnie nazywane są getbyId()
, save()
i delete()
nawet nie udaje, że robią coś innego, ale operacje CRUD DB.
Metody repozytorium Magento 2 można łatwo ujawnić jako zasoby API, co czyni je cennymi do integracji z systemami stron trzecich lub bezgłowymi instancjami Magento.
„Czy powinienem dodać repozytorium dla mojej encji niestandardowej?”.
Jak zawsze odpowiedź brzmi
"To zależy".
Krótko mówiąc, jeśli twoje podmioty będą używane przez inne moduły, to tak, prawdopodobnie chcesz dodać repozytorium.
Tutaj bierze się pod uwagę jeszcze jeden czynnik: w Magento 2 repozytoria można łatwo ujawnić jako zasoby Web API - czyli REST i SOAP.
Jeśli jest to dla ciebie interesujące z powodu integracji systemów stron trzecich lub bezgłowej konfiguracji Magento, to znowu, tak, prawdopodobnie chcesz dodać repozytorium dla swojej jednostki.
Jak dodać repozytorium dla mojej encji niestandardowej?
Załóżmy, że chcesz ujawnić swoją jednostkę jako część interfejsu API REST. Jeśli nie jest to prawdą, możesz pominąć nadchodzącą część dotyczącą tworzenia interfejsów i przejść bezpośrednio do „Utwórz repozytorium i implementację modelu danych” poniżej.
Utwórz interfejsy repozytorium i modelu danych
Utwórz foldery Api/Data/
w swoim module. To tylko konwencja, możesz użyć innej lokalizacji, ale nie powinieneś.
Repozytorium przechodzi do Api/
folderu. Data/
Podkatalogu jest na później.
W Api/
utwórz interfejs PHP metodami, które chcesz ujawnić. Zgodnie z konwencjami Magento 2 wszystkie nazwy interfejsów kończą się przyrostkiem Interface
.
Na przykład dla Hamburger
encji stworzyłbym interfejs Api/HamburgerRepositoryInterface
.
Utwórz interfejs repozytorium
Repozytoria Magento 2 są częścią logiki domeny modułu. Oznacza to, że repozytorium nie ma ustalonego zestawu metod.
Zależy to całkowicie od celu modułu.
Jednak w praktyce wszystkie repozytoria są dość podobne. Są opakowaniami dla funkcjonalności CRUD.
Większość z nich ma metody getById
, save
, delete
i getList
.
Może istnieć więcej, na przykład CustomerRepository
ma metodę get
, która pobiera klienta pocztą e-mail, przy czym getById
służy do wyszukiwania klienta według identyfikatora podmiotu.
Oto przykładowy interfejs repozytorium dla jednostki hamburgerowej:
<?php
namespace VinaiKopp\Kitchen\Api;
use Magento\Framework\Api\SearchCriteriaInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
interface HamburgerRepositoryInterface
{
/**
* @param int $id
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getById($id);
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
*/
public function save(HamburgerInterface $hamburger);
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface $hamburger
* @return void
*/
public function delete(HamburgerInterface $hamburger);
/**
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
*/
public function getList(SearchCriteriaInterface $searchCriteria);
}
Ważny! Poświęć czas!
Jest tu kilka błędów, które trudno debugować, jeśli się pomylicie:
- NIE używaj skalarnych typów argumentów PHP7 ani typów zwracanych, jeśli chcesz podłączyć to do interfejsu API REST!
- Dodaj adnotacje PHPDoc dla wszystkich argumentów i typ zwracany do wszystkich metod!
- Używaj w pełni kwalifikowanych nazw klas w bloku PHPDoc!
Adnotacje są analizowane przez środowisko Magento w celu ustalenia, jak konwertować dane do i z JSON lub XML. Importowane klasy (to znaczy use
instrukcje) nie są stosowane!
Każda metoda musi mieć adnotację z dowolnymi typami argumentów i typem zwracanym. Nawet jeśli metoda nie przyjmuje argumentów i nic nie zwraca, musi mieć adnotację:
/**
* @return void
*/
Typy skalarne ( string
, int
, float
i bool
) muszą również zostać określone, zarówno dla argumentów i jako wartość zwracana.
Zauważ, że w powyższym przykładzie adnotacje do metod zwracających obiekty są również określone jako interfejsy.
Interfejsy typu powrotu znajdują się w katalogu Api\Data
namespace /.
Oznacza to, że nie zawierają żadnej logiki biznesowej. Są po prostu workami danych.
Następnie musimy utworzyć te interfejsy.
Utwórz interfejs DTO
Myślę, że Magento nazywa te interfejsy „modelami danych”, a nazwa wcale mi się nie podoba.
Ten typ klasy jest powszechnie znany jako obiekt transferu danych lub DTO .
Te klasy DTO mają tylko metody pobierające i ustawiające dla wszystkich swoich właściwości.
Powodem, dla którego wolę używać DTO zamiast modelu danych, jest to, że łatwiej jest pomylić je z modelami danych ORM, modelami zasobów lub modelami przeglądania ... zbyt wiele rzeczy jest już modelami w Magento.
Te same ograniczenia dotyczące typowania PHP7, które dotyczą repozytoriów, dotyczą również DTO.
Ponadto każda metoda musi mieć adnotację ze wszystkimi typami argumentów i typem zwracanym.
<?php
namespace VinaiKopp\Kitchen\Api\Data;
use Magento\Framework\Api\ExtensibleDataInterface;
interface HamburgerInterface extends ExtensibleDataInterface
{
/**
* @return int
*/
public function getId();
/**
* @param int $id
* @return void
*/
public function setId($id);
/**
* @return string
*/
public function getName();
/**
* @param string $name
* @return void
*/
public function setName($name);
/**
* @return \VinaiKopp\Kitchen\Api\Data\IngredientInterface[]
*/
public function getIngredients();
/**
* @param \VinaiKopp\Kitchen\Api\Data\IngredientInterface[] $ingredients
* @return void
*/
public function setIngredients(array $ingredients);
/**
* @return string[]
*/
public function getImageUrls();
/**
* @param string[] $urls
* @return void
*/
public function setImageUrls(array $urls);
/**
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface|null
*/
public function getExtensionAttributes();
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface $extensionAttributes
* @return void
*/
public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes);
}
Jeśli metoda pobiera lub zwraca tablicę, typ elementów w tablicy musi być określony w adnotacji PHPDoc, a następnie otwierający i zamykający nawias kwadratowy []
.
Dotyczy to zarówno wartości skalarnych (np. int[]
), Jak i obiektów (np IngredientInterface[]
.).
Zauważ, że używam Api\Data\IngredientInterface
jako przykładu metody zwracającej tablicę obiektów, nie dodam kodu składników do tego postu.
ExtensibleDataInterface?
W powyższym przykładzie HamburgerInterface
rozszerza ExtensibleDataInterface
.
Technicznie jest to wymagane tylko wtedy, gdy chcesz, aby inne moduły mogły dodawać atrybuty do twojej jednostki.
Jeśli tak, musisz także dodać kolejną parę getter / setter, zgodnie z konwencją o nazwie getExtensionAttributes()
i setExtensionAttributes()
.
Nazywanie typu zwracanego przez tę metodę jest bardzo ważne!
Framework Magento 2 wygeneruje interfejs, implementację i fabrykę implementacji, jeśli odpowiednio je nazwiesz. Szczegóły tych mechanizmów są jednak poza zakresem tego postu.
Tylko wiedz, że jeśli wywoływany jest interfejs obiektu, który chcesz rozszerzyć \VinaiKopp\Kitchen\Api\Data\HamburgerInterface
, to typ atrybutów rozszerzenia musi być \VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface
. Dlatego słowo Extension
należy wstawić po nazwie encji, tuż przed Interface
sufiksem.
Jeśli nie chcesz, aby twoja jednostka była rozszerzalna, wówczas interfejs DTO nie musi rozszerzać żadnego innego interfejsu, getExtensionAttributes()
a setExtensionAttributes()
metody i można pominąć.
Dość na razie o interfejsie DTO, czas wrócić do interfejsu repozytorium.
Typ zwracany przez getList () SearchResults
Metoda repozytorium getList
zwraca jeszcze inny typ, czyli SearchResultsInterface
instancję.
Metoda getList
może oczywiście po prostu zwrócić tablicę obiektów pasujących do podanego SearchCriteria
, ale zwrócenie SearchResults
instancji pozwala na dodanie użytecznych metadanych do zwracanych wartości.
Poniżej możesz zobaczyć, jak to działa w getList()
implementacji metody repozytorium .
Oto przykładowy interfejs wyników wyszukiwania hamburgera:
<?php
namespace VinaiKopp\Kitchen\Api\Data;
use Magento\Framework\Api\SearchResultsInterface;
interface HamburgerSearchResultInterface extends SearchResultsInterface
{
/**
* @return \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[]
*/
public function getItems();
/**
* @param \VinaiKopp\Kitchen\Api\Data\HamburgerInterface[] $items
* @return void
*/
public function setItems(array $items);
}
Jedyne, co robi ten interfejs, to zastępuje typy dwóch metod getItems()
i setItems()
interfejsu nadrzędnego.
Podsumowanie interfejsów
Mamy teraz następujące interfejsy:
\VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerInterface
\VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface
Repozytorium rozciąga nic rozszerza ,
a rozszerza .
HamburgerInterface
\Magento\Framework\Api\ExtensibleDataInterface
HamburgerSearchResultInterface
\Magento\Framework\Api\SearchResultsInterface
Utwórz repozytorium i implementacje modelu danych
Następnym krokiem jest stworzenie implementacji trzech interfejsów.
Repozytorium
Zasadniczo repozytorium używa ORM do wykonywania swojej pracy.
Te getById()
, save()
i delete()
metody są dość proste.
Jest HamburgerFactory
on wstrzykiwany do repozytorium jako argument konstruktora, co można zobaczyć nieco poniżej.
public function getById($id)
{
$hamburger = $this->hamburgerFactory->create();
$hamburger->getResource()->load($hamburger, $id);
if (! $hamburger->getId()) {
throw new NoSuchEntityException(__('Unable to find hamburger with ID "%1"', $id));
}
return $hamburger;
}
public function save(HamburgerInterface $hamburger)
{
$hamburger->getResource()->save($hamburger);
return $hamburger;
}
public function delete(HamburgerInterface $hamburger)
{
$hamburger->getResource()->delete($hamburger);
}
Teraz najciekawsza część repozytorium, getList()
metoda. Metoda ma przełożyć warunki do zaproszeń metody w kolekcji.
getList()
SerachCriteria
Trudność polega na tym , aby poprawnie ustawić warunki AND
i OR
dla filtrów, zwłaszcza, że składnia do ustawiania warunków w kolekcji jest różna w zależności od tego, czy jest to jednostka EAV czy jednostka o płaskiej tabeli.
W większości przypadków getList()
można je zaimplementować, jak pokazano w poniższym przykładzie.
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\NoSuchEntityException;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterfaceFactory;
use VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\CollectionFactory as HamburgerCollectionFactory;
use VinaiKopp\Kitchen\Model\ResourceModel\Hamburger\Collection;
class HamburgerRepository implements HamburgerRepositoryInterface
{
/**
* @var HamburgerFactory
*/
private $hamburgerFactory;
/**
* @var HamburgerCollectionFactory
*/
private $hamburgerCollectionFactory;
/**
* @var HamburgerSearchResultInterfaceFactory
*/
private $searchResultFactory;
public function __construct(
HamburgerFactory $hamburgerFactory,
HamburgerCollectionFactory $hamburgerCollectionFactory,
HamburgerSearchResultInterfaceFactory $hamburgerSearchResultInterfaceFactory
) {
$this->hamburgerFactory = $hamburgerFactory;
$this->hamburgerCollectionFactory = $hamburgerCollectionFactory;
$this->searchResultFactory = $hamburgerSearchResultInterfaceFactory;
}
// ... getById, save and delete methods listed above ...
public function getList(SearchCriteriaInterface $searchCriteria)
{
$collection = $this->collectionFactory->create();
$this->addFiltersToCollection($searchCriteria, $collection);
$this->addSortOrdersToCollection($searchCriteria, $collection);
$this->addPagingToCollection($searchCriteria, $collection);
$collection->load();
return $this->buildSearchResult($searchCriteria, $collection);
}
private function addFiltersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
$fields = $conditions = [];
foreach ($filterGroup->getFilters() as $filter) {
$fields[] = $filter->getField();
$conditions[] = [$filter->getConditionType() => $filter->getValue()];
}
$collection->addFieldToFilter($fields, $conditions);
}
}
private function addSortOrdersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
foreach ((array) $searchCriteria->getSortOrders() as $sortOrder) {
$direction = $sortOrder->getDirection() == SortOrder::SORT_ASC ? 'asc' : 'desc';
$collection->addOrder($sortOrder->getField(), $direction);
}
}
private function addPagingToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
$collection->setPageSize($searchCriteria->getPageSize());
$collection->setCurPage($searchCriteria->getCurrentPage());
}
private function buildSearchResult(SearchCriteriaInterface $searchCriteria, Collection $collection)
{
$searchResults = $this->searchResultFactory->create();
$searchResults->setSearchCriteria($searchCriteria);
$searchResults->setItems($collection->getItems());
$searchResults->setTotalCount($collection->getSize());
return $searchResults;
}
}
Filtry w obrębie FilterGroup
muszą być połączone za pomocą operatora OR .
Oddzielne grupy filtrów są łączone za pomocą logicznego operatora AND .
Uff
To była największa praca. Inne implementacje interfejsu są prostsze.
DTO
Magento pierwotnie zamierzał programistom wdrożyć DTO jako osobne klasy, różne od modelu encji.
Zespół podstawowy zrobił to jednak tylko dla modułu klienta ( \Magento\Customer\Api\Data\CustomerInterface
jest wdrażany przez \Magento\Customer\Model\Data\Customer
, a nie \Magento\Customer\Model\Customer
).
We wszystkich innych przypadkach model encji implementuje interfejs DTO (na przykład \Magento\Catalog\Api\Data\ProductInterface
jest implementowany przez \Magento\Catalog\Model\Product
).
Pytałem o to członków zespołu podstawowego na konferencjach, ale nie otrzymałem jednoznacznej odpowiedzi na pytanie, co należy uznać za dobrą praktykę.
Mam wrażenie, że to zalecenie zostało porzucone. Byłoby miło uzyskać oficjalne oświadczenie w tej sprawie.
Na razie podjąłem pragmatyczną decyzję o zastosowaniu modelu jako implementacji interfejsu DTO. Jeśli uważasz, że użycie oddzielnego modelu danych jest czystsze, możesz to zrobić. Oba podejścia sprawdzają się w praktyce.
Jeśli interfejs DTO rozszerza Magento\Framework\Api\ExtensibleDataInterface
, model musi zostać rozszerzony Magento\Framework\Model\AbstractExtensibleModel
.
Jeśli nie zależy ci na rozszerzalności, model może po prostu kontynuować rozszerzanie podstawowej klasy modelu ORM Magento\Framework\Model\AbstractModel
.
Ponieważ przykład HamburgerInterface
rozszerza ExtensibleDataInterface
model hamburgera rozszerza AbstractExtensibleModel
, jak widać tutaj:
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Model\AbstractExtensibleModel;
use VinaiKopp\Kitchen\Api\Data\HamburgerExtensionInterface;
use VinaiKopp\Kitchen\Api\Data\HamburgerInterface;
class Hamburger extends AbstractExtensibleModel implements HamburgerInterface
{
const NAME = 'name';
const INGREDIENTS = 'ingredients';
const IMAGE_URLS = 'image_urls';
protected function _construct()
{
$this->_init(ResourceModel\Hamburger::class);
}
public function getName()
{
return $this->_getData(self::NAME);
}
public function setName($name)
{
$this->setData(self::NAME, $name);
}
public function getIngredients()
{
return $this->_getData(self::INGREDIENTS);
}
public function setIngredients(array $ingredients)
{
$this->setData(self::INGREDIENTS, $ingredients);
}
public function getImageUrls()
{
$this->_getData(self::IMAGE_URLS);
}
public function setImageUrls(array $urls)
{
$this->setData(self::IMAGE_URLS, $urls);
}
public function getExtensionAttributes()
{
return $this->_getExtensionAttributes();
}
public function setExtensionAttributes(HamburgerExtensionInterface $extensionAttributes)
{
$this->_setExtensionAttributes($extensionAttributes);
}
}
Wyodrębnienie nazw właściwości do stałych pozwala zachować je w jednym miejscu. Mogą być używane przez parę getter / setter, a także przez skrypt Setup, który tworzy tabelę bazy danych. W przeciwnym razie wyodrębnienie ich do stałych nie przyniesie korzyści.
SearchResult
Jest SearchResultsInterface
to najprostszy z trzech interfejsów do zaimplementowania, ponieważ może odziedziczyć całą swoją funkcjonalność z klasy frameworka.
<?php
namespace VinaiKopp\Kitchen\Model;
use Magento\Framework\Api\SearchResults;
use VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface;
class HamburgerSearchResult extends SearchResults implements HamburgerSearchResultInterface
{
}
Skonfiguruj preferencje ObjectManager
Mimo że implementacje są kompletne, nadal nie możemy używać interfejsów jako zależności innych klas, ponieważ menedżer obiektów Magento Framework nie wie, jakich implementacji użyć. Musimy dodać etc/di.xml
konfigurację z preferencjami.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" type="VinaiKopp\Kitchen\Model\HamburgerRepository"/>
<preference for="VinaiKopp\Kitchen\Api\Data\HamburgerInterface" type="VinaiKopp\Kitchen\Model\Hamburger"/>
<preference for="VinaiKopp\Kitchen\Api\Data\HamburgerSearchResultInterface" type="VinaiKopp\Kitchen\Model\HamburgerSearchResult"/>
</config>
Jak można ujawnić repozytorium jako zasób API?
Ta część jest naprawdę prosta, to nagroda za przejrzenie wszystkich prac związanych z tworzeniem interfejsów, implementacjami i łączeniem ich razem.
Wszystko, co musimy zrobić, to utworzyć etc/webapi.xml
plik.
<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route method="GET" url="/V1/vinaikopp_hamburgers/:id">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getById"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="GET" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="getList"/>
<resources>
<resource ref="anonymouns"/>
</resources>
</route>
<route method="POST" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="PUT" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="save"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
<route method="DELETE" url="/V1/vinaikopp_hamburgers">
<service class="VinaiKopp\Kitchen\Api\HamburgerRepositoryInterface" method="delete"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
Należy pamiętać, że ta konfiguracja umożliwia nie tylko użycie repozytorium jako punktów końcowych REST, ale także udostępnia metody jako część interfejsu API SOAP.
W pierwszym przykładzie trasy <route method="GET" url="/V1/vinaikopp_hamburgers/:id">
, symbol zastępczy :id
musi być zgodna z nazwą argumentu odwzorowanym metodzie public function getById($id)
.
Te dwie nazwy muszą się zgadzać, na przykład /V1/vinaikopp_hamburgers/:hamburgerId
nie działałoby, ponieważ nazwa zmiennej argumentu metody to $id
.
W tym przykładzie ustawiłem dostępność na <resource ref="anonymous"/>
. Oznacza to, że zasób jest publicznie dostępny bez żadnych ograniczeń!
Aby udostępnić zasób tylko zalogowanemu klientowi, użyj <resource ref="self"/>
. W takim przypadku me
do wypełnienia zmiennej argumentu $id
identyfikatorem aktualnie zalogowanego klienta zostanie użyte słowo specjalne w adresie URL punktu końcowego zasobu .
Spójrz na klienta Magento etc/webapi.xml
i CustomerRepositoryInterface
jeśli tego potrzebujesz.
Na koniec <resources>
można również użyć do ograniczenia dostępu do zasobu do konta administratora. W tym celu ustaw <resource>
odnośnik na identyfikator zdefiniowany w etc/acl.xml
pliku.
Na przykład <resource ref="Magento_Customer::manage"/>
ograniczy dostęp do dowolnego konta administratora, które ma uprawnienia do zarządzania klientami.
Przykładowa kwerenda API wykorzystująca curl może wyglądać następująco:
$ curl -X GET http://example.com/rest/V1/vinaikopp_hamburgers/123
Uwaga: pisanie tego zaczęło się jako odpowiedź na https://github.com/astorm/pestle/issues/195
Sprawdź tłuczek , kup Commercebug i zostań mecenasem @alanstorm