I'm trying to execute some code inside a Yii2 controller as I need some code from the model to be accessible within the behaviors section so I can pass the model as a parameter and avoid running duplicate queries; however I also need to be able to find out what action is being called, but I am not having much luck.
I have tried using beforeAction but it seems this gets run AFTER the behaviours code runs, so that doesn't help me.
I then tried using init, but it seems the action isn't available via $this->action->id at that point.
Some example code:
class MyController extends Controller {
public $defaultAction = 'view';
public function init() {
// $this->action not available in here
}
public function beforeAction() {
// This is of no use as this runs *after* the 'behaviors' method
}
public function behaviors() {
return [
'access' => [
'class' => NewAccessControl::className(),
'only' => ['view','example1','example2'],
'rules' => [
[
'allow' => false,
'authManager' => [
'model' => $this->model,
'other_param' => $foo,
'other_param' => $bar,
],
'actions' => ['view'],
],
// everything else is denied
],
],
];
}
public function viewAction() {
// This is how it is currently instantiated, but we want to instantiate *before* the behavior code is run so we don't need to instantiate it twice
// but to be able to do that we need to know the action so we can pass in the correct scenario
$model = new exampleModel(['scenario' => 'view']);
}
}
authManager is simply a reference to a member variable inside an extension of the AccessRule class.
Is there anyway I can do this?
Well, if I get you right, you are looking for something like this:
public function behaviors()
{
$model = MyModel::find()->someQuery();
$action = Yii::$app->controller->action->id;
return [
'someBehavior' => [
'class' => 'behavior/namespace/class',
'callback' => function() use ($model, $action) {
//some logic here
}
]
];
}
Because behaviors() is just a method, you can declare any variables and add any logic that you want in it, the only one convention that you must follow - is that return type must be an array.
If you use your custom behavior, you are able to use events() method where you can bind your behavior's methods to certain events. E.g.
class MyBehavior extends Behavior
{
public function events()
{
return [
\yii\web\User::EVENT_AFTER_LOGIN => 'myAfterLoginEvent',
];
}
public function myAfterLoginEvent($event)
{
//dealing with event
}
}
In this example myAfterLoginEvent will be executed after user successfully login into application. $event variable will be passed by framework and depending of event type it will contain different data. Read about event object
UPDATE:
As I can see now my answer was more generic about events and behaviors. And now when you added code, I can suggest to you to override behavior's beforeAction($action) method with the following code:
public function beforeAction($action)
{
$actionID = $action->id;
/* #var $rule AccessRule */
foreach ($this->rules as &$rule) {
$model = &$rule->authManager['model'];
//now set model scenario maybe like this
$model->scenario = $actionID;
}
//now call parent implementation
parent::beforeAction($action);
}
Also take a look at AccessControl implementation of beforeAction method, it invokes for each rule allows method with passing current action to it as a parameter. So if you have class that extends AccessRule, you can either override allows($action, $user, $request) method or matchCustom($action) method to set appropriate model scenario. Hope this will help.
One more alternative:
override controller's runAction($id, $params = []) method. Here $id is actionID - exactly what you need. Check id, set appropriate model scenario and call parent::runAction($id, $params);
Related
I am trying to access a data called "section" in my rules() function inside my FormRequest Validator Class. What I am trying to do is check what is the value of "section" and return different rules. But apparently, I am not able to access the value of the data/payload.
I have checked the answer from here, but it didn't work for me : Laravel Form Request Validation with switch statement
Here is my code :
FormController.php
class FormController extends Controller
{
public function verify(DynamicFormRequest $request , $section)
{
return response()->json(['success' => 'everything is ok']);
}
}
DynamicFormRequest.php
class DynamicFormRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
error_log($this->request->get('section'));//retruns null and depricated
error_log($this->request->input('section')); //this is absolutely wronge but , i tried it anyways
error_log($this->request->section); //retruns null
error_log($this->section); //retruns null
switch ($this->request->get('section')) {
case '1':
return [
'item_name' => 'required',
];
break;
case 'Type2':
return [
'item_favorite' => 'required',
];
break;
}
}
}
Please help to make me understand whats wronge
If you are using route model binding you can use $this->section right off the bat.
So this should work assuming your routes are set up in the correct format:
error_log($this->section);
Route (Something like....):
Route::post('section/{section}', [SectionController::class, 'update']);
This question could help: Stack Q: Laravel: Access Model instance in Form Request when using Route/Model binding
Lastly, Hard to know it's working without your other code. Have you dumped out $request to check section is in there?
you can use required_if:anotherfield,value to check the value for other fields
return [
'item_name' => 'required_if:section,1',
'item_favorite' => 'required_if:section,Type2'
]
https://laravel.com/docs/9.x/validation#rule-required-if
I hope it's helpful
I want to devide my controller to several service layer, validation layer, and logical layer.
But I got stuck when I want to send new variable to validation request, my schenario is, if there is a new sent it indicates for new data, and if new variable is not exists it indicates it's updating data.
Here is my Controller:
public function store(AlbumRequest $request, AlbumService $service)
{
$request->add(['new' => true])
try{
$service->store($request);
return redirect(route('admin.web.album.index'));
}catch(\Exception $err){
return back()->withInput()->with('error',$err->getMessage());
}
}
Here is my Request
class AlbumRequest extends FormRequest
{
public function rules()
{
dd($this->request->get('new')
}
}
I want to catch variable I have sent from Controller to Request. How to do that?
Thank you.
You can add new parameter in request from controller like that
$request->merge(array('new' => true));
before your request reaches your controller , it has to go through AlbumRequest class . so you have to merge that field in AlbumRequest class by using method prepareForValidation :
protected function prepareForValidation()
{
$this->merge([
'new' => true,
]);
}
add this method in your AlbumRequest class and see if it works
I am afraid you cannot do that because the incoming form request is validated before the controller method is called. Now if you want to know whether the request is for creating something new or updating something, you can do it by accessing the route parameters and method type
public function rules()
{
$rules = [
'something' => 'required',
];
if (in_array($this->method(), ['PUT', 'PATCH'])) {
//it is an edit request
//you can also access router parameter here, $this->route()->parameter('other thing');
$rules['somethingelse'] = [
'required',
];
}
return $rules;
}
You can enter the requested class as URL params.
class AlbumRequest extends FormRequest
{
public function rules()
{
dd(request()->get('new')
}
}
I would like to know if it's recommended to do this:
class User {
public function __construct() {
parent::__construct();
}
public function exists($token) {
//return true if token exists otherwise return false
}
public function createSession($token) {
if($this->exists($token))
//create session
else
//redirect
}
}
I think it could be not recommended in case of a change in the exists method of the class but I think that'll not happen, What do you recommend me to do?
There's nothing wrong with calling methods from other methods. In many designs, it's critical that you do this. This allows you to create subclasses that redefine the method, and the new definition will be called.
So if you do:
class SpecialUser extends User {
public function exists($token) {
// determine token existence in some other way
}
}
You can then do:
$s = new SpecialUser;
$s->createSession($someToken);
and it will make use of the special way that SpecialUser checks tokens.
I do that all the time, when i notice my method is too long i segregate it and create another private method or protected method. Another reason is so it could be reuse for another method.
Here's an example:
class Download extends PHPExcel{
public function excel2007()
{
$excelFormat = $this->excelFormat();
}
public function excel2003()
{
$excelFormat = $this->excelFormat();
}
private function excelFormat()
{
return [
'font_bold' => [
'font' => array(
'bold' => true
)
],
'font_size' => [
'font' => array(
'size' => 10
)
],
'align_center' => [
'alignment' => array('horizontal' => PHPExcel_Style_Alignment::HORIZONTAL_CENTER)
]
];
}
}
In a Laravel project I'm working on, I'm wanting to create an API. In that API there will be certain JSON keys that will be required in every request. E.g. a token, or other fields that are ALWAYS required. I am familiar with Laravel's form request feature which allows you to easily create a class with a rules method containing an array of validation logic. However, I am wanting to know if there is a way where I can make one Request class that handles the "always required" fields and then bolt on another request class containing specific field validation for that endpoint.
E.g.
// MasterRequest.php
public function rules() {
return [
'api_key' => 'required|exists:users,api_key',
];
}
// ProductRequest.php
public function rules() {
return [
'product_id' => 'required|integer',
];
}
And then some way always call MasterRequest validation on EVERY api route, and then specify the type of request validation for each route's unique needs?
Is this doable, or even the correct approach?
This is fairly easy to set up, use OOP properties of PHP.
The easiest way (and obvious one):
Make yourself "master class for always required fields" you can also declare it as abstract class.
File AlwaysRequired.php
abstract class AlwaysRequired extends FormRequest
{
public function rules() {
return [
'api_key' => 'required|exists:users,api_key',
];
}
}
and ProductRequest.php
class ProductRequest extends AlwaysRequired
{
public function rules() {
return array_merge(parent::rules(),
['product_id' => 'required|integer']);
}
}
Array merge on php.net
The property way:
Make yourself master class in which you will declare property with "always required" rules and then just array_merge(array,...) it in child class (just like example above).
The "hardest" and most confusing way, but fully automatic:
You can leverage magic functions and method/property visibility of PHP language.
Again make yourself a master class in which you will have a protected property with rules array and implementation for __call() magic method.
Note: You can test code below in interactive shell of PHP $php -a and copy-paste the code.
abstract class A { // Master class
protected $rules = ['abc' => 'required'];
function __call($name, $arg) {
if(method_exists($this, 'rules')){
return array_merge($this->rules, $this->rules());
} else {
//or handle any other method here...
die(var_dump($name, $arg));
}
}
}
class B extends A { //Generic class just like ProductRequest...
protected function rules() { // function must be declared as protected! So its invisible for outside world.
return ['def' => 'required'];
}
}
$b = new B();
var_dump($b->rules());
How it works?
Laravel behind the scenes tries to run rules() method on the request class you specify (in your case ProductRequest), declaring it as protected means that it can not be called except from itself or by another child, which means that __call() method is called instead which is declared in abstract parent class. __call() method simply identifies if caller wanted to call non-existent (because of protected visibility is set) method rules() if that is so it merges the child's rules() result with $rules and returns it.
Checking for correct API key should be handled in Middleware.
You can manually execute Request classes one by one:
public function store()
{
try{
app(MasterRequest::class);
} finally {
app(ProductRequest::class);
}
/*... */
}
I am using Codeception\Util\Stub to create unit tests. And I want to be sure that my method called several times. For this I am using method 'exactly'.
Example:
use \UnitTester;
use \Codeception\Util\Stub as StubUtil;
class someCest
{
public function testMyTest(UnitTester $I)
{
$stub = StubUtil::makeEmpty('myClass', [
'myMethod' => StubUtil::exactly(2, function () { return 'returnValue'; })
]);
$stub->myMethod();
}
}
As you can see I called myMethod once. But test passed.
The same problem with method ::once , because this method is using the same class PHPUnit_Framework_MockObject_Matcher_InvokedCount ('matcher' below).
Test will fail only if I will call more then expected times ( >2 ). Because matcher's method 'invoked' checks if count more then expected. But can't see if someone call matcher's method 'verify' to check if myMethod called less then expected.
Sorry stackoverflow, this is my first question.
UPDATE
My fast and BAD temporary solution:
Add stub into helper
$I->addStubToVerify($stub);
Add method into helper to validate:
protected $stubsToVerify = [];
public function verifyStubs()
{
foreach ($this->stubsToVerify as $stub) {
$stub->__phpunit_getInvocationMocker()->verify();
}
return $this;
}
Call this method in Cest's method _after():
public function _after(UnitTester $I)
{
$I->verifyStubs();
}
You need to pass $this as a third parameter to makeEmpty:
$stub = StubUtil::makeEmpty('myClass', [
'myMethod' => StubUtil::exactly(2, function () { return 'returnValue'; })
], $this);
Instead of use \Codeception\Util\Stub to Expected::once(), modify your unit tests to extends \Codeception\Test\Unit then use $this->make() or $this->makeEmpty() to create your stubs. It will works as you expect ;)
For example:
class MyProcessorTest extends \Codeception\Test\Unit
{
public function testSomething()
{
$processor = new MyProcessor(
$this->makeEmpty(EntityManagerInterface::class, [
'remove' => Expected::never(),
'persist' => Expected::once(),
'flush' => Expected::once(),
])
);
$something = $this->somethingFactory(Processor::OPERATION_CREATE);
$processor->process($something);
}
}
Cheers!
Looks like your method does not exist in the target class that you mock.
If the method exists then Codeception replaces it with the stub you provide. And if this method does not exist then Codeception adds a field with this name to the stub object.
It is because methods and properties are passed in the same array so Codeception has no other way to tell methods from properties.
So first create a method myMethod in your class myClass.