Why does Eloquent change relationship names on toArray call? - laravel

I'm encountering an annoying problem with Laravel and I'm hoping someone knows a way to override it...
This is for a system that allows sales reps to see inventory in their territories. I'm building an editor to allow our sales manager to go in and update the store ACL so he can manage his reps.
I have two related models:
class Store extends Eloquent {
public function StoreACLEntries()
{
return $this->hasMany("StoreACLEntry", "store_id");
}
}
class StoreACLEntry extends Eloquent {
public function Store()
{
return $this->belongsTo("Store");
}
}
The idea here is that a Store can have many entries in the ACL table.
The problem is this: I built a page which interacts with the server via AJAX. The manager can search in a variety of different ways and see the stores and the current restrictions for each from the ACL. My controller performs the search and returns the data (via AJAX) like this:
$stores = Store::where("searchCondition", "=", "whatever")
->with("StoreACLEntries")
->get();
return Response::json(array('stores' => $stores->toArray()));
The response that the client receives looks like this:
{
id: "some ID value",
store_ac_lentries: [
created_at: "2014-10-14 08:13:20"
field: "salesrep"
id: "1"
store_id: "5152-USA"
updated_at: "2014-10-14 08:13:20"
value: "salesrep ID value"
]
}
The problem is with the way the StoreACLEntries name is mutilated: it becomes store_ac_lentries. I've done a little digging and discovered it's the toArray method that's inserting those funky underscores.
So I have two questions: "why?" and "how do I stop it from doing that?"

It has something in common with automatic changing camelCase into snake_case. You should try to change your function name from StoreACLEntries to storeaclentries (lowercase) to remove this effect.

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 'has' method behaves in an unexpected way

