webservices cakePHP pogryzły się z serwerem SOAP

W poprzednich postach pokazałem sposób na stworzenie serwera soap we frameworku CakePHP, a ten po pewnym czasie przestał działać. Okazało się, że włączenie webserwices w /app/config/core.php spowodował problem. Otóż po tej operacji link_do_aplikacji/soap nie oznaczał już linku do SoapController::index(), ale do Controller::index().
Po prostu po właczeniu webservices oczekiwał czegoś w stylu:
link_do_aplikacji/soap/stuff, który wywołał by StuffController::index() i wyrenderował widok w /app/views/stuff/soap/index.thtml.
Niby nic, a może życie uprzykrzyć.

Share Button

CakePHP + nuSOAP serwer + autoryzacja othAuth

Autoryzacja serwera soap jest o tyle niewdzięczna, o ile nie została zaimplementowana w samym protokole. Do tego sam cake nie ułatwia nam sprawy. Jednak po wielu dniach zmagań udało mi się nagiąć tą materię i z chęcią się doświadczeniami podzielę (tak naprawdę po uporaniu się z tym problemem postanowiłem założyć ten blog).
Załóżmy, że serwer, który opisałem w poprzednim poście już stoi. Załóżmy też, że jest zrobione wszystko, co musiało być zrobione, aby autoryzacja othAuth działała w naszej aplikacji (jeśli nie- przed kontynuowaniem zapraszam tutaj )
Po pierwsze zmieńmy w /app/config/core.php definicję stałej CAKE_SESSION_COOKIE na ‘PHPSESSID’ bez tego nie uda nam się “odzyskać” sesji znając jej id.
Dalej… logowanie “normalnego” usera wygląda mniej więcej tak:
/app/controllers/user_controller.php

function login()
{
if(isset($this->params['data']))
{
$auth_num = $this->othAuth->login($this->params['data']['User']);
return $auth_num;// potrzebne dla soap-owego logowania (1 jeśli udane)
}
}

Wracajmy teraz szybko do naszego oczka w głowie: /app/controllers/soap_controller.php.
W metodzie _defineTypes dodajmy definicję złożonych typów:

$this->server->wsdl->addComplexType (
'LoginData',
'complexType',
'struct',
'all',
'',
array(
'Login' => array('name' => 'Login',
'type' => 'xsd:string'), //
'Password' => array('name' => 'Password',
'type' => 'xsd:string'), //
)
);
/**
* odpowiedĹş na logowanie
*/
$this->server->wsdl->addComplexType (
'LoginResponse',
'complexType',
'struct',
'all',
'',
array(
'Result' => array('name' => 'Result',
'type' => 'xsd:int'), //
'SID' => array('name' => 'SID',
'type' => 'xsd:string'), //
)
);

A w metodzie _registerMethods() zarejestrujmy metodę SoapController.login:

$this->server->register('SoapController.login',
array('SoapParam' => 'tns:LoginData'),
array('return' => 'tns:LoginResponse') ,
$this->namespace,
$this->namespace . '#login',
'logowanie'
);

Teraz właściwe logowanie:

function login($login_data) {
$login_details['User'] = array('username' => $login_data['Login'],
'password' => $login_data['Password'],
'cookie' => "0");
$result = $this->requestAction('users/login', array('data' => $login_details));
return array('Result' => $result, 'SID' => session_id() );
}

Kilka słów wyjaśnienia: w pierwszej linii tworzymy trochę sztucznie tablicę $login_details, która jest taka jaką oczekuje metoda UsersController::login() (zgodnie z zasadą DRY nie będziemy tworzyć osobnej metody dla logowania przez soap). Wywołujemy akcję users/login przekazując do niej spreparowane dane i zwracamy zgodnie z definicją typu LoginResponse- tablicę z danymi.

