I wanna make a simple URL to Controller mapping very much like what Symfony2 does. But that's all I want from Symfony2, rest of it is just too much for me.
For those who don't know what Symfony2 does:
blog_home:
pattern: /blog
defaults: { _controller: BlogBundle:Blog:index }
blog_show:
pattern: /blog/{slug}
defaults: { _controller: BlogBundle:Blog:show }
in a YAML config file.
YAML doesn't matter to me at all. I just wanna achieve the same functionality. To be able to map custom URLs to controller functions.
Maybe an open source mapping class or routing framework? Maybe some tutorials? Ideas to make my own? Any suggestions would be helpful.
I should mention I'm no PHP whiz, I know just enough or maybe a little less than enough. Which is why I don't wanna get into a full featured framework.
This is my url routing framework:
function route($url, $map) {
foreach($map as $re => $fn) {
if(preg_match("~^$re$~", $url, $args)) {
list($class, $method) = explode(".", $fn);
return call_user_func_array(
array(new $class, $method),
array_slice($args, 1));
}
}
error_404();
}
The $map is an array whose keys are regular expressions to match the url against and the values are strings "ClassName.method", like
$map = array(
"/blog/(.+)" => "BlogController.show",
"/blog" => "BlogController.blog",
"/foobar/(\d+)/(\w+)" => "Foobar.stuff",
);
The routing function finds the first matching pattern, instantiates a class and calls a method passing regexp subgroups as arguments. So, an url "/foobar/123/hello" will be routed to Foobar->stuff(123, 'hello').
Related
I'm after a custom routing method so I can have below links to be handled by single controller action:
/q-query_term
/category-category_name/city-city_name/q-query_term
/city-city_name/category-category_name/q-query_term
/city-city_name/q-query_term
/category-category_name/q-query_term
/city-city_name
/category-category_name
Is it possible even possible? I don't use SensioExtraBundle so routes has to be written down in yaml.
For instance in Zend Framework 2 it is possible quite easily, because I can write a class, which would handle the routing as a wish. Don't know how to achieve the same in Symfony though, any help would be appreciated.
The only way I can come up with is to create a custom route loader, calculate all route permutations and return that collection, but don't know if it the best solution possible.
This question is unique, because I need to use single route name instead of specyfing tens of different route definitions.
A simple solution would be to define the route as catch-all …
# Resources/config/routing.yml
catch_all:
path: /{path}
defaults: { _controller: FooBarBundle:Catchall:dispatcher, path : "" }
requirements:
path: ".*"
… and do all further processing in the controller:
# Controller/CatchallController.php
class CatchallController extends Controller
{
public function dispatcherAction(Request $request, string $path)
{
if (/* path matches pattern 1 */)
return $this->doQuery($request, $path);
elseif (/* path matches pattern 2 */)
return $this->doCategoryCityQuery($request, $path);
// ... and so on
else
return new Response("Page not found", 404);
}
private function doQuery($request, $path)
{
// do stuff and return a Response object
}
private function doCategoryCityQuery($request, $path)
{
// do stuff and return a Response object
}
}
I decided to make my own mini - framework for PHP that I am going to use for a real life job, creation of a Web Service for a social app.
I got started with Fabien Potencier's guide to creating your own framework on top of the Symfony's components - http://symfony.com/doc/current/create_framework/index.html.
I really liked his classLoader and http-foundation libraries and decided to integrate them.
I read the whole tutorial but I decided to stop integrating Symfony's components up to part 5 of the tutorial where he gets to the Symfony http kernel, route matcher and controller resolver (excluding those).
There are the front controller and route mapper file of my framework that qre in question.
front.php(the front controller)
<?php
require 'config/config.php';
require 'config/routing.php';
require 'src/autoloader.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
$path = $request->getPathInfo();
if(isset($siteMap[$path])) {
call_user_func_array('src\Controllers' .'\\' . $siteMap[$path]['controller'] . '::' . $siteMap[$path]['action'], array($siteMap[$path]['arguments']));
} else {
$response->setStatusCode('404');
$response->setContent('Page not found');
}
$response->send();
?>
My routing file :
/*
Map of routes
*/
$siteMap = array('/' => array('controller' => 'IndexController', 'action' => 'indexAction', 'arguments' => ''),
'/categories' => array('controller' => 'CategoriesController', 'action' => 'indexAction', '
What I want to know now is without further using Symfony's components, how can I do something like this:
In my routing file I wanna add an URL like '/hello' with Controller - Hello Controller and arguments name,age,gender that would correspond to a request in the browser GET www.base/hello/samuel/11/male.
And in the HelloController have an indexAction($name, $age, $gender) { ... }. I have tried looking at Symfony's source but it eludes me so far(I spent a lot of time going trough the source code of the libraries). I am going to modularize and separate the functions of the front controller and controller resolving even further but I want to get this down.
Ah and also any advice on building my framework further would be welcome(I am creating a framework for a REST - like web service that needs to be scalable and fast and handle tens of thousands requests per second maybe).
You must define regexes for your routes and match them against the requests, if you want to know how Symfony transforms its own route format e.g: '/path/to/{id}' to a regex, see RouteCompiler::compilePattern. That's out of the scope of this question and I will provide no code for it.
The regex for your example route will be something like ^/hello/(\w*)/(\d*)/(\w*)/$, this will match '/hello/any string/any number/any string/' e.g: /hello/samuel/11/male (don't forget the ^ and the $, they match the beginning and end of the string).
You must provide a regex for each route, and do a preg_match(rawurldecode($request->getPathInfo(), $regex /* as created earlier*/, $matches) against all of the routes (until one matches), instead of the isset($sitemap[$path]) if this returns false, the route does not match.otherwise you must get the arguments for your controller like so:
preg_match(rawurldecode($request->getPathInfo(), $regex /* as created earlier*/, $matches)
$args = array();
// we omit the first element of $matches, since it's the full string for the match
for($i = 1; $i < count($matches); $i++){
$args[] = $matches[$i];
}
Now you can merge $args with the default arguments and call the controller.
See UrlMatcher::matchCollection() on how Symfony matches each route against the regex, and constructs the arguments.
I would like to add some additional information. In Symfony you can get the Arguments of your request into the method arguments:
public function fooAction($bar);
In this case it would look in the $request->attributes for a key with bar. if the value is present it will inject this value into your action. In Symfony 3.0 and lower, this is done via the ControllerResolver::getArguments(). As of Symfony 3.1 you will be able to implement or extend an ArgumentResolver.
Utilizing the ArgumentResolver means you can easily add your own functionality to the resolving. The existing value resolvers are located here.
I need my website to be accessible with 2 different URL, for example:
/blog => Homepage
/blog/article/1 => Article page (etc.)
/abcde/blog => Homepage
/abcde/blog/article/1 => Article page (etc.)
When "abcde" is in my URL, I need to have the same controller/action running, but being able to get this "abcde" value (a few changes will result from this parameter).
I have found many similar questions but never exactly what I am looking for:
I am not looking for a language/translation routing bundle
"abcde" parameter must be optional
My bundle has several controllers and actions, I don't want to write 2 routes for each. (Neither do I want to have to update anything each time I add a new controller/action.)
Initial app/config/routing.yml
mywebsite_blog:
resource: "#MywebsiteBlogBundle/Resources/config/routing.yml"
prefix: /blog/
If I simply add a second route, as resource is the same, it overwrites the first one.
I tried to add an optional parameter on the initial route:
mywebsite_blog:
resource: "#MywebsiteBlogBundle/Resources/config/routing.yml"
prefix: /{_label}/blog/
defaults: {_label: mylabel}
requirements:
_label: .*
That way I can access /abcde/blog, //blog, but not /blog (404).
Not finding how to resolve my issue simply with using routing.yml (if it is possible I will be happy to learn how and stop there), I read How to Create a custom Route Loader on Symfony doc. I am not sure I'm going to the right direction but I tried it and manage to do a few things, still not exactly what I want.
LabelLoader.php
class LabelLoader extends Loader {
private $loaded = false;
public function load($resource, $type = null) {
if (true === $this->loaded) {
throw new \RuntimeException('Do not add the "label" loader twice');
}
$routes = new RouteCollection();
// Prepare a new route (this is were I tried to play with it)
$path = '/{_label}/blog/';
$defaults = array(
'_controller' => 'MywebsiteBlogBundle:Index:home',
);
$route = new Route($path, $defaults);
// Add the new route to the route collection
$routeName = 'labelRoute';
$routes->add($routeName, $route);
$this->loaded = true;
return $routes;
}
public function supports($resource, $type = null) {
return 'label' === $type;
}
}
That way, /blog and /abcde/blog works the way I want to. I can access the _label variable in my _init() function (I'm using listeners and InitializableControllerInterface, in case it is important to know here), checking if empty or not, etc.
But obviously it works for only one controller/action (Index:home). I would like to change this so that it can works for my whole MywebsiteBlogBundle.
In my custom Loader, I wanted to test something like:
$routes = new RouteCollection();
// Hypotetical function returning all the routes I want to have twice.
// Methods getPath() and getController() called below are hypotetical too.
$mywebsiteBlogRoutes = getExisitingMywebsiteBlogBundleRoutes();
foreach($mywebsiteBlogRoutes as $blogRoute) {
// Prepare a new route
$path = '/{_label}/blog/'.$blogRoute->getPath();
$defaults = array(
'_controller' => $blogRoute->getController(),
);
$route = new Route($path, $defaults);
// Add the new route to the route collection
$routeName = 'labelRoute';
$routes->add($routeName, $route);
}
But:
It does not seem very clean (but if that's the only way I find to do it for now, I'll try it)
I tried to get all my routes reading this answer and as soon as I try $collection = $router->getRouteCollection(); I have a "Circular reference detected" error that I don't manage to fix. I'm trying to understand what's happening here. Edit: I fixed it. Still, I don't think it's a very clean solution.
Should I go back to routing.yml or is it possible to do something here with the custom Routing Loader?
You should try this in your routing.yml:
mywebsite_blog:
resource: "#MywebsiteBlogBundle/Resources/config/routing.yml"
prefix: /{_label}blog/
defaults: {_label: mylabel/}
requirements:
_label: ([\w\d]+/)?
Results:
app/console router:match /mylabel/blog/ # OK
app/console router:match /blog/ # OK
Not sure if I properly wrote the subject but anyway.
Since you can create specific routes with different parameters for eg:
_search:
pattern: /page/{category}/{keyword}
defaults: { _controller: Bundle:Default:page, category: 9, keyword: null }
is there any way from a form with GET method to get to that route specific url format?
At the moment the url is like /page?category=2?keyword=some+keyword
As such is not passing to the route format as you may noticed.
What do I need to do to get it working through this specific format? I really have no idea how to rewrite the page url to match the route settings for the specific url. Even in plain php was stumbled on this ...
Thanks in advance.
It's the default behavior of HTML forms with GET method. You will need to build that URL yourself.
Backend way
Drawback: It makes two requests to the server instead of one
Advantage: It's more maintainable because the URL is built using the routing service
Your routing file
_search:
pattern: /page/{category}/{keyword}
defaults: { _controller: Bundle:Default:page, category: 9, keyword: null }
_search_endpoint:
pattern: /page
defaults: { _controller: Bundle:Default:redirect }
Your controller
public function redirectAction()
{
$category = $this->get('request')->query->get('category');
$keyword = $this->get('request')->query->get('keyword');
// You probably want to add some extra check here and there
// do avoid any kind of side effects or bugs.
$url = $this->generateUrl('_search', array(
'category' => $category,
'keyword' => $keyword,
));
return $this->redirect($url);
}
Frontend way
Using Javascript, you can build the URL yourself and redirect the user afterwards.
Drawback: You don't have access to the routing service (although you could use the FOSJsRoutingBundle bundle)
Advantage: You save one request
Note: You will need to get your own query string getter you can find a Stackoverflow thread here, below I'll use getQueryString on the jQuery object.
(function (window, $) {
$('#theFormId').submit(function (event) {
var category, keyword;
event.preventDefault();
// You will want to put some tests here to make
// sure the code behaves the way you are expecting
category = $.getQueryString('category');
keyword = $.getQueryString('keyword');
window.location.href = '/page/' + category + '/' + keyword;
}):
})(window, jQuery);
You could add a second route that will just match /page
then in the controller you can get the defaults. and merge them with any that are passed.
Take a look at a similar question I answered for some code examples.
KendoUI Grid parameters sending to a symfony2 app
I've too encountered this issue and I managed to resolve it with a slightly different solution.
You could also reroute like #Thomas Potaire suggested, but in the same controller, beginning your controller with :
/**
* #Route("/myroute/{myVar}", name="my_route")
*/
public function myAction(Request $request, $myVar = null)
{
if ($request->query->get('myVar') !== null) {
return $this->redirectToRoute('my_route', array(
'myVar' => str_replace(' ','+',$request->query->get('myVar')) // I needed this modification here
));
}
// your code...
}
I have set up friendly URLs for a few search result pages, using a custom route for each:
friendly_search_resultpage:
url: /products/my-friendly-alias
param:
module: products
action: search
querystring: searchattribute
querystring2: searchattribute2
This works fine, but when doing the search directly (i.e. browsing to /products/search?querystring=search...) I want to set a <link rel="canonical"> containing the matching friendly URL. This will help Google understand the relation and that there isn't duplicate content.
I put my friendly URL route at the top of routes.yml and hoped for a magic match, but URL parameters aren't recognised in the checking done by symfony. I have dug into sfRoute, with no luck. Is it possible?
I ended up writing a custom routing class to use for these routes, which is executed when url_for() is called. Here is the code:
<?php
class mySearchFriendlyRoute extends sfRoute
{
public function matchesParameters($params, $context = array())
{
// I can't find symfony sorting parameters into order, so I do
// (so that foo=x&bar=y is treated the same as bar=y&foo=x)
ksort($params);
$mine = $this->defaults;
ksort($mine);
if ($params == $mine) {
return true;
}
return false;
}
}
To use, add class: mySearchFriendlyRoute to the routes.yml entry.
Try to use Your own Route:
friendly_search_resultpage:
class: YourRouteClassName
...
And overload sfRoute::generate() there by concrete cannonicalization (return the canonical URL).
This redirects with 301 in my project upon of last sf1.4 revision.