I want to get the collection of a Section model if it has at least one User. from the docs the has() method does this, great. The collection retrieved does not have the the users relationship in it. Yet when I loop through the collection , I can get the properties of the users. Why?
class Section extends Model
{
protected $guarded = [];
public function users()
{
return $this->hasMany('App\User');
}
}
class User extends Authenticatable
{
protected $guarded = [];
public function section()
{
return $this->belongsTo('App\Section');
}
}
what I did is this:
$section = Section::where('id' , 1)->has('users')->get();
the collection is this:
Illuminate\Database\Eloquent\Collection {#3025
all: [
App\Section {#3015
id: 1,
class_id: 1,
section_name: "A",
created_at: "2019-12-14 18:26:01",
updated_at: "2019-12-14 18:26:01",
},
],
}
Now the weird part is that when I do the following, it gives the properties of users even though in the collection the users relationship is not present.
Why?
#foreach ($section as $section)
#foreach ($section->users as $student)
<p>{{$student->name}}</p>
#endforeach
#endforeach
solomon
uche
kene
This is just how Laravel works.
Accessing $model->{relationship}, in your case $section->users, is a magic function that checks if you have explicitly loaded the relationship via something like Section::with('users'), and if you haven't, loads it then.
The reason you're not seeing users when you run dd($section) is that you didn't explicitly load the relationship, but this does not mean that it is unavailable. If you included with('users') in your initial query, you'd see the following:
$section = Section::where('id' , 1)->has('users')->with('users')->get();
App\Section {#3015
id: 1,
class_id: 1,
section_name: "A",
created_at: "2019-12-14 18:26:01",
updated_at: "2019-12-14 18:26:01",
users: [
0 => App\User {#3016}
id: ...
name: ...
]
},
// Or similar
Basically, you weren't loading the relationship, so you aren't able to see it when using dd($section), but it is still available in PHP, due to Laravel's magic methods.
I should note as well, use the correct variable naming and closure (->get(), ->first(), etc) for your query.
$section is a poor name when using ->get(), as you're getting multiple records back from the database. Either use $sections, or change the closure to ->first(), and don't use a foreach() if you use ->first().
Looks like your first echo:
$section = Section::where('id' , 1)->has('users')->get();
Just prints the section where it has a user, but you're not specifically saying give me the users as well.
In the next loop you are looping over each section in the view, and specifically saying loop the relationship. this is visible by this line: #foreach ($section->users as $student)
Im reading the docs here: https://laravel.com/docs/6.x/eloquent-relationships#querying-relations
In your first echo when printing the section you can get the users like so: echo $section-> users()
Ok, now I understand your question.
First
The has method does not mean that it's going to include User. It means that will return all the sections that have at least one user. I think there is/are users for Section id ===1. So, in your code with has or without it, it does not make any difference.
if you want to load a relationship explicit you should use with
Section::where('id' , 1)->with('users')->get();. Then you should have users collection under per section.
Second
The reason why you can still access the user properties in your blade file, is because of lazy loading. even it's not included in the original DB query and result but when you are trying to access it laravel still tries to fetch those for you. And this may cause N+1 problem.

Can I require a one-to-one relationship in Laravel Eloquent?

I've got a Product class and a Detail class. I wanted to separate the big, long description from the basic data (sku, price) just for speed. However there will always be a big long description. So each item in the store will always have two records.
Is there a way to force or require both records? Is there a way to require that the 1-to-1 relationship exists - so that this would always work...
$P = new App\Product(['sku'=>'123','price'=>69]);
$P->Detail->html = '<p>Lorem ipsum.</p>';
$P->push();
My hasOne/belongsTo stuff is all fine. But if the details record does not exist yet ~ I get errors. I'm wondering if Detail can auto-magically exist.
There's not a lot you can do to 'force' the paired record to exist, however you could use a getter to make the retrieval of the description a little safer. Something like:
class Product extends Model {
public function detail()
{
return $this->hasOne('App\Detail');
}
public function getDetailHtml()
{
if ($this->detail()->count()) {
return $this->detail->html;
}
}
}

Filtering eager-loaded data in Laravel 4

I have the following setup:
Clubs offer Activities, which are of a particular Type, so 3 models with relationships:
Club:
function activities()
{
return $this->hasMany('Activity');
}
Activity:
function club()
{
return $this->belongsTo('Club');
}
function activityType()
{
return $this->hasMany('ActivityType');
}
ActivityType:
function activities()
{
return $this->belongsToMany('Activity');
}
So for example Club Foo might have a single Activity called 'Triathlon' and that Activity has ActivityTypes 'Swimming', 'Running', and 'Cycling'.
This is all fair enough but I need to show a list of ActivityTypes on the Club page - basically just a list. So I need to get the ActivityTypes of all the related Activities.
I can do that like so from a controller method that receives an instance of the Club model:
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)
That gets me an object with all the related Activities along with the ActivityTypes related to them. Also fair enough. But I need to apply some more filtering. An Activity might not be in the right status (it could be in the DB as a draft entry or expired), so I need to be able to only get the ActivityTypes of the Activities that are live and in the future.
At this point I'm lost... does anybody have any suggestions for handling this use case?
Thanks
To filter, you can use where() as in the fluent DB queries:
$data = Club::with(array('activities' => function($query)
{
$query->where('activity_start', '>', DB::raw('current_time'));
}))->activityType()->get();
The example which served as inspiration for this is in the laravel docs, check the end of this section: http://laravel.com/docs/eloquent#eager-loading
(the code's not tested, and I've taken some liberties with the property names! :) )
I think if you first constraint your relationship of activities, the activity types related to them will be automatically constrained as well.
So what I would do is
function activities()
{
return $this->belongsToMany('Activity')->where('status', '=', 'active');
}
and then your
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)`
query will be working as you would expect.

Can I include a convenience query in a Doctrine 2 Entity Method?

I'd like to include some additional functions in my Doctrine 2 entities to contain code that I'm going to have to run quite frequently. For example:
User - has many Posts
Post - has a single user
I already have a function $user->getPosts(), but this returns all of my posts. I'm looking to write a $user->getActivePosts(), which would be like:
$user->getPosts()->where('active = true') //if this were possible
or:
$em->getRepository('Posts')->findBy(array('user'=>$user,'active'=>true)) //if this were more convenient
As far as I can tell, there's no way to get back to the entity manager though the Entity itself, so my only option would be
class User {
function getActivePosts() {
$all_posts = $this->getPosts();
$active_posts = new ArrayCollection();
foreach ($all_posts as $post) {
if ($post->getActive()) {
$active_posts->add($post);
}
}
return $active_posts;
}
However, this requires me to load ALL posts into my entity manager, when I really only want a small subset of them, and it requires me to do filtering in PHP, when it would be much more appropriate to do so in the SQL layer. Is there any way to accomplish what I'm looking to do inside the Entity, or do I have to create code outside of it?
I think you should implement the method on the PostRepository rather than on the entity model.
I try to keep all model related logic in the repositories behind "domain specific" methods. That way if you change the way you represent whether a post is active or not, you only have to change the implementation of a single method instead of having to find all the active = true statements scattered around in your application or making changes in an "unrelated" entity model.
Something like this
PostRepository extends EntityRepository {
public function findActiveByUser($user){
// whatever it takes to get the active posts
}
}

Resources