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.
Related
I'm having some issues with the Codeigniter 4 validation rules. I'm using the is_unique function within the ruleset in Order to except one single row from the validation.
The problem here is that there are two fields in the table that need to be checked:
'episodeTitle' => [
'label' => 'episodeTitle',
'rules' => 'required|max_length[100]|is_unique[episodes.episodeTitle,episodes.episodeID,' . $episodeID . ',episodes.podcastID,' . $podcastID . ']',
'errors' => [
'required' => lang('Errors.nested.episode.episodeTitleRequired'),
'max_length' => lang('Errors.nested.messages.maxLength100'),
'is_unique' => lang('Errors.nested.episode.episodeTitleUnique'),
],
],
Is it possible, to make the exception depending on 2 or more fields?
I want to check the episodeID AND the podcastID not only one of these values.
You need to create a custom rule to achieve what you're looking for.
Rules are stored within simple, namespaced classes. They can be stored any location you would like, as long as the autoloader can find it. These files are called RuleSets. To add a new RuleSet, edit Config/Validation.php and add the new file to the $ruleSets array:
public $ruleSets = [
\CodeIgniter\Validation\Rules::class,
\CodeIgniter\Validation\FileRules::class,
\CodeIgniter\Validation\CreditCardRules::class,
];
Within the file itself, each method is a rule and must accept a string as the first parameter, and must return a boolean true or false value signifying true if it passed the test or false if it did not:
class MyRules
{
public function check_unique(string $str): bool
{
// Your code to check the values and don't forget to return true or false.
}
}
Then in your validation just use the check_unique rule.
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 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.
How can i configure a route connection to handle...
/users/{nameofuser_as_param}/{action}.json?limit_as_param=20&offset_as_param=20&order_as_param=created_at
in the routes.php file such that it calls my controller action like...
/users/{action}/{nameofuser_as_param}/{limit_as_param}/{offset_as_param}/{order_as_param}.json?
Note: Iam using Cakephp 2.X
to handle...
/users/{nameofuser_as_param}/{action}.json
That's pretty easy, and in the docs.
Assuming there is a call to parseExtensions in the route file, a route along the lines of this is required:
Router::connect(
'/users/:username/:action',
['controller' => 'users'],
[
'pass' => ['username'],
// 'username' => '[a-Z0-9]+' // optional param pattern
]
);
The pass key in the 3rd argument to Router::connect is used to specify which of the route parameters should be passed to the controller action. In this case the username will be passed.
For the rest of the requirements in the question it would make more sense for the action to simply access the get arguments. E.g.:
public function view($user)
{
$defaults = [
'limit_as_param' => 0,
'offset_as_param' => 0,
'order_as_param' => ''
];
$args = array_intersect_key($this->request->query, $defaults) + $defaults;
...
}
It is not possible, without probably significant changes or hacks, to make routes do anything with get arguments since at run time they are only passed the path to determine which is the matching route.
I have a controller which I use for a login form. In the view, I have a {error} variable which I want to fill in by using the parser lib, when there is an error. I have a function index() in my controller, controlled by array $init which sets some base variables and the error message to '':
function index()
{
$init = array(
'base_url' => base_url(),
'title' => 'Login',
'error' => ''
);
$this->parser->parse('include/header', $init);
$this->parser->parse('login/index', $init);
$this->parser->parse('include/footer', $init);
}
At the end of my login script, I have the following:
if { // query successful }
else
{
$init['error'] = "fail";
$this->parser->parse('login/index', $init);
}
Now, of course this doesn't work. First of all, it only loads the index view, without header and footer, and it fails at setting the original $init['error'] to (in this case) "fail". I was trying to just call $this->index() with perhaps the array as argument, but I can't seem to figure out how I can pass a new $init['error'] which overrides the original one. Actually, while typing this, it seems to impossible to do what I want to do, as the original value will always override anything new.. since I declare it as nothing ('').
So, is there a way to get my error message in there, or not? And if so, how. If not, how would I go about getting my error message in the right spot? (my view: {error}. I've tried stuff with 'global' to bypass the variable scope but alas, this failed. Thanks a lot in advance.
$init musst be modified before generating your view.
To load your header and footer you can include the following command and the footer's equivalent into your view.
<?php $this->load->view('_header'); ?>
to display errors, you can as well use validation_errors()
if you are using the codeigniter form validation.
if you are using the datamapper orm for codeigniter you can write model validations, and if a query fails due to validation rule violation, you get a proper error message in the ->error property of your model.
Code for your model:
var $validation = array(
'user_name' => array(
'rules' => array('required', 'max_length' => 120),
'label' => 'Name'
)
);
You might try this:
function index() {
$init = array(
'base_url' => base_url(),
'title' => 'Login',
'error' => ''
);
$string = $this->parser->parse('include/header', $init, TRUE);
$string .= $this->parser->parse('login/index', $init, TRUE);
$string .= $this->parser->parse('include/footer', $init, TRUE);
$this->parser->parse_string(string);
}
In parse()you can pass TRUE (boolean) to the third parameter, when you want data returned instead of being sent (immediately) to the output class. By the other hand, the method parse_string works exactly like `parse(), only accepts a string as the first parameter in place of a view file, thus it works in conjunction.