Backend DevelopmentWeb Services

Manipulating Soap Web Services With PHP and Laravel

using soap in php and laravel

Working with web services is an important aspect if every modern web application. In this article i will demonstrate how to deal with Web services based on Soap xml.

 

 

Perhaps you heard about the term Soap before in the world of web services and you asked what is soap. Soap is a protocol for making remote procedure calls (RPC) using xml, so unlike the Rest web services which uses json, Soap web services expressed as xml documents that have a specific structure understandable by the Soap Engine.

To make a Soap Api you need two components, (a soap server and a soap client). The server which has the actual api functions that for example fetch data from the database. The client makes requests to the server component and calls the appropriate api using it’s name.

Most programming languages have already built in classes to deal with Soap clients and servers and php among them have the native Soap classes and also there are other php libraries like the nusoap and zend-soap library so we don’t need to worry about generating the Soap xml documents as they are generated automatically.

 

Anatomy of Soap Messages

Example Soap Request

<?xml version="1.0" ?>
<SOAP-ENV:Envelope
	SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
        xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
	>
        <!-- Optional Soap Header -->
	<SOAP-ENV:Header>
		
	</SOAP-ENV:Header>
	<SOAP-ENV:Body>
               
                <!-- Function names and attributes -->
                <getProduct>
                          <id>1</id>
                </getProduct>

                <!-- Optional Soap Fault -->
		<SOAP-ENV:Fault>
		
		</SOAP-ENV:Fault>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Example Soap Response

<?xml version="1.0" ?>
<SOAP-ENV:Envelope
	SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
        xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
	>
        <!-- Optional Soap Header -->
	<SOAP-ENV:Header>
		
	</SOAP-ENV:Header>
	<SOAP-ENV:Body>
               
                <!-- Function names and attributes -->
                <getProductResponse>
                          <return>
                                 <id>1</i>
                                 <title>Apple PC</title>
                                 <price>1000</price>
                          </return>
                </getProductResponse>

                <!-- Optional Soap Fault -->
		<SOAP-ENV:Fault>
		
		</SOAP-ENV:Fault>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The anatomy shown above show an example of a Soap message whether it’s a response or request the soap generate and sends those messages behind the scenes when it makes a request and receives a response.

As you see Soap document must be a valid xml document that have root element SOAP-ENV:Envelope. Inside this element there are SOAP-ENV:Header element this element not mandatory and it can contain optional things like authentication data.

The most important element is SOAP-ENV:Body and this have two forms depending on whether this is a request message or response message.

On request messages SOAP-ENV:Body element holds the actual function or api names and their arguments, in the above example there are a function “<getProduct>” and inside it an “<id>” and this calls function getProduct($id) in php. On response messages the SOAP-ENV:Body holds the return values of the functions as shown above the “<getProductResponse>

 

Example

Let’s see an example to demonstrate Soap in pure php first then i will show you how to implement this in laravel framework. In this example we will create a simple api for retrieving products and their details so create a new database with this table:

CREATE TABLE `products` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `title` varchar(150) NOT NULL,
 `description` text NOT NULL,
 `price` varchar(50) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `products` (`id`, `title`, `description`, `price`) VALUES (NULL, 'Details about  Canon EF 50mm f/1.8 STM Lens', 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', '$89.99'), (NULL, 'Refurbished Dell Latitude E5420 Laptop - i3 - 4GB RAM - 250GB HDD - Win 10 Pro', 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting.', 'US $4.08');

INSERT INTO `products` (`id`, `title`, `description`, `price`) VALUES (NULL, ' Details about  M3 Sport Fitness Smart Wrist Band Pedometer Activity Tracker Bracelet Watch New ', 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.', '$5.50'), (NULL, ' Details about  Samsung Galaxy S9+ Plus(LATEST) SM-G965U 64GB T-Mobile AT&T Verizon GSM Unlocked', 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries', '$399.88');

Next create a new folder called soap in your web root directory with the following files:

\soap
ـــ client.php
ـــ server.php
ـــ api.php

server.php will hold the php code for creating the real apis, connecting to the database and calling the soap server. client.php hold the php code for initializing the soap client and connecting to the soap server in server.php.

The api.php file represent the front controller that you already navigate to it in your browser to access the api. in fact you can omit it and use client.php directly but to make things clean.

server.php

<?php

define("DB_HOST", "localhost");
define("DB_NAME", "soap");
define("DB_USER", "root");
define("DB_PASSWORD", "");
define("TABLE", "products");


class server
{

	private $db_handle;

	public function __construct()
	{
		$this->connect();
	}

