How to change dynamically APP_KEY in for different different users - php

Basically i want to assign different different APP_KEY to different different user i have stored generated app_key in "users table" so, my question is how can i achieve this? i have try but can't get proper solution
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => Auth::User()->APP_KEY
'cipher' => 'AES-256-CBC',

you can create one middleware, and inside of handle method change config value...
class SetConfig
{
/**
* Handle an incoming request.
*
* #param Request $request
* #param Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
config()->set("app.key", Auth::User()->APP_KEY);
return $next($request);
}
}
and need to init middleware on App\Http\Kernel like this:
protected $middleware = [
...
SetConfig::class,
...
];
or
on provider :
$router = $this->app['router'];
$router->pushMiddlewareToGroup('web', SetConfig::class);

Related

Remove cache files in Laravel

I am beginner in Laravel.
I use in my project Laravel 7.
I have cache system in my project.
I have cache in my project with keys:
category
category.id
category.subcategory.id
product.all
etc.
I need function to remove cache.
I write this:
private function deleteCache(string $keyToRemove)
{
Cache::forget($keyToRemove);
}
Is universal cache removal possible?
I need a function that will be
Remove selected keys:
deleteCache(['category.100', 'product.all', 'category.1'])
Remove all cache with category (for example: category.1, category.all, category, category.tree, category.subcategory.1 etc).
deleteCache(['category.*'])
How can I make it?
TL:DR What you need is not available by default, you need customized, wrapper methods that requires "technical" knowledge about the cache driver(underlying technology) you choose.
Laravel cache supports multiple technologies(drivers) including redis, database, file, memcached etc. All these drivers implement the same interface.
namespace Illuminate\Contracts\Cache;
interface Store
{
/**
* Retrieve an item from the cache by key.
*
* #param string|array $key
* #return mixed
*/
public function get($key);
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* #param array $keys
* #return array
*/
public function many(array $keys);
/**
* Store an item in the cache for a given number of minutes.
*
* #param string $key
* #param mixed $value
* #param float|int $minutes
* #return void
*/
public function put($key, $value, $minutes);
/**
* Store multiple items in the cache for a given number of minutes.
*
* #param array $values
* #param float|int $minutes
* #return void
*/
public function putMany(array $values, $minutes);
/**
* Increment the value of an item in the cache.
*
* #param string $key
* #param mixed $value
* #return int|bool
*/
public function increment($key, $value = 1);
/**
* Decrement the value of an item in the cache.
*
* #param string $key
* #param mixed $value
* #return int|bool
*/
public function decrement($key, $value = 1);
/**
* Store an item in the cache indefinitely.
*
* #param string $key
* #param mixed $value
* #return void
*/
public function forever($key, $value);
/**
* Remove an item from the cache.
*
* #param string $key
* #return bool
*/
public function forget($key);
/**
* Remove all items from the cache.
*
* #return bool
*/
public function flush();
/**
* Get the cache key prefix.
*
* #return string
*/
public function getPrefix();
}
Depending on the driver you choose - you need customized methods to achieve what you need.
For your first question, the following method would be useful to remove multiple keys.
public function deleteCache(array $keys)
{
foreach ($keys as $key) {
Cache::forget($key);
}
}
I am familiar with redis so i will give examples around it. If you are going to use redis as cache driver - it is better to modify that method like this; Since redis's delete command supports deleting multiple keys at once. This one is more effective than the previous one.
public function deleteCache(array $keys)
{
Redis::del($keys);
}
One trick is to be careful about cache prefix. If you are using cache prefix(defined in your cache config file) - then you need to prepend those prefix to keys.
For your second question(Remove all cache with category) there are several ways to do it but some of them wouldn't be performance/production friendly. In redis you may execute some command such as keys or scan to iterate through database and then invoke the previously defined method with the returned results.
Especially keys command should only be used in production environments with extreme care.
redis key
redis scan
Redis is only example - if you are going to use database cache driver - then you need to implement methods to satisfy your case. It will require technical knowledge about the both how laravel implements it via database(tables, queries etc) and how your extended methods will use it(tables, queries, columns, indexes etc)

