Laravel - How to dynamically resolve an instance of a model that has different Namespace? - laravel

tldr:
How do you dynamically get an instance of a model just by its DB table name?
What you get from the request:
ID of the model
table name of the model (it varies all the time!)
What you don't know:
Namespace of the model
Longer explanation:
I have a reporting system, that users can use to report something. For each reporting, the ID and the table name is sent.
Until now, every model was under the Namespace App\*. However, since my project is too big, I needed to split some code into Modules\*
Here is an example, how the report is saved in the database:
Example:
Request contains rules:
public function rules()
{
return [
'id' => 'required|string',
'type' => 'required|in:users,comments,offer_reviews, ......',
'reason' => 'required|string',
'meta' => 'nullable|array',
'meta.*' => 'string|max:300'
];
}
In the database, we save the data into :
id reportable_type ...
1 App\User ...
4 Modules\Review\OfferReview ...
How would you create an instance of a model dynamically, when you just know the database table name for example offer_reviews?
There is one solution that jumps to my mind, however, I'm not sure if it adds more security issues. What is if the user sends the full namespace + class name? With that, I know directly where to resolve an instance.
Have a look what I'm doing right now
(before I changed to modules)
//In my controller
class ReportController extends Controller
{
/**
* Stores the report in DB.
*/
public function store(StoreReportRequest $request)
{
$model = $request->getModel();
$model->report([
'reason' => $request->reason,
'meta' => $request->meta
], auth()->user());
return response()->json(['status' => 'Submitted'], 201);
}
}
//in StoreReportRequest
/**
* Gets the Model dynamically.
* If not found we throw an error
* #return \App\Model
*/
public function getModel()
{
return get_model($this->type)
->findOrFail(\Hashids::decode($this->id)[0]);
}
//in Helpers
/**
* Gets me the model of a table name.
* #param String $table Has to be the name of a table of Database
* #return Eloquent The model itself
*/
function get_model($table)
{
if (Schema::hasTable(strtolower($table))) {
return resolve('App\\' . Str::studly(Str::singular($table)));
}
throw new TableNotFound;
}

I don't know if there is a better solution, but here you go. My code is looking for a method with namespace when it's not found we are using App\ as the namespace.
Maybe this code helps someone :)
class StoreReportRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'id' => 'required|string',
'type' => 'required|in:mc_messages,profile_tweets,food,users,comments,offer_reviews,user_reviews',
'reason' => 'required|string',
'meta' => 'nullable|array',
'meta.*' => 'string|max:300'
];
}
/**
* Gets the Model dynamically.
* If not found we throw an error
* #return \App\Model
*/
public function getModel()
{
$namespace = $this->getNamespace();
return $this->resolveModel($namespace);
}
protected function getNamespace(): string
{
$method = $this->typeToMethod();
if (method_exists($this, $method)) {
return $this->$method();
}
return 'App\\';
}
protected function typeToMethod(): string
{
return 'get' . \Str::studly(\Str::singular($this->type)) . 'Namespace';
}
protected function resolveModel(string $namespace)
{
return get_model($this->type, $namespace)
->findOrFail(\Hashids::decode($this->id)[0]);
}
protected function getOfferReviewNamespace(): string
{
return 'Modules\Review\Entities\\';
}
protected function getUserReviewNamespace(): string
{
return 'Modules\Review\Entities\\';
}
}

Related

Validating Nest JSON with Parameters

I am trying to validate a nested JSON object in Laravel. I have created a custom rule to do this however I have an issue currently, I want to be able to pass the object at the current array index to my custom validator:
<?php
namespace App\Http\Requests\App;
use App\Rules\CheckoutDepatureCheck;
use App\Rules\SeatIsAvailable;
use Illuminate\Foundation\Http\FormRequest;
class CheckoutRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
"company" => "required",
"seats" => "required|array",
"seats.*.seat_no" => ['required', new SeatIsAvailable()], // would like to pass seat.* to the constructor of my custom validator here
"seats.*.schedule_id" => "required|numeric",
"seats.*.date" => "required|date"
];
}
}
The point for this is my custom validator needs schedule_id and data as well as the seat_no to successfully validate the request.
How do I do this in Laravel?
You can dynamically add rules depending on the length of the seats' array input
<?php
namespace App\Http\Requests\App;
use App\Rules\CheckoutDepatureCheck;
use App\Rules\SeatIsAvailable;
use Illuminate\Foundation\Http\FormRequest;
class CheckoutRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
$rules = [
'company' => 'required',
'seats' => 'required|array',
];
return array_merge($rules, $this->seatsRules());
}
private function seatsRules(): array
{
$rules = [];
foreach ((array) $this->request->get('seats') as $key => $seat) {
$rules["seats.$key.seat_no"] = ['required', new SeatIsAvailable($seat)];
$rules["seats.$key.schedule_id"] = 'required|numeric';
$rules["seats.$key.date"] = 'required|date';
}
return $rules;
}
}

