Overriding Laravel get and first methods - laravel

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

Related

Laravel Model Define Parameters Won't Insert

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.

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']);
});
}
}

Best Practice - Laravel Controller Eloquent merge

I have a scope on my Supplier model that returns results where active = true.
This works great when creating new entries, as I only want the user to see active suppliers.
Current entries may have an inactive supplier; When I edit it, I want to see all active Suppliers, plus the current supplier (if it is inactive)
I have this code in my controller:
$suppliers = Supplier::active()->get();
if (!$suppliers->contains('id', $record->supplier->id))
{
$suppliers->add(Supplier::find($record->supplier->id));
}
Two questions: Is this the correct way to do this? Should this code be in my controller or should I have it somewhere else? (perhaps a scope but I wouldn't know how to code that).
Edit:
Thanks for the help guys. I have applied advice from each of the answers and refactored my code into a new scope:
public function scopeActiveIncluding($query, Model $model = null)
{
$query->where('active', 1);
if ($model && !$model->supplier->active)
{
$query->orWhere('id', $model->supplier->id);
}
}
What you've written will work, but the Collection::contains function can potentially be pretty slow if the collection is large.
Since you have the id, I would probably make the following change:
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
Of course, the downside to this is that you may be making an unnecessary query on the database.
So you have to consider:
is the record's supplier more likely to be active or inactive?
is the size of the collection of active suppliers large enough to justify another (potentially wasted) call to the database?
Make the choice that makes the most sense, based on what you know of your application's data.
As for the second question, if you will only need this specific set of suppliers in this one part of your application, then the controller is a good place for this code.
If, however, you will need this particular set of suppliers in other parts of your application, you should probably move this code elsewhere. In that case, it might make sense to create a function on the the related model (whatever type $record is...) that returns that model's suppliers set. Something like:
public function getSuppliers()
{
$suppliers = Supplier::active()->get();
$supplier = $this->supplier;
if (!$supplier->active) {
$suppliers->add($supplier);
}
return $suppliers;
}
I saw #Vince's answer about 1st question, and I'm agree with him.
About 2nd question:
Write scope in Supplier model like this:
public function scopeActive($query){
$query->where('active', 1); // for boolean type
}
For good practice, you need to write the logic parts in services like "App\Services\SupplierService.php". And there write the function you want:
public function activeSuppliersWithCurrent($record) {
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
}
In your SupplierController's constructor inject the instance of that service and use the function, for example:
use App\Servives\SupplierService;
protected $supplierService = null;
public function __construct(SupplierService $supplierService) {
$this->supplierService = $supplierService;
}
public function getActiveSuppliersWithCurrent(...) {
$result = $this->supplierService->activeSuppliersWithCurrent($record);
}
As you can see, later you will not need to change anything in controller. If you'll need to change for example the query of suppliers selection, you will just have to change something only in service. This way will make your code blocks separated and shorter.
Also the sense for this pattern: you don't need to access the models from controller. All logic related with models will implemented in services.
For other projects you can grab only services or only controllers, and implement another part differently. But in that case if you had all codes in controller, that will prevent you to grab the portions of necessary codes, cuz may you don't remember what doing each blocks...
You could add a where clause to the query to also find that id.
$suppliers = Supplier::active()->orWhere('id', $record->supplier->id)->get();
You could potentially slide this into the active scope by passing the 'id' as an argument.
public function scopeActive($query, $id = null)
{
$query->where('active', true);
if ($id) {
$query->orWhere('id', $id);
}
}
Supplier::active($record->supplier->id)->get();
Or make another scope that does this.

Can I send a variable into laravel model?

So I'm trying to add get a specific data from a related table using the below method, but I don't know if that is the correct way to do it. here is what it looks like.
public function transspecific($lid){
return $this->belongsTo('raplet\Keepertrans')->where("lang_id", $lid);
}
and then I try to get data from it
dd($akeeper->transspecific($akeeper->id));
it doesn't act like there is anything but when I type dd("hello") inside the model, it works. so clearly I have something wrong with my relationship context
What are you are trying to do is adding a [dynamic scope in laravel][1] model, which is totally fine. Except you need to declare the scope seperated from relationship method.
Relationship:
public function keepertrans(){
return $this->belongsTo('raplet\Keepertrans');
}
Scoped:
public function transspecific($lid){
return $this->keepertrans()->where("lang_id", $lid);
}
Then you can call the scope with a get() to execute the query builder:
(SomeOtherRelatedModel::first())->transspecific($someId)->get();
The methods available in Eloquent model for relationship are different than what you need. Whenever you need to add a custom function which internally adds some filters to your query (builder), you need to use scopes
The generic rule of scope function is scope + yourfunction
In your case you will need to create scopeTranspecific function.
Each scope gets the first argument as builder which you update inside the function. The later arguments are optional.
public function scopeTranspecific($query, $lid){
return $query->keepertrans()->where("lang_id", $lid);
}
And then you can use it :
Model::where('column1' , 'value')->transpecific($id)->get()
If you just dump it without ->get() you will get the query builder instance. You will have to do ->get() to get the data

Laravel overwrite toArray with custom parameter

In my controller I have:
$Locations = Locations::where(something);
$Locations->get()->toArray(true);
And inside the model:
function toArray($include_all = false) {
var_dump($include_all);
}
The include all variable is false, although the function gets called.
Is there a reason why it's doing that ?
I want to call a custom toArray because I have more oneToMany relations with different structures that I want to change (some of them are serialized for example)
Thank you
You can use Illuminate\Support\Collection methods such as map() and filter() to modify the collection and at the end of that call toArray() method.
It would be fairly easy to overwrite, however I think there is some confusion that should be cleared up first.
First, the toArray() method you are calling in this case is on the Collection which is the object which is returned when you use get() on your model.
With that said, you can add the following to your Location model to return a custom collection...
public function newCollection(array $models = [])
{
return new CustomCollection($models);
}
Then you write the new CustomCollection class with appropriate namespaces just to make sure it gets auto loaded fine, have it extend \Illuminate\Database\Eloquent\Collection and then you can proceed to override the toArray method.
However, it feels like you randomly selected this toArray() as a proper candidate to perform your logic just because you are already using it. You should think about creating a new function which calls $this->toArray() to grab the results and modify them as you need and return that.
If you need this same functionality on other models, just keep adding that newCollection method where needed.
This is also in the docs as well, might be worth checking out...
https://laravel.com/docs/5.2/eloquent-collections#custom-collections

Resources