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-2017 © Letscode.hu. Minden jog fenntartva. Build verzió: 1.2.37

Mik azok a sütik?


As is common practice with almost all professional websites this site uses cookies, which are tiny files that are downloaded to your computer, to improve your experience. This page describes what information they gather, how we use it and why we sometimes need to store these cookies. We will also share how you can prevent these cookies from being stored however this may downgrade or 'break' certain elements of the sites functionality.

Áltálnos információkat nyújthat ez a Wikipedia cikk a HTTP sütikről...

Hogy használjuk a sütiket?


We use cookies for a variety of reasons detailed below. Unfortunately is most cases there are no industry standard options for disabling cookies without completely disabling the functionality and features they add to this site. It is recommended that you leave on all cookies if you are not sure whether you need them or not in case they are used to provide a service that you use.

Sütik kikapcsolása


You can prevent the setting of cookies by adjusting the settings on your browser (see your browser Help for how to do this). Be aware that disabling cookies will affect the functionality of this and many other websites that you visit. Disabling cookies will usually result in also disabling certain functionality and features of the this site. Therefore it is recommended that you do not disable cookies.

A sütik amiket mi használunk


This site offers newsletter or email subscription services and cookies may be used to remember if you are already registered and whether to show certain notifications which might only be valid to subscribed/unsubscribed users.

In order to provide you with a great experience on this site we provide the functionality to set your preferences for how this site runs when you use it. In order to remember your preferences we need to set cookies so that this information can be called whenever you interact with a page is affected by your preferences.


Harmadik féltől származó sütik


In some special cases we also use cookies provided by trusted third parties. The following section details which third party cookies you might encounter through this site.

This site uses Google Analytics which is one of the most widespread and trusted analytics solution on the web for helping us to understand how you use the site and ways that we can improve your experience. These cookies may track things such as how long you spend on the site and the pages that you visit so we can continue to produce engaging content.

For more information on Google Analytics cookies, see the official Google Analytics page.

The Google AdSense service we use to serve advertising uses a DoubleClick cookie to serve more relevant ads across the web and limit the number of times that a given ad is shown to you.

For more information on Google AdSense see the official Google AdSense privacy FAQ.

We also use social media buttons and/or plugins on this site that allow you to connect with your social network in various ways. For these to work the following social media sites including; Facebook, will set cookies through our site which may be used to enhance your profile on their site or contribute to the data they hold for various purposes outlined in their respective privacy policies.


More Information


Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site. However if you are still looking for more information then you can contact us through one of our preferred contact methods.

Email: fejlesztes@letscode.hu

Bezárás