Yii2 Gridview filter is showing required field with message, how to disable these required field for gridview?

I'm viewing the list of my masters data in grid view. The name field is required, but while I'm listing the data in grid view the filter for master filed name is showing required with it's required message as name cannot be blank.
Please help me with it, what I'm doing wrong in it.
My Search Model is
class MasterFeeSearch extends MasterFee
{
public function rules()
{
return [
[['masterfee_id',], 'integer'],
[['masterfee_name',], 'required'],
[['created_at','updated_at'], 'safe'],
];
}
/**
* #inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search($params)
{
$query = MasterFee::find();
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere(['like', 'masterfee_name', $this->masterfee_name,]);
return $dataProvider;
}
}
Remove [['masterfee_name'], 'required'], from rule and add [['masterfee_name'], 'string'], in searchModel.
class MasterFeeSearch extends MasterFee
{
public function rules()
{
return [
[['masterfee_id'], 'integer'],
[['masterfee_name'], 'string'],
[['created_at','updated_at'], 'safe'],
];
}
.
.
.
.

Laravel: How do i fix this trait (with attributes) to also work when creating a model?

I'm using a trait to dynamically add e-mail attribute(s) to a model. It gives me the possibility to reuse code amongst many models. However, this code fails when i try to create a new model (but succeeds when i update an existing model).
The issue is the assumption that $this->id is available in Traits/Contact/HasEmails > setEmailTypeAttribute. Id is not yet available, because saving is not finished.
My question: How can i fix this trait to also work when creating a model?
Google, no results
Thinking about something of model events (static::creating($model))
\app\Traits\Contact\HasEmails.php
/*
* EmailGeneric getter. Called when $model->EmailGeneric is requested.
*/
public function getEmailGenericAttribute() :?string
{
return $this->getEmailTypeAttribute(EmailType::GENERIC);
}
/*
* EmailGeneric setter. Called when $model->EmailGeneric is set.
*/
public function setEmailGenericAttribute($email)
{
return $this->setEmailTypeAttribute(EmailType::GENERIC, $email);
}
/*
* Get single e-mail model for model owner
*/
private function getEmailTypeAttribute($emailType) :?string
{
$emailModel = $this->emailModelForType($emailType);
return $emailModel ? $emailModel->email : null;
}
/*
* Update or create single e-mail model for model owner
*
* #return void
*/
private function setEmailTypeAttribute($emailType, $email) :void
{
$this->emails()->updateOrCreate([
'owned_by_type' => static::class,
'owned_by_id' => $this->id,
'type' => $emailType
],['email' => $email]);
}
\app\Models\Email.php
namespace App\Models;
class Email extends Model
{
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'email'
];
/*
* Get polymorphic owner
*/
public function ownedBy(): \Illuminate\Database\Eloquent\Relations\MorphTo
{
return $this->morphTo();
}
/*
* Default attributes are prefilled
*/
protected function addDefaultAttributes(): void
{
$attributes = [];
$attributes['type'] = \App\Enums\EmailType::GENERIC;
$this->attributes = array_merge($this->attributes, $attributes);
}
}
\migrations\2019_10_16_101845_create_emails_table.php
Schema::create('emails', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('owned_by_id');
$table->string('owned_by_type');
$table->string('type'); //f.e. assumes EmailType
$table->string('email');
$table->unique(['owned_by_id', 'owned_by_type', 'type'], 'owner_type_unique');
});
I expect a related model to be created/updated, but it fails on creating.
Trick was using a saved model event and also not forgetting to set the fillable attribute on the email model:
/*
* Update or create single e-mail model for model owner
*
* #return void
*/
private function setEmailTypeAttribute($emailType, $email) :void
{
static::saved(static function($model) use($emailType, $email) {
$model->emails()
->updateOrCreate(
[
'owned_by_type' => get_class($model),
'owned_by_id' => $model->id,
'type' => $emailType
],
[
'email' => $email
]);
});
}

