From 0a1b2841f7ca964551c8339ef031ae2fb84c50b5 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Thu, 28 Jan 2016 11:45:34 +0100 Subject: [PATCH 1/4] [WebProfiler] [HttpKernel] profile redirections closes #17501. The profiler stores the current request attributes in a current session when a `RedirectionResponse` is returned. So the next request profile will inherit the previous request attributes. The main profiler layout displays a shortcut to a previous redirection profile, along with some useful informations. The web debug toolbar shows a notifying icon, meaning a shortcut to a redirection profile is available in the request toolbar panel. --- .../views/Collector/request.html.twig | 98 ++++++++----- .../Resources/views/Icon/redirect.svg | 10 ++ .../Resources/views/Profiler/layout.html.twig | 24 ++++ .../views/Profiler/profiler.css.twig | 10 +- .../Resources/views/Profiler/toolbar.css.twig | 23 ++- .../DataCollector/RequestDataCollector.php | 134 ++++++++++++------ .../RequestDataCollectorTest.php | 11 +- 7 files changed, 224 insertions(+), 86 deletions(-) create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig index db842c5e0e64d..1b2545fa83b75 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig @@ -2,59 +2,72 @@ {% block toolbar %} {% set request_handler %} - {% if collector.controller.class is defined %} - {% set link = collector.controller.file|file_link(collector.controller.line) %} - {% if link %}{% else %}{% endif %} - - {{ collector.controller.class|abbr_class|striptags }} - - {%- if collector.controller.method -%} -  :: {{ collector.controller.method }} - {%- endif -%} - - {% if link %}{% else %}{% endif %} - {% else %} - {{ collector.controller }} - {% endif %} + {% import _self as helper %} + {{ helper.set_handler(collector.controller) }} {% endset %} + {% if collector.redirect %} + {% set redirect_handler %} + {% import _self as helper %} + {{ helper.set_handler(collector.redirect.controller, collector.redirect.route, 'GET' != collector.redirect.method ? collector.redirect.method) }} + {% endset %} + {% endif %} + {% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %} {% set icon %} {{ collector.statuscode }} {% if collector.route %} + {% if collector.redirect %}{{ include('@WebProfiler/Icon/redirect.svg') }}{% endif %} @ {{ collector.route }} {% endif %} {% endset %} {% set text %} -
- HTTP status - {{ collector.statuscode }} {{ collector.statustext }} -
+
+
+ HTTP status + {{ collector.statuscode }} {{ collector.statustext }} +
-
- Controller - {{ request_handler }} -
+
+ Controller + {{ request_handler }} +
+ + {% if collector.controller.class is defined -%} +
+ Controller class + {{ collector.controller.class }} +
+ {%- endif %} - {% if collector.controller.class is defined %}
- Controller class - {{ collector.controller.class }} + Route name + {{ collector.route|default('NONE') }}
- {% endif %} -
- Route name - {{ collector.route|default('NONE') }} +
+ Has session + {% if collector.sessionmetadata|length %}yes{% else %}no{% endif %} +
-
- Has session - {% if collector.sessionmetadata|length %}yes{% else %}no{% endif %} -
+ {% if redirect_handler is defined -%} +
+
+ + {{ collector.redirect.status_code }} + Redirect from + + + {{ redirect_handler }} + ({{ collector.redirect.token }}) + +
+
+ {% endif %} {% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} @@ -224,3 +237,22 @@ {% endif %}
{% endblock %} + +{% macro set_handler(controller, route, method) %} + {% if controller.class is defined -%} + {%- if method|default(false) %}{{ method }}{% endif -%} + {%- set link = controller.file|file_link(controller.line) %} + {%- if link %}{% else %}{% endif %} + + {%- if route|default(false) -%} + @{{ route }} + {%- else -%} + {{- controller.class|abbr_class|striptags -}} + {{- controller.method ? ' :: ' ~ controller.method -}} + {%- endif -%} + + {%- if link %}{% else %}{% endif %} + {%- else -%} + {{ route|default(controller) }} + {%- endif %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg new file mode 100644 index 0000000000000..9cb3b89bcfbdd --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig index e2bc8f8013443..b40253b7ad482 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig @@ -19,6 +19,30 @@ {% endif %} + {% if profile.collectors.request is defined and profile.collectors.request.redirect -%} + {%- set redirect = profile.collectors.request.redirect -%} + {%- set controller = redirect.controller -%} + {%- set redirect_route = '@' ~ redirect.route %} +
+
+ {{ redirect.status_code }} + Redirect from +
+
+ {{ 'GET' != redirect.method ? redirect.method }} + {% if redirect.controller.class is defined -%} + {%- set link = controller.file|file_link(controller.line) -%} + {% if link %}{% endif -%} + {{ redirect_route }} + {%- if link %}{% endif -%} + {%- else -%} + {{ redirect_route }} + {%- endif %} + ({{ redirect.token }}) +
+
+ {%- endif %} +
Method
{{ profile.method|upper }}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 9b6ba13d0908e..feabb5c8b062f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -483,11 +483,11 @@ tr.status-warning td { #summary .status-error { background: {{ colors.error|raw }}; } #summary .status-success h2, -#summary .status-success h2 a, +#summary .status-success a, #summary .status-warning h2, -#summary .status-warning h2 a, +#summary .status-warning a, #summary .status-error h2, -#summary .status-error h2 a { +#summary .status-error a { color: #FFF; } @@ -510,6 +510,10 @@ tr.status-warning td { margin: 0 1.5em 0 0; } +#summary dl.metadata .label { + background: rgba(255, 255, 255, 0.2); +} + {# Sidebar ========================================================================= #} #sidebar { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig index 723541fc8298a..6990f98fecbd1 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig @@ -36,7 +36,7 @@ .sf-toolbarreset { background-color: #222; bottom: 0; - box-shadow: 0 -1px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); color: #EEE; font: 11px Arial, sans-serif; left: 0; @@ -138,7 +138,7 @@ .sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status { padding: 2px 5px; - margin-bottom: 0px; + margin-bottom: 0; } .sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status + .sf-toolbar-status { margin-left: 4px; @@ -232,6 +232,16 @@ .sf-toolbar-block-request .sf-toolbar-info-piece a:hover { text-decoration: underline; } +.sf-toolbar-block-request .sf-toolbar-redirection-status { + font-weight: normal; + padding: 2px 4px; + line-height: 18px; +} +.sf-toolbar-block-request .sf-toolbar-info-piece span.sf-toolbar-redirection-method { + font-size: 12px; + height: 17px; + line-height: 17px; +} .sf-toolbar-status-green .sf-toolbar-label, .sf-toolbar-status-yellow .sf-toolbar-label, @@ -380,7 +390,7 @@ .sf-toolbarreset { bottom: auto; - box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); top: 0; } @@ -450,9 +460,12 @@ padding-left: 0; padding-right: 0; } - .sf-toolbar-block-request .sf-toolbar-status + .sf-toolbar-label { - margin-left: 4px; + .sf-toolbar-block-request .sf-toolbar-status { + margin-right: 5px; } + .sf-toolbar-block-request .sf-toolbar-icon svg + .sf-toolbar-label { + margin-left: 0; + } .sf-toolbar-block-request .sf-toolbar-label + .sf-toolbar-value { margin-right: 10px; } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php index 9a499a737ad02..75fd441ee1c9d 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php @@ -122,48 +122,25 @@ public function collect(Request $request, Response $response, \Exception $except } if (isset($this->controllers[$request])) { - $controller = $this->controllers[$request]; - if (is_array($controller)) { - try { - $r = new \ReflectionMethod($controller[0], $controller[1]); - $this->data['controller'] = array( - 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], - 'method' => $controller[1], - 'file' => $r->getFileName(), - 'line' => $r->getStartLine(), - ); - } catch (\ReflectionException $e) { - if (is_callable($controller)) { - // using __call or __callStatic - $this->data['controller'] = array( - 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], - 'method' => $controller[1], - 'file' => 'n/a', - 'line' => 'n/a', - ); - } - } - } elseif ($controller instanceof \Closure) { - $r = new \ReflectionFunction($controller); - $this->data['controller'] = array( - 'class' => $r->getName(), - 'method' => null, - 'file' => $r->getFileName(), - 'line' => $r->getStartLine(), - ); - } elseif (is_object($controller)) { - $r = new \ReflectionClass($controller); - $this->data['controller'] = array( - 'class' => $r->getName(), - 'method' => null, - 'file' => $r->getFileName(), - 'line' => $r->getStartLine(), - ); - } else { - $this->data['controller'] = (string) $controller ?: 'n/a'; - } + $this->data['controller'] = $this->parseController($this->controllers[$request]); unset($this->controllers[$request]); } + + if ($request->hasSession() && $request->getSession()->has('sf_redirect')) { + $this->data['redirect'] = $request->getSession()->get('sf_redirect'); + $request->getSession()->remove('sf_redirect'); + } + + if ($request->hasSession() && $response->isRedirect()) { + $request->getSession()->set('sf_redirect', array( + 'token' => $response->headers->get('x-debug-token'), + 'route' => $request->attributes->get('_route', 'n/a'), + 'method' => $request->getMethod(), + 'controller' => $this->parseController($request->attributes->get('_controller')), + 'status_code' => $statusCode, + 'status_text' => Response::$statusTexts[(int) $statusCode], + )); + } } public function getPathInfo() @@ -276,15 +253,27 @@ public function getRouteParams() } /** - * Gets the controller. + * Gets the parsed controller. * - * @return string The controller as a string + * @return array|string The controller as a string or array of data + * with keys 'class', 'method', 'file' and 'line' */ public function getController() { return $this->data['controller']; } + /** + * Gets the previous request attributes. + * + * @return array|bool A legacy array of data from the previous redirection response + * or false otherwise + */ + public function getRedirect() + { + return isset($this->data['redirect']) ? $this->data['redirect'] : false; + } + public function onKernelController(FilterControllerEvent $event) { $this->controllers[$event->getRequest()] = $event->getController(); @@ -339,4 +328,65 @@ private function getCookieHeader($name, $value, $expires, $path, $domain, $secur return $cookie; } + + /** + * Parse a controller. + * + * @param mixed $controller The controller to parse + * + * @return array|string An array of controller data or a simple string + */ + private function parseController($controller) + { + if (is_string($controller) && false !== strpos($controller, '::')) { + $controller = explode('::', $controller); + } + + if (is_array($controller)) { + try { + $r = new \ReflectionMethod($controller[0], $controller[1]); + + return array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } catch (\ReflectionException $e) { + if (is_callable($controller)) { + // using __call or __callStatic + return array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => 'n/a', + 'line' => 'n/a', + ); + } + } + } + + if ($controller instanceof \Closure) { + $r = new \ReflectionFunction($controller); + + return array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } + + if (is_object($controller)) { + $r = new \ReflectionClass($controller); + + return array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } + + return (string) $controller ?: 'n/a'; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index 2eb1c41e8dda8..4234b90832d6a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -66,7 +66,7 @@ public function testControllerInspection() '"Regular" callable', array($this, 'testControllerInspection'), array( - 'class' => 'Symfony\Component\HttpKernel\Tests\DataCollector\RequestDataCollectorTest', + 'class' => __NAMESPACE__.'\RequestDataCollectorTest', 'method' => 'testControllerInspection', 'file' => __FILE__, 'line' => $r1->getStartLine(), @@ -86,8 +86,13 @@ function () { return 'foo'; }, array( 'Static callback as string', - 'Symfony\Component\HttpKernel\Tests\DataCollector\RequestDataCollectorTest::staticControllerMethod', - 'Symfony\Component\HttpKernel\Tests\DataCollector\RequestDataCollectorTest::staticControllerMethod', + __NAMESPACE__.'\RequestDataCollectorTest::staticControllerMethod', + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\DataCollector\RequestDataCollectorTest', + 'method' => 'staticControllerMethod', + 'file' => __FILE__, + 'line' => $r2->getStartLine(), + ), ), array( From 227ac77f11ae8c420d486891f7e5138ca470997a Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Wed, 24 Feb 2016 06:07:15 +0100 Subject: [PATCH 2/4] [WebProfilerBundle] [FrameworkBundle] profile forward controller action closes #14358 The `Symfony\Bundle\FrameworkBundle\Controller\Controller::forward()` now passes request attributes to its sub request, so when the profiler will profile the sub request it will add its token to its hold by reference parent request attributes. The profiler main profiler layout displays a shortcut to the forwarded sub request profile which is actually responsible for returning the present response. The web debug toolbar shows a notifying icon, meaning a shortcut to a forwarded profile is available in the toolbar request panel. --- .../FrameworkBundle/Controller/Controller.php | 4 ++- .../views/Collector/request.html.twig | 20 +++++++++++++ .../Resources/views/Icon/forward.svg | 4 +++ .../Resources/views/Profiler/layout.html.twig | 21 ++++++++++++-- .../DataCollector/RequestDataCollector.php | 29 +++++++++++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index a0c3e6e19d4bf..db436a03c40be 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -65,8 +65,10 @@ protected function generateUrl($route, $parameters = array(), $referenceType = U */ protected function forward($controller, array $path = array(), array $query = array()) { + $request = $this->container->get('request_stack')->getCurrentRequest(); + $path['_forwarded'] = $request->attributes; $path['_controller'] = $controller; - $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate($query, null, $path); + $subRequest = $request->duplicate($query, null, $path); return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig index 1b2545fa83b75..9a7ab39db4bfa 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig @@ -13,12 +13,20 @@ {% endset %} {% endif %} + {% if collector.forward %} + {% set forward_handler %} + {% import _self as helper %} + {{ helper.set_handler(collector.forward.controller) }} + {% endset %} + {% endif %} + {% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %} {% set icon %} {{ collector.statuscode }} {% if collector.route %} {% if collector.redirect %}{{ include('@WebProfiler/Icon/redirect.svg') }}{% endif %} + {% if collector.forward %}{{ include('@WebProfiler/Icon/forward.svg') }}{% endif %} @ {{ collector.route }} {% endif %} @@ -68,6 +76,18 @@ {% endif %} + + {% if forward_handler is defined %} +
+
+ Forwarded to + + {{ forward_handler }} + ({{ collector.forward.token }}) + +
+
+ {% endif %} {% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg new file mode 100644 index 0000000000000..acb128509a9f5 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig index b40253b7ad482..7193158b2b802 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig @@ -19,8 +19,9 @@ {% endif %} - {% if profile.collectors.request is defined and profile.collectors.request.redirect -%} - {%- set redirect = profile.collectors.request.redirect -%} + {% set request_collector = profile.collectors.request|default(false) %} + {% if request_collector is defined and request_collector.redirect -%} + {%- set redirect = request_collector.redirect -%} {%- set controller = redirect.controller -%} {%- set redirect_route = '@' ~ redirect.route %} {%- endif %} + {% if request_collector and request_collector.forward and request_collector.forward.controller.class is defined -%} + {%- set forward = request_collector.forward -%} + {%- set controller = forward.controller -%} + + {%- endif %} +