yiisoft / yii-core

Yii Framework 3.0 core
https://www.yiiframework.com/
432 stars 75 forks source link

cannot use @web or @webroot in config. #186

Closed jpodpro closed 5 years ago

jpodpro commented 8 years ago

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?

samdark commented 8 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.

cebe commented 8 years ago

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.

jpodpro commented 8 years ago

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.

samdark commented 8 years ago

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?

jpodpro commented 8 years ago

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.

jpodpro commented 8 years ago

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.

mikehaertl commented 8 years ago

@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.

jpodpro commented 8 years ago

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?

mikehaertl commented 8 years ago

@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.

jpodpro commented 8 years ago

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'
    ],
];
mikehaertl commented 8 years ago

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.

jpodpro commented 8 years ago

so there's no built-in way of getting the base server url?

samdark commented 8 years ago

Nope. The app has no idea.

samdark commented 8 years ago

I like the idea of about resolving aliases when these are used. That is not a guarantee that the app was initialized though.

jpodpro commented 8 years ago

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.

mikehaertl commented 8 years ago

@jpodpro I still don't get it: What is the "base server url"? Do you maybe mean the hostInfo in the request component?

jpodpro commented 8 years ago

geez - you don't know what i mean by base server url? yes - the hostinfo of the request component.

mikehaertl commented 8 years ago

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.

jpodpro commented 8 years ago

so you think it is unnecessary to define an alias based on this value?

mikehaertl commented 8 years ago

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.

jpodpro commented 8 years ago

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.

janisto commented 8 years ago

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).
    ],
    ...
]
jpodpro commented 8 years ago

apparently i'm not the only one who has ever wanted to use these aliases to define new ones.

mikehaertl commented 8 years ago

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.

nkostadinov commented 8 years ago

I agree with @mikehaertl . May be setAlias should have an optional parameter whether to be resolve the alias when adding it.

mdmunir commented 8 years ago

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
m00nk commented 8 years ago

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?

samdark commented 8 years ago

Milestone is set to 2.1 means it's likely to be implemented in Yii 2.1.0.

mdmunir commented 8 years ago

if we use @mikehaertl or my suggestion, it can be done in 2.0.x

samdark commented 8 years ago

@mdmunir yes but that would change behavior significantly which means possible compatibiltiy breaks.

Faryshta commented 8 years ago

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();
    }
}
Faryshta commented 8 years ago

@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

samdark commented 8 years ago

What about what @mikehaertl suggested? What's the benefit of your solution compared to his?

Faryshta commented 8 years ago

@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.

samdark commented 8 years ago

Results could be cached after first access so it's not necessary "everytime".

Faryshta commented 8 years ago

@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

samdark commented 8 years ago

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;
}
samdark commented 8 years ago

Why order is important?

Faryshta commented 8 years ago

@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.

mikehaertl commented 8 years ago

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.

samdark commented 8 years ago

@mikehaertl Exactly.

@Faryshta You've made the point about possible infinite recursion though. It should be considered.

Faryshta commented 8 years ago

Honestly I still don't see it and still think we need to parse them in order but maybe can @mikehaertl make a pr?

Faryshta commented 8 years ago
$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?

mikehaertl commented 8 years ago
  1. You can not set an alias like @web/uploads. It can only be @web or the like
  2. You can of course not use getAlias() before you have set the alias

UPDATE: I was wrong about 1) above. Still you should only get an alias after you have set it.

mikehaertl commented 8 years ago

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.

mikehaertl commented 8 years ago

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.

Faryshta commented 8 years ago

@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');

mikehaertl commented 8 years ago

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.

Faryshta commented 8 years ago

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.

cyphix333 commented 8 years ago

Also waiting for this - really bothers me that @web is always empty as it makes things difficult. :(