I have an action to make an 'autologin' based in a id that the system gets from $_SERVER['AUTH_USER']. In my business server that value is always set for authenticated user. Now, I am trying test my autologin (and so many other things that depends the autologin to work) so I need to set some user to that global (just a string).
What I tryed
$_SERVER['AUTH_USER'] = 'someUser';
$I->amOnPage('some-route'); // this page redirects to autologin action where $_SERVER is used to get the user logged.
But when the action autologin is loaded that value is no more inside $_SERVER global and my test crashes.
What I would like to know
Where or how I can set that global value so that my page could behave normally, reading the value and just going on.
I will appreciate any help.
Thank you.
It looks like lack of proper abstraction. You should avoid accessing $_SERVER['AUTH_USER'] directly in your app and do it in at most in one place - in component which will provide abstraction for this. So you should probably extend yii\web\Request and add related method for $_SERVER['AUTH_USER'] abstraction:
class MyRequest extends \yii\web\Request {
private $_myAuthUser;
public function getMyAuthUser() {
if ($this->_myAuthUser === null) {
$this->_myAuthUser = $_SERVER['AUTH_USER'];
}
return $this->_myAuthUser;
}
public function setMyAuthUser($value) {
$this->_myAuthUser = $value;
}
}
Use new class in your config:
return [
'id' => 'app-web',
// ...
'components' => [
'request' => [
'class' => MyRequest::class,
],
// ...
],
];
And use abstraction in your action:
$authUser = explode('\\', Yii::$app->request->getMyAuthUser())[0];
In your tests you can set value using setter in MyRequest:
Yii::$app->request->setMyAuthUser('domain\x12345');
Or configure this at config level:
return [
'id' => 'app-test',
// ...
'components' => [
'request' => [
'class' => MyRequest::class,
'myAuthUser' => 'domain\x12345',
],
// ...
],
];
UPDATE:
According to slinstj comments, Codeception may loose state of request component, including myAuthUser value. In that case it may be a good idea to implement getMyAuthUser() and setMyAuthUser() on different component (for example Yii::$app->user) or create separate component for that:
return [
'id' => 'app-web',
// ...
'components' => [
'authRequest' => [
'class' => MyRequest::class,
],
// ...
],
];
For now, I am using a workaround because there is only one place where that variable value it is checked:
//Inside my action autologin:
$authUser = explode('\\', ($_SERVER['AUTH_USER'] ?? (YII_ENV_TEST ? 'domain\x12345' : 'domain\xInvalid')))[1];
The only relevant point here is YII_ENV_TEST that is true when testing. Using this I can set get an specific value that is enough to that simple test.
However I hope to see any other better idea here!
Thanks.
Related
I'm new in Yii2 framework. To give structure to my web application, I want to put each controller in a subfolder and make a separate controller for each action in each subfolder. Like that one!
controllers
**User**
IndexController
EditController
UpdateController
**Profile**
IndexController
EditController
UpdateController
How can I arrange that in Yii2.
thanks in advance
Well your example is right.
controllers/user/IndexController.php
views/user/index/index.php
Then in IndexController/EditController/UpdateController you have actionIndex and if you run domain.com/user/index or domain.com/user/edit it will execute actionIndex in current controller (IndexController or EditController)
domain.com/user/index = domain.com/user/index/index
and
domain.com/user/edit = domain.com/user/edit/index
Not sure if there are other more effective ways, but one that works would be the following.
Note: This example assumes that you're using https://github.com/yiisoft/yii2-app-advanced but it can work for the basic app also, just changing the namespaces.
So, let's say you say we have a controller, and we want to store some of its actions into different php files.
<?php
// frontend\controllers\SiteController.php
namespace frontend\controllers;
use yii\web\Controller;
class SiteController extends Controller {
public function actions() {
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
'hello-world' => [
'class' => 'frontend\controllers\site\HelloWorldAction',
],
];
}
public function actionIndex() {
// ...
}
So you can see we've got 3 external actions and one internal one.
The first two ones, are framework's tools for Error page and Captcha generation, actually they've inspired my answer.
And the third one, is defined by us:
'hello-world' => [
'class' => 'frontend\controllers\site\HelloWorldAction',
],
So we've named the action and we created our new action class into a separate directory.
<?php
// frontend\controllers\site\HelloWorldAction.php
namespace frontend\controllers\site;
use yii\base\Action;
class HelloWorldAction extends Action {
public function run($planet='Earth') {
return $this->controller->render('hello-world', [
'planet'=>$planet,
]);
}
}
And last, our view:
<?php
// frontend\views\site\hello-world.php
/* #var $this yii\web\View */
use yii\helpers\Html;
$this->title = 'Hello world page';
?>
<h1>Hello world!</h1>
<p>We're on planet <?php echo Html::encode($planet); ?></p>
And seeing it in action:
Update
After posting the answer I realized that maybe you could benefit from another technique also.
The previous answer is good if you want to do just that: Extract actions into individual files.
But, if your application will be of certain size, maybe you should consider using Modules.
You can create them manually or generate them with Gii:
And once generated, include it in your config:
<?php
......
'modules' => [
'profile' => [
'class' => 'frontend\modules\profile\Module',
],
],
......
Modules do just that, group application logic into one directory, controllers, models, views, components, etc.
Two more tips:
Now to access your module, simply visit http://www.your-site.local/profile/default/index, as you can see, it goes like module/controller/action.
And if you want to generate links to actions inside modules, you would do:
<?php
echo Url::to([
'profile/default/index',
'param'=>'value',
]);
?>
Again as you can see we're using module/controller/action as the route.
Last thing, if you're inside a module, let's say profile/picture/edit, and you want to link to Contact page from SiteController, you would do:
<?php
echo Url::to([
'//site/contact',
'param'=>'value',
]);
?>
Note the double slash // at the beginning of the route. Without it, it will generate the url to the current module profile/site/contact.
I have site with subdomains (news.site.com, events.site.com etc).
I am added to config my custom url rule, who find by requested url article in DB and call article controller with action show
for example: 'news.site.com/some-article-with-custom-url'
I want find in db article with url = 'some-article-with-custom-url' and call actionShow() in ArticleController.
But on news.site.com and events.site.com i want find articles with different site_id (column in db)
news.site.com - select * from article where site_id = 1 ...
events.site.com - select * from article where site_id = 2 ...
where I can init some subdomains settings, if on UrlManager level I must already have this settings?
'urlManager' => [
'rules' = [
...
[
'class' => 'common\components\ArticleUrlRule'
],
]
]
Implementation of parse req of this class:
public function parseRequest($manager, $request) {
$articleId = Article::getArticleIdByUrl($request->pathInfo);
if ( !empty($articleId) ) {
return [
'article/show',
[
'id' => $articleId
]
];
}
return false;
}
The easiest way to deal with this, probably there are other ways, starts from defining each sub-domain as a site on your web server. As you regarded, you may have three sites, one main and two sub-domains, make two additional copies of the web folder naming them news and events respectively, so in your Yii2 project root directory you will have web, events and news directories which they are going to be three documents root for three websites on your web server.
In the index.php of each web directory define a variable or a constant to express the site id to use it in your application when it is going to be needed:
<?php
defined('SITE_ID') or define('SITE_ID', x); // where x is an integer
// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'prod');
...
There are a couple of way of doing this, ranging from easy and hacky to hard and well formed.
I think the best way to do this is to have one URL config:
'urlManager' => [
'rules' = [
[
'<controller:[\w-]+>/<id:\d+>'=>'<controller>/view',
]
]
]
Then in your ArticleController:
public function actionShow($id)
{
// This might be better as a definition list stored in config or something
$site = null;
$sub = Yii::$app->request->hostInfo; // Put some logic here to split out subdomain
switch($sub){
case 'site1':
$site = 1;
}
$articleId = Article::find()->where(['id' => $id, 'site' => $site]);
}
Sort of thing. It can be made more robust and, in general, better from there but I believe that's a good way to do it.
I have a typical / classical ACF declaration, in my app-basic application, that uses matchCallback:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['#'],
'matchCallback' => function ($rule, $action) {
echo 'Yii::$app->controller->actionParams = '.print_r(Yii::$app->controller->actionParams, TRUE);
echo '$action->controller->actionParams = '.print_r($action->controller->actionParams, TRUE);
echo '$action = '.print_r($action, TRUE);
die();
}
],
],
]
];
}
To my surprise, I found this:
How can this be true? Why action parameters are not available when evaluating matchCallback?
And -- of course -- how to read them, if my access rule requires to check one of action parameters to judge, whether user can access particular action or not?
EDIT: You can, of course, read action parameters using the brutal way of:
echo '$_GET = '.print_r($_GET, TRUE);
But, I don't like brutal solutions and it really bothers me, why actionParams are empty at this stage?
Controller class has method bindActionParams() which is invoked by \yii\base\Action when it begins to run with the given parameters. So at first controller invoke an action method and run it with params and then action bind its params to controller. But AccessControl check is executing much earlier before action is running, that's why actionParams are empty. Here is an request lifecycle. As you can see before action gets executed controller performs filters, one of which is AccessControl.
If you dont like "brutal" way, consider use of Yii::$app->request->get() which also will test parameter for isset.
I just started working with Laravel. I need to rewrite a whole system I made some years ago, using Laravel 4 as base framework. In my old system, I used to have a constant.php file with some constants declared, and a globals.php file which contained lots of array sets (for example, categories statuses, type of events, langs, etc.). By doing so, I could use something like
foreach ( $langs as $code => $domain ) {
// Some stuff
}
anywhere in my app.
My question is, how can I store that info in the so called "laravel way". I tried using some sort of object to store this info, setting this as a service and creating for it a facade:
app/libraries/Project/Constants.php
namespace PJ;
class Constants {
public static $langs = [
'es' => 'www.domain.es',
'en' => 'www.domain.us',
'uk' => 'www.domain.uk',
'br' => 'www.domain.br',
'it' => 'www.domain.it',
'de' => 'www.domain.de',
'fr' => 'www.domain.fr'
];
}
app/libraries/Project/ConstantsServiceProvider.php
namespace PJ;
use Illuminate\Support\ServiceProvider;
class ConstantsServiceProvider extends ServiceProvider {
public function register() {
$this->app->singleton('PJConstants', function() {
return new Constants;
});
}
}
app/libraries/Project/ConstantsFacade.php
namespace PJ;
use Illuminate\Support\Facades\Facade;
class ConstantsFacade extends Facade {
protected static function getFacadeAccessor() {
return 'PJConstants';
}
}
composer.json
"psr-4": {
"PJ\\": "app/libraries/Project"
},
and so I access that property as PJ\Constants::$langs.
This works, but I doubt it is the most efficient or correct way of doing it. I mean, is it the right way to "propagate" a variable by creating a whole Service Provider and facades and all such stuff? Or where should I put this data?
Thanks for any advice.
EDIT # 01
Data I want to pass to all controllers and views can be directly set in script, like in the example at the beginning of my post, but it can also be generated dynamically, from a database for example. This data could be a list of categories. I need them in all views to generate a navigation bar, but I also need them to define some routing patterns (like /category/subcategory/product), and also to parse some info in several controllers (Like get info from the category that holds X product).
My array is something like:
$categories = [
1 => ['name' => 'General', 'parent' => 0, 'description' => 'Lorem ipsum...'],
2 => ['name' => 'Nature', 'parent' => 0, 'description' => 'Lorem ipsum...'],
3 => ['name' => 'World', 'parent' => 0, 'description' => 'Lorem ipsum...'],
4 => ['name' => 'Animals', 'parent' => 2, 'description' => 'Lorem ipsum...']
]
Just as an example. Index is the id of the category, and the Value is info associated with the category.
I need this array, also, available in all Controllers and Views.
So, should I save it as a Config variable? How else could I store these data; what would be the best and semantically correct way?
For most constants used globally across the application, storing them in config files is sufficient. It is also pretty simple
Create a new file in the app/config directory. Let's call it constants.php
In there you have to return an array of config values.
return [
'langs' => [
'es' => 'www.domain.es',
'en' => 'www.domain.us'
// etc
]
];
And you can access them as follows
Config::get('constants.langs');
// or if you want a specific one
Config::get('constants.langs.en');
And you can set them as well
Config::set('foo.bar', 'test');
Note that the values you set will not persist. They are only available for the current request.
Update
The config is probably not the right place to store information generated from the database. You could just use an Eloquent Model like:
class Category extends Eloquent {
// db table 'categories' will be assumed
}
And query all categories
Category::all();
If the whole Model thing for some reason isn't working out you can start thinking about creating your own class and a facade. Or you could just create a class with all static variables and methods and then use it without the facade stuff.
For Constants
Create constants.php file in the config directory:-
define('YOUR_DEFINED_CONST', 'Your defined constant value!');
return [
'your-returned-const' => 'Your returned constant value!'
];
You can use them like:-
echo YOUR_DEFINED_CONST . '<br>';
echo config('constants.your-returned-const');
For Static Arrays
Create static_arrays.php file in the config directory:-
class StaticArray
{
public static $langs = [
'es' => 'www.domain.es',
'en' => 'www.domain.us',
'uk' => 'www.domain.uk',
'br' => 'www.domain.br',
'it' => 'www.domain.it',
'de' => 'www.domain.de',
'fr' => 'www.domain.fr'
];
}
You can use it like:-
echo StaticArray::$langs['en'];
Note: Laravel includes all config files automatically, so no need of manual include :)
Create common constants file in Laravel
app/constants.php
define('YOUR_CONSTANT_VAR', 'VALUE');
//EX
define('COLOR_TWO', 'red');
composer.json
add file location at autoload in composer.json
"autoload": {
"files": [
"app/constants.php"
]
}
Before this change can take effect, you must run the following command in Terminal to regenerate Laravel’s autoload files:
composer dump-autoload
For global constants in Laravel 5, I don't like calling Config for them. I define them in Route group like this:
// global contants for all requests
Route::group(['prefix' => ''], function() {
define('USER_ROLE_ADMIN','1');
define('USER_ROLE_ACCOUNT','2');
});
I think the best way is to use localization.
Create a new file messages.php in resources/lang/en (en because that is what is set in my config/app 'locale'=>'en')
return an array of all your values
return [
'welcome' => 'Welcome to our application'
];
to retrieve for laravel 5.3 and below
echo trans('messages.welcome');
or
echo Lang::get('messages.welcome');
for 5.4 use
echo __('messages.welcome')
laravel 5.0 localization
or
laravel 5.4 localization
Just to add to the above answer you will have to include the config class before you could start using it in Laravel 5.3
use Illuminate\Support\Facades\Config;
Atleast in Laravel 5.4, in your constructor you can create them;
public function __construct()
{
\Config::set('privileged', array('user1','user2');
\Config::set('SomeOtherConstant', 'my constant');
}
Then you can call them like this in your methods;
\Config::get('privileged');
Especially useful for static methods in the Model, etc...
Reference on Laracasts.com https://laracasts.com/discuss/channels/general-discussion/class-apphttpcontrollersconfig-not-found
Just put a file constants.php file into the config directory and define your constants in that file, that file will be auto loaded,
Tested in Laravel 6+
Create a constants class:
<?php
namespace App\Support;
class Constants {
/* UNITS */
public const UNIT_METRIC = 0;
public const UNIT_IMPERIAL = 1;
public const UNIT_DEFAULT = UNIT_METRIC;
}
Then use it in your model, controller, whatever:
<?php
namespace App\Models;
use App\Support\Constants;
class Model
{
public function units()
{
return Constants::UNIT_DEFAULT;
}
}
I would like to connect to a second database with Yii at runtime. The database name would come from a database table after the user to login.
I saw in a tutorial I should do this:
$db2 = Yii::createComponent(array(
'class' => 'EMongoClient',
'server' => 'mongodb://127.0.0.1:27017',
'db' => $emp['database']
));
Yii::app()->setComponent('db2',$db2);
But in my controler when I access Yii::app()->db2 get the error:
Property "CWebApplication.db2" is not defined
What am I doing wrong?
The following works for me:
Yii::app()->mongodb->setActive(false);
Yii::app()->mongodb->setServer('mongodb://localhost:27017');
Yii::app()->mongodb->setDb('db1');
Yii::app()->mongodb->setActive(true);
UPDATED: Try, instead instance, pass configurations:
Yii::app()->setComponent( 'db2', array(
'class' => 'EMongoClient',
'server' => 'mongodb://127.0.0.1:27017',
'db' => $emp['database']
)
);
Or, you may create special index on params in configurations, such as:
...
'params' => array(
'db2' => null,
),
And the use Yii::app()->params['db2'] = $db2
From this comment:
My problem is not with the creation of the component. Soon after
creating if I access Yii::app()->db2 its works, but when I try to
access via another model or controller I get the error
I think you are setting this component only once somewhere, and then making subsequent requests to different controllers.
You need to put the code, somewhere it is being called EVERYTIME, on every Request. thats how PHP works, there is no "global application state"
by default Yii comes with protected/components/controller.php has base controller for the rest of the app.
my suggestion would be to put your code on the init() method of that controller, so that it always gets called.
You mentioned the database name comes from a table once the user logs in, so you need to save that value in the session, in other to be able to access it in the other requests:
<?php
// After login in
Yii::app()->user->setState('db_name', $db_name);
// in protected/components/controller.php
public function init()
{
if (!Yii::app()->user->isGuest) {
$db2 = Yii::createComponent(array(
'class' => 'EMongoClient',
'server' => 'mongodb://127.0.0.1:27017',
'db' => Yii::app()->user->getState('db_name')
));
Yii::app()->setComponent('db2',$db2);
}
}
Hope it helps, I am assuming many things here :)