	private function connect()
	{
		try {

			$this->db_handle = mysqli_connect(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);

		} catch (Exception $ex) {
			exit($ex->getMessage());
		}
	}

	public function getAllProducts()
	{
		$query = mysqli_query($this->db_handle, "SELECT * FROM " . TABLE);

		$products = [];

		while($row = mysqli_fetch_array($query, MYSQLI_ASSOC)) {

			array_push($products, new Product($row['id'], $row['title'], $row['description'], $row['price']));
		}

		return $products;
	}


	public function getProduct($params)
	{
		$query = mysqli_query($this->db_handle, "SELECT * FROM " . TABLE . " WHERE id='{$params['id']}'");

		$row = mysqli_fetch_row($query);

		if($row) {
			return new Product($row[0], $row[1], $row[2], $row[3]);
		}

		return "no such product";
	}
}


class Product 
{
	public $id, $title, $description, $price;

	public function __construct($id, $title, $description, $price) 
	{
		$this->id = $id;
		$this->title = $title;
		$this->description = $description;
		$this->price = $price;
	}
}

$params = array('uri' => 'http://localhost/soap/server.php');

$soapServer = new SoapServer(null, $params);

$soapServer->setClass('server');

$soapServer->handle();

As you see above the best approach to use is to add a class with all the apis that we need, in the example above we have two apis  getAllProducts() and getProduct(), remember the api names as we will use them when creating the client.

After that we initialized the soap server using php SoapServer() class passing two arguments, the first is null in that case and the other parameter is an array of options with a uri item that pointing to the server.php file.

PHP SoapServer() and SoapClient() have two cases, the first case is to provide one parameter only which is a url to .wsdl file. working with .wsdl files is another topic so we will stick with the second case which is to pass null as the first parameters and a second parameter for an options array.

Then we call $soapServer->setClass(‘server’), this we take our class and maps all the functions in it to an xml document entries corresponding to each function. Finally the most important call $soapServer->handle() to turn on the soap server.

 

client.php

<?php

class client
{

	private $soap_instance;

	public function __construct()
	{
		$params = array('location' => 'http://localhost/soap/server.php', 'uri' => 'urn://localhost/soap/server.php', 'trace' => 1);

		$this->soap_instance = new SoapClient(null, $params);
	}


	public function getAll()
	{
		try {
			
			return $this->soap_instance->getAllProducts();

		} catch (Exception $ex) {
			exit("soap error: " . $ex->getMessage());
		}
	}


	public function getById($params)
	{
		try {

			return $this->soap_instance->getProduct($params);

		} catch (Exception $ex) {
			exit("soap error: " . $ex->getMessage());
		}
	}
}

$client = new client();

We initialized the soap client in the constructor above passing the same options as the soap server using php SoapClient() class, the second parameter must be an array of options with a uri and location, the third parameter trace=1 will display any error messages if they occur.

Then we created two functions getAll() and getById() and those functions call the soap server getAllProducts() and getProduct() respectively using this syntax:

$this->soap_instance->function_name();

You can also call a soap server function using another way with __soapCall(‘func name’, parameters) like this:

$products = $this->soap_instance->__soapCall('getAllProducts', []);


$product = $this->soap_instance->__soapCall('getProduct', $params);

Now let’s move to calling the apis this will be achieved in the api.php file:

 

api.php

<?php

require_once('./client.php');

if(!isset($_REQUEST['op'])) {
	die("opertion name required");
}

if(!method_exists($client, $_REQUEST['op']) or !in_array($_REQUEST['op'], ['getAll', 'getById'])) {
	die("invalid operation name");
}

switch ($_REQUEST['op']) {
	case 'getAll':

		$products = $client->getAll();

		echo "<pre>";
		print_r($products);
		echo "</pre>";
		break;

	case 'getById': 

		if(!isset($_REQUEST['id'])) {
			die("id parameter required");
		}

		$product = $client->getById(['id' => $_REQUEST['id']]);

		echo "<pre>";
		print_r($product);
		echo "</pre>";
		break;
	
	default:
		
		break;
}

As you see in the code above i have made some simple checks to check for the “op” parameter, this represent the method name in the client class that we need to call, in that case we have two methods only which is getAll() and getById(). Then we called the apis using a switch statement using simple OOP call $client->getAll() or $client->getById([‘id’ => $id]).

 

Now navigate to http://localhost/soap/api.php?op=getAll, and http://localhost/soap/api.php?op=getById&id=1 you will see the results of both apis.

 

When the client calls any of the functions in the server the client sends an xml request and receives xml response. The request and response similar to the xml structure described above.  To output the request or the response you may use any of the functions below:

