Load more attributes into existing eloquent model instance - laravel

Is it possible to load additional attributes into model instance without multiple queries or hackery? Let me explain:
// I got a tiny model with only id loaded
$model = Model::first(['id']);
// Then some code runs
// Then I decide I'd need `name` and `status` attributes
$model->loadMoreAttributes(['name', 'status']);
// And now I can joyously use name and status without additional queries
$model->name;
$model->status;
Does Eloquent have something similar to my fictional loadMoreAttributes function?
Notice kindly that I'm not a novice and am very well aware of Model::find($model->id) and such. They're just too wordy.
Thanks for your attention in advance.

You may extend the Eloquent model to have this loadMoreAttributes method like so:
use Illuminate\Database\Eloquent\Model;
class YourModel extends Model
{
public function loadMoreAttributes(array $columns)
{
// LIMITATION: can only load other attributes if id field is set.
if (is_null($this->id)) {
return $this;
}
$newAttributes = self::where('id', $this->id)->first($columns);
if (! is_null($newAttributes)) {
$this->forceFill($newAttributes->toArray());
}
return $this;
}
}
This way you can do this on your model:
$model = YourModel::first(['id']);
$model->loadMoreAttributes(['name', 'status']);
LIMITATION
However there's a limitation to this hack. You may only call loadMoreAttributes() method if the unique id of your model instance is already fetched.
Hope this help!

Related

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

Laravel: Eloquent "join", how to get only a property of the referred table

I have a model A and a model B.
I made the relations between them, so i can do A->B->property.
But now I'm facing some problems.
I need to make a query and get only that B->property, not the B object.
So i use this:
A::with(['B'])->get()
But then a get a property called B in A with the complete B model.
Is there anyway to achieve something like this.
A::with(['B->property'])->get()
So then in the B property inside A I get the B->property instead the B object.
Is it possible?
PS: I can't use the query builder because i need that eloquent model.
I think this article will help you out:
http://laraveldaily.com/why-use-appends-with-accessors-in-eloquent/
You can put
$appends = ['property'] in your model to add a property field to your model.
Then, with an accessor method in the model you can describe how to populate that field (ie: with a field from another model via relationship).
It seems like that ought to give you what you want.
Try below code
In your A model:
protected $appends = ['property'];
public function B()
{
return $this->hasOne('\App\B');
}
public function getPropertyAttribute()
{
return $this->B()->property;
}
then A->property will give you B->property. Change model name and property name as per your requirement.
The answer by #shoieb0101 did not work for me because I had a belongsTo relationship rather than hasOne. If this is the case for you too, you just need to modify the accessor function as illustrated below.
protected $appends = ['property'];
public function B()
{
return $this->belongsTo('\App\B');
}
public function getPropertyAttribute()
{
return $this->B()->first()->property;
}
Note: I added ->first() in since belongsTo returns an array of results, but we can get the property from a single result only.
You should be able to constrain eagerloaded queries:
try
A::with(["B" => function($query){
$query->select('property');
}])->get();

Laravel 4 Use the same Model and Controller for 12 lists for selects

