I have to implement a SOAP Web Service using PHP.
I did it by using the SoapServer class and all works fine.
I need to use a specific format for the request: they have to contain a "Header" tag with an "Authentication" tag in which there is a token that I have to use to authenticate the client that performed the request.
I used "file_get_contents('php //input')" to get the entire request that I received and then parsed it to retrieve the token that I needed.
This works fine if I try to simulate a SOAP request by using SoapUI. But, if I try to do the request by using PHP SoapClient and use the function SoapHeader to set the header, on the server side "file_get_contents('php //input')" returns only the fields of the entire request (contained in the XML tags of the XML request) merged together in a string, instead of returning the entire XML in a string format.
I cannot understand why.
The SoapServer class isn 't well documented in the PHP documentation. The SoapServer class does everything that you have in mind completely automatically. You have to use a decorator class. What a decorator is and what it does I 'll explain in the next lines. I 'm trying to give you a push in the right direction.
A while ago I had to implement the WSSE authentication standard. I 'll take some parts from the WSSE standard for this example.
The incoming request had a header that looked like this ...
<soapenv:Header>
<wsse:Security xmlns:wsc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsc:SecurityContextToken>
<wsc:Identifier>identifier</wsc:Identifier>
</wsc:SecurityContextToken>
</wsse:Security>
</soapenv:Header>
The key (identifier) identifies an authorized user to perform a function of the web service. In this sense, we must check that the key is valid before executing any function. For this purpose we need a decorator class, that is executed before the actual function is executed.
class AuthDecorator
{
/**
* Name of the class, which contains the webservice methods
* #var string
*/
protected $class;
/**
* Flag, if the recieved identifier is valid
* #var boolean
*/
protected $isValid = false;
public function getClass() : string
{
return $this->class;
}
public function setClass($class) : AuthDecorator
{
$this->class = $class;
return $this;
}
public function getIsValid() : bool
{
return $this->isValid;
}
public function setIsValid(bool $isValid) : AuthDecorator
{
$this->isValid = $isValid;
return $this;
}
public function __call(string $method, array $arguments)
{
if (!method_exists($this->class, $method)) {
throw new \SoapFault(
'Server',
sprintf(
'The method %s does not exist.',
$method
)
);
}
if (!$this->getIsValid()) {
// return a status object here, wenn identifier is invalid
}
return call_user_func_array(
[ $this->class, $method ],
$arguments
);
}
/**
* Here 's the magic! Method is called automatically with every recieved request
*
* #param object $security Security node form xml request header
*/
public function Security($security) : void
{
// auth against session or database or whatever here
$identifier = $this->getIdentifierFromSomewhereFunc();
if ($security->SecurityContextToken->Identifier == $identfier) {
$this->setIsValid(true);
}
}
}
That 's the decorator class. Looks easy, hm? The decorator contains a class named like the first child of the xml header of the recieved request. This method will be executed automatically every time we recieve a request with the soap server. Beside that the decorator checks, if the called soap server function is available. If not a soap fault is thrown that the soap client on the consumer side recieves. If a method exists is quite easy, too. Every webservice method we put in a class.
class SimpleWebservice
{
public function doSomeCoolStuff($withCoolParams) : \SoapVar
{
// do some fancy stuff here and return a SoapVar object as response
}
}
For illustrative purposes, our web service just has this one function.
But how the hell we bring the decorator to work with the soap server?
Easy, mate. The SoapServer class has some pretty tricky functionality, that is not documented. The class has a method called setObject. This method will do the trick.
$server = new \SoapServer(
$path_to_wsdl_file,
[
'encoding' => 'UTF-8',
'send_errors' => true,
'soap_version' => SOAP_1_2,
]
);
$decorator = new AuthDecorator();
$decorator->setClass(SimpleWebservice::class);
$server->setObject($decorator);
$server->handle();
That 's awesome, right? Just initializing the SoapServer class, add the decorator with the setObject method and run it with the handle method. The soap server recieves all requests and before calling the webservice method the decorator will check, if the identifier is valid. Only if the identifier is valid, the called webservice method will be executed.
How 's the soap client request looking?
On the other side the soap client can look like this ...
$client = new SoapClient(
$path_to_wsdl_file,
[
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'exceptions' => true,
'trace' => true,
]
);
$securityContextToken = new \stdClass();
$securityContextToken->Identifier = 'identifier';
$securityContextToken = new \SoapVar(
$securityContextToken,
SOAP_ENC_OBJ,
null,
null,
'SecurityContextToken',
'http://schemas.xmlsoap.org/ws/2005/02/sc'
);
$security = new stdClass();
$security->SecurityContextToken = $securityContextToken;
$security = new \SoapVar(
$security,
SOAP_ENC_OBJ,
null,
null,
'Security',
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
);
$header = new \SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
$security
);
$client->__setSoapHeaders($header);
$result = $client->doSomeCoolStuff(new \SoapParam(...));
Conclusion
When working in an object orientated context the SoapServer and SoapClient classes are pretty cool. Because the documentation doesn 't really give much about both classes, you have to test and learn. You can easily create a SOAP webservice when you know how. Without writing any xml as a string.
Before you productively use the code examples seen here, please make sure that they are only examples and not intended for productive use. The shown examples should push you in the right direction. ;)
Questions?
Related
I test code with PHPUnit 9.0.
I use Laravel framework 8.* and PHP 7.4
I struggle to test a function that uses request()
Here is a very short version of the code I have to test:
trait SomeTrait
{
function someFunction()
{
//1. retrieve only the documents
$documents = request()->only('documents');
....
//set an array called $header
$header = [ 'Accept-Encoding' => 'application/json'];
//2. add to Array $header if someKey is available in headers
if (request()->headers->has('someKey'))
{
$header = Arr::add($header, 'someKey', request()->header('someKey'));
}
}
}
At first (1.) it has to get the documents from a request. I solved this with an mock of the request and it works:
$requestMock = Mockery::mock(Request::class)
->makePartial()
->shouldReceive('only')
->with('documents')
->andReturn($document_data);
app()->instance('request', $requestMock->getMock());
$this->someFunction();
I create a mock of request class, that returns $document_data when request()->only('documents'); is called in someFunction().
But then the code request()->headers->has('someKey') returns the error:
Call to a member function has() on null
Can anybody help and explain how I can test the code?
Thanks for the help! I found a solution without mocking the request - sometimes it's easier than you think :D
//create a request
$request = new Request();
//replace the empty request with an array
$request->replace(['documents' => $all_documents]);
//replace the empty request header with an array
$request->headers->replace(['someKey' => 'someValue']);
//bind the request
app()->instance('request', $request);
I've tried to configure Pact for PHP using example configuration. My problem is I can run a mockServer, but every request I make returns 404 response. Of course I set everything up like in a GitHub readme. Still, I know server is visible (localhost config) but routes could not be registered.
Code example:
class PactTest extends \Tests\BaseTestCases\V2TestCase
{
/** #var MockServerConfig */
private $config;
public function setUp()
{
// Create your basic configuration. The host and port will need to match
// whatever your Http Service will be using to access the providers data.
$this->config = new MockServerConfig();
$this->config->setHost('localhost');
$this->config->setPort(7200);
$this->config->setConsumer('someConsumer');
$this->config->setProvider('someProvider');
$this->config->setHealthCheckTimeout(60);
$this->config->setCors(true);
// Instantiate the mock server object with the config. This can be any
// instance of MockServerConfigInterface.
$server = new MockServer($this->config);
// Create the process.
$server->start();
// Stop the process.
$server->stop();
}
public function testSimple()
{
$matcher = new Matcher();
// Create your expected request from the consumer.
$request = new ConsumerRequest();
$request
->setMethod('GET')
->setPath('/test/abc')
->addHeader('Content-Type', 'application/json');
// Create your expected response from the provider.
$response = new ProviderResponse();
$response
->setStatus(200)
->addHeader('Content-Type', 'application/json;charset=utf-8')
->setBody([
'message' => $matcher->term('Hello, Bob', '(Hello, )[A-Za-z]')
]);
// Create a configuration that reflects the server that was started. You can
// create a custom MockServerConfigInterface if needed. This configuration
// is the same that is used via the PactTestListener and uses environment variables.
$builder = new InteractionBuilder($this->config);
$builder
->given('a thing exists')
->uponReceiving('a get request to /test/abc')
->with($request)
->willRespondWith($response); // This has to be last. This is what makes an API request to the Mock Server to set the interaction.
$service = new HttpClientService($this->config->getBaseUri()); // Pass in the URL to the Mock Server.
$result = $service->getTestAbc(); // Make the real API request against the Mock Server.
$builder->verify();
self::assertEquals('Hello, Bob', $result); // Make your assertions.
}
Where getTestAbc() is:
public function getTestAbc(): string
{
$uri = $this->baseUri;
$response = $this->httpClient->get("{$uri->getHost()}/test/abc", [
'headers' => ['Content-Type' => 'application/json']
]);
$body = $response->getBody();
$object = \json_decode($body);
return $object->message;
}
What do I do wrong?
You're stopping the mock server in setUp. You should stop the server after the test in tearDown. I've noticed that's the code from the manual and it may be quite misleading, but I think it was intended as an example how to start/stop mock server by hand.
I have to do requets to a SOAP API with PHP and I need the following SOAP-Header structure:
<soapenv:Header>
<ver:authentication>
<pw>xxx</pw>
<user>xxx</user>
</ver:authentication>
</soapenv:Header>
How can I build this header?
I tried
$auth = [
"ver:authentication" => [
"pw" => $this->pw,
"user" => $this->user
]
];
$options = [];
$options["trace"] = TRUE;
$options["cache_wsdl"] = WSDL_CACHE_NONE;
$options["compression"] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP;
$client = new SoapClient("www.my-url.com/wsdl", $options);
$header = new SoapHeader("www.my-url.com", "authentication", $auth, false);
$client->__setSoapHeaders($header);
but it does not work. The respons is "failure" which I get, when the header structure is incorrect...
please help
the solution could be object driven. In the following code an example is given. Please keep in mind, that the following code is not testet.
class Authentication
{
protected $user;
protected $pw;
public function getUser() : ?string
{
return $this->user;
}
public function setUser(string $user) : Authentication
{
$this->user = $user;
return $this;
}
public function getPw() : string
{
return $this->pw;
}
public function setPw(string $pw) : Authentication
{
$this->pw = $pw;
return $this;
}
}
The above shown class is a simple entity, which contains two properties $user fpr the username and $pw for the password. Further it contains the getter and setter functions for retrieving or setting the values for the two properties.
For the next step just fill the class with data and store it in a SoapVar object.
$authentication = (new Authentication())
->setUser('Username')
->setPw('YourEncodedPassword');
$soapEncodedObject = new \SoapVar(
$authentication,
SOAP_ENC_OBJECT,
null,
null,
'authentication',
'http://www.example.com/namespace'
);
As you can see above, your authentication class will be stored as soap var object. It is encoded as soap object. The only thing you have to do is setting the namespace for this object. In your given example it is ver:. With this namespace prefix somewhere in your wsdl file a namespace is noted. You have to find out this namespace url and just replace the example url http://www.example.com/namespace with the right url noted in your wsdl.
The next step is setting this as soap header. That 's quite simple.
try {
$client = new SoapClient('http://www.example.com/?wsdl', [
'trace' => true,
'exception' => true,
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
]);
// set the soap header
$header = new SoapHeader('http://www.example.com/namespace', 'authentication', $authentication, false);
$client->setSoapHeaders($header);
// send the request
$result = $client->someWsdlFunction($params);
} catch (SoapFault $e) {
echo "<pre>";
var_dump($e);
echo "</pre>";
if ($client) {
echo "<pre>";
var_dump($client->__getLastRequest());
echo "</pre>";
echo "<pre>";
var_dump($client->__getLastResponse());
echo "</pre>";
}
}
As you can see it 's a bit different from your given example. Instead of an array it 's the soap encoded authentication object, that is given to the soap header class. For failure purposes there is a try/catch block around your soap client. In that case you can identify the error and if the client was initiated correctly, you can also see the last request and last response in xml.
I hope, that I helped you. ;)
I would strongly advise you 2 things:
Use a WSDL to PHP generator in order to properly construct your request. In addition, it will ease you the response handling. Everything is then using the OOP which is much better. Take a look to the PackageGenerator project.
Use the WsSecurity project in order to easily add your dedicated SoapHeader without wondering how to construct it neither.
There are several weeks that I'm trying to create a soap server in php that at first serves a wsdl with authentication header on it and at second it accepts only authenticated users in every request. But I' ve only made it fully working only without authentication. Every search I 've made and every solution I 've found contains a SoapClient,Zend_Soap_Client,nu_soap_client (you name it) and either some kind of wrapper class around my class or only addition of username & password on the client.
But at my solution only the server is in php and client are various programs written in java etc, not in php. Here is my code for (I use zend here but the idea is the same on plain php) the wsdl generation and the server part:
use Zend\Soap\AutoDiscover as Zend_Soap_AutoDiscover;
use Zend\Soap\Server as Zend_Soap_Server;
if (isset($_GET['wsdl'])) {
$autodiscover = new Zend\Soap\AutoDiscover();
$autodiscover->setClass('MyClass');
$autodiscover->setUri('http://Myclass/path/');
$autodiscover->handle();
exit;
}
$server = new Zend_Soap_Server(null, array(
'uri' => 'http://Myclass/path/',
));
$server->setClass('Myclass');
$server->handle();
I also used piotrooo's wsdl generator and plain php soap library like this:
use WSDL\WSDLCreator;
// use WSDL\XML\Styles\DocumentLiteralWrapped;
if (isset($_GET['wsdl'])) {
$wsdl = new WSDL\WSDLCreator('Myclass', 'http://Myclass/path/');
$wsdl->setNamespace("http://Myclass/path/");
$wsdl->renderWSDL();
exit;
}
$server = new SoapServer(null, array(
'uri' => 'http://Myclass/path/',
// 'style' => SOAP_DOCUMENT,
// 'use' => SOAP_LITERAL,
));
$server->setClass('Myclass');
$server->handle();
And my class:
class Myclass
{
public function __construct()
{
/*some db stuff with doctrine*/
}
/**
* #param string $id
* #return object
*/
public function Id($id)
{
/*I'm using doctrine to fetch data from db and then return an object/or array*/
}
}
At last this is the auto generated wsdl:
<definitions name="Myclass" targetNamespace="http://Myclass/path/"><types><xsd:schema targetNamespace="http://Myclass/path/"/></types><portType name="MyclassPort"><operation name="Id"><documentation>Id</documentation><input message="tns:IdIn"/><output message="tns:IdOut"/></operation></portType><binding name="MyclassBinding" type="tns:MyclassPort"><soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/><operation name="Id"><soap:operation soapAction="http://Myclass/path/#Id"/><input><soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://Myclass/path/"/></input><output><soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://Myclass/path/"/></output></operation></binding><service name="MyclassService"><port name="MyclassPort" binding="tns:MyclassBinding"><soap:address location="http://Myclass/path/"/></port></service><message name="IdIn"><part name="id" type="xsd:string"/></message><message name="IdOut"><part name="return" type="xsd:struct"/></message></definitions>
Annotations vary on each generator.
I also tried nusoap but I was disappointed because of it's pure class method discovery. I must add that I'm testing usage of service with soapui (that's why I don't want php's SoapClient or equivalent examples).
Last I also must say that I tried solutions of adding authentication method inside my class and this worked BUT this didn't prevent unauthenticated user from accessing ws.
A little extra information. As of my research every answer I was found was about SOAPClient for example the $client->__addheader() function etc. Please tell me if I' m wrong or if this can't be done with PHP because I ll have to find someone else to do this for me with another programming language like Java etc.
Thanks in advance
Dimitris
You could have your clients send the authentication information in basically 3 places: SOAP Body, SOAP Headers, HTTP Headers. Of the three I think the proper one is SOAP Headers so I'll go with that for this example. (Note I'm using some custom auth header format, but I suggest you research about the WS-Security protocol.)
class Myclass
{
public function __construct()
{
/*some db stuff with doctrine*/
}
public function Auth($auth)
{
if (! $this->validateUser($auth->Username, $auth->Password)) {
throw new SoapFault('Client.Authentication', 'Invalid username or password');
}
}
/**
* #param string $id
* #return object
*/
public function Id($id)
{
/*...*/
}
private function validateUser($user, $password)
{
/*...*/
}
}
Now for the client: I think you in your server role don't have to worry about which particular clients will be consuming your service, simply because you can't consider them all. That's why there are standards, to abstract from particular implementations. So, as long as you follow the standard, you can trust any client will comply. And luckily, SOAP headers are part of the standard.
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Header>
<Auth>
<Username>foo</Username>
<Password>bar</Password>
</Auth>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<Id>4</Id>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
That is the XML you are expecting to receive, regardless of the client.
Finally, the WSDL. From what I've read, neither Zend Autodiscover nor piotrooo's library support headers definition. Apparently Yii's CWebService does (with proper method annotations).
In the worst case scenario, you can write the WSDL yourself, or adapt it from the one generated with your library of choice.
Links:
http://www.yiiframework.com/doc/api/1.1/CWebService
http://www.ibm.com/developerworks/library/ws-tip-headers/
UPDATE:
class Auth
{
/**
* #soap
* #var string
*/
public $Username;
/**
* #soap
* #var string
*/
public $Password;
}
class MyClass
{
private $authenticated = false;
public function Auth($auth)
{
if ($this->validateUser($auth->Username, $auth->Password)) {
$this->authenticated = true;
}
}
/**
* #soap
* #header Auth $auth
* #param string $id
* #return object
*/
public function Id($id)
{
if (! $this->authenticated) {
throw new SoapFault('Client.Authentication', 'Invalid username or password');
}
//return $id;
}
private function validateUser($user, $password)
{
return $user == 'foo';
}
}
require __DIR__ . '/vendor/yiisoft/yii/framework/yii.php';
$URL = 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'];
if (isset($_GET['wsdl'])) {
$gen = new CWsdlGenerator();
$wsdl = $gen->generateWsdl(MyClass::class, $URL);
header("Content-type: text/xml; charset=utf-8");
echo $wsdl;
}
else {
$server = new SoapServer($URL . '?wsdl');
$server->setClass(MyClass::class);
echo $server->handle();
}
This is indeed working as expected with the sample XML I provided before.
(You just need to change <Id>4</Id> for <Id><id>4</id></Id>).
I have a SoapClient instance and I'm trying to make a request (duh!). I am able to pass an array of parameters as key => value in the first level like, securityToken. But I can't send to the second namespace (I think that's what it is) stap. The following is a simplified version of what the inside of my ENV should look like. I know the Envelope should contain a reference to xmlns:stap but I can't work out how to get SoapClient to do that.
<soapenv:Body>
<ns:PlaceOrder>
<ns:securityToken></ns:securityToken>
<ns:orderRequest>
<stap:Headers>
<stap:OrderRequestHeader>
<stap:Lines>
<stap:OrderRequestLine>
<stap:QuantityRequested></stap:QuantityRequested>
<stap:StockCode></stap:StockCode>
</stap:OrderRequestLine>
</stap:Lines>
</stap:OrderRequestHeader>
</stap:Headers>
</ns:orderRequest>
</ns:PlaceOrder>
And here's my _soap function
protected function _soap($request, $parameters = array(), $service = null, $options = array()) {
$client = new SoapClient($service, $options);
$response = $client->{$request}($parameters);
return $response;
}