23 Jan

SOAP, avagy 'Run you fools!'


Az eddigi cikkekben főleg a RESTful irányába mentünk el, ami mostanában elterjedőben van, viszont óhatatlan, hogy az ember belefusson a jó öreg SOAP-ba. Már volt róla szó, hogy hogy is néz ki mindez, viszont az még nem derült ki, hogy is tudunk ilyet létrehozni, valamint egy kliens se ártana, mert "jó" esetben mi inkább használni fogjuk ezeket, nem pedig szervert írni rá PHP-ben. Mindenesetre mindkét opcióról szót ejtünk majd. Na de zuhanjunk is neki, mert semmi se lesz belőle!



Az első lépés most legyen a kliens, ugyanis szerencsénkre vannak a neten elszórtan olyan webservice-ek, amiket tudunk használni kívülről, ezért nem kell feltétlenül megírjuk a sajátunkat, hogy tesztelni tudjunk. Ez jelen esetben egy időjárással kapcsolatos szolgáltatás lesz, amit itt találtok. Igen, ez egy ún. WSDL fájl, vagyis egy csodás XML, ami leírja, hogy is épül fel ez a webservice, milyen távoli eljáráshívások találhatóak benne, milyen típusokkal dolgozik, de ne rohanjunk előre.

Ahhoz, hogy klienst írjunk, mégpedig a lehető legegyszerűbben, szükségünk lesz a SOAP PHP kiterjesztésre (már aki nem akar kézzel XML-eket összerakni). Ennek telepítését most nem részletezném, van jó leírás róla, pofonegyszerű.

Ha ezzel megvagyunk, akkor megnyílnak előttünk a SOAP sötét bugyrai, a kérdés már csak az, hogy készen állunk-e belevetni magunkat?

Az osztály, amit mi keresünk jelenleg a SoapClient lesz, globális névtérben. Később persze írhatunk rá wrappert is, hiszen eléggé általános felhasználásra van. Na de nézzük mit is tudunk vele kezdeni.

Az első lényeges tudnivaló, hogy kétféle módban tudunk SOAP szervert/klienst létrehozni. Az egyik az WSDL, a másik pedig a non-WSDL mód. Értelemszerűen az egyikhez van WSDL, ami leírja a szolgáltatást, a másik esetben vakon lövöldözünk API doksi alapján mennek a dolgok.

Mivel most van WSDL, ezért nézzük meg a dolgot azzal. A példában egy Laraveles projektben fogom ezt használni, így az első dolog az az, hogy kézzel példányosítjuk egy kontrollerben bebindoljuk azt az  AppServiceProvider.php-ben:

public function register()
{
    $this->app->bind(\SoapClient::class, function() {
        return new \SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL");
    });
}

Ezután pedig felveszünk egy sima route-ot, ami meghívja azt, hogy lássuk hogy is működik.
Route::get("/", function(SoapClient $client) { // akinek nem tiszta a Laravel, ez a $client ugyanaz, mint a fent létrehozott

  dd($client->__getFunctions());
})

Ha meglőjük ezt az URL-t, akkor egy szép 4 elemű tömböt fog nekünk kiírni:
array:4 [▼

 0 => "GetWeatherResponse GetWeather(GetWeather $parameters)"
 1 => "GetCitiesByCountryResponse GetCitiesByCountry(GetCitiesByCountry $parameters)"
 2 => "GetWeatherResponse GetWeather(GetWeather $parameters)"
 3 => "GetCitiesByCountryResponse GetCitiesByCountry(GetCitiesByCountry $parameters)"
]

Na most a dd az egy formázott var_dump és die kombója. A __getFunctions pedig arra szolgál, hogy kiszedje az elérhető távoli eljáráshívásokat a WSDL alapján. Itt látható, hogy milyen metódusok, milyen paraméterekkel és visszatérési értékekkel elérhetőek. Ez eddig tök jó, viszont egy a bibi. Nem tudjuk, hogy a paraméterként szolgáló objektumoknak milyen fieldjei vannak. Ahhoz, hogy ezt megtudjuk, egy másik hívás szükségeltetik, mégpedig a __getTypes()
  dd($client->__getTypes());

