Product.supplierID = Supplier.supplierID
--------- ----------
|Product|---------|Supplier|
--------- ----------
|
| Supplier.supplierID = User.supplierID
|
---------
| User |
---------
Using the above table structure, the application uses sub-classes of ActiveController, with overridden prepareDataProvider to limit the index list of each Product a logged in User can see to those with matching supplierID values. Something like this in the actions() method of ProductController.
$actions['index']['prepareDataProvider'] = function($action)
{
$query = Product::find();
if (Yii::$app->user->can('supplier') &&
Yii::$app->user->identity->supplierID) {
$query->andWhere(['supplierID' => Yii::$app->user->identity->supplierID]);
}
return new ActiveDataProvider(['query' => $query]);
};
This works fine, however I'm looking to use checkAccess() to limit actionView() for a single Product.
At the moment, a logged in User can access a Product by changing the productID in the URL, whether or not the have the appropriate supplierID.
It looks like I can't access the particular instance of Product, to check the supplierID, until actionView() has returned which is when I want the check to happen.
Can I override checkAccess() to restrict access and throw the appropriate ForbiddenHttpException?
What about a function that checks if a model exist :
protected function modelExist($id)
{
return Product::find()
->where([ 'productID' => $id ])
->andWhere(['supplierID' => Yii::$app->user->identity->supplierID ])
->exists();
}
If productID is your Product Primary Key, then a request to /products/1 will be translated by yii\rest\UrlRule to /products?productID=1.
In that case, when productID is provided as a param, you can use beforeAction to make a quick check if such model exist & let the action be executed or throw an error if it doesn't :
// this array will hold actions to which you want to perform a check
public $checkAccessToActions = ['view','update','delete'];
public function beforeAction($action) {
if (!parent::beforeAction($action)) return false;
$params = Yii::$app->request->queryParams;
if (isset($params['productID']) {
foreach ($this->checkAccessToActions as $action) {
if ($this->action->id === $action) {
if ($this->modelExist($params['productID']) === false)
throw new NotFoundHttpException("Object not found");
}
}
}
return true;
}
update
As the question is about Overriding the checkAccess method in rest ActiveController I thought it would be useful to leave an example.
In the way how Yii2 REST was designed, all of delete, update and view actions will invoke the checkAccess method once the model instance is loaded:
// code snippet from yii\rest\ViewAction
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
The same is true for the create and index actions except that they won't pass any model instance to it: call_user_func($this->checkAccess, $this->id).
So what you are trying to do (throwing a ForbiddenHttpException when a user is trying to view, update or delete a product he is not its supplier) may also be achieved this way:
public function checkAccess($action, $model = null, $params = [])
{
if ($action === 'view' or $action === 'update' or $action === 'delete')
{
if ( Yii::$app->user->can('supplier') === false
or Yii::$app->user->identity->supplierID === null
or $model->supplierID !== \Yii::$app->user->identity->supplierID )
{
throw new \yii\web\ForbiddenHttpException('You can\'t '.$action.' this product.');
}
}
}
Related
I actually am not able to understand why I am getting the following error.
App\Models\User::team must return a relationship instance, but "null" was returned. Was the "return" keyword used?
I am basically creating test cases for simple orders for ecommerce.
User Modal
public function team(): BelongsTo|null
{
if (!empty($this->team_id)) {
return $this->belongsTo(Team::class);
}
return null;
}
Test case
public function test_order_status_update()
{
$order = $this->create_order($this->books->id, $this->appUser->id, $this->address->id);
$response = $this->actingAs($this->user)->put('orders/' . $order->json('order.id'), [
'order_status' => 'ordered',
]);
$response->assertRedirect('orders/' . $order->json('order.id'))->assertSessionHas('success');
}
In addition, I have another feature in my application called pages access control, which controls page access for multiple users (admin, developer, and users).
I have implemented this feature manually using middleware.
Middlware.php
public function handle(Request $request, Closure $next)
{
//teams 1-Developer 2-Admin 3-Management 4-Marketing 5-Audit 6-Sales 7-Bookstores 8-Delivery 9-User
$team = $request->user()->team;
if ($team->id == 1 || $team->id == 2) {
return $next($request);
}
$pages = auth()->user()->pages->merge(auth()->user()->team->pages);
$currentRouteName = $request->route()->getName();
$pages->contains('route_name', $currentRouteName) ?: abort(403);
return $next($request);
}
Based on the error above, I believe the actingAs function is unable to obtain authenticated user information, which is why my test failed.
How can I fix this?
Simply don't check your team_id:
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
Laravel tries to be smart. If team_id isn't set, it will just return null. However, if you don't return the BelongsTo, the magic code of Laravel will trip when you try to access user->team
In my application I want to keep track of who has performed certain operations on different models in my application.
Default Laravel model with timestamps automatically updates fields like created_at and updated_at. I can modify this behavior to set the created_by field automatically by calling the static::updating() function as mentioned in this answer: https://stackoverflow.com/a/64241347/4112883 . This works very well. Additionally, I came across this package (https://github.com/WildsideUK/Laravel-Userstamps), but that is limited to only created, updated, and deleted.
For my Post model, I have more timestamps: created_at, updated_at, completed_at, checked_at, and published_at. When a user ends the post, it must be verified by that user's manager. If all is well, some logic will publish the message, but if not, the manager can create one or more actions for the user to complete the message, which will undo the finishing attributes. An action is created with the following timestamps: created, updated, and completed (null). When the user completes an action, the actions.finished_at and actions.finished_by fields are set.
Now comes the challenge. For each custom timestamp, I want to set the relationship and three functions to handle certain states of the timestamp: set, undo and check for isset:
class Post extends Model
{
//…
public function finishedBy() //relationship belongsTo User::class
{
return $this->belongsTo(User::class, 'finished_by');
}
public function finish() { //function to finish post (SET)
$this->update([
'finished_by' => auth()->id(),
'finished_at' => now(),
]);
}
public function undoFinish() { //function to undo finishing (UNSET)
$this->update([
'finished_at' => null,
'finished_by' => null,
]);
}
public function isFinished() { //function to check if is finished (ISSET)
return !empty($this->finished_by) && !empty($this->finished_at);
}
//…
All four functions must be repeated for ‘checked’ and ‘published’ in the Post model, and for the ‘finished’ attribute in Action model, leading to a lot of almost-duplicate code. (Maybe in the future I want to repeat this logic in other models.)
Is there a possibility to make this more elegant with a Trait or something?
E.g. create something like an protected array $timestamps_with_user by which the application automatically adds the relationship and the three functions?
protected $timestamps_with_users = [
'finish', 'check', 'publish'
];
// foreach in a trait?? Need your help here :D
foreach($timestamps_with_users as $perform) {
public function $perform() { … } //$post->finish()
public function $perform.edBy() :User { … } //$post->finishedBy()
public function undo.$perform() { … } //$post->undoFinish()
public function is.$perform.ed() { … } //$post->isFinished()
}
Thanks in advance and looking forward to your answers.
Just create a new trait and create functions that works with any timestamp:
<?php
namespace App\Traits;
trait CustomTimestamps {
public function perform(string $action)
{
$this->update([
$action . 'ed_by' => auth()->id(),
$action . 'ed_at' => now(),
]);
}
public function undo(string $action)
{
$this->update([
$action . 'ed_by' => null,
$action . 'ed_at' => null,
]);
}
public function check(string $action)
{
$at = $action . 'ed_at';
$by = $action . 'ed_by';
return !empty($this->{$by}) && !empty($this->{$at});
}
}
This is a category table I am using in my project using Laravel.
I have checks applied in the view files, for the category parent selection dropdown, so that the category itself and it's child's will not appear in the dropdown.
But form input fields value can be easily overridden using dev console.
Is there a way in models so that if parent id is equal to the category id itself or parent id is the child of current category then it will stop execution.
I have recently started laravel, a month ago, and still learning and building, so help here will be appreciated.
I was able to resolve the issue by overriding the update method in model -
Controller update method -
public function update(Request $request, $id)
{
$this->validate($request,
['name' => 'required',]);
$data = [];
$data = ['name' => Input::get('name'),
'parent' => !empty(Input::get('parent')) ? Posts_categories::find(Input::get('parent'))->id : NULL,];
$category = Posts_categories::find($id);
if(is_null($category))
{
Session::flash('flash-message', 'Category type with the given id does not exist.');
Session::flash('alert-class', 'alert-warning');
return redirect()->route('admin.post.category.index');
}
if($category->update($data)) {
Session::flash('flash-message', 'Category succesfully updated.');
Session::flash('alert-class', 'alert-success');
}
return redirect()->route('admin.post.category.index');
}
Model update method -
public function update(array $attributes = [], array $options = [])
{
$parent = SELF::find($attributes['parent']);
if($this->id == $parent->id || $this->id == $parent->parent)
{
Session::flash('flash-message', 'Invalid parent selection for category.');
Session::flash('alert-class', 'alert-warning');
return 0;
}
return parent::update($attributes, $options); // TODO: Change the autogenerated stub
}
I'm using a REST API to receive the data.
The data model is polymorphic related, similar to the one on the documentation:
https://laravel.com/docs/5.4/eloquent-relationships#polymorphic-relations
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
Let's say, for example, the API is receiving this new comment:
{
"body": "This a test comment",
"commentable_type": "posts",
"commentable_id": "1"
}
How can I validate if the received commentable_type exists and is valid?
If I correctly understand your question, you are trying to validate that the object of the polymorphic relation exists, for the given commentable_type and commentable_id.
If that is the case, there is no existing validation rule to do so, but you can create one.
Based on the documentation, here is what you could do:
First, add the new rule in the boot method of a service provider (e.g. AppServiceProvider):
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (!$objectType = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
return !empty(resolve($objectType)->find($value));
});
And this is how you would use it:
'commentable_id' => 'required|poly_exists:commentable_type
What the rule does is it tries and fetches the commentable type from the input values (based on the parameter passed on to the rule, i.e. commentable_type in our case), and then resolves the object and tries to find a record for the given ID ($value).
Please note that for this to work however, the value of commentable_type must be the fully qualified class name (e.g. App\Models\Post).
Hope this helps!
Better approach that includes morphs map:
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (! $type = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
if (Relation::getMorphedModel($type)) {
$type = Relation::getMorphedModel($type);
}
if (! class_exists($type)) {
return false;
}
return ! empty(resolve($type)->find($value));
});
You can dynamically define a model_exists rule in your Request class. Something like this:
public function rules()
{
$polymorphExistsRule = '';
if ($this->has('commentable_type')) {
$polymorphExistsRule .= '|exists:' . $this->commentable_type . ',id';
}
return [
'commentable_type' => 'required_with:commentable_id',
'commentable_id' => 'required_with:commentable_type' . $polymorphExistsRule,
];
}
Edit
I might've misunderstood the first time. If you want to check that the model saved in commentable_type exists you could do something like this:
$type = $comment->commentable_type;
if(class_exists($type)) echo "it exists";
Depending on your needs you could do additional checking for it's inheritance (for example that it extends class Model). Or anything else that fits your needs really.
Edit2
This is what I would do if I were you. I would add property protected $allRelations to your Comment model and manually put all the relationships in. Then make some helper models to check if it's in the array.
Simple example:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
// ..
protected $allRelations= [
'posts' => '\App\Post',
'videos' => '\App\Video',
];
public static function validateRelationNs($ns) {
return in_array($ns, $this->allRelations);
}
public static function validateRelationName($name) {
return array_key_exists($name, $this->allRelations);
}
// ...
}
Old answer:
Laravel expects full namespace name of the model for polymorphic type columns (in your case commentable_type should be \Full\Ns\Post, not posts).
The easiest way to ensure correctness is to always save it through the relationship. For example:
$post = Post::first();
$comment = new Comment($attributes);
$post->comments()->save($comment).
This will automatically set both commentable_id and commentable_type correctly (assuming your relationsare correctly defined).
Additional checking
Other then that you could check through model events. You could validate it before saving to the database.
My final version work for validate type and id:
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (!$objectType = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
if (!class_exists($objectType)) {
return false;
}
return !empty(resolve($objectType)->find($value));
});
Yii-jedis!
I'm working on some old Yii-project and must to add to them some features. Yii is quite logical framework but it has some things I couldn't understand. Perhaps I haven't understand Yii-way yet. So I'll describe my problem step-by-step. For impatients - briefly question at the end.
Intro: I want to add human-readable URLs to my project.
Now URLs looks like: www.site.com/article/359
And I want them to look like this: www.site.com/article/how-to-make-pretty-urls
Very important: old articles must be available on old format URLs, and new - on new URLs.
Step 1: First, I've updated rewrite rules in config/main.php:
'<controller:\w+>/<id:\S+>' => '<controller>/view',
And I've added new texturl column to article table. So we will store here human-readable-part-of-url for new articles. Then I've updated one article with texturl for tests.
Step 2: Application show articles in actionView of ArticleController so I've added there this code for preproccessing ID parameter:
if (is_numeric($id)) {
// User try to get /article/359
$model = $this->loadModel($id); // Article::model()->findByPk($id);
if ($model->text_url !== null) {
// If article with ID=359 have text url -> redirect to /article/text-url
$this->redirect(array('view', 'id' => $model->text_url), true, 301);
}
} else {
// User try to get /article/text-url
$model = Article::model()->findByAttributes(array('text_url' => $id));
$id = ($model !== null) ? $model->id : null ;
}
And then begin legacy code:
$model = $this->loadModel($id); // Load article by numeric ID
// etc
It works perfectly! But...
Step 3: But we have many actions with ID parameter! What we have to do? Update all actions with that code? I think it's ugly. I've found CController::beforeAction method. Looks good! So I declare beforeAction and place ID preproccessing there:
protected function beforeAction($action) {
$actionToRun = $action->getId();
$id = Yii::app()->getRequest()->getQuery('id');
if (is_numeric($id)) {
$model = $this->loadModel($id);
if ($model->text_url !== null) {
$this->redirect(array('view', 'id' => $model->text_url), true, 301);
}
} else {
$model = Article::model()->findByAttributes(array('text_url' => $id));
$id = ($model !== null) ? $model->id : null ;
}
return parent::beforeAction($action->runWithParams(array('id' => $id)));
}
Yes, it works with both URL-formats, but it executes actionView TWICE and shows page two times! What can I do with this? I've totally confused. Have I choose a right way to solve my problem?
Briefly: Can I proceess ID (GET-parameter) before execute of any actions and then run requested action (once!) with modified only ID parameter?
Last line should be:
return parent::beforeAction($action);
Also to ask you i didnt get your step:3.
As you said you have many controller and you don't need to write code in each file, so you are using beforeAction:
But you have only text_url related to article for all controllers??
$model = Article::model()->findByAttributes(array('text_url' => $id));
===== updated answer ======
I have changed this function, check now.
If $id is not nummeric then we will find it's id using model and set $_GET['id'], so in further controller it will use that numberic id.
protected function beforeAction($action) {
$id = Yii::app()->getRequest()->getQuery('id');
if(!is_numeric($id)) // $id = how-to-make-pretty-urls
{
$model = Article::model()->findByAttributes(array('text_url' => $id));
$_GET['id'] = $model->id ;
}
return parent::beforeAction($action);
}
Sorry, I haven't read it all carefully but have you considered using this extension?