22
33namespace 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 ;
510use Symfony \Component \DependencyInjection \Exception \ServiceNotFoundException ;
611use Symfony \Component \HttpFoundation \Request ;
712use 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
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 /**
0 commit comments