<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Soap; use Zend\Server\Reflection; use Zend\Soap\AutoDiscover\DiscoveryStrategy\DiscoveryStrategyInterface as DiscoveryStrategy; use Zend\Soap\AutoDiscover\DiscoveryStrategy\ReflectionDiscovery; use Zend\Soap\Wsdl\ComplexTypeStrategy\ComplexTypeStrategyInterface as ComplexTypeStrategy; use Zend\Uri; class AutoDiscover { /** * @var string */ protected $serviceName; /** * @var Reflection */ protected $reflection = null; /** * Service function names * @var array */ protected $functions = array(); /** * Service class name * @var string */ protected $class; /** * @var bool */ protected $strategy; /** * Url where the WSDL file will be available at. * @var WSDL Uri */ protected $uri; /** * soap:body operation style options * @var array */ protected $operationBodyStyle = array( 'use' => 'encoded', 'encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/" ); /** * soap:operation style * @var array */ protected $bindingStyle = array( 'style' => 'rpc', 'transport' => 'http://schemas.xmlsoap.org/soap/http' ); /** * Name of the class to handle the WSDL creation. * @var string */ protected $wsdlClass = 'Zend\Soap\Wsdl'; /** * Class Map of PHP to WSDL types. * @var array */ protected $classMap = array(); /** * Discovery strategy for types and other method details. * @var DiscoveryStrategy */ protected $discoveryStrategy; /** * Constructor * * @param null|ComplexTypeStrategy $strategy * @param null|string|Uri\Uri $endpointUri * @param null|string $wsdlClass * @param null|array $classMap */ public function __construct( ComplexTypeStrategy $strategy = null, $endpointUri = null, $wsdlClass = null, array $classMap = array() ) { $this->reflection = new Reflection(); $this->setDiscoveryStrategy(new ReflectionDiscovery()); if (null !== $strategy) { $this->setComplexTypeStrategy($strategy); } if (null !== $endpointUri) { $this->setUri($endpointUri); } if (null !== $wsdlClass) { $this->setWsdlClass($wsdlClass); } $this->setClassMap($classMap); } /** * Set the discovery strategy for method type and other information. * * @param DiscoveryStrategy $discoveryStrategy * @return self */ public function setDiscoveryStrategy(DiscoveryStrategy $discoveryStrategy) { $this->discoveryStrategy = $discoveryStrategy; return $this; } /** * Get the discovery strategy. * * @return DiscoveryStrategy */ public function getDiscoveryStrategy() { return $this->discoveryStrategy; } /** * Get the class map of php to wsdl mappings. * * @return array */ public function getClassMap() { return $this->classMap; } /** * Set the class map of php to wsdl mappings. * * @param array $classmap * @return self * @throws Exception\InvalidArgumentException */ public function setClassMap($classMap) { if (!is_array($classMap)) { throw new Exception\InvalidArgumentException(sprintf( '%s expects an array; received "%s"', __METHOD__, (is_object($classMap) ? get_class($classMap) : gettype($classMap)) )); } $this->classMap = $classMap; return $this; } /** * Set service name * * @param string $serviceName * @return self * @throws Exception\InvalidArgumentException */ public function setServiceName($serviceName) { $matches = array(); // first character must be letter or underscore {@see http://www.w3.org/TR/wsdl#_document-n} $i = preg_match('/^[a-z\_]/ims', $serviceName, $matches); if ($i != 1) { throw new Exception\InvalidArgumentException('Service Name must start with letter or _'); } $this->serviceName = $serviceName; return $this; } /** * Get service name * * @return string * @throws Exception\RuntimeException */ public function getServiceName() { if (!$this->serviceName) { if ($this->class) { return $this->reflection->reflectClass($this->class)->getShortName(); } else { throw new Exception\RuntimeException('No service name given. Call AutoDiscover::setServiceName().'); } } return $this->serviceName; } /** * Set the location at which the WSDL file will be available. * * @param Uri\Uri|string $uri * @return self * @throws Exception\InvalidArgumentException */ public function setUri($uri) { if (!is_string($uri) && !($uri instanceof Uri\Uri)) { throw new Exception\InvalidArgumentException( 'Argument to \Zend\Soap\AutoDiscover::setUri should be string or \Zend\Uri\Uri instance.' ); } $uri = trim($uri); $uri = htmlspecialchars($uri, ENT_QUOTES, 'UTF-8', false); if (empty($uri)) { throw new Exception\InvalidArgumentException('Uri contains invalid characters or is empty'); } $this->uri = $uri; return $this; } /** * Return the current Uri that the SOAP WSDL Service will be located at. * * @return Uri\Uri * @throws Exception\RuntimeException */ public function getUri() { if ($this->uri === null) { throw new Exception\RuntimeException( 'Missing uri. You have to explicitly configure the Endpoint Uri by calling AutoDiscover::setUri().' ); } if (is_string($this->uri)) { $this->uri = Uri\UriFactory::factory($this->uri); } return $this->uri; } /** * Set the name of the WSDL handling class. * * @param string $wsdlClass * @return self * @throws Exception\InvalidArgumentException */ public function setWsdlClass($wsdlClass) { if (!is_string($wsdlClass) && !is_subclass_of($wsdlClass, '\Zend\Soap\Wsdl')) { throw new Exception\InvalidArgumentException( 'No \Zend\Soap\Wsdl subclass given to Zend\Soap\AutoDiscover::setWsdlClass as string.' ); } $this->wsdlClass = $wsdlClass; return $this; } /** * Return the name of the WSDL handling class. * * @return string */ public function getWsdlClass() { return $this->wsdlClass; } /** * Set options for all the binding operations soap:body elements. * * By default the options are set to 'use' => 'encoded' and * 'encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/". * * @param array $operationStyle * @return self * @throws Exception\InvalidArgumentException */ public function setOperationBodyStyle(array $operationStyle = array()) { if (!isset($operationStyle['use'])) { throw new Exception\InvalidArgumentException('Key "use" is required in Operation soap:body style.'); } $this->operationBodyStyle = $operationStyle; return $this; } /** * Set Binding soap:binding style. * * By default 'style' is 'rpc' and 'transport' is 'http://schemas.xmlsoap.org/soap/http'. * * @param array $bindingStyle * @return self */ public function setBindingStyle(array $bindingStyle = array()) { if (isset($bindingStyle['style'])) { $this->bindingStyle['style'] = $bindingStyle['style']; } if (isset($bindingStyle['transport'])) { $this->bindingStyle['transport'] = $bindingStyle['transport']; } return $this; } /** * Set the strategy that handles functions and classes that are added AFTER this call. * * @param ComplexTypeStrategy $strategy * @return self */ public function setComplexTypeStrategy(ComplexTypeStrategy $strategy) { $this->strategy = $strategy; return $this; } /** * Set the Class the SOAP server will use * * @param string $class Class Name * @return self */ public function setClass($class) { $this->class = $class; return $this; } /** * Add a Single or Multiple Functions to the WSDL * * @param string $function Function Name * @return self * @throws Exception\InvalidArgumentException */ public function addFunction($function) { if (is_array($function)) { foreach($function as $row) { $this->addFunction($row); } } elseif (is_string($function)) { if (function_exists($function)) { $this->functions[] = $function; } else { throw new Exception\InvalidArgumentException( 'Argument to Zend\Soap\AutoDiscover::addFunction should be a valid function name.' ); } } else { throw new Exception\InvalidArgumentException( 'Argument to Zend\Soap\AutoDiscover::addFunction should be string or array of strings.' ); } return $this; } /** * Generate the WSDL for a service class. * * @return Wsdl */ protected function _generateClass() { return $this->_generateWsdl($this->reflection->reflectClass($this->class)->getMethods()); } /** * Generate the WSDL for a set of functions. * * @return Wsdl */ protected function _generateFunctions() { $methods = array(); foreach (array_unique($this->functions) as $func) { $methods[] = $this->reflection->reflectFunction($func); } return $this->_generateWsdl($methods); } /** * Generate the WSDL for a set of reflection method instances. * * @param array $reflectionMethods * @return Wsdl */ protected function _generateWsdl(array $reflectionMethods) { $uri = $this->getUri(); $serviceName = $this->getServiceName(); $wsdl = new $this->wsdlClass($serviceName, $uri, $this->strategy, $this->classMap); // The wsdl:types element must precede all other elements (WS-I Basic Profile 1.1 R2023) $wsdl->addSchemaTypeSection(); $port = $wsdl->addPortType($serviceName . 'Port'); $binding = $wsdl->addBinding($serviceName . 'Binding', Wsdl::TYPES_NS . ':' . $serviceName . 'Port'); $wsdl->addSoapBinding($binding, $this->bindingStyle['style'], $this->bindingStyle['transport']); $wsdl->addService($serviceName . 'Service', $serviceName . 'Port', Wsdl::TYPES_NS . ':' . $serviceName . 'Binding', $uri); foreach ($reflectionMethods as $method) { $this->_addFunctionToWsdl($method, $wsdl, $port, $binding); } return $wsdl; } /** * Add a function to the WSDL document. * * @param $function Reflection\AbstractFunction function to add * @param $wsdl Wsdl WSDL document * @param $port \DOMElement wsdl:portType * @param $binding \DOMElement wsdl:binding * @throws Exception\InvalidArgumentException */ protected function _addFunctionToWsdl($function, $wsdl, $port, $binding) { $uri = $this->getUri(); // We only support one prototype: the one with the maximum number of arguments $prototype = null; $maxNumArgumentsOfPrototype = -1; foreach ($function->getPrototypes() as $tmpPrototype) { $numParams = count($tmpPrototype->getParameters()); if ($numParams > $maxNumArgumentsOfPrototype) { $maxNumArgumentsOfPrototype = $numParams; $prototype = $tmpPrototype; } } if ($prototype === null) { throw new Exception\InvalidArgumentException(sprintf( 'No prototypes could be found for the "%s" function', $function->getName() )); } $functionName = $wsdl->translateType($function->getName()); // Add the input message (parameters) $args = array(); if ($this->bindingStyle['style'] == 'document') { // Document style: wrap all parameters in a sequence element $sequence = array(); foreach ($prototype->getParameters() as $param) { $sequenceElement = array( 'name' => $param->getName(), 'type' => $wsdl->getType($this->discoveryStrategy->getFunctionParameterType($param)) ); if ($param->isOptional()) { $sequenceElement['nillable'] = 'true'; } $sequence[] = $sequenceElement; } $element = array( 'name' => $functionName, 'sequence' => $sequence ); // Add the wrapper element part, which must be named 'parameters' $args['parameters'] = array('element' => $wsdl->addElement($element)); } else { // RPC style: add each parameter as a typed part foreach ($prototype->getParameters() as $param) { $args[$param->getName()] = array( 'type' => $wsdl->getType($this->discoveryStrategy->getFunctionParameterType($param)) ); } } $wsdl->addMessage($functionName . 'In', $args); $isOneWayMessage = $this->discoveryStrategy->isFunctionOneWay($function, $prototype); if ($isOneWayMessage == false) { // Add the output message (return value) $args = array(); if ($this->bindingStyle['style'] == 'document') { // Document style: wrap the return value in a sequence element $sequence = array(); if ($prototype->getReturnType() != "void") { $sequence[] = array( 'name' => $functionName . 'Result', 'type' => $wsdl->getType($this->discoveryStrategy->getFunctionReturnType($function, $prototype)) ); } $element = array( 'name' => $functionName . 'Response', 'sequence' => $sequence ); // Add the wrapper element part, which must be named 'parameters' $args['parameters'] = array('element' => $wsdl->addElement($element)); } elseif ($prototype->getReturnType() != "void") { // RPC style: add the return value as a typed part $args['return'] = array( 'type' => $wsdl->getType($this->discoveryStrategy->getFunctionReturnType($function, $prototype)) ); } $wsdl->addMessage($functionName . 'Out', $args); } // Add the portType operation if ($isOneWayMessage == false) { $portOperation = $wsdl->addPortOperation( $port, $functionName, Wsdl::TYPES_NS . ':' . $functionName . 'In', Wsdl::TYPES_NS . ':' . $functionName . 'Out' ); } else { $portOperation = $wsdl->addPortOperation( $port, $functionName, Wsdl::TYPES_NS . ':' . $functionName . 'In', false ); } $desc = $this->discoveryStrategy->getFunctionDocumentation($function); if (strlen($desc) > 0) { $wsdl->addDocumentation($portOperation, $desc); } // When using the RPC style, make sure the operation style includes a 'namespace' // attribute (WS-I Basic Profile 1.1 R2717) $operationBodyStyle = $this->operationBodyStyle; if ($this->bindingStyle['style'] == 'rpc' && !isset($operationBodyStyle['namespace'])) { $operationBodyStyle['namespace'] = '' . $uri; } // Add the binding operation if ($isOneWayMessage == false) { $operation = $wsdl->addBindingOperation($binding, $functionName, $operationBodyStyle, $operationBodyStyle); } else { $operation = $wsdl->addBindingOperation($binding, $functionName, $operationBodyStyle); } $wsdl->addSoapOperation($operation, $uri . '#' . $functionName); } /** * Generate the WSDL file from the configured input. * * @return Wsdl * @throws Exception\RuntimeException */ public function generate() { if ($this->class && $this->functions) { throw new Exception\RuntimeException('Can either dump functions or a class as a service, not both.'); } if ($this->class) { $wsdl = $this->_generateClass(); } else { $wsdl = $this->_generateFunctions(); } return $wsdl; } /** * Proxy to WSDL dump function * * @param string $filename * @return bool * @throws Exception\RuntimeException */ public function dump($filename) { return $this->generate()->dump($filename); } /** * Proxy to WSDL toXml() function * * @return string * @throws Exception\RuntimeException */ public function toXml() { return $this->generate()->toXml(); } /** * Handle WSDL document. */ public function handle() { header('Content-Type: text/xml'); echo $this->toXml(); } }