/** * {@inheritdoc} */ public function getTitle(Request $request, Route $route) { $route_title = NULL; // A dynamic title takes priority. Route::getDefault() returns NULL if the // named default is not set. By testing the value directly, we also avoid // trying to use empty values. if ($callback = $route->getDefault('_title_callback')) { $callable = $this->controllerResolver->getControllerFromDefinition($callback); $arguments = $this->controllerResolver->getArguments($request, $callable); $route_title = call_user_func_array($callable, $arguments); } elseif ($title = $route->getDefault('_title')) { $options = array(); if ($context = $route->getDefault('_title_context')) { $options['context'] = $context; } $args = array(); if ($raw_parameters = $request->attributes->get('_raw_variables')) { foreach ($raw_parameters->all() as $key => $value) { $args['@' . $key] = $value; $args['%' . $key] = $value; } } if ($title_arguments = $route->getDefault('_title_arguments')) { $args = array_merge($args, (array) $title_arguments); } // Fall back to a static string from the route. $route_title = $this->t($title, $args, $options); } return $route_title; }
/** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Exception $exception = NULL) { parent::collect($request, $response, $exception); $controller = $this->controllerResolver->getController($request); $this->data['controller'] = $this->getMethodData($controller[0], $controller[1]); $this->data['access_check'] = $this->accessCheck; }
/** * Checks access for the account and route using the custom access checker. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match object to be checked. * @param \Drupal\Core\Session\AccountInterface $account * The account being checked. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { $callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access')); $arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account); $arguments = $arguments_resolver->getArguments($callable); return call_user_func_array($callable, $arguments); }
/** * Returns the result of invoking the sub-controller. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param mixed $controller_definition * A controller definition string, or a callable object/closure. * * @return mixed * The result of invoking the controller. Render arrays, strings, HtmlPage, * and HtmlFragment objects are possible. */ public function getContentResult(Request $request, $controller_definition) { if ($controller_definition instanceof \Closure) { $callable = $controller_definition; } else { $callable = $this->controllerResolver->getControllerFromDefinition($controller_definition); } $arguments = $this->controllerResolver->getArguments($request, $callable); $page_content = call_user_func_array($callable, $arguments); return $page_content; }
/** * Sets a response given a (main content) render array. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event * The event to process. */ public function onViewRenderArray(GetResponseForControllerResultEvent $event) { $request = $event->getRequest(); $result = $event->getControllerResult(); // Render the controller result into a response if it's a render array. if (is_array($result) && ($request->query->has(static::WRAPPER_FORMAT) || $request->getRequestFormat() == 'html')) { $wrapper = $request->query->get(static::WRAPPER_FORMAT, 'html'); // Fall back to HTML if the requested wrapper envelope is not available. $wrapper = isset($this->mainContentRenderers[$wrapper]) ? $wrapper : 'html'; $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]); $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch)); } }
/** * Tests a YAML file containing both static permissions and a callback. */ public function testPermissionsYamlStaticAndCallback() { vfsStreamWrapper::register(); $root = new vfsStreamDirectory('modules'); vfsStreamWrapper::setRoot($root); $this->moduleHandler = $this->getMock('Drupal\\Core\\Extension\\ModuleHandlerInterface'); $this->moduleHandler->expects($this->once())->method('getModuleDirectories')->willReturn(array('module_a' => vfsStream::url('modules/module_a'))); $url = vfsStream::url('modules'); mkdir($url . '/module_a'); file_put_contents($url . '/module_a/module_a.permissions.yml', "'access module a':\n title: 'Access A'\n description: 'bla bla'\npermission_callbacks:\n - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'\n"); $modules = array('module_a'); $extensions = array('module_a' => $this->mockModuleExtension('module_a', 'Module a')); $this->moduleHandler->expects($this->any())->method('getImplementations')->with('permission')->willReturn(array()); $this->moduleHandler->expects($this->any())->method('getModuleList')->willReturn(array_flip($modules)); $this->controllerResolver->expects($this->once())->method('getControllerFromDefinition')->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')->willReturn(array(new TestPermissionCallbacks(), 'titleDescription')); $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver); // Setup system_rebuild_module_data(). $this->permissionHandler->setSystemRebuildModuleData($extensions); $actual_permissions = $this->permissionHandler->getPermissions(); $this->assertCount(2, $actual_permissions); $this->assertEquals($actual_permissions['access module a']['title'], 'Access A'); $this->assertEquals($actual_permissions['access module a']['provider'], 'module_a'); $this->assertEquals($actual_permissions['access module a']['description'], 'bla bla'); $this->assertEquals($actual_permissions['access module b']['title'], 'Access B'); $this->assertEquals($actual_permissions['access module b']['provider'], 'module_a'); $this->assertEquals($actual_permissions['access module b']['description'], 'bla bla'); }
/** * {@inheritdoc} */ public function rebuild() { if ($this->building) { throw new \RuntimeException('Recursive router rebuild detected.'); } if (!$this->lock->acquire('router_rebuild')) { // Wait for another request that is already doing this work. // We choose to block here since otherwise the routes might not be // available, resulting in a 404. $this->lock->wait('router_rebuild'); return FALSE; } $this->building = TRUE; $collection = new RouteCollection(); foreach ($this->getRouteDefinitions() as $routes) { // The top-level 'routes_callback' is a list of methods in controller // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods // should return a set of \Symfony\Component\Routing\Route objects, either // in an associative array keyed by the route name, which will be iterated // over and added to the collection for this provider, or as a new // \Symfony\Component\Routing\RouteCollection object, which will be added // to the collection. if (isset($routes['route_callbacks'])) { foreach ($routes['route_callbacks'] as $route_callback) { $callback = $this->controllerResolver->getControllerFromDefinition($route_callback); if ($callback_routes = call_user_func($callback)) { // If a RouteCollection is returned, add the whole collection. if ($callback_routes instanceof RouteCollection) { $collection->addCollection($callback_routes); } else { foreach ($callback_routes as $name => $callback_route) { $collection->add($name, $callback_route); } } } } unset($routes['route_callbacks']); } foreach ($routes as $name => $route_info) { $route_info += array('defaults' => array(), 'requirements' => array(), 'options' => array()); $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options']); $collection->add($name, $route); } } // DYNAMIC is supposed to be used to add new routes based upon all the // static defined ones. $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection)); // ALTER is the final step to alter all the existing routes. We cannot stop // people from adding new routes here, but we define two separate steps to // make it clear. $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection)); $this->checkProvider->setChecks($collection); $this->dumper->addRoutes($collection); $this->dumper->dump(); $this->lock->release('router_rebuild'); $this->dispatcher->dispatch(RoutingEvents::FINISHED, new Event()); $this->building = FALSE; $this->rebuildNeeded = FALSE; return TRUE; }
/** * {@inheritdoc} */ public function getTitle(LocalTaskInterface $local_task) { $controller = array($local_task, 'getTitle'); $request = $this->requestStack->getCurrentRequest(); $arguments = $this->controllerResolver->getArguments($request, $controller); return call_user_func_array($controller, $arguments); }
/** * Tests the rebuild with routes provided by a callback. * * @see \Drupal\Core\Routing\RouteBuilder::rebuild() */ public function testRebuildWithProviderBasedRoutes() { $this->lock->expects($this->once())->method('acquire')->with('router_rebuild')->will($this->returnValue(TRUE)); $this->yamlDiscovery->expects($this->once())->method('findAll')->will($this->returnValue(array('test_module' => array('route_callbacks' => array('\\Drupal\\Tests\\Core\\Routing\\TestRouteSubscriber::routesFromArray', 'test_module.route_service:routesFromCollection'))))); $container = new ContainerBuilder(); $container->set('test_module.route_service', new TestRouteSubscriber()); $this->controllerResolver->expects($this->any())->method('getControllerFromDefinition')->will($this->returnCallback(function ($controller) use($container) { $count = substr_count($controller, ':'); if ($count == 1) { list($service, $method) = explode(':', $controller, 2); $object = $container->get($service); } else { list($class, $method) = explode('::', $controller, 2); $object = new $class(); } return array($object, $method); })); $route_collection_filled = new RouteCollection(); $route_collection_filled->add('test_route.1', new Route('/test-route/1')); $route_collection_filled->add('test_route.2', new Route('/test-route/2')); $route_build_event = new RouteBuildEvent($route_collection_filled); // Ensure that the alter routes events are fired. $this->dispatcher->expects($this->at(0))->method('dispatch')->with(RoutingEvents::DYNAMIC, $route_build_event); $this->dispatcher->expects($this->at(1))->method('dispatch')->with(RoutingEvents::ALTER, $route_build_event); // Ensure that the routes are set to the dumper and dumped. $this->dumper->expects($this->at(0))->method('addRoutes')->with($route_collection_filled); $this->dumper->expects($this->at(1))->method('dump'); $this->assertTrue($this->routeBuilder->rebuild()); }
/** * Invokes the form and returns the result. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * * @return array * The render array that results from invoking the controller. */ public function getContentResult(Request $request) { $form_object = $this->getFormObject($request, $this->formDefinition); // Add the form and form_state to trick the getArguments method of the // controller resolver. $form_state = array(); $request->attributes->set('form', array()); $request->attributes->set('form_state', $form_state); $args = $this->controllerResolver->getArguments($request, array($form_object, 'buildForm')); $request->attributes->remove('form'); $request->attributes->remove('form_state'); // Remove $form and $form_state from the arguments, and re-index them. unset($args[0], $args[1]); $form_state['build_info']['args'] = array_values($args); return $this->formBuilder->buildForm($form_object, $form_state); }
/** * Sets a response given a (main content) render array. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event * The event to process. */ public function onViewRenderArray(GetResponseForControllerResultEvent $event) { $request = $event->getRequest(); $result = $event->getControllerResult(); $format = $request->getRequestFormat(); // Render the controller result into a response if it's a render array. if (is_array($result)) { if (isset($this->mainContentRenderers[$format])) { $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$format]); $event->setResponse($renderer->renderResponse($result, $request, $this->routeMatch)); } else { $supported_formats = array_keys($this->mainContentRenderers); $supported_mimetypes = array_map([$request, 'getMimeType'], $supported_formats); $event->setResponse(new JsonResponse(['message' => 'Not Acceptable.', 'supported_mime_types' => $supported_mimetypes], 406)); } } }
/** * Invokes the form and returns the result. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The route match. * * @return array * The render array that results from invoking the controller. */ public function getContentResult(Request $request, RouteMatchInterface $route_match) { $form_arg = $this->getFormArgument($route_match); $form_object = $this->getFormObject($route_match, $form_arg); // Add the form and form_state to trick the getArguments method of the // controller resolver. $form_state = new FormState(); $request->attributes->set('form', []); $request->attributes->set('form_state', $form_state); $args = $this->controllerResolver->getArguments($request, [$form_object, 'buildForm']); $request->attributes->remove('form'); $request->attributes->remove('form_state'); // Remove $form and $form_state from the arguments, and re-index them. unset($args[0], $args[1]); $form_state->addBuildInfo('args', array_values($args)); return $this->formBuilder->buildForm($form_object, $form_state); }
/** * Test the access method. */ public function testAccess() { $request = new Request(array()); $this->controllerResolver->expects($this->at(0))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessDeny')->will($this->returnValue(array(new TestController(), 'accessDeny'))); $this->argumentsResolver->expects($this->at(0))->method('getArguments')->will($this->returnValue(array())); $this->controllerResolver->expects($this->at(1))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessAllow')->will($this->returnValue(array(new TestController(), 'accessAllow'))); $this->argumentsResolver->expects($this->at(1))->method('getArguments')->will($this->returnValue(array())); $this->controllerResolver->expects($this->at(2))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessParameter')->will($this->returnValue(array(new TestController(), 'accessParameter'))); $this->argumentsResolver->expects($this->at(2))->method('getArguments')->will($this->returnValue(array('parameter' => 'TRUE'))); $route = new Route('/test-route', array(), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessDeny')); $account = $this->getMock('Drupal\\Core\\Session\\AccountInterface'); $this->assertSame(AccessInterface::DENY, $this->accessChecker->access($route, $request, $account)); $route = new Route('/test-route', array(), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessAllow')); $this->assertSame(AccessInterface::ALLOW, $this->accessChecker->access($route, $request, $account)); $route = new Route('/test-route', array('parameter' => 'TRUE'), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessParameter')); $this->assertSame(AccessInterface::ALLOW, $this->accessChecker->access($route, $request, $account)); }
/** * Tests a dynamic title. * * @see \Drupal\Core\Controller\TitleResolver::getTitle() */ public function testDynamicTitle() { $request = new Request(); $route = new Route('/test-route', array('_title' => 'static title', '_title_callback' => 'Drupal\\Tests\\Core\\Controller\\TitleCallback::example')); $callable = array(new TitleCallback(), 'example'); $this->controllerResolver->expects($this->once())->method('getControllerFromDefinition')->with('Drupal\\Tests\\Core\\Controller\\TitleCallback::example')->will($this->returnValue($callable)); $this->controllerResolver->expects($this->once())->method('getArguments')->with($request, $callable)->will($this->returnValue(array('example'))); $this->assertEquals('test example', $this->titleResolver->getTitle($request, $route)); }
/** * Ensures bubbleable metadata from early rendering is not lost. * * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event * The controller event. */ public function onController(FilterControllerEvent $event) { $controller = $event->getController(); // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw(). $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller); $event->setController(function () use($controller, $arguments) { return $this->wrapControllerExecutionInRenderContext($controller, $arguments); }); }
/** * Creates a controller instance using route defaults. * * By design we cannot support all possible routes, but just the ones which * use the defaults provided by core, which are _content, _controller * and _form. * * @param array $defaults * The default values provided by the route. * * @return array|null * Returns the controller instance if it is possible to instantiate it, NULL */ protected function getController(array $defaults) { $controller = NULL; if (isset($defaults['_content'])) { $controller = $this->controllerResolver->getControllerFromDefinition($defaults['_content']); } if (isset($defaults['_controller'])) { $controller = $this->controllerResolver->getControllerFromDefinition($defaults['_controller']); } if (isset($defaults['_form'])) { $form_arg = $defaults['_form']; // Check if the class exists first as the class resolver will throw an // exception if it doesn't. This also means a service cannot be used here. if (class_exists($form_arg)) { $controller = array($this->classResolver->getInstanceFromDefinition($form_arg), 'buildForm'); } } return $controller; }
/** * Sets a response given a (main content) render array. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event * The event to process. */ public function onViewRenderArray(GetResponseForControllerResultEvent $event) { $request = $event->getRequest(); $result = $event->getControllerResult(); // Render the controller result into a response if it's a render array. if (is_array($result) && ($request->query->has(static::WRAPPER_FORMAT) || $request->getRequestFormat() == 'html')) { $wrapper = $request->query->get(static::WRAPPER_FORMAT, 'html'); // Fall back to HTML if the requested wrapper envelope is not available. $wrapper = isset($this->mainContentRenderers[$wrapper]) ? $wrapper : 'html'; $renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]); $response = $renderer->renderResponse($result, $request, $this->routeMatch); // The main content render array is rendered into a different Response // object, depending on the specified wrapper format. if ($response instanceof CacheableResponseInterface) { $main_content_view_subscriber_cacheability = (new CacheableMetadata())->setCacheContexts(['url.query_args:' . static::WRAPPER_FORMAT]); $response->addCacheableDependency($main_content_view_subscriber_cacheability); } $event->setResponse($response); } }
/** * Test the access method. */ public function testAccess() { $route_match = $this->getMock('Drupal\\Core\\Routing\\RouteMatchInterface'); $this->controllerResolver->expects($this->at(0))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessDeny')->will($this->returnValue(array(new TestController(), 'accessDeny'))); $resolver0 = $this->getMock('Drupal\\Component\\Utility\\ArgumentsResolverInterface'); $resolver0->expects($this->once())->method('getArguments')->will($this->returnValue(array())); $this->argumentsResolverFactory->expects($this->at(0))->method('getArgumentsResolver')->will($this->returnValue($resolver0)); $this->controllerResolver->expects($this->at(1))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessAllow')->will($this->returnValue(array(new TestController(), 'accessAllow'))); $resolver1 = $this->getMock('Drupal\\Component\\Utility\\ArgumentsResolverInterface'); $resolver1->expects($this->once())->method('getArguments')->will($this->returnValue(array())); $this->argumentsResolverFactory->expects($this->at(1))->method('getArgumentsResolver')->will($this->returnValue($resolver1)); $this->controllerResolver->expects($this->at(2))->method('getControllerFromDefinition')->with('\\Drupal\\Tests\\Core\\Access\\TestController::accessParameter')->will($this->returnValue(array(new TestController(), 'accessParameter'))); $resolver2 = $this->getMock('Drupal\\Component\\Utility\\ArgumentsResolverInterface'); $resolver2->expects($this->once())->method('getArguments')->will($this->returnValue(array('parameter' => 'TRUE'))); $this->argumentsResolverFactory->expects($this->at(2))->method('getArgumentsResolver')->will($this->returnValue($resolver2)); $route = new Route('/test-route', array(), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessDeny')); $account = $this->getMock('Drupal\\Core\\Session\\AccountInterface'); $this->assertEquals(AccessResult::neutral(), $this->accessChecker->access($route, $route_match, $account)); $route = new Route('/test-route', array(), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessAllow')); $this->assertEquals(AccessResult::allowed(), $this->accessChecker->access($route, $route_match, $account)); $route = new Route('/test-route', array('parameter' => 'TRUE'), array('_custom_access' => '\\Drupal\\Tests\\Core\\Access\\TestController::accessParameter')); $this->assertEquals(AccessResult::allowed(), $this->accessChecker->access($route, $route_match, $account)); }
/** * {@inheritdoc} */ public function generateCachePlaceholder($callback, array &$context) { if (is_string($callback) && strpos($callback, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callback); } else { $callable = $callback; } if (!is_callable($callable)) { throw new \InvalidArgumentException('$callable must be a callable function or of the form service_id:method.'); } // Generate a unique token if one is not already provided. $context += ['token' => Crypt::randomBytesBase64(55)]; return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>'; }
/** * {@inheritdoc} */ public function transform(array $tree, array $manipulators) { foreach ($manipulators as $manipulator) { $callable = $manipulator['callable']; $callable = $this->controllerResolver->getControllerFromDefinition($callable); // Prepare the arguments for the menu tree manipulator callable; the first // argument is always the menu link tree. if (isset($manipulator['args'])) { array_unshift($manipulator['args'], $tree); $tree = call_user_func_array($callable, $manipulator['args']); } else { $tree = call_user_func($callable, $tree); } } return $tree; }
/** * @covers \Drupal\Core\Menu\LocalActionManager::getActionsForRoute() * * @dataProvider getActionsForRouteProvider */ public function testGetActionsForRoute($route_appears, array $plugin_definitions, array $expected_actions) { $this->discovery->expects($this->any())->method('getDefinitions')->will($this->returnValue($plugin_definitions)); $map = array(); foreach ($plugin_definitions as $plugin_id => $plugin_definition) { $plugin = $this->getMock('Drupal\\Core\\Menu\\LocalActionInterface'); $plugin->expects($this->any())->method('getRouteName')->will($this->returnValue($plugin_definition['route_name'])); $plugin->expects($this->any())->method('getRouteParameters')->will($this->returnValue(isset($plugin_definition['route_parameters']) ? $plugin_definition['route_parameters'] : array())); $plugin->expects($this->any())->method('getTitle')->will($this->returnValue($plugin_definition['title'])); $this->controllerResolver->expects($this->any())->method('getArguments')->with($this->request, array($plugin, 'getTitle'))->will($this->returnValue(array())); $plugin->expects($this->any())->method('getWeight')->will($this->returnValue($plugin_definition['weight'])); $this->controllerResolver->expects($this->any())->method('getArguments')->with($this->request, array($plugin, 'getTitle'))->will($this->returnValue(array())); $map[] = array($plugin_id, array(), $plugin); } $this->factory->expects($this->any())->method('createInstance')->will($this->returnValueMap($map)); $this->assertEquals($expected_actions, $this->localActionManager->getActionsForRoute($route_appears)); }
/** * Builds all permissions provided by .permissions.yml files. * * @return array[] * Each return permission is an array with the following keys: * - title: The title of the permission. * - description: The description of the permission, defaults to NULL. * - provider: The provider of the permission. */ protected function buildPermissionsYaml() { $all_permissions = array(); $all_callback_permissions = array(); foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) { // The top-level 'permissions_callback' is a list of methods in controller // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods // should return an array of permissions in the same structure. if (isset($permissions['permission_callbacks'])) { foreach ($permissions['permission_callbacks'] as $permission_callback) { $callback = $this->controllerResolver->getControllerFromDefinition($permission_callback); if ($callback_permissions = call_user_func($callback)) { // Add any callback permissions to the array of permissions. Any // defaults can then get processed below. foreach ($callback_permissions as $name => $callback_permission) { if (!is_array($callback_permission)) { $callback_permission = array('title' => $callback_permission); } $callback_permission += array('description' => NULL); $callback_permission['provider'] = $provider; $all_callback_permissions[$name] = $callback_permission; } } } unset($permissions['permission_callbacks']); } foreach ($permissions as &$permission) { if (!is_array($permission)) { $permission = array('title' => $permission); } $permission['title'] = $this->t($permission['title']); $permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL; $permission['provider'] = $provider; } $all_permissions += $permissions; } return $all_permissions + $all_callback_permissions; }
/** * Setups the controller resolver to return the given controller definition. * * @param string $controller_definition * The definition of a controller */ protected function setupControllerResolver($controller_definition) { $controller = $controller_definition; list($class, $method) = explode('::', $controller); $this->controllerResolver->expects($this->atLeastOnce())->method('getControllerFromDefinition')->with($controller_definition)->will($this->returnValue(array(new $class(), $method))); }
/** * See the docs for ::render(). */ protected function doRender(&$elements, $is_root_call = FALSE) { if (!isset($elements['#access']) && isset($elements['#access_callback'])) { if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); } $elements['#access'] = call_user_func($elements['#access_callback'], $elements); } // Early-return nothing if user does not have access. if (empty($elements) || isset($elements['#access']) && !$elements['#access']) { return ''; } // Do not print elements twice. if (!empty($elements['#printed'])) { return ''; } if (!isset(static::$stack)) { static::$stack = new \SplStack(); } static::$stack->push(new BubbleableMetadata()); // Set the bubbleable rendering metadata that has configurable defaults, if: // - this is the root call, to ensure that the final render array definitely // has these configurable defaults, even when no subtree is render cached. // - this is a render cacheable subtree, to ensure that the cached data has // the configurable defaults (which may affect the ID and invalidation). if ($is_root_call || isset($elements['#cache']['keys'])) { $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; if (isset($elements['#cache']['contexts'])) { $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); } else { $elements['#cache']['contexts'] = $required_cache_contexts; } } // Try to fetch the prerendered element from cache, replace any placeholders // and return the final markup. if (isset($elements['#cache']['keys'])) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache // in case of nested elements with #cache set. if ($is_root_call) { $this->replacePlaceholders($elements); } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the // individual children must be safe as well. $elements['#markup'] = SafeMarkup::set($elements['#markup']); if (!empty($elements['#cache_properties'])) { foreach (Element::children($cached_element) as $key) { SafeMarkup::set($cached_element[$key]['#markup']); } } // The render cache item contains all the bubbleable rendering metadata // for the subtree. $this->updateStack($elements); // Render cache hit, so rendering is finished, all necessary info // collected! $this->bubbleStack(); return $elements['#markup']; } } // Two-tier caching: track pre-bubbling elements' #cache for later // comparison. // @see \Drupal\Core\Render\RenderCacheInterface::get() // @see \Drupal\Core\Render\RenderCacheInterface::set() $pre_bubbling_elements = []; $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : []; // If the default values for this element have not been loaded yet, populate // them. if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { $elements += $this->elementInfo->getInfo($elements['#type']); } // First validate the usage of #lazy_builder; both of the next if-statements // use it if available. if (isset($elements['#lazy_builder'])) { // @todo Convert to assertions once https://www.drupal.org/node/2408013 // lands. if (!is_array($elements['#lazy_builder'])) { throw new \DomainException('The #lazy_builder property must have an array as a value.'); } if (count($elements['#lazy_builder']) !== 2) { throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.'); } if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) { return is_null($v) || is_scalar($v); }))) { throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); } $children = Element::children($elements); if ($children) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children))); } $supported_keys = ['#lazy_builder', '#cache', '#create_placeholder', '#weight', '#printed']; $unsupported_keys = array_diff(array_keys($elements), $supported_keys); if (count($unsupported_keys)) { throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys))); } } // If instructed to create a placeholder, and a #lazy_builder callback is // present (without such a callback, it would be impossible to replace the // placeholder), replace the current element with a placeholder. if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) { if (!isset($elements['#lazy_builder'])) { throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); } $elements = $this->createPlaceholder($elements); } // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { $callable = $elements['#lazy_builder'][0]; $args = $elements['#lazy_builder'][1]; if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $new_elements = call_user_func_array($callable, $args); // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements)->merge(CacheableMetadata::createFromRenderArray($new_elements))->applyTo($new_elements); if (isset($elements['#cache']['keys'])) { $new_elements['#cache']['keys'] = $elements['#cache']['keys']; } $elements = $new_elements; $elements['#lazy_builder_built'] = TRUE; } // Make any final changes to the element before it is rendered. This means // that the $element or the children can be altered or corrected before the // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements = call_user_func($callable, $elements); } } // Defaults for bubbleable rendering metadata. $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { // The #printed element contains all the bubbleable rendering metadata for // the subtree. $this->updateStack($elements); // #printed, so rendering is finished, all necessary info collected! $this->bubbleStack(); return ''; } // Add any JavaScript state information associated with the element. if (!empty($elements['#states'])) { drupal_process_states($elements); } // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE); // Initialize this element's #children, unless a #pre_render callback // already preset #children. if (!isset($elements['#children'])) { $elements['#children'] = ''; } if (isset($elements['#markup'])) { // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. $elements['#markup'] = SafeMarkup::checkAdminXss($elements['#markup']); } // Assume that if #theme is set it represents an implemented hook. $theme_is_implemented = isset($elements['#theme']); // Check the elements for insecure HTML and pass through sanitization. if (isset($elements)) { $markup_keys = array('#description', '#field_prefix', '#field_suffix'); foreach ($markup_keys as $key) { if (!empty($elements[$key]) && is_scalar($elements[$key])) { $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]); } } } // Call the element's #theme function if it is set. Then any children of the // element have to be rendered there. If the internal #render_children // property is set, do not call the #theme function to prevent infinite // recursion. if ($theme_is_implemented && !isset($elements['#render_children'])) { $elements['#children'] = $this->theme->render($elements['#theme'], $elements); // If ThemeManagerInterface::render() returns FALSE this means that the // hook in #theme was not found in the registry and so we need to update // our flag accordingly. This is common for theme suggestions. $theme_is_implemented = $elements['#children'] !== FALSE; } // If #theme is not implemented or #render_children is set and the element // has an empty #children attribute, render the children now. This is the // same process as Renderer::render() but is inlined for speed. if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { foreach ($children as $key) { $elements['#children'] .= $this->doRender($elements[$key]); } $elements['#children'] = SafeMarkup::set($elements['#children']); } // If #theme is not implemented and the element has raw #markup as a // fallback, prepend the content in #markup to #children. In this case // #children will contain whatever is provided by #pre_render prepended to // what is rendered recursively above. If #theme is implemented then it is // the responsibility of that theme implementation to render #markup if // required. Eventually #theme_wrappers will expect both #markup and // #children to be a single string as #children. if (!$theme_is_implemented && isset($elements['#markup'])) { $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']); } // Let the theme functions in #theme_wrappers add markup around the rendered // children. // #states and #attached have to be processed before #theme_wrappers, // because the #type 'page' render array from drupal_prepare_page() would // render the $page and wrap it into the html.html.twig template without the // attached assets otherwise. // If the internal #render_children property is set, do not call the // #theme_wrappers function(s) to prevent infinite recursion. if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { foreach ($elements['#theme_wrappers'] as $key => $value) { // If the value of a #theme_wrappers item is an array then the theme // hook is found in the key of the item and the value contains attribute // overrides. Attribute overrides replace key/value pairs in $elements // for only this ThemeManagerInterface::render() call. This allows // #theme hooks and #theme_wrappers hooks to share variable names // without conflict or ambiguity. $wrapper_elements = $elements; if (is_string($key)) { $wrapper_hook = $key; foreach ($value as $attribute => $override) { $wrapper_elements[$attribute] = $override; } } else { $wrapper_hook = $value; } $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); } } // Filter the outputted content and make any last changes before the content // is sent to the browser. The changes are made on $content which allows the // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { if (is_string($callable) && strpos($callable, '::') === FALSE) { $callable = $this->controllerResolver->getControllerFromDefinition($callable); } $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); } } // We store the resulting output in $elements['#markup'], to be consistent // with how render cached output gets stored. This ensures that placeholder // replacement logic gets the same data to work with, no matter if #cache is // disabled, #cache is enabled, there is a cache hit or miss. $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : ''; $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : ''; $elements['#markup'] = $prefix . $elements['#children'] . $suffix; // We've rendered this element (and its subtree!), now update the stack. $this->updateStack($elements); // Cache the processed element if both $pre_bubbling_elements and $elements // have the metadata necessary to generate a cache ID. if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); } $this->renderCache->set($elements, $pre_bubbling_elements); } // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache in // case of nested elements with #cache set. // // By running them here, we ensure that: // - they run when #cache is disabled, // - they run when #cache is enabled and there is a cache miss. // Only the case of a cache hit when #cache is enabled, is not handled here, // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); if (static::$stack->count() !== 1) { throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); } } // Rendering is finished, all necessary info collected! $this->bubbleStack(); $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; }