To była ta łatwiejsza część. Teraz to nad czym spędziłem kilka intensywnych dni- jak odzyskać sesję??
Od razu sprostowanie: wygląda na to, że sesja “trzyma się” tak długo, jak długo klient się nie rozłączy (testowałem to za pomocą również nuSOAP + php i w okresie trwania skryptu – było ok). Jednak ja potrzebowałem możliwości podania przez klienta id sesji, którą otrzymał podczas logowania. Jak to zrobić?
Po kolei:
Dodajmy typ, który będzie służył do przekazania informacji na temat zalogowanego usera:

$this->server->wsdl->addComplexType (
'UserDetails',
'complexType',
'struct',
'all',
'',
array(
'Id' => array('name' => 'id',
'type' => 'xsd:int'), //
'Username' => array('name' => 'Username',
'type' => 'xsd:string'), //
'Email' => array('name' => 'Email',
'type' => 'xsd:string'), //
'GroupId' => array('name' => 'GroupId',
'type' => 'xsd:string'), //
'Error' => array('name' => 'Error',
'type' => 'xsd:string')
)
);

Zarejestrujmy metodę SoapController.check():

$this->server->register('SoapController.check',
array('data' => 'xsd:string'),
array('return' => 'tns:UserDetails') ,
$this->namespace,
$this->namespace . '#check',
'logowanie'
);

I zdefiniujmy ją:

function check($data="") {
$data = $this->requestAction('/users/soapCheck', array('data' => $data, 'SID'=>$data));
$this->log($data, LOG_DEBUG);

if($data){
$return = array ( 'Id' => $data['User']['id'],
'Username' => $data['User']['username'],
'Email' => $data['User']['email'],
'GroupId' => $data['Group']['id'],
'Error' => 'ewrifing ok');
}else{
$return = array ( 'Id' => -1,
'Username' => ''
'Email' => '',
'GroupId' => '',
'Error' => 'not logged in');
}

return $return ;
}

Wyjaśnienia: oprócz danych jak przy logowaniu pojawiło się jeszcze pole w tablicy ‘SID’, to tędy przekażemy informację, że chcemy “wymusić” jakieś id sesji.
Teraz metoda UsersController::soapCheck():

function soapCheck($data) {
return $this->othAuth->getData();
}

No i na koniec gdzieś musimy wymusić inne id sesji (no bo nie ma przeglądarki, ani kochanych cookies, które zrobią to za nas). W app/app_controller.php:

if(isset($this->params['SID'])){
session_id($this->params['SID']);
}

I ok… jaaasne. Nie działa, prawda? Dlaczego? A to dlatego, że w momencie kiedy klient SOAP łączy się z naszym ukochanym serwerem SOAP wygląda to tak:
request (soap/index) -> app_controller -> soap_contropper -> check -> request (/users/soapCheck) -> app_controller*->users_controller -> soapCheck ...

miejsce oznaczone gwiazdką oznacza moment, kiedy app_controller dostaje info o tym, że jest jakieś SID… tylko że wtedy to już jest za późno (jak to mówią ślązocy: “po ptokach” :P) $this->requestAction to moment, kiedy ciasteczka już nie są wysyłane i szanowna pani Sesja ma już głęboko w … nosie fakt, że tam jakieś id jest przesyłane.
Nie lękaj się jednak, jest na to rada:
zmień w app_controller poprzednio dodany blok na:

if(isset($this->params['url']['SID'])){
session_id($this->params['url']['SID']);
}

I teraz łącząc się z serwerem do linku /twoja_aplikacja/soap/ doklejaj parametr SID=. W takim wypadku już przy pierwszym request->app_controller id sesji trafia do “świadomości” Sesji i zazwyczaj rozpatruje je pozytywnie.

Nie jest to może najbardziej eleganckie rozwiązanie. Jednak najlepsze na jakie teraz mnie stać ;) Jeśli masz coś fajnieszego- pisz w komentarzach, z chęcią się podszkolę :)

Share Button

CakePHP + serwer SOAP (pierwsze kroki)

