Skip to content

Commit 9ceb41d

Browse files
committed
Remove dependency from JMS Serializer
make it possible to use Symfony serializer instead. Configuration changes: - option 'jms_serialization_context' is now 'serialization_context' - option 'max_depth_checks' is now 'enable_max_depth'
1 parent 1f2bb06 commit 9ceb41d

18 files changed

Lines changed: 497 additions & 172 deletions

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
strategy:
1717
matrix:
18-
php: [7.4, 8.0, 8.1, 8.2]
18+
php: [8.0, 8.1, 8.2, 8.3, 8.4]
1919
dependency-version: [prefer-lowest, prefer-stable]
2020

2121
steps:

Controller/JsonRpcController.php

Lines changed: 99 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace Wa72\JsonRpcBundle\Controller;
44

5+
use JMS\Serializer\SerializationContext as JMS_SerializationContext;
6+
use JMS\Serializer\SerializerInterface as JMS_SerializerInterface;
7+
use Symfony\Component\HttpFoundation\JsonResponse;
8+
use Symfony\Component\Serializer\SerializerInterface;
9+
use Symfony\Component\DependencyInjection\ContainerInterface;
510
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
611
use Symfony\Component\HttpFoundation\Request;
712
use Symfony\Component\HttpFoundation\Response;
8-
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
9-
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
1013

