Yii 2 canonical URL in urlManager configuration - php

I've got urlManager section in app configuration with several URLs per route:
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'enableStrictParsing' => true,
'rules' => [
'article-a' => 'article/a', // canonic comes first
'article-deprecated-a' => 'article/a',
'article-another-a-is-deprecated' => 'article/a',
'b-annoucement' => 'announcement/b', // canonic comes first
'legacy-b-annoncement' => 'announcement/b',
...
SEF URLs for routes are stored in frontend/config/main.php as an array, with multiple URLs per route. The first URL for the given route (i.e. /article-a) is canonical and the rest are legacy URLs.
What's the most natural way to specify canonical URL for a group of URLs that are pointing to the same route? It can be either rel="canonical" in view or 301/302 redirect to canonical URL.
Canonical URLs should be preferably specified in a place where the routes were defined (frontend/config/main.php configuration file in this case). The requirement here is that canonical URL should be defined outside of the controller, not hard-coded to controller.

I'm not sure how exactly you need to manage your rules so I'll go for a general use case and I'll base my answer on what I did understand from Paddy Moogan's Article which I will resume within the following example and I hope it helps on designing your required solution:
requirement:
Assuming a Search Engine did send a robot to check page B in my website and I'm not fine with people getting to page B instead of page A. So this is how I can clarify my point to the robot:
Forcing a 301 redirect to page A:
Telling the Search Engine that this page is permanently moved to page A. So please don't send more people to it. Send them to page A instead.
Forcing a 302 redirect to page A:
Telling the Search Engine that this page is temporary moved to page A. So do whatever you think it
is appropriate.
Opening page B (200 status code) but insert a Canonical
link element
pointing to page A:
Telling the Search Engine that this page is working fine but it is to me a secondary page and I would suggest sending the next visitors to page
A instead.
design:
So based on that this is how I would see a possible structure to my rules configuration:
'rules' => [
[
// by default: 'class' => 'yii\web\UrlRule',
'pattern' => '/',
'route' => 'site/index',
],
[
// the custom class
'class' => 'app\components\SEOUrlRule',
'pattern' => 'about',
'route' => 'site/about',
'permanents' => [
'deprecated-about',
'an-older-deprecated-about'
],
'temporaries' => [
'under-construction-about',
],
'secondaries' => [
'about-page-2'
]
],
[
// different route with own action but canonical should be injected
'class' => 'app\components\SEOUrlRule',
'pattern' => 'experimental-about',
'route' => 'whatever/experimental',
'canonical' => 'about'
],
]
This way I can chain as much arrays as I need to use Yii's default class yii\web\UrlRule while I can have a custom one in my app components folder dedicated to SEO related controllers.
Before going to code, this is how I would expect my website to behave :
You visit the /about page you get a 200 response (no
canonical added).
You visit the /deprecated-about page you get redirected to
/about with 301 status code.
You visit the /under-construction-about page you get redirected to
/about with 302 status code.
You visit the /about-page-2 page you get a 200 response (rendered by index/about action). No redirections except a similar tag to this is automatically injected into source code:
<link href="http://my-website/about" rel="canonical">
You visit the /experimental-about page you get a 200 response (rendered by its own action whatever/experimental) but with that same canonical tag above injected.
code:
The SEOUrlRule will simply extend \yii\web\UrlRule and override its parseRequest method to define the extra attributes based on which we will force a HTTP redirection or call parent::parseRequest() after registering the canonical link tag to the Yii::$app->view:
namespace app\components;
use Yii;
class SEOUrlRule extends \yii\web\UrlRule
{
public $permanents = [];
public $temporaries = [];
public $secondaries = [];
public $canonical = null;
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
if(in_array($pathInfo, $this->permanents))
{
$request->setPathInfo($this->name);
Yii::$app->response->redirect($this->name, 301);
}
else if(in_array($pathInfo, $this->temporaries))
{
$request->setPathInfo($this->name);
Yii::$app->response->redirect($this->name, 302);
}
else if($this->canonical or in_array($pathInfo, $this->secondaries))
{
$route = $this->name;
if ($this->canonical === null) $request->setPathInfo($route);
else $route = $this->canonical;
Yii::$app->view->registerLinkTag([
'rel' => 'canonical',
'href' => Yii::$app->urlManager->createAbsoluteUrl($route)
]);
}
return parent::parseRequest($manager, $request);
}
}
And that is all what it needs. Note that Yii::$app->controller or its related actions won't be yet available at this early stage of solving routes as it is shown in this lifecycle diagram but it seems that Yii::$app->view is already initialized and you can use its $params property to set custom parameters (as it is done in this example) which may be useful for more advenced cases where more data should be shared or populated to final output.