I have to meke models, controllers and views for 12 tables. They have all the same structure id, name, order.
I was thinking and maybe using:
Controller
index($model)
$model::all()
return View::make(all_tables,compact('model'))
edit($model,$id)... and so on.
But and don't know if there's a way for using only one model.
Did anybody do anything like this?
Any idea?
Thanks
Although each model has the same table structure, what you're trying to achieve would not be advisable as you'd lose a lot of the fluent capabilities of Laravel's Eloquent ORM.
Regarding the controller, this would work:
<?php
namespace App\Http\Controllers;
use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class GenericModelController extends Controller
{
public function loadModelById($model, $id)
{
$instance = \App::make('App\\' . ucfirst($model));
return $instance->find($id);
}
}
You'll need the following route:
Route::get('show/{model}/{id}', 'GenericModelController#loadModelById');
Example, to load a user with an id of 1:
http://www.yourdomain.com/show/user/1
Edit: I just saw that you're using Laravel 4, so the syntax for defining a route will be a little different I believe but the general concept will still work. Testing in Laravel 5 and works perfectly.
You should get get some idea from here.Please use the link below.
https://scotch.io/tutorials/a-guide-to-using-eloquent-orm-in-laravel
// app/models/Bear.php
class Bear extends Eloquent {
// MASS ASSIGNMENT -------------------------------------------------------
// define which attributes are mass assignable (for security)
// we only want these 3 attributes able to be filled
protected $fillable = array('name', 'type', 'danger_level');
// DEFINE RELATIONSHIPS --------------------------------------------------
// each bear HAS one fish to eat
public function fish() {
return $this->hasOne('Fish'); // this matches the Eloquent model
}
// each bear climbs many trees
public function trees() {
return $this->hasMany('Tree');
}
// each bear BELONGS to many picnic
// define our pivot table also
public function picnics() {
return $this->belongsToMany('Picnic', 'bears_picnics', 'bear_id', 'picnic_id');
}
}
I find a simple way.
Only one model, one controller and one view(index,edit, etc) too.
A single table with
id, name of list, value (name to appears in the list)
Yo pass can pass to de view all the values per list, and for any list in the table you can create de select if it's no empty.

Laravel 4: How to add more data to Auth::user() without extra queries?

I'm rather new to Laravel 4 and can't seem to find the right answer, maybe you can help:
A User in our application can have many Accounts and all data is related to an Account, not a User. The account the User is currently logged into is defined by a subdomain, i.e. accountname.mydomain.com.
We added a method account() to our User model:
/**
* Get the account the user is currently logged in to
*/
public function account()
{
$server = explode('.', Request::server('HTTP_HOST'));
$subdomain = $server[0];
return Account::where('subdomain', $subdomain)->first();
}
The problem is that there is always an extra query when we now use something like this in our view or controller:
Auth::user()->account()->accountname
When we want to get "Products" related to the account, we could use:
$products = Product::where('account_id', Auth::user()->account()->id)->get();
And yet again an extra query...
Somehow we need to extend the Auth::user() object, so that the account data is always in there... or perhaps we could create a new Auth::account() object, and get the data there..
What's the best solution for this?
Thanks in advance
Just set it to a session variable. This way, you can check that session variable before you make the database call to see if you already have it available.
Or instead of using ->get(), you can use ->remember($minutes) where $minutes is the amount of time you wish to keep the results of the query cached.
You should take a look at Eloquent relationships : http://laravel.com/docs/eloquent#relationships
It provides simple ways to get the account of a user and his products. You said that a user can have many accounts but you used a first() in your function I used a hasOne here.
Using Eloquent relationships you can write in your User model:
<?php
public function account()
{
// I assume here 'username' is the local key for your User model
return $this->hasOne('Account', 'subdomain', 'username');
}
public function products()
{
// You should really have a user_id in your User Model
// so that you will not have to use information from the
// user's account
return $this->hasMany('Product', 'account_id', 'user_id');
}
You should define the belongsTo in your Account model and Product model.
With Eager Loading you will not run a lot of SQL queries : http://laravel.com/docs/eloquent#eager-loading
You will be able to use something like
$users = User::with('account', 'products')->get();
To get all users with their account and products.
I think this is a good example for the purpose of Repositories.
You shouldn't query the (involved) models directly but wrap them up into a ProductRepository (or Repositories in general) that handles all the queries.
For instance:
<?php
class ProductRepository
{
protected $accountId;
public function __construct($accountId)
{
$this->accountId = $accountId;
}
public function all()
{
return Product::where('account_id', $this->accountId)->get();
}
}
//now bind it to the app container to make it globaly available
App::bind('ProductRepository', function() {
return new ProductRepository(Auth::user()->account()->id);
});
// and whenever you need it:
$productRepository = App::make('ProductRepository');
$userProducts = $productRepository->all();
You could group the relevant routes and apply a filter on them in order to bind it on each request so the account-id would be queried only once per repository instance and not on every single query.
Scopes could also be interesting in this scenario:
// app/models/Product.php
public function scopeCurrentAccount($query)
{
return $query->where('account_id', Auth::user()->account()->id);
}
Now you could simply call
$products = Product::currentAccount()->get();

Inspect Model relations

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.

Resources