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']);
});
}
}
Related
I am trying to make a function that will help me get needed data quickly.
With all the trials I have been able to get to the following
Tables:
Users (id,name)
Projects (id,name)
User-Project (id, user_id, project_id, manager) where manager is a boolean , there can only be one manager for each project (but employees can still see the project reason why we have a pivot table, manager = 0 for other normal users that can access that project)
In the Project Model I have:
public function Manager(){
return $this->belongsToMany('App\User')->wherePivot('manager', true);
}
In the View I have:
<p><strong>Project Manager:</strong> {{$project->manager}}</p>
On the actual page I get:
Project Manager: [{"id":4,"name":"Daniel Doe","email":"danieldoe#hotmail.com","phone":"70846556","email_verified_at":null,"created_at":"2020-12-20 21:05:50","updated_at":"2020-12-20 21:05:50","pivot":{"project_id":1,"user_id":4,"manager":1}}]
When I change the view to:
<p><strong>Project Manager:</strong> {{$project->manager[0]->name}}</p>
I get:
Project Manager: Daniel Doe
This is what I actually want but I would like to do it from the model if possible. So I tried:
public function Manager(){
return $this->belongsToMany('App\User')->wherePivot('manager', true)->first()->name;
}
But I get the following error:
must return a relationship instance
Can this be done from the model's function?
You can keep your defined relationship, but to access ->first()->name, you'll need to use an "Accessor":
public function manager() {
return $this->belongsToMany('App\User')->wherePivot('manager', true);
}
public function getManagerNameAttribute() {
return $this->manager->first() ? $this->manager->first()->name : 'No Manager';
}
Then, in your code, you simple access:
{{ $project->manager_name }}
If your manager() function returns a Collection of at least 1 record, it will return the name, otherwise it will display 'No Manager' as a fallback.
If you don't want to change the structure of this you can use an accessor to get this information, roughly something like this:
class Project ...
{
public function users()
{
return $this->belongsToMany(...)->withPivot(...);
}
public function getManagerAttribute()
{
return $this->users()->wherePivot('manager', 1)->first()?->name;
}
}
You can do this in different ways, you could use the loaded users relation and use a the Collection methods to filter the manager. You could create another relationship called managers that uses the wherePivot off of users(), etc ...
The only thing to worry about with this setup is that every call to $model->manager would be causing that query, so it may be a good idea to create another relationship manager so that you can load that once and keep using it without the need to keep querying the database:
public function managers()
{
return $this->users()->wherePivot(...);
}
public function getManagerAttribute()
{
return $this->managers->first()?->name;
}
Though, as mentioned already it is probably better to have something like a manager_id on the Project itself.
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.
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
I have a problem in my Laravel structure because I need to add many reports to my app, so I think it's not a good idea to put everything in the controller because my eloquent models allow me to list, add, insert and update, and my queries need more than one table with joins, and some math functions like sum(), max(), min().
When I used Codeigniter, I added methods with each query in the model file.
So I can call it $sales->salesReport() and it gave me the data.
The question is really a matter of what is being done and what is responsible. There are some excellent posts on where logic should be kept and what can be used. I am a little unclear as to whether you are asking about chaining something like scopes or more just where to put your logic. I would probably have a service:
<?php
class SalesReportService {
public function generateReport(Sales $sales)
{
// logic here...
return $result;
}
}
and then in the controller it would be something like:
<?php
class SalesController extends Controller {
public function __construct(SalesReportService $reportService)
{
$this->reportService = $reportService;
}
public function show(Sales $sales)
{
return $this->reportService->generateReport($sales);
}
}
Laravel offers something similar to Codeigniter that matches what you described. It's called query scopes, or more precisely local scopes. You can keep them in your model and call them whenever you want.
You add in your model
public function scopeSalesReport($query) {
return $query->join(...);
}
Source: https://laravel.com/docs/5.4/eloquent#local-scopes
I am trying to inspect my Eloquent models to find out their relations to other Models. The problem is that relations are simply defined as a single method and no central index of relations exists:
public function posts()
{
return $this->hasMany('Post');
}
In order to inspect all relations I need to extract the list of methods, take out the ones inherited from Eloquent, execute each single one and check the return type:
$all = get_class_methods($model);
$inherited = get_class_methods('Eloquent');
$unique = array_diff($all, $inherited);
foreach($unique AS $method)
{
$relation = $model->$method();
if(is_a($relation, 'Illuminate\Database\Eloquent\Relations\Relation'))
{
//... this is a relation, do something with it
}
}
Needless to say, this is very dangerous. Is there a way to do this kind of inspection in a different, more secure way?
You could add PHPDoc comments to your relationship methods and then use the PHP reflection API to extract those from the source.