I think you will have problems when creating the URL from the application to "article/a".
Why not use htaccess or the vhost file to do a 302 redirect to the proper URL?
If you want to handle it through the urlManager, I think you can just register the canonical link
$this->registerLinkTag(['rel' => 'canonical', 'href' => 'article/a']);
in the view.
Mode details here: http://www.yiiframework.com/doc-2.0/yii-helpers-baseurl.html#canonical()-detail

Yii2 provides a tool to generate canonnical urls based on your rules.
\helpers\Url::canonical()
The idea is that it will provide you an url to 'article-a'.

Related

react with yii urlManager rule to a specific url

I have a noob question, but googling for several hours didn't get me closer to the answer.
I want that when I enter the following url into my browser, that is gets assigned to a specific controller action.
Imagine I have the following url:
http://localhost:5555/tractor-unit/?param1=1&param2=abc
How should the rules looks like to process this request in a certain action in a certain controller.
What I've tried so far:
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'tractor-unit/?param1=<param1:\d+>&param2=<param2:\w+>' => 'tractor/do'
]
],
That doesn't work. I just don't understand how the rules entry should be written in order to pass this request to a controller named tractor and its action do. Or maybe I totally misunderstood the whole url manager concept.
simple add to action "do" in tractor-unit controller next:
public function actionDo($param1=null, $param2 = null)
if this don't work
remove section "rules" from config

yii2 rewrite rule for customer portal

I have an yii advanced app with frontend and backend.
What I try to achieve is that I can access the frontend with the name of a customer.
Example (local): http://localhost/myproject/frontend/web/customer1 should become http://localhost/myproject/frontend/web/customer1/site/login at first visit
And after login the name of the customer should stay in the URL. At the moment the URL changes after login to http://localhost/myproject/frontend/web/
Info:
customer is a GET parameter. It should always be the first argument after http://localhost/myproject/frontend/web/ but I don't want to specify the argument in each redirect or custom link. I hope there's a way to keep this argument and pass it to each of the following site changes.
What I have tried so far:
'urlManager' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false,
'enableStrictParsing' => true,
'rules' => [
'<controller>/<action>' => '<controller>/<action>',
'<customer:\w+>' => '/site/login',
]
],
But this is not working. I can only access the login page and afterwards the customer name is not showing anymore in the URL.
My .htaccess file looks like this:
RewriteEngine on
# If a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward it to index.php
RewriteRule . index.php
I'm really appreciate any hints on this topic.
To prepend customer name to all urls modify your url rule:
<customer:\w+>/<controller>/<action>' => '<controller>/<action>,
If you now call yii\helpers\Url::to(['site/index', 'customer' => 'customer'])the output will be as you want - /customer/site/index.
Howewer calling it like that in entire project is not flexible approach.
Most of the times Url::to() method is used for generating internal urls.
If you pass array in $route, it will call Url::toRoute(). So you can simply override that method in your custom component.
namespace frontend\components;
use yii\helpers\Url as BaseUrl;
class Url extends BaseUrl
{
public static function toRoute($route, $scheme = false)
{
$customer = ... // Get saved after login customer name (for example from the session)
$route['customer'] = $customer;
return parent::toRoute($route, $scheme);
}
}
Then you can simply call frontend\components\Url::to(['site/index']) to achieve the same result.
Alternative way of customizing helper classes described in official documentation here.
Update:
Also this url rule '<customer:\w+>' => '/site/login', is redundant and the url should be just site/login, because any user before signing in is guest.

ZF2: Define URL http or https