1114
/**
1215
* Controller for executing JSON-RPC 2.0 requests
@@ -42,9 +45,9 @@
4245
* @author Christoph Singer
4346
*
4447
*/
45-
class JsonRpcController implements ContainerAwareInterface
48+
class JsonRpcController
4649
{
47-
use ContainerAwareTrait;
50+
private ContainerInterface $container;
4851

4952
const PARSE_ERROR = -32700;
5053
const INVALID_REQUEST = -32600;
@@ -54,46 +57,50 @@ class JsonRpcController implements ContainerAwareInterface
5457

5558
/**
5659
* Functions that are allowed to be called
57-
*
58-
* @var array $functions
5960
*/
60-
private $functions = array();
61+
private array $functions = [];
6162

6263
/**
6364
* Array of names of fully exposed services (all methods of this services are allowed to be called)
64-
*
65-
* @var array $services
6665
*/
67-
private $services = array();
66+
private array $services = [];
6867

69-
/**
70-
* @var \JMS\Serializer\SerializationContext
71-
*/
72-
private $serializationContext;
68+
69+
private JMS_SerializerInterface|SerializerInterface $serializer;
70+
71+
72+
private JMS_SerializationContext|array $serializationContext = [];
7373

7474
/**
75-
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
75+
* @param ContainerInterface $container
7676
* @param array $config Associative array for configuration, expects at least a key "functions"
7777
* @throws \InvalidArgumentException
7878
*/
79-
public function __construct($container, $config)
79+
public function __construct(ContainerInterface $container, array $config)
8080
{
8181
if (isset($config['functions'])) {
8282
if (!is_array($config['functions'])) throw new \InvalidArgumentException('Configuration parameter "functions" must be array');
8383
$this->functions = $config['functions'];
8484
}
85-
$this->setContainer($container);
85+
$this->container = $container;
86+
if ($this->container->has('jms_serializer')) {
87+
$this->serializer = $this->container->get('jms_serializer');
88+
} elseif ($this->container->has('wa72_jsonrpc.serializer')) {
89+
$this->serializer = $this->container->get('wa72_jsonrpc.serializer');
90+
} else {
91+
throw new \InvalidArgumentException('No serializer service found in container. Please install jms/serializer-bundle or symfony/serializer.');
92+
}
8693
}
8794

8895
/**
8996
* @param Request $httprequest
9097
* @return Response
9198
*/
92-
public function execute(Request $httprequest)
99+
public function execute(Request $httprequest): Response
93100
{
94101
$json = $httprequest->getContent();
95102
$request = json_decode($json, true);
96-
$requestId = (isset($request['id']) ? $request['id'] : null);
103+
$requestId = ($request['id'] ?? null);
97104

98105
if ($request === null) {
99106
return $this->getErrorResponse(self::PARSE_ERROR, null);
@@ -119,9 +126,9 @@ public function execute(Request $httprequest)
119126
} catch (ServiceNotFoundException $e) {
120127
return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId);
121128
}
122-
$params = (isset($request['params']) ? $request['params'] : array());
129+
$params = ($request['params'] ?? []);
123130

124-
if (is_callable(array($service, $method))) {
131+
if (is_callable([$service, $method])) {
125132
$r = new \ReflectionMethod($service, $method);
126133
$rps = $r->getParameters();
127134

@@ -136,9 +143,8 @@ public function execute(Request $httprequest)
136143

137144
}
138145
if ($this->isAssoc($params)) {
139-
$newparams = array();
146+
$newparams = [];
140147
foreach ($rps as $i => $rp) {
141-
/* @var \ReflectionParameter $rp */
142148
$name = $rp->name;
143149
if (!isset($params[$rp->name]) && !$rp->isOptional()) {
144150
return $this->getErrorResponse(self::INVALID_PARAMS, $requestId,
@@ -156,37 +162,25 @@ public function execute(Request $httprequest)
156162
// correctly deserialize object parameters
157163
foreach ($params as $index => $param) {
158164
// 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
165+
// re-encode the array value to json and correctly decode it using the serializer.
166+
//
167+
// TODO: since PHP 8, the method type hints can include union types, so we need to handle those as well.
160168
if (is_array($param) && !$rps[$index]->isArray() && $rps[$index]->getClass() != null) {
161169
$class = $rps[$index]->getClass()->getName();
162-
$param = json_encode($param);
163-
$params[$index] = $this->container->get('jms_serializer')->deserialize($param, $class, 'json');
170+
$params[$index] = $this->deserialize(json_encode($param), $class);
164171
}
165172
}
166173

167174
try {
168-
$result = call_user_func_array(array($service, $method), $params);
175+
$result = call_user_func_array([$service, $method], $params);
169176
} catch (\Exception $e) {
170177
return $this->getErrorResponse(self::INTERNAL_ERROR, $requestId, $this->convertExceptionToErrorData($e));
171178
}
172-
173-
$response = array('jsonrpc' => '2.0');
179+
$response = ['jsonrpc' => '2.0'];
174180
$response['result'] = $result;
175181
$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'));
182+
$response = $this->serialize($response, $request['method']);
183+
return JsonResponse::fromJsonString($response);
190184
} else {
191185
return $this->getErrorResponse(self::METHOD_NOT_FOUND, $requestId);
192186
}
@@ -203,14 +197,13 @@ public function execute(Request $httprequest)
203197
*/
204198
public function addMethod($alias, $service, $method, $overwrite = false)
205199
{
206-
if (!isset($this->functions)) $this->functions = array();
207200
if (isset($this->functions[$alias]) && !$overwrite) {
208201
throw new \InvalidArgumentException('JsonRpcController: The function "' . $alias . '" already exists.');
209202
}
210-
$this->functions[$alias] = array(
203+
$this->functions[$alias] = [
211204
'service' => $service,
212205
'method' => $method
213-
);
206+
];
214207
}
215208

216209
/**
@@ -235,12 +228,12 @@ public function removeMethod($alias)
235228
}
236229
}
237230

238-
protected function convertExceptionToErrorData(\Exception $e)
231+
protected function convertExceptionToErrorData(\Exception $e): string
239232
{
240233
return $e->getMessage();
241234
}
242235

243-
protected function getError($code)
236+
protected function getError($code): array
244237
{
245238
$message = '';
246239
switch ($code) {
@@ -264,7 +257,7 @@ protected function getError($code)
264257
return array('code' => $code, 'message' => $message);
265258
}
266259

267-
protected function getErrorResponse($code, $id, $data = null)
260+
protected function getErrorResponse($code, $id, $data = null): JsonResponse
268261
{
269262
$response = array('jsonrpc' => '2.0');
270263
$response['error'] = $this->getError($code);
@@ -275,46 +268,82 @@ protected function getErrorResponse($code, $id, $data = null)
275268

276269
$response['id'] = $id;
277270

278-
return new Response(json_encode($response), 200, array('Content-Type' => 'application/json'));
271+
return new JsonResponse($response);
272+
}
273+
274+
/**
275+
* Serialize the return value of a method call to JSON.
276+
*/
277+
protected function serialize(mixed $data, string $rpc_method): string
278+
{
279+
return $this->serializer->serialize($data, 'json', $this->getSerializationContext($rpc_method));
280+
}
281+
282+
/**
283+
* Deserialize parameter values coming with the RPC request to the expected type.
284+
*/
285+
protected function deserialize(string $json, string $class): mixed
286+
{
287+
return $this->serializer->deserialize($json, $class, 'json');
279288
}
280289

281290
/**
282-
* Set SerializationContext for using with jms_serializer
291+
* Set SerializationContext
283292
*
284-
* @param \JMS\Serializer\SerializationContext $context
285293
*/
286-
public function setSerializationContext($context)
294+
public function setSerializationContext(array|JMS_SerializationContext $context): void
287295
{
296+
if ($this->serializer instanceof JMS_SerializerInterface && !($context instanceof JMS_SerializationContext)) {
297+
throw new \InvalidArgumentException('If jms_serializer is used, the SerializationContext must be an instance of JMS_SerializationContext');
298+
}
299+
if ($this->serializer instanceof SerializerInterface && !is_array($context)) {
300+
throw new \InvalidArgumentException('If symfony/serializer is used, the SerializationContext must be an array');
301+
}
288302
$this->serializationContext = $context;
289303
}
290304

291305
/**
292-
* Get SerializationContext or creates one if jms_serialization_context option is set
306+
* Get SerializationContext for a given rpc_method.
293307
*
294-
* @param array $functionConfig
295-
* @return \JMS\Serializer\SerializationContext
308+
* The context will be created from the configuration array for this method if available,
309+
* otherwise the default serialization context (set by $this->setSerializationContext()) will be used.
296310
*/
297-
protected function getSerializationContext(array $functionConfig)
311+
protected function getSerializationContext(string $rpc_method): JMS_SerializationContext|array
298312
{
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']);
313+
$functionConfig = $this->functions[$rpc_method] ?? [];
314+
if ($this->serializer instanceof JMS_SerializerInterface) {
315+
// legacy support for jms_serialization_context
316+
if (isset($functionConfig['jms_serialization_context'])) {
317+
$functionConfig['serialization_context'] = $functionConfig['jms_serialization_context'];
304318
}
305-
306-
if (isset($functionConfig['jms_serialization_context']['version'])) {
307-
$serializationContext->setVersion($functionConfig['jms_serialization_context']['version']);
319+
if (isset($functionConfig['serialization_context'])) {
320+
$context = JMS_SerializationContext::create();
321+
if (isset($functionConfig['serialization_context']['groups'])) {
322+
$context->setGroups($functionConfig['jms_serialization_context']['groups']);
323+
}
324+
if (isset($functionConfig['serialization_context']['version'])) {
325+
$context->setVersion($functionConfig['jms_serialization_context']['version']);
326+
}
327+
if (!empty($functionConfig['serialization_context']['max_depth_checks']) || !empty($functionConfig['serialization_context']['enable_max_depth'])) {
328+
$context->enableMaxDepthChecks();
329+
}
330+
} else {
331+
if ($this->serializationContext instanceof JMS_SerializationContext) {
332+
$context = $this->serializationContext;
333+
} else {
334+
$context = JMS_SerializationContext::create();
335+
}
308336
}
309-
310-
if (isset($functionConfig['jms_serialization_context']['max_depth_checks'])) {
311-
$serializationContext->enableMaxDepthChecks($functionConfig['jms_serialization_context']['max_depth_checks']);
337+
} elseif ($this->serializer instanceof SerializerInterface) {
338+
$context = $functionConfig['serialization_context'] ?? $this->serializationContext;
339+
if (!empty($context['max_depth_checks'])) { // legacy support for max_depth_checks
340+
$context['enable_max_depth'] = true;
312341
}
313342
} else {
314-
$serializationContext = $this->serializationContext;
343+
throw new \LogicException('No serializer service found in container. Please install jms/serializer-bundle or symfony/serializer.');
315344
}
316345

317-
return $serializationContext;
346+
return $context;
318347
}
319348

320349
/**

DependencyInjection/Compiler/JsonRpcExposablePass.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace Wa72\JsonRpcBundle\DependencyInjection\Compiler;
33

4+
use Symfony\Component\DependencyInjection\Alias;
45
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
56
use Symfony\Component\DependencyInjection\ContainerBuilder;
67
use Symfony\Component\DependencyInjection\Reference;
@@ -18,7 +19,18 @@ public function process(ContainerBuilder $container)
1819
$definition = $container->getDefinition('wa72_jsonrpc.jsonrpccontroller');
1920
$services = $container->findTaggedServiceIds('wa72_jsonrpc.exposable');
2021
foreach ($services as $service => $attributes) {
21-
$definition->addMethodCall('addService', array($service));
22+
$definition->addMethodCall('addService', [$service]);
23+
}
24+
25+
// Add an alias for the serializer service to make it public
26+
try {
27+
$serializer = $container->getDefinition('serializer');
28+
} catch (\Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException $e) {
29+
$serializer = null;
30+
}
31+
if ($serializer) {
32+
$a = new Alias('serializer', true);
33+
$container->setAlias('wa72_jsonrpc.serializer', $a);
2234
}
2335
}
2436
}

DependencyInjection/Configuration.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@
55
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
66
use Symfony\Component\Config\Definition\ConfigurationInterface;
77

8-
/**
9-
* This is the class that validates and merges configuration from your app/config files
10-
*
11-
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
12-
*/
138
class Configuration implements ConfigurationInterface
149
{
1510
/**
@@ -28,7 +23,28 @@ public function getConfigTreeBuilder(): TreeBuilder
2823
->children()
2924
->scalarNode('service')->end()
3025
->scalarNode('method')->end()
26+
->arrayNode('serialization_context')
27+
->children()
28+
->arrayNode('groups')
29+
->beforeNormalization()
30+
->ifTrue(function ($v) { return is_string($v); })
31+
->then(function ($v) { return array($v); })
32+
->end()
33+
->prototype('scalar')->end()
34+
->end()
35+
->scalarNode('version')->end()
36+
->booleanNode('max_depth_checks')->setDeprecated(
37+
'wa72/json-rpc-bundle', '0.8.0',
38+
'The "%node%" option is deprecated. Use "enable_max_depth" instead.'
39+
)->end()
40+
->booleanNode('enable_max_depth')->end()
41+
->end()
42+
->end()
3143
->arrayNode('jms_serialization_context')
44+
->setDeprecated(
45+
'wa72/json-rpc-bundle', '0.8.0',
46+
'The "%node%" option is deprecated. Use "serialization_context" instead.'
47+
)
3248
->children()
3349
->arrayNode('groups')
3450
->beforeNormalization()

DependencyInjection/Wa72JsonRpcExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Symfony\Component\DependencyInjection\ContainerBuilder;
66
use Symfony\Component\Config\FileLocator;
7+
use Symfony\Component\DependencyInjection\Definition;
78
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
89
use Symfony\Component\DependencyInjection\Loader;
10+
use Wa72\JsonRpcBundle\Controller\JsonRpcController;
911

1012
/**
1113
* This is the class that loads and manages your bundle configuration

0 commit comments

Comments
 (0)