Ennek a kimenete az előbbihez hasonló, megmutatja, hogy az egyes típusok hogy is épülnek fel:
array:4 [▼

 0 => """
 struct GetWeather {\n
 string CityName;\n
 string CountryName;\n
 }
 """
 1 => """
 struct GetWeatherResponse {\n
 string GetWeatherResult;\n
 }
 """
 2 => """
 struct GetCitiesByCountry {\n
 string CountryName;\n
 }
 """
 3 => """
 struct GetCitiesByCountryResponse {\n
 string GetCitiesByCountryResult;\n
 }
 """
]

Na mostmár tudjuk, hogy mit is kell átpasszolnunk, ha pl. a GetCitiesByCountry-t akarjuk meghívni, tegyük is meg!
dd($client->GetCitiesByCountry(["CountryName" => "Hungary"])->GetCitiesByCountryResult);

Na most mi is történik itt fent? Meghívjuk a fent megadott távoli eljárást, átpasszolunk neki egy asszociatív tömböt, aminek a fieldjei megegyeznek a szükséges objektum fieldjeivel és a kapott objektumnak pedig lekérjük azt a mezőjét, amiben a válasz található. Na és ez pedig egy böszme nagy XML lesz, természetesen String formájában, amiket aztán kedvünkre parse-olhatunk:
<NewDataSet>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Békéscsaba</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Budapest Met Center</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Budapest / Ferihegy</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Budaörs</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Debrecen</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Kecskemét</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Kaposvár</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Miskolc</City>\n
  </Table>\n
  <Table>\n
    <Country>Hungary</Country>\n
    <City>Nyiregyháza / Napkor</City>\n
  </Table>\n
...

Most ettől egyelőre tekintsünk el és nézzük meg, hogy milyen idő van Budapesten!
dd($client->GetWeather(["CountryName" => "Hungary", "CityName" => "Budapest Met Center"])->GetWeatherResult);

Sajnos nem jártam sikerrel:
"Data Not Found"

Viszont a célunkat elértük, ugyanis egy távoli eljárást hívtunk meg! Na de ez nem volt valami szép, ugye? Nincs erre valami szebb módszer? Dehogynincs! Mégpedig az, hogy WSDL alapján legeneráltatjuk a szükséges osztályokat és a távoli hívásokat úgy hívjuk majd, mintha lokálisak lennének. A cél, hogy aki használja mindezt, ne vegye észre a kapott interfészből, hogy itt biza valami turpisság van, max. akkor mikor meghívja és a nanomásodpercek helyett tizedmásodpercek alatt válaszol, mert a háttérben egy webservice hívás történik.

A cucc, amit használni fogunk github szinten itt található, composerből pedig a
composer require wsdl2phpgenerator/wsdl2phpgenerator

paranccsal tudjuk előcsalogatni. Ezt a libet csupán addig fogjuk használni, amíg legeneráljuk a szükséges osztályokat. Van egy CLI tool is, amit szintén használhatunk, ha nem akarunk ezért plusz függőséget lehúzni.

Hozzunk létre egy wsimport.php fájlt a projektünk gyökerében és adjuk meg neki a WSDL-t, amire használni szeretnénk (mivel az iménti WSDL nem ment, ezért az itteni doksiból loptam):
include 'vendor/autoload.php'; // composer miatt 


$generator = new \Wsdl2PhpGenerator\Generator();
$generator->generate(
 new \Wsdl2PhpGenerator\Config(array(
 'inputFile' => 'http://www.webservicex.net/CurrencyConvertor.asmx?WSDL',
 'outputDir' => 'app/CurrencyConverter',
 'namespaceName' => 'App\\CurrencyConverter'
 ))
);

Ezzel az app\CurrencyConverter mappájávba a megfelelő névtérrel be is kerültek a fájlok. A probléma sajnos ott van, hogy külön autoloadert generált neki, amit a se a composer, se a laravel nem lát, ezért azt valahova bele kell barkácsolni, Laravel esetén pl. a bootstrap/autoload.php-be, vagy composer esetén a vendor/autoload.php-be:
require __DIR__. '/../app/CurrencyConverter/autoload.php';