How to search a pivot table for rows that are owned by two users

Sorry if this is a stupid question, but I'm new to Laravel.
I have two models and a pivot table:
User
id | name | password
public function conversations(): ?BelongsToMany
{
return $this->belongsToMany(Conversation::class)->withTimestamps();
}
Conversation
id
public function users(): ?BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
conversation_user
id | conversation_id | user_id
I create a conversation and assign the users with sync like so:
$user->conversations()->syncWithoutDetaching($conversation);
$targetUser->conversations()->syncWithoutDetaching($conversation);
Users can have many conversations, and conversations can have multiple users. This is fine, but when I want to get a conversation with two specific users I don't know the best way to utilize the ORM to find the conversation they're both apart of.
I am currently using this next method, which works but it feels like there is a much better way of doing things utilizing the ORM:
/**
* Get a conversation by a target user id.
*
* #param int $targetUserId
* #return mixed
*/
public function getConversationByTargetUserId(int $targetUserId)
{
// Get the current user.
$user = Auth::guard()->user();
// Check the user exists.
if (!$user) {
throw new HttpException(500);
}
/**
* Get all pivot tables where the
* user ID is from the current user.
*/
$userConversationIdsArray = DB::table('conversation_user')->where('user_id', $user->id)->pluck('conversation_id');
/**
* Get all pivot tables where the user
* id is equal to the target id, and is
* also owned by the current user. Return
* the first instance that we come across.
*/
$targetConversation = DB::table('conversation_user')->where(['conversation_id' => $userConversationIdsArray, 'user_id' => $targetUserId])->first();
/**
* Return the conversation.
*/
return Conversation::find($targetConversation->conversation_id);
}
Thank you for your time :)
Is there a particular reason you are not utilising Eloquent? It might make it easier.
It could be done like this as you already have the user.
$user->conversations()->has('users.id', '=', $targetUserId)->first();
(I have not tested this solution so i am not sure this works 100%)
Also, there might be a typo in your first query. Might be a copy paste error might be a typo. Just making sure.
$userConversationIdsArray = DB::table('conversation_user')->where('user_id', $user->id)->pluck('id'); <---- 'id' shouldn't that be 'conversation_id'?
Thanks to #Fjarlaegur they put me on the right track. The following method works:
/**
* Get a conversation by a target user id.
*
* #param int $targetUserId
* #return mixed
*/
public function getConversationByTargetUserId(int $targetUserId)
{
// Get the current user.
$user = Auth::guard()->user();
// Check the user exists.
if (!$user) {
throw new HttpException(500);
}
return $user->conversations()->whereHas('users', function ($query) use ($targetUserId) {
$query->where('users.id', $targetUserId);
})->first();
}

Multiple Policies for a Model in Laravel