echo "<pre>";

echo htmlentities($this->soap_instance->__getLastRequest());

echo "</pre>";

echo "<pre>";

echo htmlentities($this->soap_instance->__getLastResponse());

echo "</pre>";

 

Nice isn’t it, in a real world application you don’t need to create the soap server in fact you will only create the client and use a soap server for an existing api like this apis Enrico 2 Holidays Web Service. I created a simple example for one of this apis below:

<?php
try {

    if(!isset($_REQUEST['year'])) {
        die("year required");
    }

   $soapClient = new SoapClient('http://kayaposoft.com/enrico/ws/v2.0/index.php?wsdl');

   // get public holidays for 'usa' region 'al'
   $results = $soapClient->getHolidaysForYear($_REQUEST['year'], "usa", "al", "all");

    echo "<pre>";
    foreach ($results->holiday as $result) {
       echo "<strong>" . $result->name->text . "</strong>: " . $result->holidayType . "(" . $result->date->day . '/' . $result->date->month . '/' . $result->date->year . ")" . "<br/>";
    }
    echo "</pre>";

} catch(Exception $e) {
    die($e->getMessage());
}

Using Soap in Laravel

To use soap with laravel you can use the native php Soap classes or you can use a package dedicated for this, one of those packages is artisanweb/laravel-soap. First to install it using composer:

Installation for Laravel 5.2 and above:

composer require artisaninweb/laravel-soap

Add the service provider in app/config/app.php.

Artisaninweb\SoapWrapper\ServiceProvider::class, 

To use the alias, add this to the aliases in app/config/app.php.

'SoapWrapper' => Artisaninweb\SoapWrapper\Facade\SoapWrapper::class,  

Installation for Laravel 5.1 and below :

Add artisaninweb/laravel-soap as requirement to composer.json

{
    "require": {
        "artisaninweb/laravel-soap": "0.3.*"
    }
}

To use it create a new controller and call the soap wrapper, i have rewritten the previous example using like this:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Artisaninweb\SoapWrapper\SoapWrapper;
class HomeController extends Controller
{
    protected $soapWrapper;

    public function __construct(SoapWrapper $soapWrapper)
    {
        $this->soapWrapper = $soapWrapper;
    }
    public function index()
    {
        if(!request('year')) {
            die("year required");
        }

        $this->soapWrapper->add('Holidays', function ($service) {
            $service
                ->wsdl('http://kayaposoft.com/enrico/ws/v2.0/index.php?wsdl')
                ->trace(true);
        });
        $results = $this->soapWrapper->call('Holidays.getHolidaysForYear', [[
            'year' => request('year')
        ]]);

        echo "<pre>";
        foreach ($results->holiday as $result) {
            echo "<strong>" . $result->name->text . "</strong>: " . $result->holidayType . "(" . $result->date->day . '/' . $result->date->month . '/' . $result->date->year . ")" . "<br/>";
        }
        echo "</pre>";
    }
}

I have injected the SoapWrapper into the constructor, then to call a soap server, this must be done in two steps,

The first step is to add a service like this:

$this->soapWrapper->add('Holidays', function ($service) {
            $service
                ->wsdl('http://kayaposoft.com/enrico/ws/v2.0/index.php?wsdl')
                ->trace(true);
        });

Here i added a service called “Holidays” this can be any name and this represents a namespace for all the services that you will call, next inside the callback i provided the wsdl url and specified the tracing to be true. If you don’t want to use wsdl url you can use the location uri using ->location(url) function.

The second step is to call a service:

$data = $this->soapWrapper->call('Holidays.GetHolidaysForYear', [[
	  'year' => request('year')
]]);

Here i have called the service “GetHolidaysForYear“, this is the same service name as the previous example.

Notice that i have added the namespace name then a dot then the service name like this “Holidays.GetHolidaysForYear“. If you omit the namespace name you will get an error.

routes/web.php

Route::get('/', 'HomeController@index');

Now navigate to http://project url/public?year=2020 you will see the same results in the previous example.

 

4.4 9 votes
Article Rating

What's your reaction?

Excited
10
Happy
8
Not Sure
13
Confused
25

You may also like

Subscribe
Notify of
guest

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Fadi Ramzi
Fadi Ramzi
3 years ago

Could you share wsdl file to see the body data and response.

Andres Rubio
Andres Rubio
3 years ago

hi, i am using this example, but response is:

Cannot process the message because the content type ‘text/xml; charset=utf-8’ was not the expected type ‘application/soap+xml; charset=utf-8’.

please can you help me!!!