Laravel Model Define Parameters Won't Insert - laravel

I am currently moving over from symfony to laravel, it's quite a bit different when it comes to the database. So i have a basic model, i'm just going to use an example:
class Test extends Model
{
use HasFactory;
}
All good, i have a migration and the table created. However, i don't like this:
$test = new Test();
$test->my_field = 'hello';
$test->save();
I don't like it because it's having to use a magic __set() to create the parameter, if i define the parameter in my model like this:
class Test extends Model
{
use HasFactory;
public ?string $my_field;
}
I get database errors when it tries to insert when i define the params like this. Why is that? It's doing the same thing as __set() but i'm actually physically defining them, which in my opinion is a better way to code it as my IDE can typehint and it's just nicer to follow the program knowing what params are there.
What's the reason for it inserting when i don't define them, and not when i do? From my actual table which is bookings , has a field booking_ref:
General error: 1364 Field 'booking_ref' doesn't have a default value (SQL: insert into booking_reviews (updated_at, created_at) values (2021-12-13 14:13:08, 2021-12-13 14:13:08))
This happens when i define the $booking_ref param on the model, but if i take it out and rely on the __set() method it works fine. Doesn't make any sense to me right now.

I think this is a reasonable enough misunderstanding to be useful to future visitors, so I want to try to explain what's going on with some pseudo-code and some references to the current source code.
You are correct that when setting a property on a Laravel model, that is a column in the DB, internally Laravel is using the PHP magic method __set.
What this does is allow you to 1) set properties directly instead of calling some kind of setter function, and 2) interact with your table columns without needing the boilerplate of column definitions in your model.
Where the assumptions go wrong is with what __set is doing. __set does not have to simply set an actual property with the same name. __set is just a method you may implement to do whatever you want. What you assumption implies is that it's doing something like this:
public function __set($key, $value)
{
$this->{$key} = $value;
}
However, you can do whatever you want with the $key and $value passed to the magic method.
What Laravel does is call another method defined in the HasAttributes trait - setAttribute.
public function __set($key, $value)
{
$this->setAttribute($key, $value);
}
setAttribute does a few extra things, but most importantly it adds the key/value pair to Model property $this->attributes[].
To hopefully help this difference make sense, here is what the two __set methods would yield with a basic example:
$model->my_column = 'value';
// 1st example
/**
* {
* public $my_column = 'value';
* }
*/
// Laravel way
/**
* {
* protected $attributes= ['my_column => 'value'];
* }
*/
I won't go through both saving and updating since they're very similar, but to show how this is used, we can look at the save method, which calls performInsert and after a few more calls makes it's way back to the attributes property to determine what to actually insert into the query.
Summary
Laravel does not use custom model properties when deciding what column/values to add to queries.
This is why when you create custom mutators, you interact with the attributes property just like Laravel does internally.
Anytime you introduce "magic" into code, you have some tradeoffs. In this case, that tradeoff is slightly less clarity with what database columns are actually available. However, like I mentioned in comments, there are other solutions to make models more IDE friendly like Laravel IDE helper.

Related

Repleace relationship by name of this relationship

I have a model User with fileds like - colorhair_id, nationality_id, etc. Of course I have a relationship to other model. Problem is that I want to return nationality from User i must do that:
User::find(1)->colorhair->name
In next time I need to use
User::find(1)->nationality->name
It works but it's not look professional and it's dirty. How can I change query or something else to return object with repleace field like nationality_id to nationality with name of that. Any idea?
You can use Laravel Mutators. Put below two functions into the User model
public function getHairColorNameAttribute(){
return $this->colorhair->name
}
public function getNationalityNameAttribute(){
return $this->nationality->name
}
Then when you simply access it.
User::find(1)->hair_color_name;
The next time
User::find(1)->nationality_name;
If you want to get these values by default use $append in the model. Put the following line to the top of the User model
protected $appends = ['hair_color_name','nationality_name'];
Note: In laravel 9 mutators little bit different from the above method.
Bonus Point :
if you access values in the same scopes don't use find() method in each statement.
$user = User::find(1);
then
$user->hair_color_name;
$user->nationality_name;

Laravel's Accessor call only when requested [duplicate]

Is there any possible way to lazy load a custom attribute on a Laravel model without loading it every time by using the appends property? I am looking for something akin to way that you can lazy load Eloquent relationships.
For instance, given this accessor method on a model:
public function getFooAttribute(){
return 'bar';
}
I would love to be able to do something like this:
$model = MyModel::all();
$model->loadAttribute('foo');
This question is not the same thing as Add a custom attribute to a Laravel / Eloquent model on load? because that wants to load a custom attribute on every model load - I am looking to lazy load the attribute only when specified.
I suppose I could assign a property to the model instance with the same name as the custom attribute, but this has the performance downside of calling the accessor method twice, might have unintended side effects if that accessor affects class properties, and just feels dirty.
$model = MyModel::all();
$model->foo = $model->foo;
Does anyone have a better way of handling this?
Is this for serialization? You could use the append() method on the Model instance:
$model = MyModel::all();
$model->append('foo');
The append method can also take an array as a parameter.
Something like this should work...
public function loadAttribute($name) {
$method = sprintf('get%sAttribute', ucwords($name));
$this->attributes[$name] = $this->$method();
}

Extending Eloquent Models with dynamic global scopes

