Closed jpodpro closed 5 years ago
Config is the first thing included even before the app is started. In order to have these aliases available in config these should be defined explicitly either at the very start of index.php
or on top of the config itself. I'm not sure it's a good idea though.
We can not change this. To set up @web
and @webroot
aliases we need a request component of the application. This is done after configuring the application, in the bootstrap phase. If you want to define aliases based on @web
or @webroot
you have to implement a method for application bootstrap and register the alias there.
i do not agree with "cannot change this"
software allows anything to be possible with the proper design. defining aliases from @web are a very common and essential requirement for many web apps so insisting on a design that prevents this is, in my opinion, bad practice. a request component should not be required to define basic app-wide constants.
for now i am forced to hack a solution. but i'm disappointed that this would not be up for discussion as i've seen it reported before.
https://github.com/yiisoft/yii2/blob/master/framework/web/Application.php#L60 that's what @cebe is referring to.
@jpodpro how do you propose to solve that?
well the obvious answer to me is to move the setting of these aliases into the main config:
'aliases' => [
'@web' => stripos($_SERVER['SERVER_PROTOCOL'],'https') === true ? 'https://' : 'http://' . $_SERVER['SERVER_NAME']
],
there might require fancier logic to determine if the site isn't installed into the web root. but this doesn't seem very complicated to me.
here's another related problem: if i'm doing an ajax query on a relative path ( ie: /track/owner ) then the @web alias is empty. this is terrible as i would again expect @web to be universally defined as an app constant and not dependent on the current request. obviously i need to define my own version of @web (which seems silly). if you guys are stuck on this way of doing things then your documentation should be much more clear somewhere that @web and @webroot are not absolute values that can be depended on.
@samdark One way would be to "lazy init" the aliases: Resolve them in getAlias()
instead of setAlias()
. But I'm not sure about implications on backwards compatibility, though. Users may have done nifty things and already rely on the current behavior. Definitely something for 2.1.x.
@jpodpro I'm not sure what you mean by "ajax query on a relative path". In general @web and @webroot are always available after your app has been initialized. This has nothing to do with AJAX requests.
app initialization happens for each request. the application state is not "saved" once the initial page load is complete. so an ajax request for a relative path is a new request and thus a whole new app initialization. since the request is not an absolute url, @web ends up being null in any requests for it during the life of the ajax request because clearly it is determined by the request url and not by a more reliable method.
Yii::setAlias('@web', $request->getBaseUrl());
am i to understand it is bad practice making ajax requests on a relative url?
@jpodpro It's very unclear what exactly you talk about. From a server perspective there's no such thing like a "relative request". And there's also no difference for AJAX requests. So of course, the app is initialized for each request. And also @web
is set with exactly the line that you pasted (it's in yii\web\Application
.).
So maybe you can provide a concrete boiled down example inlcuding the relevant parts of configuration (i.e. URL rules, ...) and the request you make, that exemplifies your problem.
it appears that @web is always empty. i think i remember battling with this a while back and giving up. here's my main config
<?php
return [
'name' => 'YewBeats',
//'language' => 'sr',
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'modules' => [
'user' => 'common\modules\user\Module',
'track' => 'common\modules\track\Module',
],
'components' => [
'assetManager' => [
'bundles' => [
// we will use bootstrap css from our theme
'yii\bootstrap\BootstrapAsset' => [
'css' => [], // do not use yii default one
],
// // use bootstrap js from CDN
// 'yii\bootstrap\BootstrapPluginAsset' => [
// 'sourcePath' => null, // do not use file from our server
// 'js' => [
// 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js']
// ],
// // use jquery from CDN
// 'yii\web\JqueryAsset' => [
// 'sourcePath' => null, // do not use file from our server
// 'js' => [
// 'ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js',
// ]
// ],
],
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'urlManager' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false
//'rules' => array( )
],
'user' => [
'identityClass' => 'common\modules\user\models\User',
'enableSession' => false,
'enableAutoLogin' => true,
'loginUrl' => null,
],
'session' => [
'class' => 'yii\web\DbSession',
],
'authManager' => [
'class' => 'yii\rbac\DbManager',
],
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource',
'basePath' => '@common/translations',
'sourceLanguage' => 'en',
],
'yii' => [
'class' => 'yii\i18n\PhpMessageSource',
'basePath' => '@common/translations',
'sourceLanguage' => 'en'
],
],
],
'request' => [
'class' => '\yii\web\Request',
'enableCookieValidation' => false,
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
],
'response' => [
'class' => 'common\components\CustomResponse'
],
], // components
// set allias for our uploads folder so it can be shared by both frontend and backend applications
// @appRoot alias is definded in common/config/bootstrap.php file
'aliases' => [
'@uploads' => '@root/uploads',
'@uploadsurl' => stripos($_SERVER['SERVER_PROTOCOL'],'https') === true ? 'https://' : 'http://' . $_SERVER['SERVER_NAME'] . '/uploads',
'@temp' => '@root/uploads/tmp'
],
];
From the guide:
@web
, the base URL of the currently running Web application. It has the same value as yii\web\Request::$baseUrl.
So if your application is not installed in a subdirectory of your domain, then @web
will always be empty.
so there's no built-in way of getting the base server url?
Nope. The app has no idea.
I like the idea of about resolving aliases when these are used. That is not a guarantee that the app was initialized though.
perhaps leaving @web and @webroot alone is a good idea. instead you could add a new one like @baseurl, defined in main config so other aliases can depend on it. when i was first learning Yii it was confusing that @web didn't mean this so adding a new one that is explicitly defined as the base url, factoring in secure state and any sub-directory installation would be my suggestion.
@jpodpro I still don't get it: What is the "base server url"? Do you maybe mean the hostInfo in the request component?
geez - you don't know what i mean by base server url? yes - the hostinfo of the request component.
There are many terms floating around without being well defined. So what is a "base server url" for you can be somethign else for someone else. I still wonder why you can't simply use Yii::$app->request->hostInfo
now in that case. Why do you need that information so badly? If for example you need an absolute URL to some part of your app, you should use Url::to() instead of messing around with URL components manually.
so you think it is unnecessary to define an alias based on this value?
I'm waiting for your use case, why you need it. I've built a couple of Yii2 apps and never found myself in a situation where I would need this.
i suppose, understanding where to get the hostInfo now, it should be possible to always build the value i need. it was originally unclear why @web was null and what it is even supposed to be.
I'm using this in my config:
$webroot = dirname(__DIR__) . '/www';
$web = rtrim(dirname($_SERVER["SCRIPT_NAME"]), '/');
return [
...
// Path aliases.
'aliases' => [
'@uploadPath' => $webroot . '/uploads', // @webroot isn't available yet (@webroot/uploads).
'@uploadUrl' => $web . '/uploads', // @web isn't available yet (@web/uploads).
],
...
]
apparently i'm not the only one who has ever wanted to use these aliases to define new ones.
Ok, as I've suggested above, aliases could be resolved on getAlias
and not alread on setAlias
. This way you could use aliases inside aliases here. Let's see if it gets accepted.
I agree with @mikehaertl . May be setAlias should have an optional parameter whether to be resolve the alias when adding it.
How about this?
Yii::setAlias('@bar','@foo/bar');
echo Yii::getAlias('@bar'); // <- @foo/bar
Yii::setAlias('@foo','path/to/foo');
echo Yii::getAlias('@bar'); // <- path/to/foo/bar
just a ping.
I see that issue was not updated more than 4 monthes. When are you going to implement the solution? Or am I waiting in vain?
Milestone is set to 2.1 means it's likely to be implemented in Yii 2.1.0.
if we use @mikehaertl or my suggestion, it can be done in 2.0.x
@mdmunir yes but that would change behavior significantly which means possible compatibiltiy breaks.
how about this logic.
By default setAlias
just stores 'alias' => 'path'
as array.
New method Yii::parseAlias()
will parse all the alias at once, this command will be called by the application on bootstraps()
After that method is called then a new configuration is called too Yii::autoParseAlias()
so after that the Yii::setAlias()
method will automatically parse any new alias.
The most important alias are called before the method bootstrap()
on this method https://github.com/yiisoft/yii2/blob/1f7134634b8de22d6bac1ec2bc6f5d657f1ecb7b/framework/base/Application.php#L217
So the bootstrap code would look something like this
class Yii
{
/**
* @param prepend wheter to add this param at the end of the alias or at the beggining.
* the order is used during the alias parsing.
*/
public function setAlias($alias, $path, $prepend = false)
{
// code
}
}
namespace yii\base;
class Application extends Module
{
public function bootstrap()
{
Yii::parseAlias();
Yii::autoParseAlias();
// existing code
}
}
namespace yii\web;
class Application extends \yii\base\Application
{
public function bootstrap()
{
$request = $this->getRequest();
// make web and webroot alias to get parsed first
Yii::setAlias('@webroot', dirname($request->getScriptFile()), true);
Yii::setAlias('@web', $request->getBaseUrl(), true);
parent::bootstrap();
}
}
@samdark want me to make a PR with my solution? it would be a BC break in the sense that the Yii::setAlias()
method will change signature and might be a problem for anyone extending the method
What about what @mikehaertl suggested? What's the benefit of your solution compared to his?
@samdark our solutions are very similar in the sense that both of us want to avoid parsing the alias on setAlias()
but for what i understand his solution implies to parse the alias everytime getAlias()
is called. I might be wrong on this understanding and we have to ask @mikehaertl abou it.
Results could be cached after first access so it's not necessary "everytime".
@samdark ok i think i still don't understand that solution clearly then.
and we will still need a $prepend
param when setting alias so they know the order in which alias are processed
Umm, why? The solution is like:
public static function getAlias($alias, $throwException = true)
{
// ...
if (!isset(static::$aliases[$root])) {
// calculate alias value
static::$aliases[$root] = $value;
}
// ...
return $alias;
}
Why order is important?
@samdark this entire thread is about the order in which the alias are defined.
Yii::setAlias('@docs', '@web/uploads/docs');
Yii::setAlias('@docsroot', '@webroot/uploads/docs');
Neither will work if @web
and @webroot
alias are not defined first.
if (!isset(static::$aliases[$root])) {
// calculate alias value
static::$aliases[$root] = $value;
}
// ...
return $alias;
The same applies here, you will need to calculate all other alias or at least make a filter about which ones might be related and calculate them first.
The same applies here, you will need to calculate all other alias or at least
You'd use a recursive algorithm in getAlias()
.
Yii::setAlias('@docs', '@web/uploads/docs');
Yii::setAlias('@docsroot', '@webroot/uploads/docs');
So in this example, when you resolve @docs
you'd find that it points to another alias @web/uploads/docs
and call getAlias('@web/uploads/docs')
. Works with any order.
@mikehaertl Exactly.
@Faryshta You've made the point about possible infinite recursion though. It should be considered.
Honestly I still don't see it and still think we need to parse them in order but maybe can @mikehaertl make a pr?
$imgPath = Yii::getAlias('@web/uploads/images');
Yii::setAlias('@web/uploads', '@web/files/uploads');
asertEquals($imgPath, Yii::getAlias('@web/uploads/images'); // will fail
Is this the expected behavior on the proposal by @mikehaertl proposal?
@web/uploads
. It can only be @web
or the likegetAlias()
before you have set the aliasUPDATE: I was wrong about 1) above. Still you should only get an alias after you have set it.
Here's my suggested fix. I have to admit, that I did not test this deeply, but it fixes the initial issue addressed on top of this issue.
Note that it resolves the alias each time you call getAlias()
. So it adds a minor overhead to the existing code.
I've tested with:
'aliases' => [
'@uploads' => '@web/uploads',
'@foo' => '@bar',
'@foo2' => '@bar/2',
'@foo/bar' => '@bar/bar',
'@foo/baz' => '@bar/baz',
'@foo/baz2' => '@bar/baz/2',
'@bar' => '/tmp',
'@bar/baz' => '/xyz',
],
All the above aliases can be set in any order and resolve to the expected result.
As pointed out by @Faryshta there could be a situation when an target alias is changed, after it was already fetched from getAlias()
:
Yii::setAlias('@foo', '/x');
Yii::setAlias('@bar', '@foo');
$a = Yii::getAlias('@bar');
// $a === '/x';
Yii::setAlias('@foo', '/y');
$b = Yii::getAlias('@bar');
// $b === ???
The question is: Which value should $b
have now? I tend to /y
, because @bar
obviously should be an alias for @foo
, so if @foo
is changed this should be reflected in the value of @bar
. That's how the code from my PR works now. The same should be true for mixed aliases that have /
in them.
So if we resolve them each time getAlias()
is called, we always get the current path for the target alias.
@mikehaertl on my solution i added the $prepend
parameter to the setAlias()
method to know the order in which the alias are resolved.
Example.
Yii::setAlias('@bar', '@foo/bar');
// first case, $prepend = false;
Yii::setAlias('@foo', '@web/foo', false);
Yii::getAlias('@bar'); // throw error/exception since @foo is not defined
// second case, $prepend = true;
Yii::setAlias('@foo', '@web/foo', true);
Yii::getAlias('@bar'); // returns the same as Yii::getAlias('@web/foo/bar');
Second example.
$bazPath = Yii::getAlias('@foo/bar/baz');
// if $prepend = false
Yii::setAlias('@foor/bar', '@web/bar', false);
$bazPath == Yii::getAlias('@foo/bar/baz'); // is true since the @web/
// if $prepend = true;
Yii::setAlias('@foor/bar', '@web/bar', true);
$bazPath == Yii::getAlias('@foo/bar/baz'); // is false
Also with this method you can avoid recursive loops like Yii::setAlias('@foo/bar', '@bar/foo'); Yii::setAlias('@bar/foo', '@foo/bar');
Hmm, to be honest: This seems to unnecessarily complicate things. Aliases can already get quite complex, given that an alias can look like @foo/bar/baz
. IMO they should not be overused and we should not encourage users to do too complex things with them. In your case I would have to read a couple of times over the documentation to understand, what exactly that $prepend
option does. I still don't quite get it.
In that sense circular references are a clear sign of overusing aliases. And if this happens, the developer will immediately know it, because it simply won't resolve.
Actually my fix really only addresses the initial use case of setting alias @a
to alias @b
, before alias @b
is even defined. And it adds the "late resolving" on top, so that you could even change the target alias and @a
would still resolve, similar to how symlinks work on Linux filesystems.
In that sense circular references are a clear sign of overusing aliases. And if this happens, the developer will immediately know it, because it simply won't resolve.
Agree. Thats my point. So we need to show an exception error or something when the user does it.
what exactly that $prepend option does.
Its not documented, since its a new parameter. I explained what it does on this discussion. Basically its tohave the alias ordered and then thats used to decide in which order they are calculated.
Also waiting for this - really bothers me that @web
is always empty as it makes things difficult. :(
i want to setup some aliases in the app config such as an uploads directory: 'aliases' => [ '@uploadsurl' => '@web/uploads' ],
however this fails because @web is unavailable in the config stage which sucks. @web and @webroot shouldn't need to wait until Request to be defined. are they not extremely independent values?