I'm working on a developer tool (closed source at the moment, unfortunately) that reports on Laravel route names. It does so with code that works mostly like this (this is simplified to make asking this question easier).
function identifyRoute() {
$router = app('router');
$route = $router->current();
$name = $route->name;
if($name) {
return $name;
}
$action = $route->getAction();
if(isset($action["controller"]) && $action["controller"]) {
return $action["controller"];
}
if($name = $route->uri())
{
return $name;
}
return 'Could Not Identify Name';
}
So, for a route like
Route::get('foo/{id}/bar', function($id ) {
//...
});
Our function returns the string foo/{id}/bar. Or, it usually returns the string foo/{id}/bar. We've had reports from users that sometimes this method of identifying routes returns results like
foo/1234/bar
foo/1235/bar
foo/1236/bar
foo/1237/bar
That is, it's returning the entire URI for the request.
Is there some Laravel setting (or popular extension/plugin) that could replace the results of calls to getName, uri, or the controller name, with the full URI of the request?
A bit of a guess, but an OPTIONS HTTP request will return a 200 response with the proper allowable verbs, using the requested path as-is instead of a pattern.
Current source code in 5.7
Method is almost the same in 5.4.9
Related
I've followed the unit testing tutorial and modified it to test a HTTP request to Micro MVC app, based on this post. I can successfully validate the output string, however I'm not sure how to assert the response status code or change the request path.
index.php
<?php
$app = new \Phalcon\Mvc\Micro();
#Default handler for 404
$app->notFound(function () use ($app) {
$app->response->setStatusCode(404, "Not Found")->sendHeaders();
});
$app->post('/api/robots', function() use ($app) {
//Parse JSON as an object
$robot = $app->request->getJsonRawBody();
//Build the response
$app->response->setJsonContent($robot);
return $app->response;
});
$app->get('/', function() {
echo 'Hello';
});
$app->handle();
tests/UnitTest.php
class MvcMicroUnitTest extends \UnitTestCase {
public function testNotFound() {
$path = '/invalid';
$mockRequest = $this->getMock("\\Phalcon\\Http\\Request");
//TODO: Set an invalid URL $path in the mock
$this->di->set('request', $mockRequest, true);
include("../index.php");
//TODO: Assert status is 404
$this->expectOutputString('');
}
public function testPostRobot() {
$rawJson = '{"name":"C-3PO","type":"droid","year":1977}';
$path = '/api/robots';
$mockRequest = $this->getMock("\\Phalcon\\Http\\Request", array(
"getJsonRawBody"));
$mockRequest->expects($this->any())
->method("getRawBody")
->will($this->returnValue($rawJson));
//TODO: Set the $path in the mock
$this->di->set('request', $mockRequest, true);
include("../index.php");
//TODO: Assert status is 200
$this->expectOutputString($rawJson);
}
}
Good news and bad news. Good: as far as you use the standard dispatching principle you will have a response, that would contain the information you need. Small trick – when status is success the header is set to false.
/**
* #param $expected
* #throws ExpectationFailedException
* #return $this
*/
protected function assertResponseCode($expected)
{
$actual = $this->di->getResponse()->getHeaders()->get('Status');
if ($actual !== false && $expected !== 200 && !preg_match(sprintf('/^%s/', $expected), $actual)) {
throw new ExpectationFailedException(sprintf('Failed asserting that response code is "%s".', $expected));
}
$this->assertTrue(true);
return $this;
}
Bad: you are doing it the wrong way. This is area of functional / acceptance testing and there is a fabulous framework called Behat. You should do your own research, but essentially, while PHPUnit is great at testing more or less independent blocks of functionality it sucks at testing bigger things like full request execution. Later you will start experiencing issues with session errors, misconfigured environment, etc., all because each request is supposed to be executed in it's own separate space and you force it into doing the opposite. Behat on the other hand works in a very different way, where for each scenario (post robot, view non-existing page), it sends a fresh request to the server and checks the result. It is mostly used for final testing of everything working together by making assertions on the final result (response object / html / json).
I just upgraded to Laravel 4.1 and can no longer use a function I was using in the past. I wrote a function to redirect an incoming request to another route, get the result, and replace the current route with the original incoming route. I used this on my frontend controllers to consume my own API which is defined in the same application.
Here is the function:
public static function redirectRequest($newRoute, $verb, $args = null)
{
// store the original request data and route
$originalInput = Request::input();
$originalRoute = Route::current();
$request = $args === null ? Request::create($newRoute, $verb) : Request::create($newRoute, $verb, $args);
// replace the request input for the new route...
Request::replace($request->input());
try
{
$response = Route::dispatch($request);
return $response;
}
catch (\Exception $e)
{
throw $e;
}
finally
{
// replace the request input and route back to the original state
Request::replace($originalInput);
Route::setCurrentRoute($originalRoute);
}
}
And I would use it like:
Helpers::redirectRequest('/api/v1/someroute', 'GET');
The problem is that, when I try to return things to the way they were before the redirect, I can't. setCurrentRoute has been removed from 4.1 and I can't figure out how to reset the current route.
One thing that I have done in the past is used the actual routes.php to handle that. If you register a "catchall" route as:
Route::get('api/v1', 'ApiController#route');
As long as this is after all other routes (Laravel uses the first matching route) you can then handle that within your ApiController as
public function route($uri) {
// Handle your API route using the $uri variable
}
This may not be the solution that you are looking for, but I have found it very convenient.
I want to redirect admins to /admin and members to /member when users are identified but get to the home page (/).
The controller looks like this :
public function indexAction()
{
if ($this->get('security.context')->isGranted('ROLE_ADMIN'))
{
return new RedirectResponse($this->generateUrl('app_admin_homepage'));
}
else if ($this->get('security.context')->isGranted('ROLE_USER'))
{
return new RedirectResponse($this->generateUrl('app_member_homepage'));
}
return $this->forward('AppHomeBundle:Default:home');
}
If my users are logged in, it works well, no problem. But if they are not, my i18n switch makes me get a nice exception :
The merge filter only works with arrays or hashes in
"AppHomeBundle:Default:home.html.twig".
Line that crashes :
{{ path(app.request.get('_route'), app.request.get('_route_params')|merge({'_locale': 'fr'})) }}
If I look at the app.request.get('_route_params'), it is empty, as well as app.request.get('_route').
Of course, I can solve my problem by replacing return $this->forward('AppHomeBundle:Default:home'); by return $this->homeAction();, but I don't get the point.
Are the internal requests overwritting the user request?
Note: I'm using Symfony version 2.2.1 - app/dev/debug
Edit
Looking at the Symfony's source code, when using forward, a subrequest is created and we are not in the same scope anymore.
/**
* Forwards the request to another controller.
*
* #param string $controller The controller name (a string like BlogBundle:Post:index)
* #param array $path An array of path parameters
* #param array $query An array of query parameters
*
* #return Response A Response instance
*/
public function forward($controller, array $path = array(), array $query = array())
{
$path['_controller'] = $controller;
$subRequest = $this->container->get('request')->duplicate($query, null, $path);
return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
By looking at the Symfony2's scopes documentation, they tell about why request is a scope itself and how to deal with it. But they don't tell about why sub-requests are created when forwarding.
Some more googling put me on the event listeners, where I learnt that the subrequests can be handled (details). Ok, for the sub-request type, but this still does not explain why user request is just removed.
My question becomes :
Why user request is removed and not copied when forwarding?
So, controller actions are separated part of logic. This functions doesn't know anything about each other. My answer is - single action handle kind of specific request (e.g. with specific uri prarams).
From SF2 docs (http://symfony.com/doc/current/book/controller.html#requests-controller-response-lifecycle):
2 The Router reads information from the request (e.g. the URI), finds a
route that matches that information, and reads the _controller
parameter from the route;
3 The controller from the matched route is
executed and the code inside the controller creates and returns a
Response object;
If your request is for path / and you wanna inside action (lets say indexAction()) handling this route, execute another controller action (e.g. fancyAction()) you should prepare fancyAction() for that. I mean about using (e.g.):
public function fancyAction($name, $color)
{
// ... create and return a Response object
}
instead:
public function fancyAction()
{
$name = $this->getRequest()->get('name');
$color = $this->getRequest()->get('color');
// ... create and return a Response object
}
Example from sf2 dosc:
public function indexAction($name)
{
$response = $this->forward('AcmeHelloBundle:Hello:fancy', array(
'name' => $name,
'color' => 'green',
));
// ... further modify the response or return it directly
return $response;
}
Please notice further modify the response.
If you really need request object, you can try:
public function indexAction()
{
// prepare $request for fancyAction
$response = $this->forward('AcmeHelloBundle:Hello:fancy', array('request' => $request));
// ... further modify the response or return it directly
return $response;
}
public function fancyAction(Request $request)
{
// use $request
}
I'm trying to implement canonical URLs and combine it with custom route-classes.
The URL-scheme is something like this:
/category-x/article/123
/category-y/article/123
I create a custom route-class extending Zend_Controller_Router_Route_Regex and checks that the article 123 exists and that the URL includes the correct category-name. If article 123 belongs in category-x and the user is accessing category-y I want to redirect to the correct URL.
But the routes does not have any obvious possibility to do this directly. What's the best practice approach here?
I often do this in my action controller. Something like this...
// assuming GET /category-y/article/123
// $article->url is generated, and contains /category-x/article/123
if (this->_request->getRequestUri() != $article->url) {
return $this->_helper->redirector->goToUrl($article->url);
}
In this example, $article->url would need to be generated from your database data. I often use this to verify a correct slug, when I also pull in the object id.
You could also potentially move this to your routing class, if you wanted to use a custom one instead of using Regex (you could subclass it).
I ended up with this solution:
The custom route-class creates the canonical URL in its match()-method like this:
public function match($path, $partial = false) {
$match = parent::match($path, $partial);
if (!empty($match)) {
$article = $this->backend->getArticle($match['articleId']);
if (!$article) {
throw new Zend_Controller_Router_Exception('Article does not exist', 404);
}
$match['canonicalUrl'] = $this->assemble(array(
'title' => $article->getTitle(),
'articleId' => $article->getId()
));
}
return $match;
}
$article is populated inside match() if the parent::match() returns array.
I've created a front controller plugin which hooks on the routeShutdown() like this:
public function routeShutdown(Zend_Controller_Request_Abstract $request) {
if ($request->has('canonicalUrl')){
$canonicalUrl = $request->getBaseUrl() . '/' . $request->get('canonicalUrl');
if ($canonicalUrl != $request->getRequestUri()) {
$this->getResponse()->setRedirect($canonicalUrl, 301);
}
}
}
It simply checks if the route(custom or native Zend) created a canonical URL and if the requested URL does not match, redirect to the correct canonical URL.
In symfony, is there a method that I can use which does a reverse lookup on my routes to determine the module and action a URL points to?
Say, something like:
get_module("http://host/cars/list"); // ("cars")
get_action("http://host/frontend_dev.php/cars/list"); // ("list")
Bear in mind, I don't want to perform simple string-hacking to do this as there may be mappings that are not quite so obvious:
get_module("/"); // (What this returns is entirely dependent on the configured routes.)
Thanks!
Use the sfRouting class to match URLs. This is part of the sfContext object. Your code (in an action) would look like this:
public function executeSomeAction(sfWebRequest $request)
{
if($params = $this->getContext()->getRouting()->parse('/blog'))
{
$module = $params['module']; // blog
$action = $params['action']; // index
$route = $params['_sf_route'] // routing instance (for fun)
}
else
{
// your URL did not match
}
}