Mimo tego, że cake chwali się, że jest WebServices ready tyczy się to jedynie tak zwanego routingu (np. gdy użyjesz users/index – będzie “normalnie” czyli wyrenderuje widok /vews/users/index.thtml, a gdy “xml/user/index” – /views/users/xml/index.thtml) ale nie jest to prawdziwe WebSerwice. Nie wiem ile jest sposobów na zmianę CakePHP w prawdziwy serwer SOAP, ja znam jeden i Wam go pokażę.

1. Potrzebna nam biblioteka SOAP (jako prawdziwy programiści pozwalamy, żeby najtrudniejsze rzeczy pisali za nas inni). Ja wybrałem NuSOAP. Wrzuć pliki do /app/vendors/nusoap/*.
2. Stwórz kontroler do obsługi Soap. W moim przypadku soap_controller.php


/**
* Soap Serwer
* @package package
*/
vendor('nusoap/nusoap');
class SoapController extends AppController {
var $name = 'Soap';
var $server;
var $namespace;
var $layout = 'blank';
var $components = array('othAuth','Conf');
var $othAuthRestrictions = null;

/**
* Inicjalizacja ustawień serwera
*
*/
function _init() {
$this->namespace = $this->Session->host;
$this->server = new soap_server();
$this->server->debug_flag = false;
$this->server->configureWSDL('MyWsdl', $this->namespace, 'http://'.$_SERVER['HTTP_HOST'] . $this->webroot . $this->params['controller'] ); // należy nadpisać endpoint, gdyż domyślnie ustawi się na /twoja_aplikacja/app/index.php
$this->server->wsdl->schemaTargetNamespace = $this->namespace;
$this->_defineTypes();
$this->_registerMethods();
}

/**
* Serwer endpoint handler
*
*/
function index(){
Configure::write('debug', 0);

$this->_init();

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
$this->server->service($HTTP_RAW_POST_DATA);
exit();
}

/**
* define types required by this server
* przyda się później
*/
function _defineTypes() {

}

function _registerMethods() {

$this->server->register(
'SoapController.hello', // method name
array('name' => 'xsd:string'), // input parameters
array('return' => 'xsd:string'),
$this->namespace,
$this->namespace . '#hello',
'document', // style
'encoded' // use
);

}

function hello($name){
return array('return' => 'hello, '.$name);
}
}

3. Do tego stwórz model, który nic nie robi :)

class Soap extends AppModel {
var $name = 'Soap';
var $useTable = false;
}

Teraz objaśnienia:
SoapController::index() – to twój endpoint serwera SOAP. SoapController::_registerMethods() zajmuje się rejestrowaniem metod dostępnych przez serwer. W tym wypadku tylko jednej: SoapController::hello (notacja metod soap to Klasa.metoda).
Metoda hello przyjmuje string jako parametr i zrwaca ‘Hello, ‘+ to co zostało jej przekazane.
Przykładowy klient (poza cake.php):

require('./nusoap/nusoap.php');

function pr($var) {
echo "

";
var_dump($var);
echo "

";
}

/* create client */
$endpoint = "http://localhost/meta/application_in_cakePHP/soap/index";

$mynamespace = "";
$client = new soapclient($endpoint);

$err = $client->getError();
if ($err) {
// Display the error
echo 'Constructor error: '. $err . ' ';
// At this point, you know the call that follows will fail
}

$response = $client->call('SoapController.hello', array('name' =>"Greg"));
echo "Call SoapController.hello";
if ($client->fault) {
echo 'Fault: ';
print_r($response);
echo ' ';
} else {
// Check for errors
$err = $client->getError();
if ($err) {
// Display the error
echo 'Error: '. $err . '';
} else {
echo('response:');
pr($response);
}
}

I ładny response:

string(11) "hello, Greg"

Dodatkowo, gdy w przeglądarce wpiszesz ścieżkę do kontrolera Soap zobaczysz ładną dokumentację Twojego serwera. A dodając do ścieżki ?wsdl – dokument wsdl.

żródła:
http://www.scottnichol.com/nusoapprog.htm
http://dietrich.ganx4.com/nusoap/faq.php

Share Button