Na de nézzük csak meg a generált fájlokat!

A Currency az egy hatalmas enum, illetve annak PHP megfelelője, egy osztály rahedli konstanssal:
class Currency
{
    const __default = 'AFA';
    const AFA = 'AFA';
    const ALL = 'ALL';
    const DZD = 'DZD';
    const ARS = 'ARS';
    const AWG = 'AWG';
    const AUD = 'AUD';
    const BSD = 'BSD';
...

A ConversionRate egy szimpla DTO, ami két Currency-t foglal magába a hozzájuk szükséges getter/setterekkel:
class ConversionRate
{

    /**
     * @var Currency $FromCurrency
     */
    protected $FromCurrency = null;

    /**
     * @var Currency $ToCurrency
     */
    protected $ToCurrency = null;
...

A ConversionRateResponse pedig a válasz DTO-ja, amiben egy float érték érkezik az átváltási arányokkal:
class ConversionRateResponse
{

    /**
     * @var float $ConversionRateResult
     */
    protected $ConversionRateResult = null;
...

 

Maga a kliens a CurrencyConvertor (typo?) lesz:
class CurrencyConvertor extends \SoapClient
{

    /**
     * @var array $classmap The defined classes
     */
    private static $classmap = array (
      'ConversionRate' => 'App\\CurrencyConverter\\ConversionRate',
      'ConversionRateResponse' => 'App\\CurrencyConverter\\ConversionRateResponse',
    );

    /**
     * @param array $options A array of config values
     * @param string $wsdl The wsdl file to use
     */
    public function __construct(array $options = array(), $wsdl = null)
    {
      foreach (self::$classmap as $key => $value) {
        if (!isset($options['classmap'][$key])) {
          $options['classmap'][$key] = $value;
        }
      }
      $options = array_merge(array (
      'features' => 1,
          'trace' => 1,
    ), $options);
      if (!$wsdl) {
        $wsdl = 'http://www.webservicex.net/CurrencyConvertor.asmx?WSDL';
      }
      parent::__construct($wsdl, $options);
    }