I have a MySQL table that receives many different Jotform reports in JSON payloads. This has helped tremendously in capturing queryable data quickly without adding to the front-end developer's workload.
I created an eloquent model for the table. I now would like to be able to create models that extend it for each Jotform we create. I feel like it will increase the readability of my code drastically.
My eloquent model is called RawDataReport. It has created_at, updated_at, data, and report name columns in the table. I want to create the model ShiftInspectionReport extending the RawDataReport.
I have two JotForm reports one is called Shift Inspection Report and one is called Need Inspection Report. Both are part of the ShiftInspectionReport model.
So I need to query the RawDataReports table for any reports matching those names. I frequently need to query the RawDataReports report_name column with either one or more report names.
To help with this I created a local scope to query the report name which accepts either a string report name or an array of string report names. Here is the local scope on the RawDataReports model.
protected function scopeReportName($query, $report_name): \Illuminate\Database\Eloquent\Builder
{
if (is_array($report_name)) {
return $query->orWhere(function ($query) USE ($report_name) {
ForEach($report_name as $report) {
if (is_string($report) === false) {
throw new \Exception('$report_name must be an array of strings or a string');
}
$query->where('report_name', $report);
}
});
} else {
if (is_string($report_name) === false) {
throw new \Exception('$report_name must be an array of strings or a string');
}
return $query->where('report_name', $report_name);
}
}
EDIT - after comments I simplified the reportName scope
protected function scopeReportName($query,array $report_name): \Illuminate\Database\Eloquent\Builder
{
return $query->whereIn('report_name',$report_name);
}
Now in my ShiftInspectionReport model, I'd like to add a global scope that can use that local scope and pass in the $report_name. But according to this article, Laravel 5 Global Scope with Dynamic Parameter, it doesn't look like Laravel global scopes allow you to use dynamic variables.
I could just create a local scope in ShiftInspectionReport but the readability would look like
$reports = ShiftInspectionReport::shiftInspectionReport()->startDate('2021-05-15')->get()
when I'd really like to be able to just call
ShiftInspectionReport::startDate('2021-05-15')->get()
Any suggestions or comments would be appreciated.
Thank you
Thanks to IGP I figured out that I can just call the local scope right from my boot function.
My extended class looks like this now and it works.
class ShiftInspection extends RawDataReport
{
use HasFactory;
protected static function booted()
{
static::addGlobalScope('shift_inspection_report', function(\Illuminate\Database\Eloquent\Builder $builder) {
$builder->reportName(['Shift Safety Inspection','Need Safety Inspection']);
});
}
}

Overriding Laravel get and first methods

I need to override above mentioned methods to skip some database records. Using where is not an option since I would have to use it every single time as there are records in database that I do not need most of the time and I am not allowed to delete them from DB. Here is my attempt of doing this:
class SomeTable extends BaseModel {
public static function first() {
$query = static::query();
$data = $query->first();
if($data && $data->type == 'migration_type') return null;
return $data;
}
public static function get() {
$query = static::query();
$data = $query->get();
foreach($data as $key => $item) {
if($item->type == 'migration_type') unset($data[$key]);
}
return $data;
}
}
The problem with this code is that it works only when direct called on model. If I am using some other functions, like where, before get or first methods, it just skips my overridden method.
What would be the right way to do this and should I put this code within model?
My question is not duplicate as in the answer from mentioned question it is said:
all queries made from Models extending your CustomModel will get this new methods
And I need to override those two functions only for specific model, not for each one in application as not all tables have type column. That's the reason why I have written them within model class.
I need to override above mentioned methods to skip some database records.
Consider a global query scope on the model.
https://laravel.com/docs/5.8/eloquent#global-scopes
Global scopes allow you to add constraints to all queries for a given model. Laravel's own soft delete functionality utilizes global scopes to only pull "non-deleted" models from the database. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints.
The issue here is that the where() method on the model returns a QueryBuilder instance where get() will return a Collection instance.
You should be able to override collection's default methods by adding a macro in it's place and can be done like so...
Collection::macro('toUpper', function () {
return $this->map(function ($value) {
return Str::upper($value);
});
});
Extending the query builder instance is not so easy but a good tutorial exists here and involves overriding the application's default connection class, which is not great when it comes to future upgrades.
Because after calling where you're dealing with the database builder and theses methods inside your model aren't being called .. about the issue you might overcome it by using select instead of first directly so will deal with the builder ..
example:
SomeTable::select('col1','col2')->take(1)->get();
another thing overriding these kind of methods is not a good idea if you're working with other developer on the same project.
good luck

CodeIgniter - Datamapper ORM - Autopopulate Instance

Just wondering if there is an easy way to auto-populate an object’s related fields on an instance-by-instance basis rather than globally in the config file, or, for the entire class.
I’d like to include all related models for a single instance without chaining a ton of include_related() functions.
Something like this would be nice:
$x = new Model();
$x->include_all_related();
Thought I'd have to get my hands dirty in the core. For whatever reason, it didn't occur to me that I could access the $has_many and $has_one arrays.
Solution was simple:
class Model extends Datamapper{
var $has_one = array('foo', 'bar', 'baz');
var $table = 'models';
function __construct($id = NULL){
parent::__construct($id);
}
function include_all_related(){
foreach($this->has_one as $h){
$this->include_related($h['class']);
}
return $this;
}
}
You might wonder why I'm using the class key in the $h variable. Under the hood, Datamapper ORM keeps track of some other keys too as part of a bigger array. If you call print_r($h), you can see them. The class key keeps track of foo, bar and baz.
You can enable it on the relation definition, if you want is on a particular instance, you need to change the relation configuration of that instance at runtime.

Resources