Does Laravel allow us to add multiple Policies for a Model? I.e. consider App\Providers\ASuthServiceProvider's $policies property:
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Team' => 'App\Policies\RoundPolicy',
'App\Team' => 'App\Policies\AnotherPolicy',
];
I haven't tested it in an application, because even if it worked, I would be here asking a similar question, regarding whether this is considered bad practise or prone to unexpected behaviour.
The alternative I have is a very messy Policy, containing policies relating to several controllers, named in camel case:
/**
* Allows coach of Team and admin to see the Team management view.
* Used in TeamManagementController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function manage(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows a coach to detach themself from a Team.
* Used in TeamController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function detach(User $user, Team $team)
{
return $user->id === $team->user_id;
}
/**
* Below function are used for controllers other than TeamController and TeamManagementController.
* Reason: We need to authorize, based on a Team. Hence, using this Policy.
*/
/**
* Allows coach of Team, as well as admin to view players of a Team.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function indexPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows coach of Team, as well as admin to view players of a Team as an array.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function fetchPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
etc. etc.
You could use traits to separate the logic for your policy.
You would create a base TeamPolicy and then multiple traits with the various methods that you would want within the base class.
<?php
class TeamPolicy
{
use RoundPolicy, AnotherPolicy;
}
The $policies variable uses the model as key and as value a policy. Keys are unique so you can only set one policy per model. However you can use a policy on multiple models.
In your case the App\Policies\AnotherPolicy is the only one which will be used. Also assigning multiple models the same policy really depends on what you want to do. Basically you do not want messy or gross code. So if you create a policy for two models and the policy code becomes too large, it is time to consider if creating another policy would make the code simpler/less gross.
You need to create a model class that will be associated with the policy.
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Round' => 'App\Policies\RoundPolicy',
'App\Another' => 'App\Policies\AnotherPolicy',
];
Create model classes which extend the Team class. The advantage of this approach is to have separate relationships and functions for respective business logic.
namespace App\Models;
class Round extend Team

What is "a" correct way to integrate a Zend2 application with PHPBB3 authentication?

Ok, this is a bit complicated, so bear with me.
I'm running a PHPBB Forum for some time now and my goal is to create a Zend2 PHP Application using its User Administration and Authentication Features instead of building up a completely new Authorization component which would in turn need to synchronize with the Forum again.
Following Components will be used in the live environment: PHPBB3, Zend Framework 2 (latest stable), Apache, PHP 5.6+, MySQL running on a virtual Linux server without root access.
My Development Environment (running all examples below)is: PHPBB3, Zend Framework 2 (latest stable), XAMPP 3.2.2, PHP 5.6.21 with xdebug enabled, MariaDB running on Windows 8.
Whenever integration of PHPBB is asked for the following lines inevitably turn up in searches:
global $phpbb_root_path, $phpEx, $user, $db, $config, $cache, $template;
define('IN_PHPBB', true);
$phpbb_root_path = './forum/phpBB3/'; // this path is from an external example
$phpEx = substr(strrchr(__FILE__, '.'), 1);
$phpBBFile = $phpbb_root_path . 'common.' . $phpEx;
include($phpBBFile);
// Start session management
$user->session_begin();
$auth->acl($user->data);
$user->setup();
I have already had success including those without using a framework or by calling php directly through ajax, but now - using the Zend 2 Framework - there are multiple problems surfacing when including native PHPBB3 code.
I have to say I am not an experienced PHP programmer and I have been learning about Zend for just a couple of days now.
My first try centered on integrating the above code before the Zend Application is called in Zends index.php:
....
// Setup autoloading
require 'init_autoloader.php';
global $phpbb_root_path, $phpEx, $user, $db, $config, $cache, $template;
define('IN_PHPBB', true);
$phpbb_root_path = 'public/forums/';
$phpEx = substr(strrchr(__FILE__, '.'), 1);
$phpBBFile = $phpbb_root_path . 'common.' . $phpEx;
include($phpBBFile);
// Run the application!
Zend\Mvc\Application::init(require 'config/application.config.php')->run();
....
Resulting in this error:
Catchable fatal error: Argument 1 passed to Zend\Stdlib\Parameters::__construct() must be of the type array,
object given, called in C:\xampp\htdocs\myZendApp\vendor\zendframework\zend-http\src\PhpEnvironment\Request.php
on line 72 and defined in C:\xampp\htdocs\myZendApp\vendor\zendframework\zend-stdlib\src\Parameters.php on line 24
So calling PHPBB this early on seems to mess up Zend in a bad way I went on to other implementations.
My favored design would include a separate Authentication Zend Module which handles PHPBB authentication and is available as a service for all routes and their controllers.
Including and calling the phpbb scripts however lead to various problems probably related to the heavy use of globals.
Here some example code from the checkAction in the PhpbbAuthController:
public function checkAction(){
global $phpbb_root_path, $phpEx, $user, $db, $config, $cache, $template;
define('IN_PHPBB', true);
$phpbb_root_path = 'public/forums/';
$phpEx = substr(strrchr(__FILE__, '.'), 1);
$phpBBFile = $phpbb_root_path . 'common.' . $phpEx;
include($phpBBFile);
$user->session_begin();
$auth->acl($user->data);
$user->setup();
$response = array();
if ($user->data['user_id'] == ANONYMOUS) {
$response['loginState'] = "logged_out";
} else {
$response['loginState'] = "logged_in";
}
return new ViewModel($response);
}
And here the error from executing session_begin()
Fatal error: Call to a member function header() on null in
C:\xampp\htdocs\myZendApp\public\forums\phpbb\session.php on line 228
After debugging into it it seemed that all references to the $request and $symfony_request inside those authentication functions where NULL.
After sinking lots of hours into discerning a way to execute the scripts from Zend context I have set my eyes on a way to execute the scripts in a separate context.
The easiest way that came to my mind was to call the script from an HttpClient and use the Result text to drive my Authentication Service.
To do that I would need to retrieve the session cookie from the called scripts and store it for use in the Zend application.
If I channel the scripts through the Zend Framework I seem to run into the same problem again (having PHBB code in a Zend Controller), so I can't use Zends routing to access them.
Since I am using an http request I have to store the scripts in the public directory or a subdirectory of it.
And that is where I am right now. The internal call to the php files that use PHPBB work fine on their own, but the HttpClient I use (from a Zend Controller class for now) does run into a timeout at every turn, which I formulated into another question here: Zend 2 Http Client Request times out when requesting php file from localhost/public directory.
I would appreciate your views, hints and possible architectures or even part solutions to my problem/s mentioned above.
What I do not want to do under any circumstances is to invent my own authentication and user administration as it would always be inferior to the complex but proven system which is already in PHPBB and lead to security issues in the long run.
Also the Zend application is considered an "Extra" since the Forum is the core of the site as things stand now.
Thank you very much for your time and please ask for additional information. (I couldn't possible include all the code and I don't know what else would be relevant to you at this point)
PHPBB 3.x is based on symfony and uses symfony components. The posts you are referencing are extremely outdated.
Please take a look at:
https://github.com/phpbb/phpbb/blob/3.1.x/phpBB/config/auth.yml (the container's definition of authentication providers for PHPBB3)
Version on master
https://github.com/phpbb/phpbb/blob/master/phpBB/config/default/container/services_auth.yml
AND
https://github.com/phpbb/phpbb/blob/3.1.x/phpBB/phpbb/auth/provider/provider_interface.php (shown below)
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* #copyright (c) phpBB Limited <https://www.phpbb.com>
* #license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\auth\provider;
/**
* The interface authentication provider classes have to implement.
*/
interface provider_interface
{
/**
* Checks whether the user is currently identified to the authentication
* provider.
* Called in acp_board while setting authentication plugins.
* Changing to an authentication provider will not be permitted in acp_board
* if there is an error.
*
* #return boolean|string False if the user is identified, otherwise an
* error message, or null if not implemented.
*/
public function init();
/**
* Performs login.
*
* #param string $username The name of the user being authenticated.
* #param string $password The password of the user.
* #return array An associative array of the format:
* array(
* 'status' => status constant
* 'error_msg' => string
* 'user_row' => array
* )
* A fourth key of the array may be present:
* 'redirect_data' This key is only used when 'status' is
* equal to LOGIN_SUCCESS_LINK_PROFILE and its value is an
* associative array that is turned into GET variables on
* the redirect url.
*/
public function login($username, $password);
/**
* Autologin function
*
* #return array|null containing the user row, empty if no auto login
* should take place, or null if not impletmented.
*/
public function autologin();
/**
* This function is used to output any required fields in the authentication
* admin panel. It also defines any required configuration table fields.
*
* #return array|null Returns null if not implemented or an array of the
* configuration fields of the provider.
*/
public function acp();
/**
* This function updates the template with variables related to the acp
* options with whatever configuraton values are passed to it as an array.
* It then returns the name of the acp file related to this authentication
* provider.
* #param array $new_config Contains the new configuration values that
* have been set in acp_board.
* #return array|null Returns null if not implemented or an array with
* the template file name and an array of the vars
* that the template needs that must conform to the
* following example:
* array(
* 'TEMPLATE_FILE' => string,
* 'TEMPLATE_VARS' => array(...),
* )
* An optional third element may be added to this
* array: 'BLOCK_VAR_NAME'. If this is present,
* then its value should be a string that is used
* to designate the name of the loop used in the
* ACP template file. When this is present, an
* additional key named 'BLOCK_VARS' is required.
* This must be an array containing at least one
* array of variables that will be assigned during
* the loop in the template. An example of this is
* presented below:
* array(
* 'BLOCK_VAR_NAME' => string,
* 'BLOCK_VARS' => array(
* 'KEY IS UNIMPORTANT' => array(...),
* ),
* 'TEMPLATE_FILE' => string,
* 'TEMPLATE_VARS' => array(...),
* )
*/
public function get_acp_template($new_config);
/**
* Returns an array of data necessary to build custom elements on the login
* form.
*
* #return array|null If this function is not implemented on an auth
* provider then it returns null. If it is implemented
* it will return an array of up to four elements of
* which only 'TEMPLATE_FILE'. If 'BLOCK_VAR_NAME' is
* present then 'BLOCK_VARS' must also be present in
* the array. The fourth element 'VARS' is also
* optional. The array, with all four elements present
* looks like the following:
* array(
* 'TEMPLATE_FILE' => string,
* 'BLOCK_VAR_NAME' => string,
* 'BLOCK_VARS' => array(...),
* 'VARS' => array(...),
* )
*/
public function get_login_data();
/**
* Performs additional actions during logout.
*
* #param array $data An array corresponding to
* \phpbb\session::data
* #param boolean $new_session True for a new session, false for no new
* session.
*/
public function logout($data, $new_session);
/**
* The session validation function checks whether the user is still logged
* into phpBB.
*
* #param array $user
* #return boolean true if the given user is authenticated, false if the
* session should be closed, or null if not implemented.
*/
public function validate_session($user);
/**
* Checks to see if $login_link_data contains all information except for the
* user_id of an account needed to successfully link an external account to
* a forum account.
*
* #param array $login_link_data Any data needed to link a phpBB account to
* an external account.
* #return string|null Returns a string with a language constant if there
* is data missing or null if there is no error.
*/
public function login_link_has_necessary_data($login_link_data);
/**
* Links an external account to a phpBB account.
*
* #param array $link_data Any data needed to link a phpBB account to
* an external account.
*/
public function link_account(array $link_data);
/**
* Returns an array of data necessary to build the ucp_auth_link page
*
* #param int $user_id User ID for whom the data should be retrieved.
* defaults to 0, which is not a valid ID. The method
* should fall back to the current user's ID in this
* case.
* #return array|null If this function is not implemented on an auth
* provider then it returns null. If it is implemented
* it will return an array of up to four elements of
* which only 'TEMPLATE_FILE'. If 'BLOCK_VAR_NAME' is
* present then 'BLOCK_VARS' must also be present in
* the array. The fourth element 'VARS' is also
* optional. The array, with all four elements present
* looks like the following:
* array(
* 'TEMPLATE_FILE' => string,
* 'BLOCK_VAR_NAME' => string,
* 'BLOCK_VARS' => array(...),
* 'VARS' => array(...),
* )
*/
public function get_auth_link_data($user_id = 0);
/**
* Unlinks an external account from a phpBB account.
*
* #param array $link_data Any data needed to unlink a phpBB account
* from a phpbb account.
*/
public function unlink_account(array $link_data);
}
The interface you can implement to create a provider for your Zend framework project.
You can see how the providers are used when a session is created
https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/session.php#L560
/* #var $provider_collection \phpbb\auth\provider_collection */
$provider_collection = $phpbb_container->get('auth.provider_collection');
$provider = $provider_collection->get_provider();
$this->data = $provider->autologin();
Make sure both projects use the same cookies, or that zend is also setting the phpBB cookies and session when a user is logging in as session_start uses this to look for session ids:
if ($request->is_set($config['cookie_name'] . '_sid', \phpbb\request\request_interface::COOKIE) || $request->is_set($config['cookie_name'] . '_u', \phpbb\request\request_interface::COOKIE))
{
$this->cookie_data['u'] = request_var($config['cookie_name'] . '_u', 0, false, true);
$this->cookie_data['k'] = request_var($config['cookie_name'] . '_k', '', false, true);
$this->session_id = request_var($config['cookie_name'] . '_sid', '', false, true);
$SID = (defined('NEED_SID')) ? '?sid=' . $this->session_id : '?sid=';
$_SID = (defined('NEED_SID')) ? $this->session_id : '';
if (empty($this->session_id))
{
$this->session_id = $_SID = request_var('sid', '');
$SID = '?sid=' . $this->session_id;
$this->cookie_data = array('u' => 0, 'k' => '');
}
}
else
{
$this->session_id = $_SID = request_var('sid', '');
$SID = '?sid=' . $this->session_id;
}
Thank you.

