I want to add a check before save in several of my Yii2 models.
In Yii1 this was simply a case of adding a behavior that had a beforeSave method, that returned false.
This doesn't work in Yii2. I can register a behavior that is called before save, but returning false from it doesn't prevent that save.
Anyone know how to achieve this without having to duplicate a beforeSave method with identical code in all my models?
namespace app\components\behaviors;
use yii\base\Behavior;
use yii\db\ActiveRecord;
class PreventSaveBehavior extends Behavior
{
public function events()
{
return [
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
];
}
public function beforeSave($insert)
{
if (SomeClass::shouldWePreventSave()) {
return false;
}
return parent::beforeSave($insert);
}
}
In Yii2 in behaviors you need to use isValid property of ModelEvent.
public function beforeSave($event)
{
if (SomeClass::shouldWePreventSave()) {
$event->isValid = false;
}
}
This is explained in event documentation.
Related
I will try to simplify the problem as much as I can.
I want to disable a relation load from a trait on a resource that happens at the retrieved event.
There is a model we will name Post that uses a trait named HasComments.
class Post extends Model
{
use HasComments;
...
}
The trait listenes for the retrieved event on the model and loads the comments relation.
trait HasComment
{
public static function bootHasComment(): void
{
self::retrieved(function ($model) {
$model->load('comments');
});
}
public function comments(): BelongsTo
{
return $this->belongsTo(Comment::class);
}
}
I want to be able to check if the comments relation was eager loaded and NOT load the relation again.
I tried to check if the relation is loaded but failed.
ex:
self::retrieved(function ($model) {
if (!isset($model->relations['comments'])) {
dd('still loads!');
$model->load('comments');
}
});
or
self::retrieved(function ($model) {
if (!$model->relationLoaded('comments')) {
dd('still loads!');
$model->load('comments');
}
});
I was also thinking of maybe there is a way to disable this behavior when constructin the query but failed again.
ex:
trait HasComment
{
public bool $load = true;
public static function bootHasComment(): void
{
self::retrieved(function ($model) {
if (!$this->load) {
dd('still loads!');
$model->load('comments');
}
});
}
public function comments(): BelongsTo
{
return $this->belongsTo(Comment::class);
}
public function disableRetrievedLoad()
{
$this->load = false;
}
}
Has someone encountered something similar and can give me some help?
There is a method named relationLoaded that returns a boolean. Seems like the right fit for your needs.
Alternatively, if you want to load a relationship if it's not been loaded yet, there's loadMissing.
$post = Post::first();
$post->relationLoaded('comments'); // false
$post->loadMissing('comments'); // makes the queries to load the relationship
$post->relationLoaded('comments'); // true
$post->loadMissing('comments'); // does nothing. comments is already loaded
I am creating a new API call for our project.
We have a table with different locales. Ex:
ID Code
1 fr_CA
2 en_CA
However, when we are calling the API to create Invoices, we do not want to send the id but the code.
Here's a sample of the object we are sending:
{
"locale_code": "fr_CA",
"billing_first_name": "David",
"billing_last_name": "Etc"
}
In our controller, we are modifying the locale_code to locale_id using a function with an extension of FormRequest:
// This function is our method in the controller
public function createInvoice(InvoiceCreateRequest $request)
{
$validated = $request->convertLocaleCodeToLocaleId()->validated();
}
// this function is part of ApiRequest which extend FormRequest
// InvoiceCreateRequest extend ApiRequest
// So it goes FormRequest -> ApiRequest -> InvoiceCreateRequest
public function convertLocaleCodeToLocaleId()
{
if(!$this->has('locale_code'))
return $this;
$localeCode = $this->input('locale_code');
if(empty($localeCode))
return $this['locale_id'] = NULL;
$locale = Locale::where(Locale::REFERENCE_COLUMN, $localeCode)->firstOrFail();
$this['locale_id'] = $locale['locale_id'];
return $this;
}
If we do a dump of $this->input('locale_id') inside the function, it return the proper ID (1). However, when it goes through validated();, it doesn't return locale_id even if it's part of the rules:
public function rules()
{
return [
'locale_id' => 'sometimes'
];
}
I also tried the function merge, add, set, etc and nothing work.
Any ideas?
The FormRequest will run before it ever gets to the controller. So trying to do this in the controller is not going to work.
The way you can do this is to use the prepareForValidation() method in the FormRequest class.
// InvoiceCreateRequest
protected function prepareForValidation()
{
// logic here
$this->merge([
'locale_id' => $localeId,
]);
}
Why is it that whenever I redirect something through the constructor of my Codeigniter 4 controller is not working?
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
if(session('username')){
return redirect()->to('/dashboard');
}
}
public function index()
{
// return view('welcome_message');
}
}
But if I put it inside index it's working as expected.
public function index()
{
if(session('username')){
return redirect()->to('/dashboard');
}
}
The thing is, I do not want to use it directly inside index because I it need on the other method of the same file.
As per the Codeigniter forum, you can no longer use the redirect method in the constructor to redirect to any of the controllers.
Please refer the below link for more information
https://forum.codeigniter.com/thread-74537.html
It clearly states that redirect() will return a class instance instead of setting a header and you cannot return an instance of another class while instantiating a different class in PHP.
So that's why you can't use redirect method in constructor.
Instead, what I can suggest to you is that use the header method and redirect to your desired controller.
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
if(session('username')){
header('Location: /dashboard');
}
}
}
If that's not feasible or difficult to achieve you can follow the below code
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
//call to session exists method
$this->is_session_available();
}
private function is_session_available(){
if(session('username')){
return redirect()->to('/dashboard');
}else{
return redirect()->to('/login');
}
}
}
The 2nd solution will be more interactive than the first one. And make sure the method is private. So that it should not be called from other class instances.
The community team has also given a solution to look into the controller filter.
https://codeigniter4.github.io/CodeIgniter4/incoming/filters.html
Please refer to the thread. I hope it may help you in finding a better solution.
In this case you shouldn't even be doing this kind of logic in your controllers. This should be done in a filter and not your controllers.
So you have your controller Register.
You should create a filter in your app/filters folder something like checkLogin.php
That filter should have the following structure:
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class CheckLogin implements FilterInterface
{
/**
* Check loggedIn to redirect page
*/
public function before(RequestInterface $request, $arguments = null)
{
$session = \Config\Services::session();
if (session('username')) {
return redirect()->to('/dashboard');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Do something here
}
}
Then in your app/config/Filters.php your should add the filter to the desired controller.
public $aliases = [
'csrf' => \CodeIgniter\Filters\CSRF::class,
'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
'honeypot' => \CodeIgniter\Filters\Honeypot::class,
'checkLogin' => \App\Filters\CheckLogin::class,
];
// List filter aliases and any before/after uri patterns
public $filters = [
'checkLogin' => ['before' => ['Register']],
];
For more information on filters and how to use then please check the documentation.
https://codeigniter.com/user_guide/incoming/filters.html?highlight=filters
You can then even create filters to your other controllers that would redirect to this one in case the user is not logged in.
Codeigniter 4 using initController() to create constructor.
You can't use redirect() inside __construct() or initController() function.
But you can use $response parameter or $this->response to call redirect in initController() before call another function in controller;
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger)
{
// Do Not Edit This Line
parent::initController($request, $response, $logger);
if(session('username')){
$response->redirect(base_url('dashboard')); // or use $this->response->redirect(base_url('dashboard'));
}
}
public function index()
{
// return view('welcome_message');
}
}
I just got into CI 4 and i had the same issue as you did, since i've been approaching the login system as with CI 3.
So here is the proper way of doing it right. Enjoy.
Codeigniter 4 do not has any return on Constructor, but you can use like this
public function __construct()
{
if (!session()->has('user_id')) {
header('location:/home');
exit();
}
}
Don't forget to use exit()
But, you better use Filters
I want to restrict or give access to all of my fuction inside a controller by checking some condition in cakephp 3.6.
Suppose I have add(), edit(), view($id) function in my PostController.php . Now I have an input field where user will put an unique id. If this id is present in the database then I want to give access to those function. Oterwise it should show a message to the user.
I am new in cakePHP, so I don't have much idea about cakePHP.
I have tried using beforeFilter. But it is not working.
Here is the code that I have tried. Right now I am not checking with database. Just checking if code is given in input field or not.
public function beforeFilter(Event $event)
{
if(!empty($event->subject()->request->data)){
return true;
}
else{
return false;
}
}
I know maybe those are not proper. But I am not getting proper idea even not proper documentation also.
Here is my controller code structure.
use App\Controller\AppController;
use Cake\Event\Event;
class CodesController extends AppController
{
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
}
public function beforeFilter(Event $event)
{
if(!empty($event->subject()->request->data)){
return true;
}
else{
return false;
}
}
public function add()
{
//code will be here
}
public function view($id)
{
//code will be here
}
}
Instead of return true from your beforeFilter, use:
$this->Auth->allow(['add', 'edit', 'view']);
No need to do anything in the false case.
Alternately, if those are your only functions, you can simplify to:
$this->Auth->allow();
More details in the manual.
I'm trying to create a trait (for models) that would automatically write all changes made to the model in 'adjustments' table. It would save changes in before and after jsons.
This is the code so far (from Laracasts):
trait LoggingTrait
{
public static function boot()
{
parent::boot();
static::updating(function($action){
$action->adjustments()->create(
[
'user_id' => Auth::id(),
'before' => json_encode(array_intersect_key($action->getOriginal(), $action->getDirty())),
'after' => json_encode($action->getDirty())
]);
});
}
public function adjustments()
{
return $this->morphMany(Adjustment::class, 'adjustable');
}
}
This is working very well, except it doesn't save changes to related models.
To make it more clear, this is my Action model:
class Action extends Model
{
use LoggingTrait;
public function actionTypes()
{
return $this->belongsToMany(ActionType::class);
}
public function users()
{
return $this->belongsToMany(User::class);
}
public function spreadingMaterial()
{
return $this->belongsTo(SpreadingMaterial::class);
}
}
The trait logs all the changes made to the actual Action model, but doesn't care for the changes made to the $action->users(), $action->spreadingMaterials() and $action->actionTypes(). How would I get these changes within the static::updating(...) event?
Or if that is not possible, any other idea on how to tackle this problem is more than welcome.