    /**
     * <br><b>Get conversion rate from one currency to another currency <b><br><p><b><font color='#000080' size='1' face='Verdana'><u>Differenct currency Code and Names around the world</u></font></b></p><blockquote><p><font face='Verdana' size='1'>AFA-Afghanistan Afghani<br>ALL-Albanian Lek<br>DZD-Algerian Dinar<br>ARS-Argentine Peso<br>AWG-Aruba Florin<br>AUD-Australian Dollar<br>BSD-Bahamian Dollar<br>BHD-Bahraini Dinar<br>BDT-Bangladesh Taka<br>BBD-Barbados Dollar<br>BZD-Belize Dollar<br>BMD-Bermuda Dollar<br>BTN-Bhutan Ngultrum<br>BOB-Bolivian Boliviano<br>BWP-Botswana Pula<br>BRL-Brazilian Real<br>GBP-British Pound<br>BND-Brunei Dollar<br>BIF-Burundi Franc<br>XOF-CFA Franc (BCEAO)<br>XAF-CFA Franc (BEAC)<br>KHR-Cambodia Riel<br>CAD-Canadian Dollar<br>CVE-Cape Verde Escudo<br>KYD-Cayman Islands Dollar<br>CLP-Chilean Peso<br>CNY-Chinese Yuan<br>COP-Colombian Peso<br>KMF-Comoros Franc<br>CRC-Costa Rica Colon<br>HRK-Croatian Kuna<br>CUP-Cuban Peso<br>CYP-Cyprus Pound<br>CZK-Czech Koruna<br>DKK-Danish Krone<br>DJF-Dijibouti Franc<br>DOP-Dominican Peso<br>XCD-East Caribbean Dollar<br>EGP-Egyptian Pound<br>SVC-El Salvador Colon<br>EEK-Estonian Kroon<br>ETB-Ethiopian Birr<br>EUR-Euro<br>FKP-Falkland Islands Pound<br>GMD-Gambian Dalasi<br>GHC-Ghanian Cedi<br>GIP-Gibraltar Pound<br>XAU-Gold Ounces<br>GTQ-Guatemala Quetzal<br>GNF-Guinea Franc<br>GYD-Guyana Dollar<br>HTG-Haiti Gourde<br>HNL-Honduras Lempira<br>HKD-Hong Kong Dollar<br>HUF-Hungarian Forint<br>ISK-Iceland Krona<br>INR-Indian Rupee<br>IDR-Indonesian Rupiah<br>IQD-Iraqi Dinar<br>ILS-Israeli Shekel<br>JMD-Jamaican Dollar<br>JPY-Japanese Yen<br>JOD-Jordanian Dinar<br>KZT-Kazakhstan Tenge<br>KES-Kenyan Shilling<br>KRW-Korean Won<br>KWD-Kuwaiti Dinar<br>LAK-Lao Kip<br>LVL-Latvian Lat<br>LBP-Lebanese Pound<br>LSL-Lesotho Loti<br>LRD-Liberian Dollar<br>LYD-Libyan Dinar<br>LTL-Lithuanian Lita<br>MOP-Macau Pataca<br>MKD-Macedonian Denar<br>MGF-Malagasy Franc<br>MWK-Malawi Kwacha<br>MYR-Malaysian Ringgit<br>MVR-Maldives Rufiyaa<br>MTL-Maltese Lira<br>MRO-Mauritania Ougulya<br>MUR-Mauritius Rupee<br>MXN-Mexican Peso<br>MDL-Moldovan Leu<br>MNT-Mongolian Tugrik<br>MAD-Moroccan Dirham<br>MZM-Mozambique Metical<br>MMK-Myanmar Kyat<br>NAD-Namibian Dollar<br>NPR-Nepalese Rupee<br>ANG-Neth Antilles Guilder<br>NZD-New Zealand Dollar<br>NIO-Nicaragua Cordoba<br>NGN-Nigerian Naira<br>KPW-North Korean Won<br>NOK-Norwegian Krone<br>OMR-Omani Rial<br>XPF-Pacific Franc<br>PKR-Pakistani Rupee<br>XPD-Palladium Ounces<br>PAB-Panama Balboa<br>PGK-Papua New Guinea Kina<br>PYG-Paraguayan Guarani<br>PEN-Peruvian Nuevo Sol<br>PHP-Philippine Peso<br>XPT-Platinum Ounces<br>PLN-Polish Zloty<br>QAR-Qatar Rial<br>ROL-Romanian Leu<br>RUB-Russian Rouble<br>WST-Samoa Tala<br>STD-Sao Tome Dobra<br>SAR-Saudi Arabian Riyal<br>SCR-Seychelles Rupee<br>SLL-Sierra Leone Leone<br>XAG-Silver Ounces<br>SGD-Singapore Dollar<br>SKK-Slovak Koruna<br>SIT-Slovenian Tolar<br>SBD-Solomon Islands Dollar<br>SOS-Somali Shilling<br>ZAR-South African Rand<br>LKR-Sri Lanka Rupee<br>SHP-St Helena Pound<br>SDD-Sudanese Dinar<br>SRG-Surinam Guilder<br>SZL-Swaziland Lilageni<br>SEK-Swedish Krona<br>TRY-Turkey Lira<br>CHF-Swiss Franc<br>SYP-Syrian Pound<br>TWD-Taiwan Dollar<br>TZS-Tanzanian Shilling<br>THB-Thai Baht<br>TOP-Tonga Pa'anga<br>TTD-Trinidad&amp;amp;Tobago Dollar<br>TND-Tunisian Dinar<br>TRL-Turkish Lira<br>USD-U.S. Dollar<br>AED-UAE Dirham<br>UGX-Ugandan Shilling<br>UAH-Ukraine Hryvnia<br>UYU-Uruguayan New Peso<br>VUV-Vanuatu Vatu<br>VEB-Venezuelan Bolivar<br>VND-Vietnam Dong<br>YER-Yemen Riyal<br>YUM-Yugoslav Dinar<br>ZMK-Zambian Kwacha<br>ZWD-Zimbabwe Dollar</font></p></blockquote>
     *
     * @param ConversionRate $parameters
     * @return ConversionRateResponse
     */
    public function ConversionRate(ConversionRate $parameters)
    {
      return $this->__soapCall('ConversionRate', array($parameters));
    }

}

Látjuk is, hogy igazából a korábban stringként látott hívásokat valósítja meg, a szükséges paraméterekkel. Akkor próbáljuk ki!
Route::get("/", function(\App\CurrencyConverter\CurrencyConvertor $currencyConvertor) {
    $conversionRate = new \App\CurrencyConverter\ConversionRate(\App\CurrencyConverter\Currency::HUF, \App\CurrencyConverter\Currency::USD);
    dd($currencyConvertor->ConversionRate($conversionRate)->getConversionRateResult());
})

Láthatjuk, hogy ez már pár fokkal szebben fest, hiszen nem magic metódusokat kell hívogatnunk, nem kell aggódni a typo-k miatt, mert legenerálja számunkra az opciókat, így közelebb kerülünk a típusosság felé.

Na de mi történik ilyenkor a háttérben? Mégis, mi az a SOAP? Kicsit magasról indultunk és nem árt, ha tudjuk, hogy a háttérben mi zajlik. Persze nem fogunk belemenni abba, hogy is készül a WSDL :)

A lényegét tekintve a SOAP hívás egy HTTP protokollon (létezik másféle protokollon közlekedő SOAP is, pl. SMTP, JMS, de remélem azok már kihaltak) át küldött POST body-ba zsúfolt XML üzenet. A neve a Simple Object Access Protocol-ból ered, habár sok mindent el lehet róla mondani azt leszámítva, hogy simple :)

Amikor a kliensünk elküldött egy üzenetet a szerver felé, az nagyjából így nézett ki:
POST /CurrencyConvertor.asmx HTTP/1.1
Host: www.webservicex.net
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 299
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.webserviceX.NET/">
 <SOAP-ENV:Body>
  <ns1:ConversionRate>
   <ns1:FromCurrency>HUF</ns1:FromCurrency>
   <ns1:ToCurrency>USD</ns1:ToCurrency>
  </ns1:ConversionRate>
 </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

A SOAP üzeneteke struktúrája elég kötött és az alábbi struktúrát követi:



Tehát az Envelope része a Header, a Body, valamint opcionálisan a Body-ban a Fault elementek, ha esetleg valami gixer van a válasz során. A kérések és a válaszok egyaránt ezt a struktúrát követik. A válaszunk az iménti kérésre a következő volt:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <soap:Body>
  <ConversionRateResponse xmlns="http://www.webserviceX.NET/">
   <ConversionRateResult>0.0034</ConversionRateResult>
  </ConversionRateResponse>
 </soap:Body>
</soap:Envelope>

A válasz hasonló, de most jön a legjobb része: ezzel nekünk nem kell törődjünk, mert a SOAP kiterjesztés és az imént telepített library megoldja helyettünk, így aki a WSDL-ek, XML sémák és azok validálása miatt van itt, annak sajnos rossz hírekkel kell szolgáljak.

Apropó SOAPFault! Az ilyen hibák a SoapClientből rendes kivételként érkezik meg hozzánk, ahol azt elkaphatjuk egy try-catch blokkban:
$conversionRate = new \App\CurrencyConverter\ConversionRate(\App\CurrencyConverter\Currency::HUF, \App\CurrencyConverter\Currency::USD);
try {
    $currencyConvertor->Conversionate($conversionRate);
} catch(SoapFault $fault) {
    dd($fault->getMessage());
}

A fenti példában elírtuk a metódus nevét, amit meghívunk. Ez ugyanúgy kiköt a SoapClient::__call magic metódjánál, viszont a szerveroldalon ezzel nem tud mit kezdeni majd. Hibát dob, ami hiba megjelenik a mi oldalunkon is és el tudjuk azt kapni.
Function ("Conversionate") is not a valid method for this service

Ízelítőnek egyelőre ennyit a SOAP-ról, később megnézzük hogy is tudunk szervert készíteni és hozzá WSDL-t kreálni, amint kijavították a hibát, amit találtam benne :)

Hozzászólások betöltése
2014-2018 © Letscode.hu. Minden jog fenntartva. Build verzió: 1.2.11