I have 2 Models (SuperRubriques and CustomRubriques) using the same table rubriques in DB.
When I delete from SuperRubriques, I would like to delegate the delete to CustomRubriques (as CustomRubriques has a hasOne association with extended_rubriques that SuperRubriques doesn't know).
For info, the rubriques table in DB has the field model containing 'CustomRubriques' (i.e. the Model with which it has been saved).
I've tried to do it in SuperRubriquesTable::beforeDete() :
// In SuperRubriquesTable.php
public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)
{
$table = TableRegistry::getTableLocator()->get($entity->model); // $entity->model contains 'CustomRubriques'
$rubriqueEntity = $table->get($entity->id);
return $table->delete($rubriqueEntity);
}
However $table->delete($rubriqueEntity) is true (when I debug) but the record is not deleted in DB, I don't know why?
I've fix the issue :
Instead of deleguating inside SuperRubriquesTable::beforeDelete(), I've done the job in SuperRubriquesController::delete() :
// In SuperRubriquesController
public function delete($id)
{
$this->request->allowMethod(['post', 'delete']);
$rubrique = $this->SuperRubriques->get($id);
$rubriqueModel = $rubrique->model;// contains 'CustomRubriques'
$this->loadModel($rubriqueModel);
$rubriqueEntity = $this->$rubriqueModel->get($id);
if ($this->$rubriqueModel->delete($rubriqueEntity)) {
return $this->redirect(['action' => 'index']);
}
}
Related
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});
}
}
Update Method:
public function update(UserUpdateRequest $request, Users $uzytkownik)
{
$this->authorize('update', $uzytkownik);
if ( $uzytkownik->update([
'birth' => $request->birth,
'sex' => $request->sex,
'about' => $request->about,
]) )
{
return 1;
}
return 0;
}
On update here in page 1 appears. Like it did the thing.
But in db nothing has changed.
$uzytkownik is proper user, and
This is the dd($uzytkownik);
And below dd($request->birth.'---'.$request->sex.'---'.$request->about); which shows proper inputs
Why it doesn't work properly?
As per the documentation
Mass Assignment
You may also use the create method to save a new model in a single line. The inserted model instance will be returned to you from the method. However, before doing so, you will need to specify either a fillable or guarded attribute on the model, as all Eloquent models protect against mass-assignment by default.
You need to make sure $fillable or $guarded is correctly set otherwise changes may not be persistant.
You can do what you want like this too:
public function update(UserUpdateRequest $request, Users $uzytkownik)
{
$this->authorize('update', $uzytkownik);
$uzytkownik->birth = $request->birth;
$uzytkownik->sex = $request->sex;
$uzytkownik->about = $request->about;
if ( $uzytkownik->save() )
{
return 1;
}
return 0;
}
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));
});
I've created a form which adds a category of product in a Categories table (for example Sugar Products or Beer), and each user has their own category names.
The Categories table has the columns id, category_name, userId, created_At, updated_At.
I've made the validation and every thing is okay. But now I want every user to have a unique category_name. I've created this in phpMyAdmin and made a unique index on (category_name and userId).
So my question is this: when completing the form and let us say that you forgot and enter a category twice... this category exist in the database, and eloquent throws me an error. I want just like in the validation when there is error to redirect me to in my case /dash/warehouse and says dude you are trying to enter one category twice ... please consider it again ... or whatever. I am new in laravel and php, sorry for my language but is important to me to know why is this happens and how i solve this. Look at my controller if you need something more i will give it to you.
class ErpController extends Controller{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return view('pages.erp.dash');
}
public function getWarehouse()
{
$welcome = Auth::user()->fName . ' ' . Auth::user()->lName;
$groups = Group::where('userId',Auth::user()->id)->get();
return view('pages.erp.warehouse', compact('welcome','groups'));
}
public function postWarehouse(Request $request)
{
$input = \Input::all();
$rules = array(
'masterCategory' => 'required|min:3|max:80'
);
$v = \Validator::make($input, $rules);
if ($v->passes()) {
$group = new Group;
$group->group = $input['masterCategory'];
$group->userId = Auth::user()->id;
$group->save();
return redirect('dash/warehouse');
} else {
return redirect('dash/warehouse')->withInput()->withErrors($v);
}
}
}
You can make a rule like this:
$rules = array(
'category_name' => 'unique:categories,category_name'
);
I am using Yii afterdelete() to update the related data which is deleted in another table. Here is my code in the controller:
Controller Action
public function actionDelete($id)
{
if(Yii::app()->request->isPostRequest)
{
// we only allow deletion via POST request
$this->loadModel($id)->delete();
// if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser
if(!isset($_GET['ajax']))
$this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
}
else
throw new CHttpException(400,'Invalid request. Please do not repeat this request again.');
}
Model function
protected function afterDelete()
{
parent::afterDelete();
$show_model = new Show();
$show_model = Show::model()->findAll('tbl_season_id='.$this->id);
$show_model->updateAll('tbl_season_id = NULL, on_season=0');
}
As #Gregor said, a good use of active record relations would make the job much easier.
So, in Show model you would have something like:
public function relations()
{
return array(
'season' => array(self::BELONGS_TO, 'Season', 'tbl_season_id'),
);
}
While in Season model you would have something like:
public function relations()
{
return array(
'shows' => array(self::HAS_MANY, 'Show', 'tbl_show_id'),
);
}
Having relations defined, will give you the ability to do this:
public function afterDelete()
{
parent::afterDelete();
$season_shows = Season::model()->findByID($id)->shows; //using the shows relation
foreach($season_shows as $season_show) do
{
$season_show->setAttributes('tbl_season_id => NULL, on_season => 0');
$season_show->save();
}
}
Hummm, but if you noticed that second line in the afterDelete which calls findByID($id) but we are inside the afterDelete and the record is actually dead (deleted)!!
To fix this you can grab the id just before the model is deleted using a variable & abeforeDelete
//at the begining of your model class
$private = $cached_season_id;
...
//then somewhere at the end
...
public function beforeDelete()
{
$this->cached_tbl_season_id = $this->id;
return parent::beforeDelete();
}
Now if you change the id in the afterDelete to $this->cached_season_id .. it should work.
Well, this solution is based on this yii-fourm-topic and i am not quite sure if it's going to work as it is!! So, give it a try & let us know what happens?
That looks an awful lot like a HAS_MANY relationship from Season to Show, so you might want to use relations in the future to get related records. There is a pretty good documentation for that in the yii-guide: http://www.yiiframework.com/doc/guide/1.1/en/database.arr
It also seems to me, that you have a jQuery-background. You are calling updateAll on an array(which is returned by the findAll-function).
A proper updateAll-call might look like this:
Show::model()->updateAll(array("tbl_season_id"=>null, "on_season"=>0), "tbl_season_id = $this->id")
This would probably be even better with some kind of constraint but since that is imo a matter of taste, I'll leave it at that.