├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── Controller └── JsonRpcController.php ├── DependencyInjection ├── Compiler │ └── JsonRpcExposablePass.php ├── Configuration.php └── Wa72JsonRpcExtension.php ├── LICENSE ├── README.md ├── Resources ├── config │ ├── routing.yml │ └── services.yml ├── doc │ └── index.rst └── meta │ └── LICENSE ├── Tests ├── Fixtures │ ├── Testparameter.php │ └── app │ │ ├── Wa72JsonRpcBundleTestKernel.php │ │ └── config │ │ └── config.yml ├── JsonRpcControllerTest.php ├── Testservice.php └── bootstrap.php ├── Wa72JsonRpcBundle.php ├── composer.json └── phpunit.xml.dist /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | php: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | php: [7.4, 8.0, 8.1, 8.2] 19 | dependency-version: [prefer-lowest, prefer-stable] 20 | 21 | steps: 22 | - name: checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | coverage: xdebug 30 | 31 | - name: install dependencies 32 | run: composer update --${{ matrix.dependency-version }} 33 | 34 | - name: run tests 35 | run: php vendor/bin/phpunit 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | composer.phar 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.6.1 2018-03-10 ### 2 | - compatibility with Symfony 4, thanks to @shreypuranik 3 | - make unit tests compatible with newer phpunit versions 4 | 5 | ### v0.6.0 2017-02-09 ### 6 | - added compatibility with Symfony 3, thanks to @JasonMiesionczek 7 | - minimum required Symfony version is now 2.7 8 | 9 | ### v0.5.0 2014-12-17 ### 10 | - It is now possible to call methods that require objects as parameters (in previous versions only methods with scalar 11 | and array parameters could be called). That's why JMSSerializerBundle is now a required dependency. 12 | For this to work, the following conditions must be met: 13 | - The method parameters must be correctly type hinted 14 | - The parameter classes must contain [jms_serializer `@Type` annotations for its properties](http://jmsyst.com/libs/serializer/master/reference/annotations#type) 15 | 16 | ### v0.4.4 2014-11-28 ### 17 | - The test service is no longer registered automatically. If you want to use it, 18 | you must define it in the config.yml of your application, see updated [Resources/doc/index.rst](Resources/doc/index.rst). 19 | 20 | ### v0.4.2 2014-11-28 ### 21 | - fixed bug introduced in v0.2.0: named parameters given in associative arrays were always passed to the method in 22 | the order they were given, not by their names. Thanks to @teet. 23 | 24 | ### v0.4.1 2014-11-19 ### 25 | - raised required symfony version to 2.3 26 | - Added mandatory LICENSE and index.rst files 27 | - only metadata, no code changes 28 | 29 | ### v0.4.0 2014-10-23 ### 30 | - All public methods of services tagged with 'wa72_jsonrpc.exposable' can be called via JSON-RPC. The method name 31 | to be used in the RPC call is "service:method", i.e. the name of the service and the method separated by colon. -------------------------------------------------------------------------------- /Controller/JsonRpcController.php: -------------------------------------------------------------------------------- 1 | array( 22 | * 'myfunction1' => array( 23 | * 'service' => 'mybundle.servicename', 24 | * 'method' => 'methodofservice' 25 | * ), 26 | * 'anotherfunction' => array( 27 | * 'service' => 'mybundle.foo', 28 | * 'method' => 'bar' 29 | * ) 30 | * ) 31 | * ); 32 | * 33 | * A method name "myfunction1" in the RPC request will then call 34 | * $this->container->get('mybundle.servicename')->methodofservice() 35 | * 36 | * If you want to add a service completely so that all public methods of 37 | * this service may be called, use the addService($servicename) method. 38 | * Methods of the services added this way can be called remotely using 39 | * "servicename:method" as RPC method name. 40 | * 41 | * @license MIT 42 | * @author Christoph Singer 43 | * 44 | */ 45 | class JsonRpcController implements ContainerAwareInterface 46 | { 47 | use ContainerAwareTrait; 48 | 49 | const PARSE_ERROR = -32700; 50 | const INVALID_REQUEST = -32600; 51 | const METHOD_NOT_FOUND = -32601; 52 | const INVALID_PARAMS = -32602; 53 | const INTERNAL_ERROR = -32603; 54 | 55 | /** 56 | * Functions that are allowed to be called 57 | * 58 | * @var array $functions 59 | */ 60 | private $functions = array(); 61 | 62 | /** 63 | * Array of names of fully exposed services (all methods of this services are allowed to be called) 64 | * 65 | * @var array $services 66 | */ 67 | private $services = array(); 68 | 69 | /** 70 | * @var \JMS\Serializer\SerializationContext 71 | */ 72 | private $serializationContext; 73 | 74 | /** 75 | * @param \Symfony\Component\DependencyInjection\ContainerInterface $container 76 | * @param array $config Associative array for configuration, expects at least a key "functions" 77 | * @throws \InvalidArgumentException 78 | */ 79 | public function __construct($container, $config) 80 | { 81 | if (isset($config['functions'])) { 82 | if (!is_array($config['functions'])) throw new \InvalidArgumentException('Configuration parameter "functions" must be array'); 83 | $this->functions = $config['functions']; 84 | } 85 | $this->setContainer($container); 86 | } 87 | 88 | /** 89 | * @param Request $httprequest 90 | * @return Response 91 | */ 92 | public function execute(Request $httprequest) 93 | { 94 | $json = $httprequest->getContent(); 95 | $request = json_decode($json, true); 96 | $requestId = (isset($request['id']) ? $request['id'] : null); 97 | 98 | if ($request === null) { 99 | return $this->getErrorResponse(self::PARSE_ERROR, null); 100 | } elseif (!(isset($request['jsonrpc']) && isset($request['method']) && $request['jsonrpc'] == '2.0')) { 101 | return $this->getErrorResponse(self::INVALID_REQUEST, $requestId); 102 | } 103 | 104 | if (in_array($request['method'], array_keys($this->functions))) { 105 | $servicename = $this->functions[$request['method']]['service']; 106 | $method = $this->functions[$request['method']]['method']; 107 | } else { 108 | if (count($this->services) && strpos($request['method'], ':') > 0) { 109 | list($servicename, $method) = explode(':', $request['method']); 110 | if (!in_array($servicename, $this->services)) { 111 | return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId); 112 | } 113 | } else { 114 | return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId); 115 | } 116 | } 117 | try { 118 | $service = $this->container->get($servicename); 119 | } catch (ServiceNotFoundException $e) { 120 | return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId); 121 | } 122 | $params = (isset($request['params']) ? $request['params'] : array()); 123 | 124 | if (is_callable(array($service, $method))) { 125 | $r = new \ReflectionMethod($service, $method); 126 | $rps = $r->getParameters(); 127 | 128 | if (is_array($params)) { 129 | if (!(count($params) >= $r->getNumberOfRequiredParameters() 130 | && count($params) <= $r->getNumberOfParameters()) 131 | ) { 132 | return $this->getErrorResponse(self::INVALID_PARAMS, $requestId, 133 | sprintf('Number of given parameters (%d) does not match the number of expected parameters (%d required, %d total)', 134 | count($params), $r->getNumberOfRequiredParameters(), $r->getNumberOfParameters())); 135 | } 136 | 137 | } 138 | if ($this->isAssoc($params)) { 139 | $newparams = array(); 140 | foreach ($rps as $i => $rp) { 141 | /* @var \ReflectionParameter $rp */ 142 | $name = $rp->name; 143 | if (!isset($params[$rp->name]) && !$rp->isOptional()) { 144 | return $this->getErrorResponse(self::INVALID_PARAMS, $requestId, 145 | sprintf('Parameter %s is missing', $name)); 146 | } 147 | if (isset($params[$rp->name])) { 148 | $newparams[] = $params[$rp->name]; 149 | } else { 150 | $newparams[] = null; 151 | } 152 | } 153 | $params = $newparams; 154 | } 155 | 156 | // correctly deserialize object parameters 157 | foreach ($params as $index => $param) { 158 | // if the json_decode'd param value is an array but an object is expected as method parameter, 159 | // re-encode the array value to json and correctly decode it using jsm_serializer 160 | if (is_array($param) && !$rps[$index]->isArray() && $rps[$index]->getClass() != null) { 161 | $class = $rps[$index]->getClass()->getName(); 162 | $param = json_encode($param); 163 | $params[$index] = $this->container->get('jms_serializer')->deserialize($param, $class, 'json'); 164 | } 165 | } 166 | 167 | try { 168 | $result = call_user_func_array(array($service, $method), $params); 169 | } catch (\Exception $e) { 170 | return $this->getErrorResponse(self::INTERNAL_ERROR, $requestId, $this->convertExceptionToErrorData($e)); 171 | } 172 | 173 | $response = array('jsonrpc' => '2.0'); 174 | $response['result'] = $result; 175 | $response['id'] = $requestId; 176 | 177 | if ($this->container->has('jms_serializer')) { 178 | $functionConfig = ( 179 | isset($this->functions[$request['method']]) 180 | ? $this->functions[$request['method']] 181 | : array() 182 | ); 183 | $serializationContext = $this->getSerializationContext($functionConfig); 184 | $response = $this->container->get('jms_serializer')->serialize($response, 'json', $serializationContext); 185 | } else { 186 | $response = json_encode($response); 187 | } 188 | 189 | return new Response($response, 200, array('Content-Type' => 'application/json')); 190 | } else { 191 | return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId); 192 | } 193 | } 194 | 195 | /** 196 | * Add a new function that can be called by RPC 197 | * 198 | * @param string $alias The function name used in the RPC call 199 | * @param string $service The service name of the method to call 200 | * @param string $method The method of $service 201 | * @param bool $overwrite Whether to overwrite an existing function 202 | * @throws \InvalidArgumentException 203 | */ 204 | public function addMethod($alias, $service, $method, $overwrite = false) 205 | { 206 | if (!isset($this->functions)) $this->functions = array(); 207 | if (isset($this->functions[$alias]) && !$overwrite) { 208 | throw new \InvalidArgumentException('JsonRpcController: The function "' . $alias . '" already exists.'); 209 | } 210 | $this->functions[$alias] = array( 211 | 'service' => $service, 212 | 'method' => $method 213 | ); 214 | } 215 | 216 | /** 217 | * Add a new service that is fully exposed by json-rpc 218 | * 219 | * @param string $service The id of a service 220 | */ 221 | public function addService($service) 222 | { 223 | $this->services[] = $service; 224 | } 225 | 226 | /** 227 | * Remove a method definition 228 | * 229 | * @param string $alias 230 | */ 231 | public function removeMethod($alias) 232 | { 233 | if (isset($this->functions[$alias])) { 234 | unset($this->functions[$alias]); 235 | } 236 | } 237 | 238 | protected function convertExceptionToErrorData(\Exception $e) 239 | { 240 | return $e->getMessage(); 241 | } 242 | 243 | protected function getError($code) 244 | { 245 | $message = ''; 246 | switch ($code) { 247 | case self::PARSE_ERROR: 248 | $message = 'Parse error'; 249 | break; 250 | case self::INVALID_REQUEST: 251 | $message = 'Invalid request'; 252 | break; 253 | case self::METHOD_NOT_FOUND: 254 | $message = 'Method not found'; 255 | break; 256 | case self::INVALID_PARAMS: 257 | $message = 'Invalid params'; 258 | break; 259 | case self::INTERNAL_ERROR: 260 | $message = 'Internal error'; 261 | break; 262 | } 263 | 264 | return array('code' => $code, 'message' => $message); 265 | } 266 | 267 | protected function getErrorResponse($code, $id, $data = null) 268 | { 269 | $response = array('jsonrpc' => '2.0'); 270 | $response['error'] = $this->getError($code); 271 | 272 | if ($data != null) { 273 | $response['error']['data'] = $data; 274 | } 275 | 276 | $response['id'] = $id; 277 | 278 | return new Response(json_encode($response), 200, array('Content-Type' => 'application/json')); 279 | } 280 | 281 | /** 282 | * Set SerializationContext for using with jms_serializer 283 | * 284 | * @param \JMS\Serializer\SerializationContext $context 285 | */ 286 | public function setSerializationContext($context) 287 | { 288 | $this->serializationContext = $context; 289 | } 290 | 291 | /** 292 | * Get SerializationContext or creates one if jms_serialization_context option is set 293 | * 294 | * @param array $functionConfig 295 | * @return \JMS\Serializer\SerializationContext 296 | */ 297 | protected function getSerializationContext(array $functionConfig) 298 | { 299 | if (isset($functionConfig['jms_serialization_context'])) { 300 | $serializationContext = \JMS\Serializer\SerializationContext::create(); 301 | 302 | if (isset($functionConfig['jms_serialization_context']['groups'])) { 303 | $serializationContext->setGroups($functionConfig['jms_serialization_context']['groups']); 304 | } 305 | 306 | if (isset($functionConfig['jms_serialization_context']['version'])) { 307 | $serializationContext->setVersion($functionConfig['jms_serialization_context']['version']); 308 | } 309 | 310 | if (isset($functionConfig['jms_serialization_context']['max_depth_checks'])) { 311 | $serializationContext->enableMaxDepthChecks($functionConfig['jms_serialization_context']['max_depth_checks']); 312 | } 313 | } else { 314 | $serializationContext = $this->serializationContext; 315 | } 316 | 317 | return $serializationContext; 318 | } 319 | 320 | /** 321 | * Finds whether a variable is an associative array 322 | * 323 | * @param $var 324 | * @return bool 325 | */ 326 | protected function isAssoc($var) 327 | { 328 | return array_keys($var) !== range(0, count($var) - 1); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/JsonRpcExposablePass.php: -------------------------------------------------------------------------------- 1 | getDefinition('wa72_jsonrpc.jsonrpccontroller'); 19 | $services = $container->findTaggedServiceIds('wa72_jsonrpc.exposable'); 20 | foreach ($services as $service => $attributes) { 21 | $definition->addMethodCall('addService', array($service)); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 22 | 23 | $rootNode 24 | ->children() 25 | ->arrayNode('functions') 26 | ->useAttributeAsKey('function') 27 | ->prototype('array') 28 | ->children() 29 | ->scalarNode('service')->end() 30 | ->scalarNode('method')->end() 31 | ->arrayNode('jms_serialization_context') 32 | ->children() 33 | ->arrayNode('groups') 34 | ->beforeNormalization() 35 | ->ifTrue(function ($v) { return is_string($v); }) 36 | ->then(function ($v) { return array($v); }) 37 | ->end() 38 | ->prototype('scalar')->end() 39 | ->end() 40 | ->scalarNode('version')->end() 41 | ->booleanNode('max_depth_checks')->end() 42 | ->end() 43 | ->end() 44 | ->end() 45 | ->end() 46 | ->end() 47 | ->end(); 48 | 49 | return $treeBuilder; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DependencyInjection/Wa72JsonRpcExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $container->setParameter('wa72.jsonrpc', $config); 26 | 27 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 28 | $loader->load('services.yml'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2023 Christoph Singer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | The software is provided "as is", without warranty of any kind, express or 14 | implied, including but not limited to the warranties of merchantability, 15 | fitness for a particular purpose and noninfringement. In no event shall the 16 | authors or copyright holders be liable for any claim, damages or other 17 | liability, whether in an action of contract, tort or otherwise, arising from, 18 | out of or in connection with the software or the use or other dealings in 19 | the software. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JsonRpcBundle 2 | ============= 3 | 4 | ![tests](https://github.com/wasinger/jsonrpc-bundle/actions/workflows/tests.yml/badge.svg?branch=master) 5 | [![Latest Version](http://img.shields.io/packagist/v/wa72/jsonrpc-bundle.svg)](https://packagist.org/packages/wa72/jsonrpc-bundle) 6 | [![Downloads from Packagist](http://img.shields.io/packagist/dt/wa72/jsonrpc-bundle.svg)](https://packagist.org/packages/wa72/jsonrpc-bundle) 7 | 8 | 9 | JsonRpcBundle is a bundle for Symfony that allows to easily build a JSON-RPC server for web services using 10 | [JSON-RPC 2.0](http://www.jsonrpc.org/specification). 11 | 12 | The bundle contains a controller that is able to expose methods of any public service registered in the Symfony service 13 | container as a JSON-RPC web service. 14 | 15 | Documentation 16 | ------------- 17 | 18 | Documentation is found in [Resources/doc/index.rst](Resources/doc/index.rst). 19 | 20 | 21 | © 2013-2023 Christoph Singer. Licensed under the MIT license. 22 | -------------------------------------------------------------------------------- /Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | wa72_json_rpc: 2 | path: / 3 | defaults: { _controller: wa72_jsonrpc.jsonrpccontroller:execute, optionalTrailingSlash : "/" } 4 | requirements: { optionalTrailingSlash : "[/]{0,1}" } 5 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | wa72_jsonrpc.jsonrpccontroller.class: Wa72\JsonRpcBundle\Controller\JsonRpcController 3 | 4 | services: 5 | wa72_jsonrpc.jsonrpccontroller: 6 | public: true 7 | class: '%wa72_jsonrpc.jsonrpccontroller.class%' 8 | arguments: ['@service_container', '%wa72.jsonrpc%'] 9 | -------------------------------------------------------------------------------- /Resources/doc/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | JsonRpcBundle 3 | ============= 4 | 5 | JsonRpcBundle is a bundle for Symfony 2.7 and up that allows to easily build a JSON-RPC server for web services using `JSON-RPC 2.0`_. 6 | 7 | The bundle contains a controller that is able to expose methods of any public service registered in the Symfony service container as a JSON-RPC web service. The return value of the service method is converted to JSON using `jms\_serializer`_. 8 | 9 | Of course, it doesn't simply expose all your services' methods to the public, but only those explicitely mentioned in the configuration. And service methods cannot be called by it's original name but by an alias to be defined in the configuration. 10 | 11 | 12 | Installation 13 | ============ 14 | 15 | 1. Add "wa72/jsonrpc-bundle" as requirement to your composer.json 16 | 17 | 2. Add "new Wa72\\JsonRpcBundle\\Wa72JsonRpcBundle()" in your AppKernel::registerBundles() function 18 | 19 | 3. Import the bundle's route in your routing.yml: 20 | 21 | .. code-block:: yaml 22 | 23 | # app/config/routing.yml 24 | wa72_json_rpc: 25 | resource: "@Wa72JsonRpcBundle/Resources/config/routing.yml" 26 | prefix: /jsonrpc 27 | 28 | Your JSON-RPC web service will then be available in your project calling the /jsonrpc/ URL. 29 | 30 | Configuration 31 | ============= 32 | 33 | You must configure which functions of the services registered in the Service Container will be available as web services. 34 | There are two methods how to do this: 35 | 36 | Method 1: Expose single methods from any service via yaml configuration 37 | ----------------------------------------------------------------------- 38 | 39 | Configuration is done under the "wa72\_json\_rpc" key of your configuration (usually defined in your app/config/config.yml). 40 | To enable a Symfony2 service method to be called as a JSON-RPC web service, add it to the "functions" array of the configuration. 41 | The key of an entry of the "functions" array is the alias name for the method to be called over RPC and it needs two sub keys: 42 | "service" specifies the name of the service and "method" the name of the method to call. Example: 43 | 44 | .. code-block:: yaml 45 | 46 | # app/config/config.yml 47 | wa72_json_rpc: 48 | functions: 49 | myfunction1: 50 | service: "mybundle.servicename" 51 | method: "methodofservice" 52 | anotherfunction: 53 | service: "bundlename.foo" 54 | method: "bar" 55 | 56 | In this example, "myfunction1" and "anotherfunction" are aliases for service methods that are used as JSON-RPC method names. 57 | A method name "myfunction1" in the JSON-RPC call will then call the method "methodofservice" of service "mybundle.servicename". 58 | 59 | If you use `jms\_serializer`_ you can also configure exclusion strategies (groups, version, or max depth checks) : 60 | 61 | .. code-block:: yaml 62 | 63 | # app/config/config.yml 64 | wa72_json_rpc: 65 | functions: 66 | myfunction1: 67 | service: "mybundle.servicename" 68 | method: "methodofservice" 69 | jms_serialization_context: 70 | group: "my_group" 71 | version: "1" 72 | max_depth_checks: true 73 | 74 | 75 | Method 2: tag a sercive to make all it's public methods callable by JSON-RPC 76 | ---------------------------------------------------------------------------- 77 | 78 | Starting with v0.4.0, it is also possible to fully expose all methods of a service by tagging it with ``wa72\_jsonrpc.exposable``. 79 | All public methods of services tagged with 'wa72\_jsonrpc.exposable' can be called via JSON-RPC. The method name 80 | to be used in the RPC call is "service\:method", i.e. the name of the service and the method separated by colon. 81 | 82 | 83 | Notes 84 | ===== 85 | 86 | **New in Version 0.5.0**: It is now possible to call methods that require objects as parameters (in previous versions 87 | only methods with scalar and array parameters could be called). 88 | That's why *JMSSerializerBundle is now a required dependency*. 89 | For this to work, the following conditions must be met: 90 | 91 | - The method parameters must be correctly type hinted 92 | - The classes used as parameters must contain `jms\_serializer @Type annotations for their properties `_ 93 | 94 | 95 | Testing 96 | ======= 97 | 98 | The bundle comes with a simple test service. If you have imported the bundle's routing to ``/jsonrpc`` (see above) 99 | register the test service in your DI container: 100 | 101 | .. code-block:: yaml 102 | 103 | # app/config/config_dev.yml 104 | services: 105 | wa72_jsonrpc.testservice: 106 | class: Wa72\JsonRpcBundle\Tests\Testservice 107 | tags: 108 | - {name: wa72_jsonrpc.exposable} 109 | 110 | You should then be able to test your service by sending a JSON-RPC request using curl: 111 | 112 | .. code-block:: bash 113 | 114 | curl -XPOST http://your-symfony-project/app_dev.php/jsonrpc/ -d '{"jsonrpc":"2.0","method":"wa72_jsonrpc.testservice:hello","id":"foo","params":{"name":"Joe"}}' 115 | 116 | and you should get the following answer: 117 | 118 | .. code-block:: json 119 | 120 | {"jsonrpc":"2.0","result":"Hello Joe!","id":"foo"} 121 | 122 | There are also unit tests for phpunit. Just install the required development dependencies using ``composer install`` and run 123 | ``vendor/bin/phpunit`` in the root directory of the project. 124 | 125 | © 2013-2018 Christoph Singer, Web-Agentur 72. Licensed under the MIT license. 126 | 127 | 128 | .. _`JSON-RPC 2.0`: http://www.jsonrpc.org/specification 129 | .. _`jms\_serializer`: https://github.com/schmittjoh/JMSSerializerBundle -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Christoph Singer, Web-Agentur 72 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | The software is provided "as is", without warranty of any kind, express or 14 | implied, including but not limited to the warranties of merchantability, 15 | fitness for a particular purpose and noninfringement. In no event shall the 16 | authors or copyright holders be liable for any claim, damages or other 17 | liability, whether in an action of contract, tort or otherwise, arising from, 18 | out of or in connection with the software or the use or other dealings in 19 | the software. 20 | -------------------------------------------------------------------------------- /Tests/Fixtures/Testparameter.php: -------------------------------------------------------------------------------- 1 | a = $a; 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getA() 32 | { 33 | return $this->a; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getB() 40 | { 41 | return $this->b; 42 | } 43 | 44 | /** 45 | * @param string $b 46 | */ 47 | public function setB($b) 48 | { 49 | $this->b = $b; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getC() 56 | { 57 | return $this->c; 58 | } 59 | 60 | /** 61 | * @param string $c 62 | */ 63 | public function setC($c) 64 | { 65 | $this->c = $c; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /Tests/Fixtures/app/Wa72JsonRpcBundleTestKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/config/config.yml'); 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getCacheDir(): string 26 | { 27 | return sys_get_temp_dir().'/Wa72JsonRpcBundle/cache'; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getLogDir(): string 34 | { 35 | return sys_get_temp_dir().'/Wa72JsonRpcBundle/logs'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Fixtures/app/config/config.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: dummy 3 | services: 4 | wa72_jsonrpc.testservice: 5 | class: 'Wa72\JsonRpcBundle\Tests\Testservice' 6 | public: true 7 | tags: 8 | - {name: wa72_jsonrpc.exposable} 9 | # override jms_serializer.form_error_handler with dummy 10 | jms_serializer.form_error_handler: 11 | class: 'Wa72\JsonRpcBundle\Tests\Testservice' -------------------------------------------------------------------------------- /Tests/JsonRpcControllerTest.php: -------------------------------------------------------------------------------- 1 | array( 27 | 'testhello' => array( 28 | 'service' => 'wa72_jsonrpc.testservice', 29 | 'method' => 'hello' 30 | ) 31 | ) 32 | ); 33 | $this->kernel = new \Wa72JsonRpcBundleTestKernel('test', false); 34 | $this->kernel->boot(); 35 | $this->controller = new JsonRpcController($this->kernel->getContainer(), $config); 36 | } 37 | 38 | public function testHello() 39 | { 40 | $requestdata = array( 41 | 'jsonrpc' => '2.0', 42 | 'id' => 'test', 43 | 'method' => 'testhello', 44 | 'params' => array('name' => 'Joe') 45 | ); 46 | $response = $this->makeRequest($this->controller, $requestdata); 47 | $this->assertEquals('2.0', $response['jsonrpc']); 48 | $this->assertEquals('test', $response['id']); 49 | $this->assertArrayHasKey('result', $response); 50 | $this->assertArrayNotHasKey('error', $response); 51 | $this->assertEquals('Hello Joe!', $response['result']); 52 | 53 | // Test: missing parameter 54 | $requestdata = array( 55 | 'jsonrpc' => '2.0', 56 | 'id' => 'test', 57 | 'method' => 'testhello' 58 | ); 59 | $response = $this->makeRequest($this->controller, $requestdata); 60 | $this->assertArrayHasKey('error', $response); 61 | $this->assertArrayNotHasKey('result', $response); 62 | $this->assertEquals(-32602, $response['error']['code']); 63 | } 64 | 65 | public function testService() 66 | { 67 | $controller = $this->kernel->getContainer()->get('wa72_jsonrpc.jsonrpccontroller'); 68 | $requestdata = array( 69 | 'jsonrpc' => '2.0', 70 | 'id' => 'testservice', 71 | 'method' => 'wa72_jsonrpc.testservice:hello', 72 | 'params' => array('name' => 'Max') 73 | ); 74 | 75 | $response = $this->makeRequest($controller, $requestdata); 76 | $this->assertEquals('2.0', $response['jsonrpc']); 77 | $this->assertEquals('testservice', $response['id']); 78 | $this->assertArrayNotHasKey('error', $response); 79 | $this->assertArrayHasKey('result', $response); 80 | $this->assertEquals('Hello Max!', $response['result']); 81 | 82 | // Test: non-existing service should return "Method not found" error 83 | $requestdata = array( 84 | 'jsonrpc' => '2.0', 85 | 'id' => 'testservice', 86 | 'method' => 'someservice:somemethod', 87 | 'params' => array('name' => 'Max') 88 | ); 89 | 90 | $response = $this->makeRequest($controller, $requestdata); 91 | $this->assertEquals('2.0', $response['jsonrpc']); 92 | $this->assertEquals('testservice', $response['id']); 93 | $this->assertArrayNotHasKey('result', $response); 94 | $this->assertArrayHasKey('error', $response); 95 | $this->assertEquals(-32601, $response['error']['code']); 96 | } 97 | 98 | public function testParameters() 99 | { 100 | // params as associative array in right order 101 | $controller = $this->kernel->getContainer()->get('wa72_jsonrpc.jsonrpccontroller'); 102 | $requestdata = array( 103 | 'jsonrpc' => '2.0', 104 | 'id' => 'parametertest', 105 | 'method' => 'wa72_jsonrpc.testservice:parametertest', 106 | 'params' => array('arg1' => 'abc', 'arg2' => 'def', 'arg_array' => array()) 107 | ); 108 | 109 | $response = $this->makeRequest($controller, $requestdata); 110 | $this->assertArrayNotHasKey('error', $response); 111 | $this->assertArrayHasKey('result', $response); 112 | $this->assertEquals('abcdef', $response['result']); 113 | 114 | // params as simple array in right order 115 | $controller = $this->kernel->getContainer()->get('wa72_jsonrpc.jsonrpccontroller'); 116 | $requestdata = array( 117 | 'jsonrpc' => '2.0', 118 | 'id' => 'parametertest', 119 | 'method' => 'wa72_jsonrpc.testservice:parametertest', 120 | 'params' => array('abc', 'def', array()) 121 | ); 122 | 123 | $response = $this->makeRequest($controller, $requestdata); 124 | $this->assertArrayNotHasKey('error', $response); 125 | $this->assertArrayHasKey('result', $response); 126 | $this->assertEquals('abcdef', $response['result']); 127 | 128 | // params as associative array in mixed order 129 | $controller = $this->kernel->getContainer()->get('wa72_jsonrpc.jsonrpccontroller'); 130 | $requestdata = array( 131 | 'jsonrpc' => '2.0', 132 | 'id' => 'parametertest', 133 | 'method' => 'wa72_jsonrpc.testservice:parametertest', 134 | 'params' => array('arg_array' => array(), 'arg2' => 'def', 'arg1' => 'abc') 135 | ); 136 | 137 | $response = $this->makeRequest($controller, $requestdata); 138 | $this->assertArrayNotHasKey('error', $response); 139 | $this->assertArrayHasKey('result', $response); 140 | $this->assertEquals('abcdef', $response['result']); 141 | 142 | // params with objects 143 | $controller = $this->kernel->getContainer()->get('wa72_jsonrpc.jsonrpccontroller'); 144 | $arg3 = new Testparameter('abc'); 145 | $arg3->setB('def'); 146 | $arg3->setC('ghi'); 147 | $requestdata = array( 148 | 'jsonrpc' => '2.0', 149 | 'id' => 'testParameterTypes', 150 | 'method' => 'wa72_jsonrpc.testservice:testParameterTypes', 151 | 'params' => array('arg1' => array(), 'arg2' => new \stdClass(), 'arg3' => $arg3) 152 | ); 153 | 154 | $response = $this->makeRequest($controller, $requestdata); 155 | $this->assertArrayNotHasKey('error', $response); 156 | $this->assertArrayHasKey('result', $response); 157 | $this->assertEquals('abcdefghi', $response['result']); 158 | } 159 | 160 | public function testAddMethod() 161 | { 162 | $requestdata = array( 163 | 'jsonrpc' => '2.0', 164 | 'id' => 'test', 165 | 'method' => 'testhi', 166 | 'params' => array('name' => 'Tom') 167 | ); 168 | // this request will fail because there is no such method "testhi" 169 | $response = $this->makeRequest($this->controller, $requestdata); 170 | $this->assertArrayHasKey('error', $response); 171 | $this->assertArrayNotHasKey('result', $response); 172 | $this->assertEquals(-32601, $response['error']['code']); 173 | 174 | // add the method definition for "testhi" 175 | $this->controller->addMethod('testhi', 'wa72_jsonrpc.testservice', 'hi'); 176 | 177 | // now the request should succeed 178 | $response = $this->makeRequest($this->controller, $requestdata); 179 | $this->assertArrayHasKey('result', $response); 180 | $this->assertArrayNotHasKey('error', $response); 181 | $this->assertEquals('Hi Tom!', $response['result']); 182 | } 183 | 184 | private function makeRequest($controller, $requestdata) 185 | { 186 | /** @var \JMS\Serializer\Serializer $serializer */ 187 | $serializer = $this->kernel->getContainer()->get('jms_serializer'); 188 | return json_decode($controller->execute( 189 | new Request(array(), array(), array(), array(), array(), array(), $serializer->serialize($requestdata, 'json')) 190 | )->getContent(), true); 191 | } 192 | 193 | /** 194 | * Shuts the kernel down if it was used in the test. 195 | */ 196 | protected function tearDown(): void 197 | { 198 | if (null !== $this->kernel) { 199 | $this->kernel->shutdown(); 200 | } 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /Tests/Testservice.php: -------------------------------------------------------------------------------- 1 | getA() . $arg3->getB() . $arg3->getC(); 29 | } 30 | } -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new JsonRpcExposablePass()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"wa72/jsonrpc-bundle", 3 | "description":"JSON-RPC server for Symfony: exposes services registered in the service container as JSON-RPC webservices", 4 | "keywords":["json", "rpc", "jsonrpc", "json-rpc", "webservice", "symfony", "symfony-bundle" ], 5 | "homepage":"http://github.com/wasinger/jsonrpc-bundle", 6 | "license":"MIT", 7 | "type": "symfony-bundle", 8 | "authors":[ 9 | { 10 | "name":"Christoph Singer", 11 | "email":"dev@ccss.de" 12 | } 13 | ], 14 | "require":{ 15 | "php":">=7.4", 16 | "ext-json": "*", 17 | "symfony/framework-bundle":"^4.2|^5|^6", 18 | "symfony/yaml": "^4|^5|^6", 19 | "jms/serializer-bundle":"^3.8|^4|^5" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^9", 23 | "doctrine/annotations": "^1.11", 24 | "jms/serializer":"^3.14" 25 | }, 26 | "autoload":{ 27 | "psr-0":{ 28 | "Wa72\\JsonRpcBundle":"" 29 | } 30 | }, 31 | "target-dir": "Wa72/JsonRpcBundle" 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./ 6 | 7 | 8 | ./Resources 9 | ./Tests 10 | ./vendor 11 | 12 | 13 | 14 | 15 | ./Tests/ 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------