📜 ⬆️ ⬇️

Symfony 2 internals in practice

The post is inspired by this issue. We will use standard symfony events to override the controller output. So, in general, it will all work:
  1. Create an Ajax annotation to handle the controller content type
  2. We will process this annotation through events
  3. We will override the content type in accordance with the selected type in the annotation

I'll warn you right away that the code does not pretend to be perfect, caching is not used (I will say more about it later), but I think the main idea will be clear. You can also read more about Symfony2 Internals in the official documentation .

So let's break.
First, let's define the annotation class:
namespace SomeNamespace\SomeBundle\Annotations; /** @Annotation */ class Ajax { /** * @var array @contentType */ public $contentType; /** * @var array @parameters */ public $parameters; public function __construct($data) { if (isset($data['value'])) { $this->contentType = $data['value']; } if (isset($data['parameters'])) { $this->parameters = $data['parameters']; } } /** * @param array $contentType */ public function setContentType($contentType) { $this->contentType = $contentType; } /** * @return array */ public function getContentType() { return $this->contentType; } } 

This annotation determines the type of content returned by the controller.
Next, create an event listener:
 namespace SomeNamespace\SomeBundle\Event; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Doctrine\Common\Annotations\Reader; use Symfony\Component\HttpFoundation\Response; /** * Controller Event listener */ class ControllerListener { /** * @var ServiceContainer */ private $container; /** * Parameters of Event Listener * * @var array */ private $parameters; /** * @var AnnotationsReader */ private $annotationReader; //     -   Core/ContentTypes public function __construct($c, $a) { $this->container = $c; $this->annotationReader = $a; //@TODO   ,        . ,    -      ,   . $classes = array(); $namespace = 'SomeNamespace\SomeBundle'; $namespace = str_replace('\\', '/', $namespace); $dir = opendir('../src/' . $namespace . '/Core/ContentTypes'); while ($classes[] = str_replace('.php', '', readdir($dir))) { ; } foreach ($classes as $key => $class) { if ($class == '') { unset($classes[$key]); continue; } if ($class[0] == '.') { unset($classes[$key]); } } $this->parameters['contentTypes'] = $classes; } /** * Controller event listener * * @param \Symfony\Component\HttpKernel\Event\KernelEvent $event */ public function onKernelController(KernelEvent $event) {//      .     .    ,     ,    ,        $controller = $event->getController(); $object = new \ReflectionObject($controller[0]); $method = $object->getMethod($controller[1]); $annotations = $this->annotationReader->getMethodAnnotations($method); $response = new Response(); $this->parameters['attributes'] = $event->getRequest()->attributes; foreach ($annotations as $annotation) { if ($annotation instanceof \ITE\JSBundle\Annotations\Ajax) { $this->parameters['annotation'] = $annotation; } } $class = NULL; $params = array(); if (isset($this->parameters['annotation'])) { if (isset($this->parameters['annotation']->parameters)) { $params = $this->parameters['annotation']->parameters; } foreach ($this->parameters['contentTypes'] as $contentType) { $className = '\ITE\JSBundle\Core\ContentTypes\\' . $contentType; $name = $className::getName(); if ($name == $this->parameters['annotation']->contentType) { $class = $className; } } if (!$class) { throw new \ITE\JSBundle\Core\Exception\ContentTypeException( 'ContentType "' . $this->parameters['annotation']->contentType . '" is not found!'); } //  -    .     . $contentType = new $class($this->container, $params); $this->parameters['contentType'] = $contentType; $contentType->hookPre($event->getRequest()); } } /** * Controller Response listener * * @param $event */ public function onKernelResponse($event) {//       .     javascript  , ,    Symfony Profiler.        $response = $event->getResponse(); $response = $this->addJavascript($response); $event->setResponse($response); } /** * Controller Request listener * * @param $event */ public function onKernelRequest($event) { //    .      $this->generateRoutes(); } /** * Controller response listener * * @param GetResponseForControllerResultEvent $event */ public function onKernelView(GetResponseForControllerResultEvent $event) { //     .    onKernelResponse if (isset($this->parameters['contentType'])) { $contentType = $this->parameters['contentType']; $response = new Response; $response->setContent($contentType->encodeParameters($event->getControllerResult())); $response = $contentType->hookPost($response); $event->setResponse($response); } } /** * Generating route array and move to javascript file */ private function generateRoutes() { //  ,       $routeCollection = $this->container->get('router')->getRouteCollection(); $routes = array(); foreach ($routeCollection->all() as $route) { $r = array(); $defaults = $route->getDefaults(); try { $method = new \ReflectionMethod($defaults['_controller']); } catch (\Exception $e) { continue; } $ann = $this->annotationReader->getMethodAnnotations($method); foreach ($ann as $a) { if ($a instanceof \Sensio\Bundle\FrameworkExtraBundle\Configuration\Route) { $r[$a->getName()] = $route->getPattern(); } } $routes += $r; } $path = __FILE__; $path = str_replace('Event' . DIRECTORY_SEPARATOR . 'ControllerListener.php', '', $path); $path .= 'Resources' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'routing_template.js'; $content = file_get_contents($path); $route_string = json_encode($routes); $content = str_replace('__routes__', $route_string, $content); $kernel = $this->container->get('kernel'); $params = array( 'env' => $kernel->getEnvironment(), 'debug' => $kernel->isDebug(), 'name' => $kernel->getName(), 'startTime' => $kernel->getStartTime(), ); $content = str_replace('__params__', json_encode($params), $content); $path = str_replace('routing_template', 'routing', $path); file_put_contents($path, $content); } /** * Adding global Symfony javascript * * @param $response * * @return mixed */ private function addJavascript($response) {//       $content = $response->getContent(); $arr = explode('</head>', $content); if (count($arr) == 1) { return $response; } $twig = $this->container->get('templating'); $c = $twig->render('SomeNamespaceSomeBundle:Javascript:js.html.twig'); $content = $arr[0] . $c . "</head>" . $arr[1]; $response->setContent($content); return $response; } } 

And register it in the system:
 #SomeBundle\Resources\config\services.yml services: my.ajax.listener: class: "SomeNamespace\SomeBundle\Event\ControllerListener" tags: [{name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -128}, {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}, {name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: -128}, {name: kernel.event_listener, event: kernel.controller, method: onKernelController}] arguments: [@service_container, @annotation_reader] 

Notice another argument: priority. It sets the priority of the event. To give an example, Drupal comes to my mind. This is an analogue of the weight of the module, only the opposite. In Drupal, the more weight, the later the hook will be called. And in symfony, the higher the priority, the earlier the event will be called.

So, how does the structure of each content-type look like:
First, create an interface:

 namespace SomeNamespace\SomeBundle\Core; interface ContentTypeInterface { /** * Get the name of ContentType * @abstract * @return mixed */ public static function getName(); /** * Encoder * @abstract * @param $data * @return mixed */ public function encodeParameters($data); /** * Decoder * @abstract * @param $data * @return mixed */ public function decodeParameters($data); /** * Prepares request * @abstract * @param Request * @return mixed */ public function hookPre($request); /** * Changes response * @abstract * @param Response * @return mixed */ public function hookPost($response); } 

And now I will tell in more detail:

Next, we will create a class that will implement our interface, from which all content-types will be inherited:
 namespace SomeNamespace\SomeBundle\Core; class ContentType implements ContentTypeInterface { /** * @var ServiceContainer */ protected $container; /** * @var array parameters */ protected $parameters; /** * Public constructor * @param $container */ public function __construct($container, $params = array()){ $this->container = $container; $this->parameters = $params; } /** * Get the name of ContentType * @return mixed */ public static function getName() { return 'contentType'; } /** * Encoder * @param $data * @return mixed */ public function encodeParameters($data) { return $data; } /** * Decoder * @param $data * @return mixed */ public function decodeParameters($data) { return $data; } /** * Prepares request * @param $data * @return mixed */ public function hookPre($request) { } /** * Changes response * @param $data * @return mixed */ public function hookPost($response) { return $response; } } 

As you can see, it implements the ContentTypeInterface interface.
Now you can create your own content-types, for example, I will give my content-type json:
 namespace SomeNamespace\SomeBundle\Core\ContentTypes; use SomeNamespace\SomeBundle\Core\ContentType; class JSONContentType extends ContentType { private $params; /** * Get the name of ContentType * @return mixed */ public static function getName() { return "json"; } /** * Changes response * @param $data * @return mixed */ public function hookPost($response) { return $response; } /** * Encoder * @param $data * @return mixed */ public function encodeParameters($data) { return json_encode($data); } /** * Decoder * @param $data * @return mixed */ public function decodeParameters($data) { return json_decode($data); } } 

')
And finally, here’s the javascript code that is used to generate routes and parameters:

 //SomeBundle\Resources\js\routing_template.js (function(){if(typeof SF!='undefined'){SF.sSet('routes',__routes__);SF.parameters = __params__;}})(); 

And also javascript which all this business saves and uses:
 (function () { SF = function () { }; SF.prototype.fn = SF.prototype; SF = new SF(); SF.fn.Storage = {}; SF.fn.hasValue = function (name) { return this.Storage[name] !== undefined; }; SF.fn.getValue = function (name) { if (this.hasValue(name)) { return this.Storage[name]; } else { return void 0; } }; SF.fn.getAllValues = function () { return this.Storage }; SF.fn.loggingEnabled = function () { return this.parameters.debug; }; SF.fn.messagingEnabled = function () { return this.parameters.messaging !== undefined && this.parameters.messaging; }; SF.fn.getMessages = function () { return !this.framework || this.framework.messaging === undefined ? { } : this.framework.messaging; }; // framework SF.fn.getLocation = function (name) { if (this.hasLocation(name)) { return this.framework.ajax[name]; } else { return void 0; } }; SF.fn.hasLocation = function (name) { return this.framework !== null && this.framework.ajax !== undefined && this.framework.ajax[name] !== undefined; }; // Storage setter and getter SF.fn.sSet = function (key, val) { this.Storage[key] = val; }; SF.fn.sGet = function (key) { return this.Storage[key] ? this.Storage[key] : null; }; // log function with debug checking SF.fn.l = function (a, b) { if (!b) b = 'log'; if (this.parameters.debug) { switch (b) { case 'log': console.log('[SF]: ', a); break; case 'info': console.info('[SF]: ', a); break; case 'warning': console.warn('[SF]: ', a); break; case 'error': console.error('[SF]: ', a); break; } } }; // SF path function SF.fn.path = function (name, arguments) { if (this.Storage.routes[name]) { var path = this.Storage.routes[name]; for (var a in arguments) { path = path.replace('{' + a + '}', arguments[a]); } return path; } else { this.l('Route "' + name + '" is not found!', 'error'); return false; } }; })(window); 

Well, now, the most interesting is an example of work. Create an action in the controller:
 //   use  ,  Symfony    /** * * @param key string * @Route("/ajax/{key}", name="JSBundle_ajax") * @Ajax("json") * @return array */ public function ajaxAction($key) { //do some work return array('a' => 'b', 'd' => 'c'); } 

The controller's response will be:
 { a: "b", d: "c" } 

Also, an example for javascript:
 SF.l(SF.path('JSBundle_ajax', {'key': 'asd'})); 

If debug is disabled in your symfony, then nothing will be printed to the console, otherwise it will print:
/ ajax / asd

PS additions are welcome. I will be glad to hear clever thoughts.

Source: https://habr.com/ru/post/149378/


All Articles