What would be the best way to create a relationship if it doesn’t exist already, within Eloquent, or at least a central location.
This is my dilemma. A User must have a Customer model relationship. If for whatever reason that customer record doesn’t exist (some bug that stopped it from being created) - I don’t want it to throw errors when I try to retrieve it, but I also request the customer object in multiple locations so I don’t want to test for existence in all those places.
I thought of trying the following in the User model:
public function getCustomerAttribute($value) {
// check $value and create if null
}
But that doesn’t work on relationships, $value is null.
EDIT
I already create a customer upon user creation, but I have come across a situation where it wasn't created and caused exceptions in many places, so I want to fallback.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
Is it possible for you to assume when a user is created that a customer needs to be created as well? If the rest of your system depends on this assumption I would make a model event.
use App\{User, Customer}; // assuming php7.0
UserServiceProvider extends ServiceProvider
{
/**
* Boot
*/
public function boot()
{
// on a side note, we're using "created" not "creating" because the $user->id needs to exist in order to save the relationship.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
}
}
Related
I have a Location Model, which contains two properties: ID and Name.
To edit this Model, I have set up this route:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following
Gate::before(function ($user, $permission) {
if ($user->permissions->pluck('name')->contains($permission)) {
return true;
}
});
Where permission is a Model that contains an ID and a name, mapped via a permission_user table.
I have these permissions set up:
edit_los_angeles
edit_new_york
edit_boston
plenty_of_other_permissions_not_related_to_location
After all this rambling, my actual question:
How can I tie these permissions to the edit the location?
The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').
Instead, I would need something like this, I guess:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_boston');
...obviously this would not work.
What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?
Thank you very much for your help in advance!
I am using Laravel 6.0 :-)
Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.
Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.
class LocationController {
public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
...
}
}
For ease of use, i would also make a policy.
class LocationPolicy
{
public function edit(User $user, Location $location) {
return $user->permissions->pluck('name')
->contains($location->permission_slug); // assuming we have a binding
}
}
Remember to register policy in the AuthServiceProvider.php.
protected $policies = [
Location::class => LocationPolicy::class,
];
Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.
class EditLocationRequest
{
public function authorize(): bool
{
return $this->user()->can('edit', $this->location);
}
}
Now you should be able to only have a single route definition.
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
EDIT
Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.
use AuthorizesRequests;
public function edit() {
$this->authorize('edit', $location);
}
If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.
Edit: to answer the question about implementation of middleware,
The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.
I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of
public function canEditLocation(Location $location): bool {
return $this->locations
->where('location_id', '=', $location->id)
->count() > 0;
}
And the actual middleware something along these lines:
public function handle($request, Closure $next, $location)
{
if (! $request->user()->canEditLocation($location)) {
\\handle failed permission as appropriate here.
}
return $next($request);
}
My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters
I'm working on a user-generated content blog that allows a user to go through the whole upload process before being prompted to sign up. Basic flow: fill out form to pick username/basic info->upload blog post->prompt to sign up with email/password. The purpose of reversing the normal flow is to increase the UX and conversion rate and avoid a wall in the beginning.
Instead of migrating, I've just created the tables manually in PHPmyAdmin. I have 3 relational models: Usermeta->hasOne(App\Mopdels\Post), Post->belongsTo(App\Models\Usermeta), and User->belongsTo(App\Models\Usermeta).
What I'm having trouble with is once the user has created a username and submits the first form to the usermeta table, and then submits the second form to upload their blog post to the post table, it doesn't seem to be attaching the usermeta.id to posts.usermeta_id linking them together. I must be missing something or not attaching it correctly. Here's my StoryController:
<?php
namespace App\Controllers\Story;
use App\Models\Post;
use App\Models\User;
use App\Models\Usermeta;
use App\Controllers\Controller;
use Respect\Validation\Validator as v;
class StoryUploadController extends Controller
{
public function guidance($request, $response)
{
return $this->view->render($response, 'storyupload/guidance.twig');
}
//set up our the Upload Story class so the user can upload their story
//render the view 'uploadstory.twig'
public function getStoryUpload($request, $response)
{
return $this->view->render($response, 'storyupload/upload.twig');
}
// This method is called when the user submits the final form
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
'usermeta_id' => usermeta()->attach($user->id),
]);
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
}
I continue to get the error 'usermeta_id cannot be null' so it's definitely not pulling the id from the usermeta table correctly.
I've used the create() method to send the usermeta data to the table in my Auth controller.
Would it be better to have all of my form submissions in the Auth controller and what is the proper way using my example to make sure that my posts.usermeta_id is linked to my usermeta.id?
The usermeta form is taken care of by my Auth Controller:
//render the view 'signup.twig'
public function getSignUp($request, $response)
{
return $this->view->render($response, 'auth/signup.twig');
}
// This method is called when the user submits the form
public function postSignUp($request, $response)
{
$validation = $this->validator->validate($request, [
'name' => v::notEmpty()->alpha(),
'username' => v::noWhitespace()->notEmpty()->UsernameAvailable(),
'city' => v::notEmpty()->alpha(),
'country' => v::notEmpty()->alpha(),
]);
//if validation fails, stay on signup page
if ($validation->failed()) {
return $response->withRedirect($this->router->pathFor('auth.signup'));
}
$usermeta = Usermeta::create([
'name' => $request->getParam('name'),
'username' => $request->getParam('username'),
'city' => $request->getParam('city'),
'country' => $request->getParam('country'),
'share_location' => $request->getParam('share_location'),
]);
//after submit, redirect to storyupload/guidance
return $response->withRedirect($this->router>pathFor('storyupload.guidance'));
}
I wrote quite a bit here. To jump directly to what I believe will solve your problem, see the "Your Issue" section. The rest is here as an educational exercise.
A Quick Intro to Laravel Relations
As you probably already know, "relations" in Laravel are virtual concepts that are derived from the hard data in the database. Because they are virtual, there is some overlap in the definition of relations.
When you say "Usermeta has one Post" - what this means is that the posts table will have a usermeta_id field.
When you say "Post belongs to Usermeta" - what this means is that the posts table will have a usermeta_id field.
Notice that these two relations map to the exact same field in the exact same table. Declaring one relation will declare the other by simple congruence. "Usermeta has one Post" and "Post belongs to Usermeta" are identical relations.
A Tweak to Your Relations
There's one other relation that share this same schema (the posts table have a usermeta_id field). That is "Usermeta has many Posts". The difference here is not in how the relations are stored to the database, but in how Laravel interprets the relations and in what queries Laravel will run.
When you say "Usermeta has one Post", Laravel will scan the database for the first Post with a matching usermeta_id and return that as an instance of the Usermeta model.
When you say "Usermeta has many Posts", Laravel will scan the database for all matching usermeta_ids and return them as a Collection of Usermeta models. You likely want this second behavior -- otherwise users won't be able to make a second post after they sign up.
Setting the usermeta_id Field
Laravel allows you to set database fields directly through a relationship. See their documentation on inserting related models for details.
Because many relationships are just ciphers for the same underlying schema, there's no need to insert or update a related model both ways. For instance, suppose we had the following two models:
class User extends Eloquent {
public function posts() {
return $this->hasMany("App\Post");
}
}
class Post extends Eloquent {
public function user() {
return $this->belongsTo("App\User");
}
}
In this case, the following two lines of code are identical and you only need to use one of them:
$post->user()->associate($user);
$user->posts()->save($post);
Both of these will have the same effect (setting the user_id field on the posts table)
The reason I mention this is that it looks like you're trying to double-dip in your code. You're using attach() (conceivably to set the usermeta_id) and you're also setting the usermeta_id directly. I've added a side-note on the attach method below - as I don't believe it's the right method, anyway.
To use Laravel's relations, you would want code like the following to set this field:
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
]);
// Set the usermeta_id field
$post->usermeta()->associate($user);
// Save the model so we write changes to the database
$post->save();
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
Manually Setting the usermeta_id Field
Instead of using Laravel's relations to set this field, you can set the field manually. This can sometimes be cleaner, but it's less explicit and can lead to minor bugs if you aren't careful. To do this, you need to treat the usermeta_id field like any other field on your model.
$post->usermeta_id = $user->id;
This also works when mass assigning attributes using fill or create like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
$post->fill([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Note that when manually setting the usermeta_id like this, you do not need to use any relationship methods. The following code is redundant:
$post->usermeta_id = $user->id;
$post->usermeta()->associate($user);
Your Issue (I Believe)
There's a caveat to mass assignment, however. Per the Laravel documentation, mass assignment requires you to fill out the model's fillable or guarded attributes.
This is one of the most common bugs, if not the most common bug, in any Laravel code - and it doesn't throw an obvious error so it's easy to miss. Consider the following model:
class Post extends Eloquent {
private $fillable = ["title", "body"];
}
If you attempt to mass assign the usermeta_id field like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Then it will silently fail. No error is thrown and the Post is created but the usermeta_id field will be NULL - because it's not mass assignable. This is fixed by updating your model like so:
class Post extends Eloquent {
private $fillable = ["title", "body", "usermeta_id"];
}
I will repeat again, as I did above, that if using mass assignment like this you do not not need to use the associate or save relationship methods. This would be redundant. Therefore you can just set usermeta_id directly to $user->id without any of the usermeta()->associate() shenanigans.
The Bugs I Mentioned
I mentioned that manually setting the field like this can cause bugs. So let's actually discuss what some of those bugs are now instead of glossing over them.
If you update the relationship field manually, Laravel will be unaware that the two models are related until it reloads the model from the database. Consider the following two chunks of code:
$post = new Post();
$post->usermeta_id = $user->id;
dd( $post->usermeta->name );
$post = new Post();
$post->usermeta()->associate($user);
dd( $post->usermeta->name );
The first code block will fail, throwing the error "cannot read attribute of null object" -- because as far as Laravel is aware, $post->usermeta is NULL. You set $post->usermeta_id, but you didn't set $post->usermeta.
The second code block will work as expected, because by running the associate function it sets both usermeta_id and usermeta.
95% of the time this doesn't really cause any issues, however. If you're using an asynchronous API call to save the post and then a separate asynchronous API call to read the post at a later time, then Laravel will read the post from the database and properly set up the relation automatically when we sees the usermeta_id field is filled out.
Side-note On the attach() Method
Laravel uses different methods for saving different types of relations - because the different relations imply different underlying database fields.
associate: This sets the *_id field on the current model's table. For instance: $post->user()->associate($user) will set the user_id on the posts table
save: This sets the *_id field on the other model's table. For instance: $post->comments()->save($comment) will set the post_id on the comments table
attach: This sets both *_id fields on a linking table for many to many relationships. For instance, if you had a tag system then $post->tags()->attach($tag) would set post_id and tag_id on the post_tags table
It can be a bit tricky to remember which of these three functions you need. In general, there's a direct mapping from relation to function:
hasOne, hasMany --> save
belongsTo --> associate
belongsToMany --> attach
We are trying to detect the changes in Laravel related models at attribute level, as we have to keep audit trail of all the changes which are made via the application.
We can track the changes via isDirty method on the Eloquent model for single model that is not related to any other model, but there is no way that we can track the changes on the related eloquent models. isDirty doesn't work on related models attributes. Can some one please help us on this?
Update to original question:
Actually we are trying to track changes on the pivot table that has extra attributes as well defined on it. IsDirty method doesn't work on those extra attributes which are defined in the pivot table.
Thanks
As much I understand your question, It's can achieve through Model Event and some sort of extra code with current and relation model.
Laravel Model Events
If you dont want to use any additional stuff, you can just use the Laravel Model Events (that in fact Ardent is wrapping in the hooks). Look into the docs http://laravel.com/docs/5.1/eloquent#events
Eloquent models fire several events, allowing you to hook into various
points in the model's lifecycle using the following methods: creating,
created, updating, updated, saving, saved, deleting, deleted,
restoring, restored.
Whenever a new item is saved for the first time, the creating and
created events will fire. If an item is not new and the save method is
called, the updating / updated events will fire. In both cases, the
saving / saved events will fire.
If false is returned from the creating, updating, saving, or deleting
events, the action will be cancelled:
Finally, reffering to your question you can utilize the above approaches in numerous ways but most obviously you can combine it (or not) with the Eloquent Models' getDirty() api docs here method and getRelation() api docs here method
It will work for example with the saving event.
Model::saving(function($model){
foreach($model->getDirty() as $attribute => $value){
$original= $model->getOriginal($attribute);
echo "Changed";
}
$relations = $model->getRelations();
foreach($relations as $relation){
$relation_model = getRelation($relation);
foreach($relation_model->getDirty() as $attribute => $value){
$original= $relation_model->getOriginal($attribute);
echo "Relation Changed";
}
}
return true; //if false the model wont save!
});
Another Thought might help you. when you saving
save() will check if something in the model has changed. If it hasn't it won't run a db query.
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
The getDirty() method simply compares the current attributes with a copy saved in original when the model is created. This is done in the syncOriginal() method:
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
check model is dirty isDirty():
if($user->isDirty()){
// changes have been made
}
Or check certain attribute:
if($user->isDirty('price')){
// price has changed
}
I did not check this code but hopeful to use as your answer by thoughts, if you have any confusion to deal such requirement or something need to optimize or change please let me know.
Working with Yii framework I've got a question which I can not solve on my own.
How can I nicely check access to certain attributes of a model?
Context:
I use RBAC, a user may have multiple roles at the same time
I have a model with a bunch of attributes (for example, let's call it Profile)
Some attributes are allowed to be edited by any registered user (email, age, favorite color, etc.), and some - only by the users with some specific role (is_banned - can be changed by moderator or administrator, balance - can be changed only by administrators, etc.)
What I've done already:
At the moment I see only one possible way to accomplish this task:
function actionUpdate($id)
{
$model = Profile::model()->findByPk($id);
if (!$this->user->checkAccess('editProfile')) {
throw new AccessDeniedException();
}
if (isset($_POST['is_banned'])) {
if (!$this->user->checkAccess('toggleBan')) {
unset($_POST['is_banned']);
}
}
if (isset($_POST['balance'])) {
if (!$this->user->checkAccess('changeBalance')) {
unset($_POST['balance']);
}
}
$model->setAttributes($_POST);
$model->save();
}
Are there some better ways to solve such task? Thanks.
Consider using model scenarios (http://www.yiiframework.com/wiki/266/understanding-scenarios/):
Add scenario condition to your model rules:
public function rules()
{
return array(
array('balance', 'integer', 'on' => 'admin'),
);
}
Set model scenario in controller:
if ($this->user->checkAccess('admin')) {
$model->scenario = 'admin';
}
Now attributes that available by scenario are saved only.
Consider also using scenarios in search models.
In my laravel app I have multiple user accounts who have resources that are assigned to them. Say, for example, a "payment". To edit or view a payment a user would visit the /payment/edit/{payment} route (where payment is the payment ID).
Although I have an auth filter to stop un-logged in users from accessing this page there is nothing to stop, for example, user 1 from editing user 2's payment.
Is there a filter I can user that checks which user a payment (or any other resource) belongs to prevent this kind of issue?
[I am using Laravel's model bindings which automatically fetches the model specified by the route rather than me get it in the controller using eloquent.]
No such filter exists by default, however you can easily create one (depending on how your database is set up). Within app/filters.php, you may do something like this:
Route::filter('restrictPermission', function($route)
{
$payment_id = $route->parameter('payment');
if (!Auth::user()->payments()->find($payment_id)) return Redirect::to('/');
});
This compares the currently logged in user's payment_id (in your database) to the {payment} argument passed into the route. Obviously, depending on how your database is set up (for instance if the payment_id is in a separate table) you need to change the conditional.
Then, apply the filter to your route:
Route::get('/payment/edit/{payment}', array('before' => 'restrictPermission'));
One way is to place a where statement in every relevant query. Although not very pretty, it works.
$payment = Payment::where('user_id', '=', Auth::user()->id)->find($id);
It's also possible to use url filters like seeARMS is suggesting, however I think it's not very elegant. The most logical place to nest such logic is in the model itself. One possibility is to use model events, but this gives you only the option to intercept update, insert or delete statements, not selects. This might change in the future. Maybe you could use boot() event, but I'm not sure if this is gonna work.
Last but not least you could use query scopes.
class Payment extends Eloquent {
public function scopeAuthuser($query)
{
return $query->where('user_id', '=', Auth::user()->id);
}
}
and in the queries you attach the scope
Payment::authuser()->find($id);
You could do this on a base Model and extend from it, so you have that method in all your relevant models.
Consider using Laravel Policies:
https://laravel.com/docs/6.x/authorization#policy-methods
<?php
namespace App\Policies;
use App\Post;
use App\User;
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* #param \App\User $user
* #param \App\Post $post
* #return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
By policies you can control if given record could be edited by logged user or not.
Cheers!