├── .gitignore ├── .travis.yml ├── DocumentSoapObjectWrapper.php ├── IWebServiceProvider.php ├── LICENSE.md ├── README.md ├── SoapObjectWrapper.php ├── WebService.php ├── WebServiceAction.php ├── WsdlGenerator.php ├── codeception.yml ├── composer.json └── tests ├── _data └── .gitkeep ├── _output └── .gitignore ├── _support ├── AcceptanceTester.php ├── FunctionalTester.php ├── Helper │ ├── Acceptance.php │ ├── Functional.php │ └── Unit.php ├── UnitTester.php └── _generated │ └── .gitignore ├── acceptance.suite.yml ├── acceptance ├── SoapCest.php └── _bootstrap.php └── app ├── config └── test-config.php ├── controllers └── ApiController.php └── models └── SoapModel.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | /.idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.6' 4 | - '7.0' 5 | - '7.1' 6 | - '7.2' 7 | 8 | install: 9 | - composer install --no-interaction 10 | 11 | script: 12 | - vendor/bin/phpcs --standard=vendor/yiisoft/yii2-coding-standards/Yii2 --extensions=php --ignore=vendor,tests . 13 | - vendor/bin/codecept run -------------------------------------------------------------------------------- /DocumentSoapObjectWrapper.php: -------------------------------------------------------------------------------- 1 | 16 | * @package system.web.services 17 | */ 18 | class DocumentSoapObjectWrapper extends Component 19 | { 20 | /** 21 | * @var object the service provider 22 | */ 23 | public $object = null; 24 | 25 | /** 26 | * Constructor. 27 | * @param object $object the service provider 28 | * @param array $config 29 | */ 30 | public function __construct($object, $config = []) 31 | { 32 | $this->object = $object; 33 | parent::__construct($config); 34 | } 35 | 36 | /** 37 | * PHP __call magic method. 38 | * This method calls the service provider to execute the actual logic. 39 | * @param string $name method name 40 | * @param array $arguments method arguments 41 | * @return mixed method return value 42 | */ 43 | public function __call($name, $arguments) 44 | { 45 | if (is_array($arguments) && isset($arguments[0])) { 46 | $result = call_user_func_array([$this->object, $name], (array)$arguments[0]); 47 | } else { 48 | $result = call_user_func_array([$this->object, $name], $arguments); 49 | } 50 | return $result === null ? $result : [$name . 'Result' => $result]; 51 | } 52 | 53 | /** 54 | * Returns the fully qualified name of this class. 55 | * @return string the fully qualified name of this class. 56 | */ 57 | public static function className() 58 | { 59 | return get_called_class(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /IWebServiceProvider.php: -------------------------------------------------------------------------------- 1 | 15 | * @package system.base 16 | * @since 1.0 17 | */ 18 | interface IWebServiceProvider 19 | { 20 | /** 21 | * This method is invoked before the requested remote method is invoked. 22 | * @param WebService $service the currently requested Web service. 23 | * @return boolean whether the remote method should be executed. 24 | */ 25 | public function beforeWebMethod($service); 26 | 27 | /** 28 | * This method is invoked after the requested remote method is invoked. 29 | * @param WebService $service the currently requested Web service. 30 | */ 31 | public function afterWebMethod($service); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Andrey Borodulin 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of test nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Web Service for Yii2 framework 2 | ================= 3 | [![Build Status](https://travis-ci.org/borodulin/yii2-services.svg?branch=master)](https://travis-ci.org/borodulin/yii2-services) 4 | 5 | ## Description 6 | 7 | WebService encapsulates SoapServer and provides a WSDL-based web service. 8 | 9 | Adaptation of Yii1 Web Services 10 | 11 | ## Installation 12 | 13 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 14 | 15 | To install, either run 16 | 17 | ``` 18 | $ php composer.phar require conquer/services "*" 19 | ``` 20 | or add 21 | 22 | ``` 23 | "conquer/services": "*" 24 | ``` 25 | 26 | to the ```require``` section of your `composer.json` file. 27 | 28 | ## Usage 29 | 30 | ```php 31 | namespace app\controllers; 32 | 33 | class SiteController extends \yii\web\Controller 34 | { 35 | public function actions() 36 | { 37 | return [ 38 | 'soap' => [ 39 | 'class' => 'conquer\services\WebServiceAction', 40 | 'classMap' => [ 41 | 'MyClass' => 'app\controllers\MyClass' 42 | ], 43 | ], 44 | ]; 45 | } 46 | /** 47 | * @param app\controllers\MyClass $myClass 48 | * @return string 49 | * @soap 50 | */ 51 | public function soapTest($myClass) 52 | { 53 | return get_class($myClass); 54 | } 55 | } 56 | 57 | class MyClass 58 | { 59 | /** 60 | * @var string 61 | * @soap 62 | */ 63 | public $name; 64 | } 65 | ``` 66 | 67 | ## License 68 | 69 | **conquer/services** is released under the BSD License. See the bundled `LICENSE.md` for details. 70 | -------------------------------------------------------------------------------- /SoapObjectWrapper.php: -------------------------------------------------------------------------------- 1 | 13 | * @package system.web.services 14 | */ 15 | class SoapObjectWrapper 16 | { 17 | /** 18 | * @var object the service provider 19 | */ 20 | public $object = null; 21 | 22 | /** 23 | * Constructor. 24 | * @param object $object the service provider 25 | */ 26 | public function __construct($object) 27 | { 28 | $this->object = $object; 29 | } 30 | 31 | /** 32 | * PHP __call magic method. 33 | * This method calls the service provider to execute the actual logic. 34 | * @param string $name method name 35 | * @param array $arguments method arguments 36 | * @return mixed method return value 37 | */ 38 | public function __call($name, $arguments) 39 | { 40 | return call_user_func_array([$this->object, $name], $arguments); 41 | } 42 | 43 | /** 44 | * Returns the fully qualified name of this class. 45 | * @return string the fully qualified name of this class. 46 | */ 47 | public static function className() 48 | { 49 | return get_called_class(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /WebService.php: -------------------------------------------------------------------------------- 1 | 31 | * @package system.web.services 32 | * @since 1.0 33 | */ 34 | class WebService extends Component 35 | { 36 | const SOAP_ERROR = 1001; 37 | /** 38 | * @var string|object the web service provider class or object. 39 | * If specified as a class name, it can be a path alias. 40 | */ 41 | public $provider; 42 | /** 43 | * @var string the URL for WSDL. This is required by {@link run()}. 44 | */ 45 | public $wsdlUrl; 46 | /** 47 | * @var string the URL for the Web service. This is required by {@link generateWsdl()} and {@link renderWsdl()}. 48 | */ 49 | public $serviceUrl; 50 | /** 51 | * @var integer number of seconds that the generated WSDL can remain valid in cache. Defaults to 0, meaning no caching. 52 | */ 53 | public $wsdlCacheDuration = 0; 54 | /** 55 | * @var string the ID of the cache application component that is used to cache the generated WSDL. 56 | * Defaults to 'cache' which refers to the primary cache application component. 57 | * Set this property to false if you want to disable caching WSDL. 58 | */ 59 | public $cacheID = 'cache'; 60 | /** 61 | * @var string encoding of the Web service. Defaults to 'UTF-8'. 62 | */ 63 | public $encoding = 'UTF-8'; 64 | /** 65 | * A list of classes that are declared as complex types in WSDL. 66 | * This should be an array with WSDL types as keys and names of PHP classes as values. 67 | * A PHP class can also be specified as a path alias. 68 | * @var array 69 | * @see http://www.php.net/manual/en/soapserver.soapserver.php 70 | */ 71 | public $classMap = []; 72 | /** 73 | * @var string actor of the SOAP service. Defaults to null, meaning not set. 74 | */ 75 | public $actor; 76 | /** 77 | * @var string SOAP version (e.g. '1.1' or '1.2'). Defaults to null, meaning not set. 78 | */ 79 | public $soapVersion; 80 | /** 81 | * @var integer the persistence mode of the SOAP server. 82 | * @see http://www.php.net/manual/en/soapserver.setpersistence.php 83 | */ 84 | public $persistence; 85 | /** 86 | * WSDL generator configuration. This property may be useful in purpose of enhancing features 87 | * of the standard {@link WsdlGenerator} class by extending it. For example, some developers may need support 88 | * of the xsd:xsd:base64Binary elements. Another use case is to change initial values 89 | * at instantiation of the default {@link WsdlGenerator}. The value of this property will be passed 90 | * to {@link \Yii::createObject()} to create the generator object. Default value is 'WsdlGenerator'. 91 | * @var string|array 92 | * @since 1.1.12 93 | */ 94 | public $generatorConfig; 95 | 96 | private $_method; 97 | 98 | 99 | /** 100 | * Constructor. 101 | * @param mixed $provider the web service provider class name or object 102 | * @param string $wsdlUrl the URL for WSDL. This is required by {@link run()}. 103 | * @param string $serviceUrl the URL for the Web service. This is required by {@link generateWsdl()} and {@link renderWsdl()}. 104 | * @param array $config 105 | */ 106 | public function __construct($provider, $wsdlUrl, $serviceUrl, $config = []) 107 | { 108 | $this->provider = $provider; 109 | $this->wsdlUrl = $wsdlUrl; 110 | $this->serviceUrl = $serviceUrl; 111 | $this->generatorConfig = WsdlGenerator::className(); 112 | parent::__construct($config); 113 | } 114 | 115 | /** 116 | * The PHP error handler. 117 | * @param Event $event the PHP error event 118 | * @throws \Exception 119 | */ 120 | public function handleError($event) 121 | { 122 | $event->handled = true; 123 | $message = $event->message; 124 | if (YII_DEBUG) { 125 | $trace = debug_backtrace(); 126 | if (isset($trace[2]) && isset($trace[2]['file']) && isset($trace[2]['line'])) { 127 | $message .= ' (' . $trace[2]['file'] . ':' . $trace[2]['line'] . ')'; 128 | } 129 | } 130 | throw new \Exception($message, self::SOAP_ERROR); 131 | } 132 | 133 | /** 134 | * Generates and displays the WSDL as defined by the provider. 135 | * @see generateWsdl 136 | * @throws \yii\base\InvalidConfigException 137 | */ 138 | public function renderWsdl() 139 | { 140 | $wsdl = $this->generateWsdl(); 141 | $response = Yii::$app->response; 142 | $response->charset = $this->encoding; 143 | $response->format = Response::FORMAT_RAW; 144 | $response->headers->add('Content-Type', 'text/xml'); 145 | // header('Content-Length: '.(function_exists('mb_strlen') ? mb_strlen($wsdl,'8bit') : strlen($wsdl))); 146 | return $wsdl; 147 | } 148 | 149 | /** 150 | * Generates the WSDL as defined by the provider. 151 | * The cached version may be used if the WSDL is found valid in cache. 152 | * @return string the generated WSDL 153 | * @throws \yii\base\InvalidConfigException 154 | * @see wsdlCacheDuration 155 | */ 156 | public function generateWsdl() 157 | { 158 | if (is_object($this->provider)) { 159 | $providerClass = get_class($this->provider); 160 | } else { 161 | $providerClass = $this->provider; 162 | } 163 | if ($this->wsdlCacheDuration > 0 && $this->cacheID !== false && ($cache = Yii::$app->get($this->cacheID, false)) !== null) { 164 | $key = 'Yii.WebService.' . $providerClass . $this->serviceUrl . $this->encoding; 165 | if (($wsdl = $cache->get($key)) !== false) { 166 | return $wsdl; 167 | } 168 | } 169 | $generator = Yii::createObject($this->generatorConfig); 170 | $wsdl = $generator->generateWsdl($providerClass, $this->serviceUrl, $this->encoding); 171 | if (isset($key, $cache)) { 172 | $cache->set($key, $wsdl, $this->wsdlCacheDuration); 173 | } 174 | return $wsdl; 175 | } 176 | 177 | /** 178 | * Handles the web service request. 179 | * @throws \ReflectionException 180 | */ 181 | public function run() 182 | { 183 | $response = Yii::$app->response; 184 | $response->format = Response::FORMAT_RAW; 185 | $response->charset = $this->encoding; 186 | $response->headers->add('Content-Type', 'text/xml'); 187 | if (YII_DEBUG) { 188 | ini_set('soap.wsdl_cache_enabled', 0); 189 | } 190 | $server = new \SoapServer($this->wsdlUrl, $this->getOptions()); 191 | // \Yii::$app->on($name, $behavior)EventHandler('onError',array($this,'handleError')); 192 | try { 193 | if ($this->persistence !== null) { 194 | $server->setPersistence($this->persistence); 195 | } 196 | if (is_string($this->provider)) { 197 | $provider = Yii::createObject($this->provider); 198 | } else { 199 | $provider = $this->provider; 200 | } 201 | if (method_exists($server, 'setObject')) { 202 | if (is_array($this->generatorConfig) && isset($this->generatorConfig['bindingStyle']) 203 | && $this->generatorConfig['bindingStyle'] === 'document' 204 | ) { 205 | $server->setObject(new DocumentSoapObjectWrapper($provider)); 206 | } else { 207 | $server->setObject($provider); 208 | } 209 | } else { 210 | if (is_array($this->generatorConfig) && isset($this->generatorConfig['bindingStyle']) 211 | && $this->generatorConfig['bindingStyle'] === 'document' 212 | ) { 213 | $server->setClass(DocumentSoapObjectWrapper::className(), $provider); 214 | } else { 215 | $server->setClass(SoapObjectWrapper::className(), $provider); 216 | } 217 | } 218 | 219 | if ($provider instanceof IWebServiceProvider) { 220 | if ($provider->beforeWebMethod($this)) { 221 | $server->handle(); 222 | $provider->afterWebMethod($this); 223 | } 224 | } else { 225 | $server->handle(); 226 | } 227 | } catch (\Exception $e) { 228 | // non-PHP error 229 | if ($e->getCode() !== self::SOAP_ERROR) { 230 | // only log for non-PHP-error case because application's error handler already logs it 231 | // php <5.2 doesn't support string conversion auto-magically 232 | Yii::error($e->__toString()); 233 | } 234 | 235 | $message = $e->getMessage(); 236 | if (YII_DEBUG) { 237 | $message .= ' (' . $e->getFile() . ':' . $e->getLine() . ")\n" . $e->getTraceAsString(); 238 | } 239 | // We need to end application explicitly because of 240 | // http://bugs.php.net/bug.php?id=49513 241 | Yii::$app->state = Application::STATE_AFTER_REQUEST; 242 | Yii::$app->trigger(Application::EVENT_AFTER_REQUEST); 243 | $reflect = new \ReflectionClass($e); 244 | $server->fault($reflect->getShortName(), $message); 245 | exit(1); 246 | } 247 | } 248 | 249 | /** 250 | * @return string the currently requested method name. Empty if no method is being requested. 251 | */ 252 | public function getMethodName() 253 | { 254 | if ($this->_method === null) { 255 | if (isset($HTTP_RAW_POST_DATA)) { 256 | $request = $HTTP_RAW_POST_DATA; 257 | } else { 258 | $request = file_get_contents('php://input'); 259 | } 260 | if (preg_match('/<.*?:Body[^>]*>\s*<.*?:(\w+)/mi', $request, $matches)) { 261 | $this->_method = $matches[1]; 262 | } else { 263 | $this->_method = ''; 264 | } 265 | } 266 | return $this->_method; 267 | } 268 | 269 | /** 270 | * @return array options for creating SoapServer instance 271 | * @see http://www.php.net/manual/en/soapserver.soapserver.php 272 | */ 273 | protected function getOptions() 274 | { 275 | $options = []; 276 | if ($this->soapVersion === '1.1') { 277 | $options['soap_version'] = SOAP_1_1; 278 | } elseif ($this->soapVersion === '1.2') { 279 | $options['soap_version'] = SOAP_1_2; 280 | } 281 | if ($this->actor !== null) { 282 | $options['actor'] = $this->actor; 283 | } 284 | $options['encoding'] = $this->encoding; 285 | foreach ($this->classMap as $type => $className) { 286 | if (is_int($type)) { 287 | $type = $className; 288 | } 289 | $options['classmap'][$type] = $className; 290 | } 291 | return $options; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /WebServiceAction.php: -------------------------------------------------------------------------------- 1 | ws 19 | * is used to differentiate these two aspects: the existence of the GET parameter 20 | * indicates performing the latter action. 21 | * 22 | * By default, WebServiceAction will use the current controller as 23 | * the Web service provider. See {@link WsdlGenerator} on how to declare 24 | * methods that can be remotely invoked. 25 | * 26 | * Note, PHP SOAP extension is required for this action. 27 | * 28 | * @property WebService $service The Web service instance. 29 | * 30 | * @author Qiang Xue 31 | * @package system.web.services 32 | * @since 1.0 33 | */ 34 | class WebServiceAction extends Action 35 | { 36 | /** 37 | * @var mixed the Web service provider object or class name. 38 | * If specified as a class name, it can be a path alias. 39 | * Defaults to null, meaning the current controller is used as the service provider. 40 | * If the provider implements the interface {@link IWebServiceProvider}, 41 | * it will be able to intercept the remote method invocation and perform 42 | * additional tasks (e.g. authentication, logging). 43 | */ 44 | public $provider; 45 | /** 46 | * @var string the URL for the Web service. Defaults to null, meaning 47 | * the URL for this action is used to provide Web services. 48 | * In this case, a GET parameter named {@link serviceVar} will be used to 49 | * deteremine whether the current request is for WSDL or Web service. 50 | */ 51 | public $serviceUrl; 52 | /** 53 | * @var string the URL for WSDL. Defaults to null, meaning 54 | * the URL for this action is used to serve WSDL document. 55 | */ 56 | public $wsdlUrl; 57 | /** 58 | * @var string the name of the GET parameter that differentiates a WSDL request 59 | * from a Web service request. If this GET parameter exists, the request is considered 60 | * as a Web service request; otherwise, it is a WSDL request. Defaults to 'ws'. 61 | */ 62 | public $serviceVar = 'ws'; 63 | /** 64 | * @var array a list of PHP classes that are declared as complex types in WSDL. 65 | * This should be an array with WSDL types as keys and names of PHP classes as values. 66 | * A PHP class can also be specified as a path alias. 67 | * @see http://www.php.net/manual/en/soapclient.soapclient.php 68 | */ 69 | public $classMap; 70 | /** 71 | * @var array the initial property values for the {@link WebService} object. 72 | * The array keys are property names of {@link WebService} and the array values 73 | * are the corresponding property initial values. 74 | */ 75 | public $serviceOptions = []; 76 | 77 | /** 78 | * @var WebService 79 | */ 80 | private $_service; 81 | 82 | public function init() 83 | { 84 | $this->controller->enableCsrfValidation = false; 85 | } 86 | 87 | 88 | /** 89 | * Runs the action. 90 | * If the GET parameter {@link serviceVar} exists, the action handle the remote method invocation. 91 | * If not, the action will serve WSDL content; 92 | * @throws \ReflectionException 93 | * @throws \yii\base\InvalidConfigException 94 | */ 95 | public function run() 96 | { 97 | $hostInfo = Yii::$app->getRequest()->getHostInfo(); 98 | $controller = $this->controller; 99 | if (($serviceUrl = $this->serviceUrl) === null) { 100 | $serviceUrl = $hostInfo . Url::toRoute([$this->getUniqueId(), $this->serviceVar => 1]); 101 | } 102 | if (($wsdlUrl = $this->wsdlUrl) === null) { 103 | $wsdlUrl = $hostInfo . Url::toRoute([$this->getUniqueId()]); 104 | } 105 | if (($provider = $this->provider) === null) { 106 | $provider = $controller; 107 | } 108 | $this->_service = $this->createWebService($provider, $wsdlUrl, $serviceUrl); 109 | 110 | if (is_array($this->classMap)) { 111 | $this->_service->classMap = $this->classMap; 112 | } 113 | foreach ($this->serviceOptions as $name => $value) { 114 | $this->_service->$name = $value; 115 | } 116 | if (isset($_GET[$this->serviceVar])) { 117 | $this->_service->run(); 118 | } else { 119 | return $this->_service->renderWsdl(); 120 | } 121 | } 122 | 123 | /** 124 | * Returns the Web service instance currently being used. 125 | * @return WebService the Web service instance 126 | */ 127 | public function getService() 128 | { 129 | return $this->_service; 130 | } 131 | 132 | /** 133 | * Creates a {@link WebService} instance. 134 | * You may override this method to customize the created instance. 135 | * @param mixed $provider the web service provider class name or object 136 | * @param string $wsdlUrl the URL for WSDL. 137 | * @param string $serviceUrl the URL for the Web service. 138 | * @return WebService the Web service instance 139 | */ 140 | protected function createWebService($provider, $wsdlUrl, $serviceUrl) 141 | { 142 | return new WebService($provider, $wsdlUrl, $serviceUrl); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /WsdlGenerator.php: -------------------------------------------------------------------------------- 1 | 27 | *
  • str/string: maps to xsd:string;
  • 28 | *
  • int/integer: maps to xsd:int;
  • 29 | *
  • float/double: maps to xsd:float;
  • 30 | *
  • bool/boolean: maps to xsd:boolean;
  • 31 | *
  • date: maps to xsd:date;
  • 32 | *
  • time: maps to xsd:time;
  • 33 | *
  • datetime: maps to xsd:dateTime;
  • 34 | *
  • array: maps to xsd:string;
  • 35 | *
  • object: maps to xsd:struct;
  • 36 | *
  • mixed: maps to xsd:anyType.
  • 37 | * 38 | * 39 | * If a type is not a primitive type, it is considered as a class type, and 40 | * WsdlGenerator will look for its property declarations. Only public properties 41 | * are considered, and they each must be associated with a doc comment block containg 42 | * the '@soap' tag. The doc comment block should declare the type of the property. 43 | * 44 | * WsdlGenerator recognizes the array type with the following format: 45 | *
     46 |  * typeName[]: maps to tns:typeNameArray
     47 |  * 
    48 | * 49 | * The following is an example declaring a remote invokable method: 50 | *
     51 |  * / **
     52 |  *   * A foo method.
     53 |  *   * @param string name of something
     54 |  *   * @param string value of something
     55 |  *   * @return string[] some array
     56 |  *   * @soap
     57 |  *   * /
     58 |  * public function foo($name,$value) {...}
     59 |  * 
    60 | * 61 | * And the following is an example declaring a class with remote accessible properties: 62 | *
     63 |  * class Foo {
     64 |  *     / **
     65 |  *       * @var string name of foo {nillable=1, minOccurs=0, maxOccurs=2}
     66 |  *       * @soap
     67 |  *       * /
     68 |  *     public $name;
     69 |  *     / **
     70 |  *       * @var Member[] members of foo
     71 |  *       * @soap
     72 |  *       * /
     73 |  *     public $members;
     74 |  * }
     75 |  * 
    76 | * In the above, the 'members' property is an array of 'Member' objects. Since 'Member' is not 77 | * a primitive type, WsdlGenerator will look further to find the definition of 'Member'. 78 | * 79 | * Optionally, extra attributes (nillable, minOccurs, maxOccurs) can be defined for each 80 | * property by enclosing definitions into curly brackets and separated by comma like so: 81 | * 82 | * {[attribute1 = value1][, attribute2 = value2], ...} 83 | * 84 | * where the attribute can be one of following: 85 | * 90 | * 91 | * Additionally, each complex data type can have assigned a soap indicator flag declaring special usage for such a data type. 92 | * A soap indicator must be declared in the doc comment block with the '@soap-indicator' tag. 93 | * Following soap indicators are currently supported: 94 | * 99 | * The Group indicators can be also injected via custom soap definitions as XML node into WSDL structure. 100 | * 101 | * In the following example, class Foo will create a XML node <xsd:Foo><xsd:sequence> ... </xsd:sequence></xsd:Foo> with children attributes expected in pre-defined order. 102 | *
    103 |  * / *
    104 |  *   * @soap-indicator sequence
    105 |  *   * /
    106 |  * class Foo {
    107 |  *     ...
    108 |  * }
    109 |  * 
    110 | * For more on soap indicators, see See {@link http://www.w3schools.com/schema/schema_complex_indicators.asp}. 111 | * 112 | * Since the variability of WSDL definitions is virtually unlimited, a special doc comment tag '@soap-wsdl' can be used in order to inject any custom XML string into generated WSDL file. 113 | * If such a block of the code is found in class's comment block, then it will be used instead of parsing and generating standard attributes within the class. 114 | * This gives virtually unlimited flexibility in defining data structures of any complexity. 115 | * Following is an example of defining custom piece of WSDL XML node: 116 | *
    117 |  * / *
    118 |  *   * @soap-wsdl 
    119 |  *   * @soap-wsdl     
    120 |  *   * @soap-wsdl     
    121 |  *   * @soap-wsdl         
    122 |  *   * @soap-wsdl         
    123 |  *   * @soap-wsdl     
    124 |  *   * @soap-wsdl 
    125 |  *   * /
    126 |  * class User {
    127 |  *     / **
    128 |  *       * @var string User name {minOccurs=1, maxOccurs=1}
    129 |  *       * @soap
    130 |  *       * /
    131 |  *     public $name;
    132 |  *     / **
    133 |  *       * @var integer User age {nillable=0, minOccurs=1, maxOccurs=1}
    134 |  *       * @example 35
    135 |  *       * @soap
    136 |  *       * /
    137 |  *     public $age;
    138 |  *     / **
    139 |  *       * @var date User's birthday {nillable=0, minOccurs=1, maxOccurs=1}
    140 |  *       * @example 1980-05-27
    141 |  *       * @soap
    142 |  *       * /
    143 |  *     public $date_of_birth;
    144 |  * }
    145 |  * 
    146 | * In the example above, WSDL generator would inject under XML node <xsd:User> the code block defined by @soap-wsdl lines. 147 | * 148 | * By inserting into SOAP URL link the parameter "?makedoc", WSDL generator will output human-friendly overview of all complex data types rather than XML WSDL file. 149 | * Each complex type is described in a separate HTML table and recognizes also the '@example' PHPDoc tag. See {@link buildHtmlDocs()}. 150 | * 151 | * @author Qiang Xue 152 | * @package system.web.services 153 | * @since 1.0 154 | */ 155 | class WsdlGenerator extends Component 156 | { 157 | const STYLE_RPC = 'rpc'; 158 | const STYLE_DOCUMENT = 'document'; 159 | const USE_ENCODED = 'encoded'; 160 | const USE_LITERAL = 'literal'; 161 | /** 162 | * @var string the namespace to be used in the generated WSDL. 163 | * If not set, it defaults to the name of the class that WSDL is generated upon. 164 | */ 165 | public $namespace; 166 | /** 167 | * @var string the name of the generated WSDL. 168 | * If not set, it defaults to "urn:{$className}wsdl". 169 | */ 170 | public $serviceName; 171 | /** 172 | * @var array 173 | * soap:body operation style options 174 | */ 175 | public $operationBodyStyle = [ 176 | 'use' => self::USE_ENCODED, 177 | 'encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/', 178 | ]; 179 | /** 180 | * @var array 181 | * soap:operation style 182 | */ 183 | public $bindingStyle = self::STYLE_RPC; 184 | /** 185 | * @var string 186 | * soap:operation transport 187 | */ 188 | public $bindingTransport = 'http://schemas.xmlsoap.org/soap/http'; 189 | 190 | protected static $typeMap = [ 191 | 'string' => 'xsd:string', 192 | 'str' => 'xsd:string', 193 | 'int' => 'xsd:int', 194 | 'integer' => 'xsd:integer', 195 | 'float' => 'xsd:float', 196 | 'double' => 'xsd:float', 197 | 'bool' => 'xsd:boolean', 198 | 'boolean' => 'xsd:boolean', 199 | 'date' => 'xsd:date', 200 | 'time' => 'xsd:time', 201 | 'datetime' => 'xsd:dateTime', 202 | 'array' => 'soap-enc:Array', 203 | 'object' => 'xsd:struct', 204 | 'mixed' => 'xsd:anyType', 205 | ]; 206 | 207 | /** 208 | * @var array List of recognized SOAP operations that will become remotely available. 209 | * All methods with declared @soap parameter will be included here in the format operation1 => description1, operation2 => description2, .. 210 | */ 211 | protected $operations; 212 | 213 | /** 214 | * @var array List of complex types used by operations. 215 | * If an SOAP operation defines complex input or output type, all objects are included here containing all sub-parameters. 216 | * For instance, if an SOAP operation "createUser" requires complex input object "User", then the object "User" will be included here with declared subparameters such as "firstname", "lastname", etc.. 217 | */ 218 | protected $types; 219 | 220 | /** 221 | * @var array 222 | */ 223 | protected $elements; 224 | 225 | /** 226 | * @var array Map of request and response types for all operations. 227 | */ 228 | protected $messages; 229 | 230 | /** 231 | * Generates the WSDL for the given class. 232 | * @param string $className class name 233 | * @param string $serviceUrl Web service URL 234 | * @param string $encoding encoding of the WSDL. Defaults to 'UTF-8'. 235 | * @return string the generated WSDL 236 | * @throws \ReflectionException 237 | * @throws \yii\base\ExitException 238 | */ 239 | public function generateWsdl($className, $serviceUrl, $encoding = 'UTF-8') 240 | { 241 | $this->operations = []; 242 | $this->types = []; 243 | $this->elements = []; 244 | $this->messages = []; 245 | 246 | $reflection = new \ReflectionClass($className); 247 | 248 | if ($this->serviceName === null) { 249 | $this->serviceName = $reflection->getShortName(); 250 | } 251 | if ($this->namespace === null) { 252 | $this->namespace = 'urn:' . str_replace('\\', '/', $className) . 'wsdl'; 253 | } 254 | foreach ($reflection->getMethods() as $method) { 255 | if ($method->isPublic()) { 256 | $this->processMethod($method); 257 | } 258 | } 259 | 260 | $wsdl = $this->buildDOM($serviceUrl, $encoding)->saveXML(); 261 | 262 | if (isset($_GET['makedoc'])) { 263 | $this->buildHtmlDocs(); 264 | } 265 | return $wsdl; 266 | } 267 | 268 | /** 269 | * @param \ReflectionMethod $method method 270 | * @throws \ReflectionException 271 | */ 272 | protected function processMethod($method) 273 | { 274 | $comment = $method->getDocComment(); 275 | if (strpos($comment, '@soap') === false) { 276 | return; 277 | } 278 | $comment = strtr($comment, ["\r\n" => "\n", "\r" => "\n"]); // make line endings consistent: win -> unix, mac -> unix 279 | 280 | $methodName = $method->getName(); 281 | $comment = preg_replace('/^\s*\**(\s*?$|\s*)/m', '', $comment); 282 | $params = $method->getParameters(); 283 | $message = []; 284 | $headers = []; 285 | $n = preg_match_all('/^@param\s+([\w\.\\\]+(\[\s*\])?)\s*?(.*)$/im', $comment, $matches); 286 | if ($n > count($params)) { 287 | $n = count($params); 288 | } 289 | if ($this->bindingStyle == self::STYLE_RPC) { 290 | for ($i = 0; $i < $n; ++$i) { 291 | $message[$params[$i]->getName()] = [ 292 | 'type' => $this->processType($matches[1][$i]), 293 | 'doc' => trim($matches[3][$i]), 294 | ]; 295 | } 296 | } else { 297 | $this->elements[$methodName] = []; 298 | for ($i = 0; $i < $n; ++$i) { 299 | $this->elements[$methodName][$params[$i]->getName()] = [ 300 | 'type' => $this->processType($matches[1][$i]), 301 | 'nillable' => $params[$i]->isOptional(), 302 | ]; 303 | } 304 | $message['parameters'] = ['element' => 'tns:' . $methodName]; 305 | } 306 | 307 | $this->messages[$methodName . 'In'] = $message; 308 | 309 | $n = preg_match_all('/^@header\s+([\w\.\\\]+(\[\s*\])?)\s*?(.*)$/im', $comment, $matches); 310 | for ($i = 0; $i < $n; ++$i) { 311 | $name = $matches[1][$i]; 312 | $type = $this->processType($matches[1][$i]); 313 | $doc = trim($matches[3][$i]); 314 | if ($this->bindingStyle == self::STYLE_RPC) { 315 | $headers[$name] = [$type, $doc]; 316 | } else { 317 | $this->elements[$name][$name] = ['type' => $type]; 318 | $headers[$name] = ['element' => $type]; 319 | } 320 | } 321 | 322 | if ($headers !== []) { 323 | $this->messages[$methodName . 'Headers'] = $headers; 324 | $headerKeys = array_keys($headers); 325 | $firstHeaderKey = reset($headerKeys); 326 | $firstHeader = $headers[$firstHeaderKey]; 327 | } else { 328 | $firstHeader = null; 329 | } 330 | 331 | if ($this->bindingStyle == self::STYLE_RPC) { 332 | if (preg_match('/^@return\s+([\w\.\\\]+(\[\s*\])?)\s*?(.*)$/im', $comment, $matches)) { 333 | $return = [ 334 | 'type' => $this->processType($matches[1]), 335 | 'doc' => trim($matches[2]), 336 | ]; 337 | } else { 338 | $return = null; 339 | } 340 | $this->messages[$methodName . 'Out'] = ['return' => $return]; 341 | } else { 342 | if (preg_match('/^@return\s+([\w\.\\\]+(\[\s*\])?)\s*?(.*)$/im', $comment, $matches)) { 343 | $this->elements[$methodName . 'Response'][$methodName . 'Result'] = [ 344 | 'type' => $this->processType($matches[1]), 345 | ]; 346 | } 347 | $this->messages[$methodName . 'Out'] = ['parameters' => ['element' => 'tns:' . $methodName . 'Response']]; 348 | } 349 | 350 | if (preg_match('/^\/\*+\s*([^@]*?)\n@/s', $comment, $matches)) { 351 | $doc = trim($matches[1]); 352 | } else { 353 | $doc = ''; 354 | } 355 | $this->operations[$methodName] = [ 356 | 'doc' => $doc, 357 | 'headers' => $firstHeader === null ? null : ['input' => [$methodName . 'Headers', $firstHeaderKey]], 358 | ]; 359 | } 360 | 361 | /** 362 | * @param string $type PHP variable type 363 | * @return mixed|string 364 | * @throws \ReflectionException 365 | */ 366 | protected function processType($type) 367 | { 368 | if (isset(self::$typeMap[$type])) { 369 | return self::$typeMap[$type]; 370 | } elseif (isset($this->types[$type])) { 371 | return is_array($this->types[$type]) ? 'tns:' . $type : $this->types[$type]; 372 | } elseif (($pos = strpos($type, '[]')) !== false) { 373 | // array of types 374 | $type = substr($type, 0, $pos); 375 | if (strpos($type, '\\') !== false) { 376 | $class = new \ReflectionClass($type); 377 | $shortType = $class->getShortName(); 378 | } else { 379 | $shortType = $type; 380 | } 381 | $this->types[$type . '[]'] = 'tns:' . $shortType . 'Array'; 382 | $this->processType($type); 383 | return $this->types[$type . '[]']; 384 | } else { 385 | // process class / complex type 386 | $class = new \ReflectionClass($type); 387 | 388 | $type = $class->getShortName(); 389 | 390 | $comment = $class->getDocComment(); 391 | $comment = strtr($comment, ["\r\n" => "\n", "\r" => "\n"]); // make line endings consistent: win -> unix, mac -> unix 392 | $comment = preg_replace('/^\s*\**(\s*?$|\s*)/m', '', $comment); 393 | 394 | // extract soap indicator flag, if defined, e.g. @soap-indicator sequence 395 | // see http://www.w3schools.com/schema/schema_complex_indicators.asp 396 | if (preg_match('/^@soap-indicator\s+(\w+)\s*?(.*)$/im', $comment, $matches)) { 397 | $indicator = $matches[1]; 398 | $attributes = $this->getWsdlElementAttributes($matches[2]); 399 | } else { 400 | $indicator = 'all'; 401 | $attributes = $this->getWsdlElementAttributes(''); 402 | } 403 | 404 | $custom_wsdl = false; 405 | if (preg_match_all('/^@soap-wsdl\s+(\S.*)$/im', $comment, $matches) > 0) { 406 | $custom_wsdl = implode("\n", $matches[1]); 407 | } 408 | $this->types[$type] = [ 409 | 'indicator' => $indicator, 410 | 'nillable' => $attributes['nillable'], 411 | 'minOccurs' => $attributes['minOccurs'], 412 | 'maxOccurs' => $attributes['maxOccurs'], 413 | 'custom_wsdl' => $custom_wsdl, 414 | 'properties' => [] 415 | ]; 416 | 417 | foreach ($class->getProperties() as $property) { 418 | $comment = $property->getDocComment(); 419 | if ($property->isPublic() && strpos($comment, '@soap') !== false) { 420 | if (preg_match('/@var\s+([\w\.\\\]+(\[\s*\])?)\s*?(.*)$/mi', $comment, $matches)) { 421 | $attributes = $this->getWsdlElementAttributes($matches[3]); 422 | 423 | if (preg_match('/{(.+)}/', $comment, $attr)) { 424 | $matches[3] = str_replace($attr[0], '', $matches[3]); 425 | } 426 | // extract PHPDoc @example 427 | $example = ''; 428 | if (preg_match('/@example[:]?(.+)/mi', $comment, $match)) { 429 | $example = trim($match[1]); 430 | } 431 | $this->types[$type]['properties'][$property->getName()] = [ 432 | $this->processType($matches[1]), 433 | trim($matches[3]), 434 | $attributes['nillable'], 435 | $attributes['minOccurs'], 436 | $attributes['maxOccurs'], 437 | $example 438 | ]; // name => type, doc, nillable, minOccurs, maxOccurs, example 439 | } 440 | } 441 | } 442 | return 'tns:' . $type; 443 | } 444 | } 445 | 446 | /** 447 | * Parse attributes nillable, minOccurs, maxOccurs 448 | * @param string $comment Extracted PHPDoc comment 449 | * @return array 450 | */ 451 | protected function getWsdlElementAttributes($comment) 452 | { 453 | $nillable = $minOccurs = $maxOccurs = null; 454 | if (preg_match('/{(.+)}/', $comment, $attr)) { 455 | if (preg_match_all('/((\w+)\s*=\s*(\w+))/mi', $attr[1], $attr)) { 456 | foreach ($attr[2] as $id => $prop) { 457 | $prop = strtolower($prop); 458 | $val = strtolower($attr[3][$id]); 459 | if ($prop == 'nillable') { 460 | if ($val == 'false' || $val == 'true') { 461 | $nillable = $val; 462 | } else { 463 | $nillable = $val ? 'true' : 'false'; 464 | } 465 | } elseif ($prop == 'minoccurs') { 466 | $minOccurs = intval($val); 467 | } elseif ($prop == 'maxoccurs') { 468 | $maxOccurs = ($val == 'unbounded') ? 'unbounded' : intval($val); 469 | } 470 | } 471 | } 472 | } 473 | return [ 474 | 'nillable' => $nillable, 475 | 'minOccurs' => $minOccurs, 476 | 'maxOccurs' => $maxOccurs 477 | ]; 478 | } 479 | 480 | /** 481 | * Import custom XML source node into WSDL document under specified target node 482 | * @param \DOMDocument $dom XML WSDL document being generated 483 | * @param \DOMElement $target XML node, to which will be appended $source node 484 | * @param \DOMNode $source Source XML node to be imported 485 | */ 486 | protected function injectDom(\DOMDocument $dom, \DOMElement $target, \DOMNode $source) 487 | { 488 | if ($source->nodeType != XML_ELEMENT_NODE) { 489 | return; 490 | } 491 | $import = $dom->createElement($source->nodeName); 492 | 493 | foreach ($source->attributes as $attr) { 494 | $import->setAttribute($attr->name, $attr->value); 495 | } 496 | foreach ($source->childNodes as $child) { 497 | $this->injectDom($dom, $import, $child); 498 | } 499 | $target->appendChild($import); 500 | } 501 | 502 | /** 503 | * @param string $serviceUrl Web service URL 504 | * @param string $encoding encoding of the WSDL. Defaults to 'UTF-8'. 505 | * @return \DOMDocument 506 | */ 507 | protected function buildDOM($serviceUrl, $encoding) 508 | { 509 | $xml = << 511 | 518 | 519 | XML; 520 | 521 | $dom = new \DOMDocument(); 522 | $dom->formatOutput = true; 523 | $dom->loadXML($xml); 524 | $this->addTypes($dom); 525 | 526 | $this->addMessages($dom); 527 | $this->addPortTypes($dom); 528 | $this->addBindings($dom); 529 | $this->addService($dom, $serviceUrl); 530 | 531 | return $dom; 532 | } 533 | 534 | /** 535 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 536 | */ 537 | protected function addTypes($dom) 538 | { 539 | if ($this->types === [] && $this->elements === []) { 540 | return; 541 | } 542 | $types = $dom->createElement('wsdl:types'); 543 | $schema = $dom->createElement('xsd:schema'); 544 | $schema->setAttribute('targetNamespace', $this->namespace); 545 | foreach ($this->types as $phpType => $xmlType) { 546 | if (is_string($xmlType) && strrpos($xmlType, 'Array') !== strlen($xmlType) - 5) { 547 | continue; // simple type 548 | } 549 | $complexType = $dom->createElement('xsd:complexType'); 550 | if (is_string($xmlType)) { 551 | if (($pos = strpos($xmlType, 'tns:')) !== false) { 552 | $complexType->setAttribute('name', substr($xmlType, 4)); 553 | } else { 554 | $complexType->setAttribute('name', $xmlType); 555 | } 556 | $arrayType = ($dppos = strpos($xmlType, ':')) !== false ? substr($xmlType, $dppos + 1) : $xmlType; // strip namespace, if any 557 | $arrayType = substr($arrayType, 0, -5); // strip 'Array' from name 558 | if ($this->operationBodyStyle['use'] == self::USE_ENCODED) { 559 | $complexContent = $dom->createElement('xsd:complexContent'); 560 | $restriction = $dom->createElement('xsd:restriction'); 561 | $restriction->setAttribute('base', 'soap-enc:Array'); 562 | $attribute = $dom->createElement('xsd:attribute'); 563 | $attribute->setAttribute('ref', 'soap-enc:arrayType'); 564 | $attribute->setAttribute('arrayType', (isset(self::$typeMap[$arrayType]) ? 'xsd:' : 'tns:') . $arrayType . '[]'); 565 | 566 | $restriction->appendChild($attribute); 567 | $complexContent->appendChild($restriction); 568 | $complexType->appendChild($complexContent); 569 | } else { 570 | $sequence = $dom->createElement('xsd:sequence'); 571 | $element = $dom->createElement('xsd:element'); 572 | $element->setAttribute('name', 'item'); 573 | $element->setAttribute('type', (isset(self::$typeMap[$arrayType]) ? self::$typeMap[$arrayType] : 'tns:' . $arrayType)); 574 | $element->setAttribute('minOccurs', '0'); 575 | $element->setAttribute('maxOccurs', 'unbounded'); 576 | $sequence->appendChild($element); 577 | $complexType->appendChild($sequence); 578 | } 579 | } elseif (is_array($xmlType)) { 580 | $pathInfo = pathinfo(str_replace('\\', '/', $phpType)); 581 | 582 | $complexType->setAttribute('name', $pathInfo['basename']); 583 | 584 | //$complexType->setAttribute('name',$phpType); 585 | if ($xmlType['custom_wsdl'] !== false) { 586 | $custom_dom = new \DOMDocument(); 587 | $custom_dom->loadXML('' . $xmlType['custom_wsdl'] . ''); 588 | foreach ($custom_dom->documentElement->childNodes as $el) { 589 | $this->injectDom($dom, $complexType, $el); 590 | } 591 | } else { 592 | $all = $dom->createElement('xsd:' . $xmlType['indicator']); 593 | 594 | if (!is_null($xmlType['minOccurs'])) { 595 | $all->setAttribute('minOccurs', $xmlType['minOccurs']); 596 | } 597 | if (!is_null($xmlType['maxOccurs'])) { 598 | $all->setAttribute('maxOccurs', $xmlType['maxOccurs']); 599 | } 600 | if (!is_null($xmlType['nillable'])) { 601 | $all->setAttribute('nillable', $xmlType['nillable']); 602 | } 603 | foreach ($xmlType['properties'] as $name => $type) { 604 | $element = $dom->createElement('xsd:element'); 605 | if (!is_null($type[3])) { 606 | $element->setAttribute('minOccurs', $type[3]); 607 | } 608 | if (!is_null($type[4])) { 609 | $element->setAttribute('maxOccurs', $type[4]); 610 | } 611 | if (!is_null($type[2])) { 612 | $element->setAttribute('nillable', $type[2]); 613 | } 614 | $element->setAttribute('name', $name); 615 | $element->setAttribute('type', $type[0]); 616 | $all->appendChild($element); 617 | } 618 | $complexType->appendChild($all); 619 | } 620 | } 621 | $schema->appendChild($complexType); 622 | } 623 | foreach ($this->elements as $name => $parameters) { 624 | $element = $dom->createElement('xsd:element'); 625 | $element->setAttribute('name', $name); 626 | $complexType = $dom->createElement('xsd:complexType'); 627 | if (!empty($parameters)) { 628 | $sequence = $dom->createElement('xsd:sequence'); 629 | foreach ($parameters as $paramName => $paramOpts) { 630 | $innerElement = $dom->createElement('xsd:element'); 631 | $innerElement->setAttribute('name', $paramName); 632 | $innerElement->setAttribute('type', $paramOpts['type']); 633 | if (isset($paramOpts['nillable']) && $paramOpts['nillable']) { 634 | $innerElement->setAttribute('nillable', 'true'); 635 | } 636 | $sequence->appendChild($innerElement); 637 | } 638 | $complexType->appendChild($sequence); 639 | } 640 | $element->appendChild($complexType); 641 | $schema->appendChild($element); 642 | } 643 | $types->appendChild($schema); 644 | $dom->documentElement->appendChild($types); 645 | } 646 | 647 | /** 648 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 649 | */ 650 | protected function addMessages($dom) 651 | { 652 | foreach ($this->messages as $name => $message) { 653 | $element = $dom->createElement('wsdl:message'); 654 | $element->setAttribute('name', $name); 655 | foreach ($this->messages[$name] as $partName => $part) { 656 | if (is_array($part)) { 657 | $partElement = $dom->createElement('wsdl:part'); 658 | $partElement->setAttribute('name', $partName); 659 | if (isset($part['type'])) { 660 | $partElement->setAttribute('type', $part['type']); 661 | } 662 | if (isset($part['element'])) { 663 | $partElement->setAttribute('element', $part['element']); 664 | } 665 | $element->appendChild($partElement); 666 | } 667 | } 668 | $dom->documentElement->appendChild($element); 669 | } 670 | } 671 | 672 | /** 673 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 674 | */ 675 | protected function addPortTypes($dom) 676 | { 677 | $portType = $dom->createElement('wsdl:portType'); 678 | $portType->setAttribute('name', $this->serviceName . 'PortType'); 679 | $dom->documentElement->appendChild($portType); 680 | foreach ($this->operations as $name => $operation) { 681 | $portType->appendChild($this->createPortElement($dom, $name, $operation['doc'])); 682 | } 683 | } 684 | 685 | /** 686 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 687 | * @param string $name method name 688 | * @param string $doc doc 689 | * @return \DOMElement 690 | */ 691 | protected function createPortElement($dom, $name, $doc) 692 | { 693 | $operation = $dom->createElement('wsdl:operation'); 694 | $operation->setAttribute('name', $name); 695 | 696 | $input = $dom->createElement('wsdl:input'); 697 | $input->setAttribute('message', 'tns:' . $name . 'In'); 698 | $output = $dom->createElement('wsdl:output'); 699 | $output->setAttribute('message', 'tns:' . $name . 'Out'); 700 | 701 | $operation->appendChild($dom->createElement('wsdl:documentation', $doc)); 702 | $operation->appendChild($input); 703 | $operation->appendChild($output); 704 | 705 | return $operation; 706 | } 707 | 708 | /** 709 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 710 | */ 711 | protected function addBindings($dom) 712 | { 713 | $binding = $dom->createElement('wsdl:binding'); 714 | $binding->setAttribute('name', $this->serviceName . 'Binding'); 715 | $binding->setAttribute('type', 'tns:' . $this->serviceName . 'PortType'); 716 | 717 | $soapBinding = $dom->createElement('soap:binding'); 718 | $soapBinding->setAttribute('style', $this->bindingStyle); 719 | $soapBinding->setAttribute('transport', $this->bindingTransport); 720 | $binding->appendChild($soapBinding); 721 | 722 | $dom->documentElement->appendChild($binding); 723 | 724 | foreach ($this->operations as $name => $operation) { 725 | $binding->appendChild($this->createOperationElement($dom, $name, $operation['headers'])); 726 | } 727 | } 728 | 729 | /** 730 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 731 | * @param string $name method name 732 | * @param array $headers array like array('input'=>array(MESSAGE,PART),'output=>array(MESSAGE,PART)) 733 | * @return \DOMElement 734 | */ 735 | protected function createOperationElement($dom, $name, $headers = null) 736 | { 737 | $operation = $dom->createElement('wsdl:operation'); 738 | $operation->setAttribute('name', $name); 739 | $soapOperation = $dom->createElement('soap:operation'); 740 | $soapOperation->setAttribute('soapAction', $this->namespace . '#' . $name); 741 | if ($this->bindingStyle == self::STYLE_RPC) { 742 | $soapOperation->setAttribute('style', self::STYLE_RPC); 743 | } 744 | 745 | $input = $dom->createElement('wsdl:input'); 746 | $output = $dom->createElement('wsdl:output'); 747 | 748 | $soapBody = $dom->createElement('soap:body'); 749 | $operationBodyStyle = $this->operationBodyStyle; 750 | if ($this->bindingStyle == self::STYLE_RPC && !isset($operationBodyStyle['namespace'])) { 751 | $operationBodyStyle['namespace'] = $this->namespace; 752 | } 753 | foreach ($operationBodyStyle as $attributeName => $attributeValue) { 754 | $soapBody->setAttribute($attributeName, $attributeValue); 755 | } 756 | $input->appendChild($soapBody); 757 | $output->appendChild(clone $soapBody); 758 | if (is_array($headers)) { 759 | if (isset($headers['input']) && is_array($headers['input']) && count($headers['input']) == 2) { 760 | $soapHeader = $dom->createElement('soap:header'); 761 | foreach ($operationBodyStyle as $attributeName => $attributeValue) { 762 | $soapHeader->setAttribute($attributeName, $attributeValue); 763 | } 764 | $soapHeader->setAttribute('message', $headers['input'][0]); 765 | $soapHeader->setAttribute('part', $headers['input'][1]); 766 | $input->appendChild($soapHeader); 767 | } 768 | if (isset($headers['output']) && is_array($headers['output']) && count($headers['output']) == 2) { 769 | $soapHeader = $dom->createElement('soap:header'); 770 | foreach ($operationBodyStyle as $attributeName => $attributeValue) { 771 | $soapHeader->setAttribute($attributeName, $attributeValue); 772 | } 773 | $soapHeader->setAttribute('message', $headers['output'][0]); 774 | $soapHeader->setAttribute('part', $headers['output'][1]); 775 | $output->appendChild($soapHeader); 776 | } 777 | } 778 | 779 | $operation->appendChild($soapOperation); 780 | $operation->appendChild($input); 781 | $operation->appendChild($output); 782 | 783 | return $operation; 784 | } 785 | 786 | /** 787 | * @param \DOMDocument $dom Represents an entire HTML or XML document; serves as the root of the document tree 788 | * @param string $serviceUrl Web service URL 789 | */ 790 | protected function addService($dom, $serviceUrl) 791 | { 792 | $service = $dom->createElement('wsdl:service'); 793 | $service->setAttribute('name', $this->serviceName . 'Service'); 794 | 795 | $port = $dom->createElement('wsdl:port'); 796 | $port->setAttribute('name', $this->serviceName . 'Port'); 797 | $port->setAttribute('binding', 'tns:' . $this->serviceName . 'Binding'); 798 | 799 | $soapAddress = $dom->createElement('soap:address'); 800 | $soapAddress->setAttribute('location', $serviceUrl); 801 | $port->appendChild($soapAddress); 802 | $service->appendChild($port); 803 | $dom->documentElement->appendChild($service); 804 | } 805 | 806 | /** 807 | * Generate human friendly HTML documentation for complex data types. 808 | * This method can be invoked either by inserting URL parameter "&makedoc" into URL link, e.g. "http://www.mydomain.com/soap/create?makedoc", or simply by calling from another script with argument $return=true. 809 | * 810 | * Each complex data type is described in a separate HTML table containing following columns: 811 | *
      812 | *
    • # - attribute ID
    • 813 | *
    • Attribute - attribute name, e.g. firstname
    • 814 | *
    • Type - attribute type, e.g. integer, date, tns:SoapPovCalculationResultArray
    • 815 | *
    • Nill - true|false - whether the attribute is nillable
    • 816 | *
    • Min - minimum number of occurrences
    • 817 | *
    • Max - maximum number of occurrences
    • 818 | *
    • Description - Detailed description of the attribute.
    • 819 | *
    • Example - Attribute example value if provided via PHPDoc property @example.
    • 820 | *
    821 | * 822 | * @param bool $return If true, generated HTML output will be returned rather than directly sent to output buffer 823 | * @return string 824 | * @throws \yii\base\ExitException 825 | */ 826 | public function buildHtmlDocs($return = false) 827 | { 828 | $html = ''; 829 | $html .= ''; 830 | $html .= ''; 836 | $html .= ''; 837 | $html .= '

    WSDL documentation for service ' . $this->serviceName . '

    '; 838 | $html .= '

    Generated on ' . date('d.m.Y H:i:s') . '

    '; 839 | $html .= ''; 840 | $html .= '
    '; 841 | 842 | if (!empty($this->types)) { 843 | foreach ($this->types as $object => $options) { 844 | if (!is_array($options) || empty($options) || !is_array($options['properties']) || empty($options['properties'])) { 845 | continue; 846 | } 847 | $params = $options['properties']; 848 | $html .= "\n\n

    Object: {$object}

    "; 849 | $html .= ''; 850 | $html .= ''; 851 | $c = 0; 852 | foreach ($params as $param => $prop) { 853 | ++$c; 854 | $html .= << 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | HTML; 866 | } 867 | $html .= '\n
    #AttributeTypeNillMinMaxDescriptionExample
    {$c}{$param}{(str_replace('xsd:', '', $prop[0]))}{$prop[2]}{($prop[3] == null ? ' ' : $prop[3])}{($prop[4] == null ? ' ' : $prop[4])}{$prop[1]}{(trim($prop[5]) == '' ? ' ' : $prop[5])}

    '; 868 | } 869 | } else { 870 | $html .= 'No complex data type found!'; 871 | } 872 | $html .= '
    '; 873 | 874 | if ($return) { 875 | return $html; 876 | } 877 | echo $html; 878 | Yii::$app->end(); // end the app to avoid conflict with text/xml header 879 | } 880 | } 881 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | settings: 12 | bootstrap: _bootstrap.php 13 | #modules: 14 | # config: 15 | # Yii2: 16 | # configFile: 'tests/app/config/test-config.php' 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conquer/services", 3 | "description": "Yii2 soap wsdl web services", 4 | "keywords": ["yii2", "extension", "wsdl", "services", "soap"], 5 | "homepage": "https://github.com/borodulin/yii2-services", 6 | "type": "yii2-extension", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Andrey Borodulin", 11 | "email": "borodulin@gmail.com" 12 | } 13 | ], 14 | "extra": { 15 | "branch-alias": { 16 | "dev-master": "1.3.2.x-dev" 17 | } 18 | }, 19 | "require": { 20 | "php": ">=5.6.0", 21 | "yiisoft/yii2": ">=2.0.6" 22 | }, 23 | "require-dev": { 24 | "yiisoft/yii2-coding-standards": "~2.0.3", 25 | "yiisoft/yii2-debug": "~2.0.0", 26 | "yiisoft/yii2-gii": "~2.0.0", 27 | "yiisoft/yii2-faker": "~2.0.0", 28 | "codeception/base": "^2.2.3", 29 | "codeception/verify": "~0.3.1", 30 | "guzzlehttp/guzzle": ">=4.1.4 <7.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "conquer\\services\\": "" 35 | } 36 | }, 37 | "repositories": [ 38 | { 39 | "type": "composer", 40 | "url": "https://asset-packagist.org" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borodulin/yii2-services/534288d749625882576f32b280ff1386b15bfd98/tests/_data/.gitkeep -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | sendGET('api/soap'); 18 | // $I->canSeeResponseJsonMatchesXpath('/'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/acceptance/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 'test-api', 5 | 'basePath' => dirname(__DIR__), 6 | 'aliases' => [ 7 | '@bower' => '@vendor/bower-asset', 8 | '@npm' => '@vendor/npm-asset', 9 | ], 10 | 'vendorPath' => dirname(__DIR__) . '/../../vendor', 11 | 'controllerNamespace' => 'conquer\services\tests\app\controllers', 12 | 'components' => [ 13 | 'urlManager' => [ 14 | 'enablePrettyUrl' => true, 15 | 'showScriptName' => false, 16 | ], 17 | ], 18 | ]; -------------------------------------------------------------------------------- /tests/app/controllers/ApiController.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'class' => 'conquer\services\WebServiceAction', 25 | 'classMap' => [ 26 | 'SoapModel' => SoapModel::className(), 27 | ], 28 | ], 29 | ]; 30 | } 31 | /** 32 | * @param conquer\services\tests\app\models\SoapModel $myClass 33 | * @return string 34 | * @soap 35 | */ 36 | public function soapTest($myClass) 37 | { 38 | return get_class($myClass); 39 | } 40 | 41 | public function actionIndex() 42 | { 43 | return 'ok'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/app/models/SoapModel.php: -------------------------------------------------------------------------------- 1 |