Laravel and Codeception - test fails on PUT forms

I have a resource controller in laravel to manage my users. This creates a route to update users info that takes a request with HTTP PUT method.
This shows artisan route:list command output:
+--------+--------------------------------+-----------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------+------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+--------------------------------+-----------------------------------------------------------+--------------------------------------+--------------------------------------------------------------------------------+------------+
...
| | PUT | users/{users} | users.update | App\Http\Controllers\Users\UsersController#update | auth |
It works correctly on my web browser but when I try to run a test with codeception and I submit the form I get a method not allowed exception and the test fails.
I tried to see why this is happening and it seems to be the request made by codeception. That request is made with POST instead of PUT method preventing Laravel from matching the route.
HTML forms doesn't suport PUT methods so Laravel Form helper class creates the form as follows:
<form method="POST" action="https://myapp.dev/users/172" accept-charset="UTF-8">
<input name="_method" value="PUT" type="hidden">
...
However, it seems that codeception is not reading the _method value.
How can I fix this?
EDIT: Looking deeply on the code I found that test don't override the request method beacause of a constant in th Request class called Request::$httpMethodParameterOverride.
/**
* Gets the request "intended" method.
*
* If the X-HTTP-Method-Override header is set, and if the method is a POST,
* then it is used to determine the "real" intended HTTP method.
*
* The _method request parameter can also be used to determine the HTTP method,
* but only if enableHttpMethodParameterOverride() has been called.
*
* The method is always an uppercased string.
*
* #return string The request method
*
* #api
*
* #see getRealMethod()
*/
public function getMethod()
{
if (null === $this->method) {
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' === $this->method) {
if ($method = $this->headers->get('X-HTTP-METHOD-OVERRIDE')) {
$this->method = strtoupper($method);
} elseif (self::$httpMethodParameterOverride) {
$this->method = strtoupper($this->request->get('_method', $this->query->get('_method', 'POST')));
}
}
}
return $this->method;
}
The previous constant value should be true but shomehow, when I run the test its value is false.
I found a solution but I don't think this is the right place to write it.
I added a simple line of code on the Connector\Laravel5 class.
public function __construct($module)
{
$this->module = $module;
$this->initialize();
$components = parse_url($this->app['config']->get('app.url', 'http://localhost'));
$host = isset($components['host']) ? $components['host'] : 'localhost';
parent::__construct($this->app, ['HTTP_HOST' => $host]);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
// Added to solve the problem of overriding the request method
Request::enableHttpMethodParameterOverride();
}
This solves my problem.
You can not use PUT method in HTML form tag. For that you need to use laravel's blade template format to define form tag.
e.g.
{!! Form::open(['url' => 'users/{users}','method' => 'put','id' => 'form' ]) !!}
Also you can use route attribute to define route instead of url.

Categories