Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Router Feature #878

Merged
merged 3 commits into from Apr 13, 2013
Merged

New Router Feature #878

merged 3 commits into from Apr 13, 2013

Conversation

jails
Copy link
Contributor

@jails jails commented Apr 6, 2013

Hi guys I need your opinion on the following feature. Since my first li3 application, I always feel limited with URL & Routes managment in general. I started my reflexion with #416 and today I would like to share what I've done.

Why enhancing the Router class ?

A good practice of nowadays development is the separation of concerns. For example splitting a monolithic application to some dedicated libraries is a way to increase this separation of concerns. If Lithium already own a solid plugins system, The Router class still an hurdle in the road since routes can't be separated. Who never had conflicts among a couple of routes (especially between app & admin routes).

So to keep routes "isolated" in an application or between libaries this PR introduce the concept of scopes and attachments. This mean you can group a bunch of routes inside a scope and attach them to an arbitraty url.

What awesome would be if I can "mount" a kind of forum library on a 'forum' subdomain or another specific URL ?

So How does it works ?

Previously you connect routes using the following syntax :

Router::connect('/', 'Pages::view');
Router::connect('/pages/{:args}', 'Pages::view');

To take profit of the scopes, you need to use the following scoping notation:

Router::scope('app', function() {
    Router::connect('/ambiguous/{:action}');
    Router::connect('/{:controller}/{:action}/{:args}');
});

Router::scope('test', function() {
    Router::connect('/ambiguous/{:action}');
    Router::connect('/test/{:args}', array('controller' => 'lithium\test\Controller'));
    Router::connect('/test', array('controller' => 'lithium\test\Controller'));
});

Note : the above routes should be in different routes.php files, but it' ok for the example.

What does it change ?

Well nothing yet. Everything works and the only difference is that Router::scope() will now returns the scope of the dispatched route.

For example with '/controller/action', Router::scope() will return 'app',
with '/test/action', Router::scope() will return 'test', and with '/ambiguous/index' it'll return 'app'.

Indeed, since the app location is not defined right now, Router::process() can't resolve the ambiguity (i.e same behavior as before)

So what can i do with that ?

So the next step is to attach scopes to kind of "mount points". For example the following example will use a prefix for the 'app' scope:

Router::attach('app', array('prefix' => 'myprefix'));

This mean this routes will only match on urls like http://www.domain.com/myprefix.

But you can set the mount point based on subdomain using absolute location:

Router::attach('app', array(
    'absolute' => true,
    'host' => 'mysubdomain.mysite.com',
    'scheme' => 'http://'
));

You can also use variables for managing subdomains like : <username>.mysite.com for example:

Router::attach('app', array(
    'absolute' => true,
    'host' => '{:subdomain:[a-z]+}.mysite.com',
    'scheme' => 'http://'
));

How can i write a link from a location to a different location ?

Since Router::process() detect the scope, the links in your templates will be generated according the scope defined with Router::attach().

Suppose you browse '/controller/action' and you need a link to the 'test' scope in your view, you only need to set the 'scope' attribute :

$this->html->link('test', '/test', array('scope' => 'test'));

Ok but how does the variables in location works ?

Suppose you wan't to manage subdomains like : <username>.mysite.com with the following configuration :

Router::attach('app', array(
    'absolute' => true,
    'host' => '{:subdomain:[a-z]+}.mysite.com',
    'scheme' => 'http://'
));

with 'http://bob.mysite.com/controller/action', all your links will start with 'http://bob.mysite.com/' but you can override the default scope params with the following syntax :

$this->html->link('Max home', '/home/index', array(
    'scope' => array(
        'app' => array(
            'subdomain' => 'max'
        )
    )
));

Since here max is in current scope you could simply write :

$this->html->link('Max home', '/home/index', array(
    'scope' => array(
        'subdomain' => 'max'
    )
));

Media class wants in !

Of course separating routes is fine but you can also separate the Medias with the same scoping system :

Media::attach('cdncss', array(
    'absolute' => true,
    'host' => 'www.cdn.com',
    'scheme' => 'http://',
    'prefix' => 'web/assets/'
));