Here is my problem.
I have a few pages in my application who uses SSL, like the lggin page. When i am in the main page (who doens't have SSL), every link created by the view ( href=$this->url(...) ) is plain html, even the login page. On the other hand, when i am in the login page, every other links displays with https.
In the controller, i manipulate if the page uses SSL or not, that is OK. But i want to show the correct link for the user when he navigates through the site, https for ssl pages and http for non-ssl ones.
Thanks.
First of all, if you have HTTPS available, you should use it on any page. It is really against the web of trust when you have some pages available via HTTPS, but others not. Sure, you might enforce HTTPS on some pages (so there is no HTTP), but vice versa is always a bad idea.
That being said, you can create a scheme route. With the scheme you are able to specify HTTPS on some routes:
'secure' => [
'type' => 'scheme',
'options' => [
'scheme' => 'https',
'defaults' => [
// the usual stuff
],
],
'may_terminate' => false,
'child_routes' => [
// all your https routes here
],
],
Because some of these "secure" routes might be defined at vendor level (e.g. you use ZfcUser), you can use "prototyping" of routes. For example all ZfcUser routes should only be accessible via HTTPS. The "main" route of ZfcUser is zfcuser:
'router' => [
'prototypes' => [
// Define "secure" prototype to add to routes
'secure' => [
'type' => 'scheme',
'options' => ['scheme': 'https'],
],
],
// Apply the scheme route to ZfcUser
'routes' => [
'zfcuser' => [
'chain_routes' => ['secure'],
],
],
],
Prototyping "prepends" the secure route to zfcuser. So this makes zfcuser and all its childs a child-route of secure. Therefore, all zfcuser routes are defined with HTTPS.
When you've come this far: if you now assemble the routes, they will get HTTPS automatically. When you have a route login inside the secure route of my first example, you get the url via $this->url('secure/login');.
In the second case (prototyping) you don't need to mention the prototype, just use $this->url('zfcuser'); for the user's route.

Custom lithium routing scenario

I've been tasked with rewriting an existing website with large pre-existing link catalog. For argument's sake, let's assume we can't do anything that would change the link catalog. Here's a few examples of the link structure we're working with:
An item page would be:
www.domain.com/widgets/some-totally-awesome-large-purple-widget
A category sub page page would be:
www.domain.com/widgets/purple-widgets
A category parent page page would be:
www.domain.com/widgets/
A custom page may be:
www.domain.com/some-random-page
The various page types are too numerous to write individual Routers for.
Using Router::connect I can easily account for the first and second scenarios using something like:
Router::connect('/{:pageroot}/{:pagekey}', 'Pages::index');
In turn, the Pages::index method looks for entries in our database with the "key" of '/widgets/purple-widgets'.
However, the framework defaults to the '/{:controller}/{:action}/{:args}' route for pages like the third and fourth. I know that this is the correct behavior for the framework. Also, best practice would state that I should write the site to match this behavior. But, that isn't an option here.
What I need is a Router that would allow the third and fourth examples to function the same as the first. All examples should be sent to the Pages::index controller, which in turn queries a database using the URL path as a key.
If you don't have any convention in the URL for what is what, between page, item and category. I'd go with a very generic router.
Router::connect('/{:category}/{:page}/{:item}', 'Pages::any');
Router::connect('/{:category}/{:page}', array('Pages::any', 'item' => null));
Router::connect('/{:category}', array('Pages::any', 'page' => null, 'item' => null));
And in Pages::any() to search for the correct stuff. Is that category a page after all (example 4)? Is that page an item (example 1)?
or
You store the URL somewhere (e.g. a mapping table in the database) and use the pattern version of a lithium Route.
Router::connect(new Route(array(
'pattern' => '#^/(?<path>.+)$#',
'params' => array('controller' => 'pages', 'action' => 'any'),
'keys' => array('path' => 'path'),
// extra stuff, if the path is `tata`, it skips this route and uses
// any of the following ones that matches.
'handler' => function($request) {
if ($request->params['path'] == 'tata') {
return false;
} else {
return $request;
}
}
)));
From that point, you'll get the full URL.
You probably should write a smart Router Helper which is maybe able to process your request based on your db defined routes.
Take a look into: net/http/Router.php
especially connect(), parse() and match()
I would start to write some kind of anonymous function and progress it to a testable Class which is located in /extension.. ?

PHP Application URL Routing

So I'm writing a framework on which I want to base a few apps that I'm working on (the framework is there so I have an environment to work with, and a system that will let me, for example, use a single sign-on)
I want to make this framework, and the apps it has use a Resource Oriented Architecture.
Now, I want to create a URL routing class that is expandable by APP writers (and possibly also by CMS App users, but that's WAYYYY ahead in the future) and I'm trying to figure out the best way to do it by looking at how other apps do it.
I prefer to use reg ex over making my own format since it is common knowledge. I wrote a small class that I use which allows me to nest these reg ex routing tables. I use to use something similar that was implemented by inheritance but it didn't need inheritance so I rewrote it.
I do a reg ex on a key and map to my own control string. Take the below example. I visit /api/related/joe and my router class creates a new object ApiController and calls it's method relatedDocuments(array('tags' => 'joe'));
// the 12 strips the subdirectory my app is running in
$index = urldecode(substr($_SERVER["REQUEST_URI"], 12));
Route::process($index, array(
"#^api/related/(.*)$#Di" => "ApiController/relatedDocuments/tags",
"#^thread/(.*)/post$#Di" => "ThreadController/post/title",
"#^thread/(.*)/reply$#Di" => "ThreadController/reply/title",
"#^thread/(.*)$#Di" => "ThreadController/thread/title",
"#^ajax/tag/(.*)/(.*)$#Di" => "TagController/add/id/tags",
"#^ajax/reply/(.*)/post$#Di"=> "ThreadController/ajaxPost/id",
"#^ajax/reply/(.*)$#Di" => "ArticleController/newReply/id",
"#^ajax/toggle/(.*)$#Di" => "ApiController/toggle/toggle",
"#^$#Di" => "HomeController",
));
In order to keep errors down and simplicity up you can subdivide your table. This way you can put the routing table into the class that it controls. Taking the above example you can combine the three thread calls into a single one.
Route::process($index, array(
"#^api/related/(.*)$#Di" => "ApiController/relatedDocuments/tags",
"#^thread/(.*)$#Di" => "ThreadController/route/uri",
"#^ajax/tag/(.*)/(.*)$#Di" => "TagController/add/id/tags",
"#^ajax/reply/(.*)/post$#Di"=> "ThreadController/ajaxPost/id",
"#^ajax/reply/(.*)$#Di" => "ArticleController/newReply/id",
"#^ajax/toggle/(.*)$#Di" => "ApiController/toggle/toggle",
"#^$#Di" => "HomeController",
));
Then you define ThreadController::route to be like this.
function route($args) {
Route::process($args['uri'], array(
"#^(.*)/post$#Di" => "ThreadController/post/title",
"#^(.*)/reply$#Di" => "ThreadController/reply/title",
"#^(.*)$#Di" => "ThreadController/thread/title",
));
}
Also you can define whatever defaults you want for your routing string on the right. Just don't forget to document them or you will confuse people. I'm currently calling index if you don't include a function name on the right. Here is my current code. You may want to change it to handle errors how you like and or default actions.
Yet another framework? -- anyway...
The trick is with routing is to pass it all over to your routing controller.
You'd probably want to use something similar to what I've documented here:
http://www.hm2k.com/posts/friendly-urls
The second solution allows you to use URLs similar to Zend Framework.
Use a list of Regexs to match which object I should be using
For example
^/users/[\w-]+/bookmarks/(.+)/$
^/users/[\w-]+/bookmarks/$
^/users/[\w-]+/$
Pros: Nice and simple, lets me define routes directly
Cons: Would have to be ordered, not making it easy to add new things in (very error prone)
This is, afaik, how Django does it
I think a lot of frameworks use a combination of Apache's mod_rewrite and a front controller. With mod_rewrite, you can turn a URL like this: /people/get/3 into this:
index.php?controller=people&method=get&id=3. Index.php would implement your front controller which routes the page request based on the parameters given.
As you might expect, there are a lot of ways to do it.
For example, in Slim Framework , an example of the routing engine may be the folllowing (based on the pattern ${OBJECT}->${REQUEST METHOD}(${PATTERM}, ${CALLBACK}) ):
$app->get("/Home", function() {
print('Welcome to the home page');
}
$app->get('/Profile/:memberName', function($memberName) {
print( 'I\'m viewing ' . $memberName . '\'s profile.' );
}
$app->post('/ContactUs', function() {
print( 'This action will be fired only if a POST request will occure');
}
So, the initialized instance ($app) gets a method per request method (e.g. get, post, put, delete etc.) and gets a route as the first parameter and callback as the second.
The route can get tokens - which is "variable" that will change at runtime based on some data (such as member name, article id, organization location name or whatever - you know, just like in every routing controller).
Personally, I do like this way but I don't think it will be flexible enough for an advanced framework.
Since I'm working currently with ZF and Yii, I do have an example of a router I've created as part of a framework to a company I'm working for:
The route engine is based on regex (similar to #gradbot's one) but got a two-way conversation, so if a client of yours can't run mod_rewrite (in Apache) or add rewrite rules on his or her server, he or she can still use the traditional URLs with query string.
The file contains an array, each of it, each item is similar to this example:
$_FURLTEMPLATES['login'] = array(
'i' => array( // Input - how the router parse an incomming path into query string params
'pattern' => '#Members/Login/?#i',
'matches' => array( 'Application' => 'Members', 'Module' => 'Login' ),
),
'o' => array( // Output - how the router parse a query string into a route
'#Application=Members(&|&)Module=Login/?#' => 'Members/Login/'
)
);
You can also use more complex combinations, such as:
$_FURLTEMPLATES['article'] = array(
'i' => array(
'pattern' => '#CMS/Articles/([\d]+)/?#i',
'matches' => array( 'Application' => "CMS",
'Module' => 'Articles',
'Sector' => 'showArticle',
'ArticleID' => '$1' ),
),
'o' => array(
'#Application=CMS(&|&)Module=Articles(&|&)Sector=showArticle(&|&)ArticleID=([\d]+)#' => 'CMS/Articles/$4'
)
);
The bottom line, as I think, is that the possibilities are endless, it just depend on how complex you wish your framework to be and what you wish to do with it.
If it is, for example, just intended to be a web service or simple website wrapper - just go with Slim framework's style of writing - very easy and good-looking code.
However, if you wish to develop complex sites using it, I think regex is the solution.
Good luck! :)
You should check out Pux https://github.com/c9s/Pux
Here is the synopsis
<?php
require 'vendor/autoload.php'; // use PCRE patterns you need Pux\PatternCompiler class.
use Pux\Executor;
class ProductController {
public function listAction() {
return 'product list';
}
public function itemAction($id) {
return "product $id";
}
}
$mux = new Pux\Mux;
$mux->any('/product', ['ProductController','listAction']);
$mux->get('/product/:id', ['ProductController','itemAction'] , [
'require' => [ 'id' => '\d+', ],
'default' => [ 'id' => '1', ]
]);
$mux->post('/product/:id', ['ProductController','updateAction'] , [
'require' => [ 'id' => '\d+', ],
'default' => [ 'id' => '1', ]
]);
$mux->delete('/product/:id', ['ProductController','deleteAction'] , [
'require' => [ 'id' => '\d+', ],
'default' => [ 'id' => '1', ]
]);
$route = $mux->dispatch('/product/1');
Executor::execute($route);
Zend's MVC framework by default uses a structure like
/router/controller/action/key1/value1/key2/value2
where router is the router file (mapped via mod_rewrite, controller is from a controller action handler which is defined by a class that derives from Zend_Controller_Action and action references a method in the controller, named actionAction. The key/value pairs can go in any order and are available to the action method as an associative array.
I've used something similar in the past in my own code, and so far it's worked fairly well.
Try taking look at MVC pattern.
Zend Framework uses it for example, but also CakePHP, CodeIgniter, ...
Me personally don't like the MVC model, but it's most of the time implemented as "View for web" component.
The decision pretty much depends on preference...

Categories