access to belongsTo method in same model laravel

I use this model but use this model show below error:
Failed calling App\User::jsonSerialize()
but remove "$this->customer->name" result is ok.
thanksssssssssssssssssssssssssssssssssssssssssssssssssssssssssss.
class User extends Authenticatable
{
/**
* Get the user's customer name.
*
* #param string $value
* #return array
*/
public function getCustomerIdAttribute($value)
{
return [
'id' => $value,
'name' => $this->customer->name
];
}
/**
* The attributes that should be casted to native types.
*
* #var array
*/
protected $casts = [
'customer_id' => 'array',
];
/**
* Get the customer record associated with the user.
*/
public function customer()
{
return $this->belongsTo(Customer::class);
}
}
Your issue is that $this->customer is returning null, which is causing $this->customer->name to cause an error.
When you json_encode a Model, or convert it to a string, or otherwise call toJson on it, it will call the jsonSerialize() method.
At some point, this ends up calling your getCustomerIdAttribute() accessor you have defined. Inside this accessor, you have the statement $this->customer->name. However, if the current model is not related to a customer record, then $this->customer will return null, and then $this->customer->name will cause an error. When $this->customer->name causes an error, it causes jsonSerialize() to fail.
In your accessor, just make sure to check if $this->customer is valid before attempting to access the name attribute:
public function getCustomerIdAttribute($value)
{
return [
'id' => $value,
'name' => ($this->customer ? $this->customer->name : null)
];
}

Add relation to Eloquent Model before create it

I have 2 related Models. They are linked using MorphOne relationship.
First Model is User:
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
/**
* Get all of the owning description models.
*/
public function description()
{
return $this->morphTo();
}
}
And second one is associated User type, which in our example is Trainer:
class Trainer extends Model
{
/**
* Get user, which trainer belongs to.
*/
public function user()
{
return $this->morphOne('App\Models\Users\User', 'description');
}
/**
* Event listeners
*/
public static function boot()
{
parent::boot();
static::created(function($item)
{
event(new TrainerCreated($item));
});
}
}
As you can see, I try to use Events for different User types. Inside those Events I want to access associated User Model, like so: $trainer->user But I cannot find right order to save those Models.
What I have for now:
class AuthController extends Controller
{
/**
* Create a new user instance after a valid registration.
*
* #param array $data
* #return User
*/
protected function create(array $data)
{
$user = Users\User::create([
'name' => $data['name'],
'surname' => $data['surname'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
switch ($data['type'])
{
case "trainer":
$supertype = Users\Trainer::create();
break;
default:
$supertype = Users\Visitor::create();
break;
}
if ($supertype)
{
$supertype->user()->save($user);
}
return $user;
}
}
Which throw me error, that I Trying to get property of non-object
I also try another way, which seems more logical:
switch ($data['type'])
{
case "trainer":
$supertype = new Users\Trainer;
break;
default:
$supertype = new Users\Visitor;
break;
}
if ($supertype)
{
$user->description()->associate($supertype);
$supertype->save();
}
Which throws another error Class '' not found.
I tried multiple other ways, like manually setting ids, but they all throw different errors and not associate models properly. Maybe manually set both description_id and description_type will help, but I don't have id to set at this point.
I found working solution:
class AuthController extends Controller
{
/**
* Create a new user instance after a valid registration.
*
* #param array $data
* #return User
*/
protected function create(array $data)
{
$user = Users\User::create([
'name' => $data['name'],
'surname' => $data['surname'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
switch ($data['type'])
{
case "trainer":
$supertype = new Users\Trainer;
break;
default:
$supertype = new Users\Visitor;
break;
}
if ($supertype)
{
$supertype->user()->save($user);
$supertype->save();
$user->description()->associate($supertype);
$user->save();
}
return $user;
}
}
At start need to create User Model and save it. $user = Users\User::create([])
Then we should create Trainer Model instance.
$supertype = new Users\Trainer;
After need to add User Model to MorphOne relationship and only
after this save Trainer Model. This will allow us to use
$trainer->user in TrainerCreated Event.
$supertype->user()->save($user); $supertype->save();
But Model not properly created in database at this point, so we need
to follow usual steps to do it, as we have both Models saved
already.
$user->description()->associate($supertype); $user->save();

Resources