Media::attach('cdnimage', array(
    'absolute' => true,
    'host' => 'www.cdn.com',
    'scheme' => 'http://',
    'prefix' => 'web/assets/'
));

The attachement above create some usable scopes for views.

$this->html->style('style.css', array('scope' => 'cdncss'));
$this->html->image('test.gif', array('scope' => 'cdnimage'));

See tests for complete features

How I can play a bit with it ?

Pretty simple just run the following commands in your document root:

git clone https://github.com/jails/li3_bigbang myapp
cd myapp
composer install
chmod -R 777 atoms/app/resources
chmod -R 777 atoms/admin/resources

And open your browser at: http://localhost/myapp/

Feel free to comment and to give your opinion on this.

Thank you for reading !

@bayleedev
Copy link
Member

This will make managing link hosts and cdn's a ton easier! 👍

@SayB
Copy link

SayB commented Apr 6, 2013

this is brilliant ! ... yet more abstraction ... asset management would be a breeze

@nervetattoo
Copy link
Member

Loving it! Major thumbs up.
Maybe it could be shorthanded for writing the routes initially as well?

Router::scope('test', [
    '/ambiguous/{:action}',
    '/test/{:args}' => array('controller' => 'lithium\test\Controller'),
    '/test' => array('controller' => 'lithium\test\Controller')
]);

@jails
Copy link
Contributor Author

jails commented Apr 6, 2013

@nervetattoo right ! Since the third parmeter of Router::connect() is still rarely used, it could be an awesome shorthanded syntax !

@rapzo
Copy link
Member

rapzo commented Apr 6, 2013

I love this!
This will avoid the messy if clauses i usually end up writing when setting up routes.
Way to go Guru 👍 awesome work!

One question: any special behavior for continuation routes? Will they only apply for the given scope (or for everything if no scope is given)?

@jails
Copy link
Contributor Author

jails commented Apr 6, 2013

Right! continuation routes are scope based. If a scope is defined outside a scope it'll only be applied to all "non scoped routes".

@ericcholis
Copy link
Contributor

If anything, this would make admin routes a breeze. I'm sure many LI3 devs would appreciate this change.

* Initialize `static::$_scopes` with a `lithium\core\Configuration` instance.
*/
protected static function _initScopes() {
$configuration = static::$_classes['configuration'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be static::$_scopes = static::_instance('configuration').

- WARNING BC BREAK: Defining the url in `$_GET['url']` is no more supported.
- Enhance REQUEST_URI based request.
- Environmement variables are populated in attributes.
- If the option `'globals'` is set to `false`, $_SERVER, $_ENV, $_GET & $_POST are not added to the Request.
- Remove infinite loop on missed PHP_SELF & SCRIPT_FILENAME
- Routes can be scoped using the following notation:
  Routed::scope(); // Returns the current scope
  Router::scope('name'); // Use a new scope
  Router::scope('name'), function(){/* executed inside the scope */});
  Router::attach('name'), array(), array()); //Attach a mount point to a scope
  Router::attached() // Returns all attachments
  Router::attached('name', array()); // Returns the attached mount point configuration
- The Media class can be scoped using the following notation: (i.e. usefull for CDN or others media location).
  Media::scope(); // Returns the current scope
  Media::scope('name'); // Use a new scope
  Media::scope('name'), function(){/* executed inside the scope */});
  Media::attach('name'), array()); // Attach a mount point to a scope
  Media::attached(); // Returns all attachements
  Media::attached('name'); // Returns the attached mount point configuration
- The paths of assets paths now called 'paths' for consistency see `Media::_asset` (BC Break)
- Include UnionOfRAD#535 it's not a good practice to use `'http:method'` at route level but it may be better to not let it buggy.
@jails
Copy link
Contributor Author

jails commented Apr 9, 2013

@nateabele since it's a big PR, I added an extra commit to make it simpler for you to review the changes. But if it's ok I'll squash the two last commit. ;-)

nateabele added a commit that referenced this pull request Apr 13, 2013
@nateabele nateabele merged commit b3b7c06 into UnionOfRAD:dev Apr 13, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants