How to use an instance of a class in its own class - php

In Laravel 4.x I have a Customer Eloqeunt Model that has a relationship to a Customer_tel Eloquent Model:
class Customer extends Eloquent
{
public function tel()
{
return $this->hasMany('Customer_tel');
}
}
The Customer_tel table has a boolean column 'main'.
When I make an instance of the Customer Class in a view, then I can filter out the main number with the filter() method:
$Customer = Customer::find(1);
$Customer->tel->filter(function($tel)
{
if ($tel->main == true)
{
return true;
}
})->shift()->tel
But when I try to make a function in the class with the filter() method:
public function mainTel()
{
return $this->tel()->filter(function($tel)
{
if ($tel->main == true)
{
return true;
}
})->shift()->tel;
}
When I try to reference it in the view $Customer->mainTel, then it gives me an error "Call to undefined method Illuminate\Database\Query\Builder::filter()".
Why can't I filter the instance only outside of the class but not in the class? And is there a right way to do it?

Calling the method (tel()) returns the HasMany instance, upon which you can then call the query builder methods. Using Eloquent's magic properties, you can short-circuit it. So $customer->tel is equivalent to $customer->tel()->get(), which returns a collection. That is why it's working for you in your first example.
See the docs for more info.
A better option would be to do it in the query itself:
return $this->tel()->where('main', true)->pluck('tel');
Also note that you can create your own magic properties in Eloquent:
class Customer extends Eloquent {
public function tel()
{
return $this->hasMany('Customer_tel');
}
public function getMainTelAttribute()
{
return $this->tel()->where('main', true)->pluck('tel');
}
}
Now when you have a $customer model, you can call your magic method directly:
$tel = $customer::find(1)->main_tel;

Related

Laravel Eloquent can relationship method return null instead of MorphMany relationship object?

My problem could be more philosophical than concrete.
I'm using Laravel 8, I have one Trait that is used by several classes and some of its methods perform two different computation by taking into account if object has or not a specific morphMany relationship (here called entries). This trait is almost implemented with the following code:
trait MyTrait {
// Some methods...
// Entries methods
abstract public function hasEntries();
abstract public function entries();
// Here is a method that depends on entries
public function computFunc() {
return $this->hasEntries()
? $this->computWithEntries()
: $this->computWithoutEntries();
}
private function computWithEntries() {
// Perform computation with entries by using entries() relationship...
}
private function computWithoutEntries() {
// Perform computation without entries...
}
// Some other methods...
}
Obviously, classes that have entries relationship also implement some criteria inside hasEntries() method to check if there are entries for the current object, as shown in this example:
class ModelA {
use MyTrait;
public function entries() {
return $this->morphMany(Entry::class, 'owner');
}
public function hasEntries() {
return $this->entries()->where([ /* Some condition */ ])->exists();
}
}
Now, my question is: what should return entries() method for classes that don't have entries relationship, meaning that is not possible/allowed for entries table to have column owner_type equal to one of those classes.
Here's an instance of this case (ModelB can't own any entry):
class ModelB {
use MyTrait;
public function entries() {
// Should return null?
return null;
}
public function hasEntries() {
return false;
}
}
What value/object should return entries() method for objects of class ModelB? I'm quite sure that to return null instead of relationship object isn't the correct thing to do, am I wrong?
I think you may accomplish this with a different approach. Try with this:
Split your trait in 2 different traits (one to create the relationship and the other to check if has entries and compute the result).
Create an interface (contract) that contains the methods of the second trait (just to get sure those methods exists in the class) so you can specify if the value to return should be boolean.
With that, you wont have problem in the computing part of the code. Hope my answer helps.
Edit - Super quick example:
//EntriesRelationTrait.php
trait EntriesRelationTrait {
public function entries() {
return $this->morphMany(Entry::class, 'owner');
}
}
//ComputeByEntriesTrait.php
trait ComputeByEntries {
public function computFunc() {
return $this->hasEntries()
? $this->computWithEntries()
: $this->computWithoutEntries();
}
private function computWithEntries() {
// Perform computation with entries by using entries() relationship...
}
private function computWithoutEntries() {
// Perform computation without entries...
}
}
//HasEntriesContract.php
interface HasEntriesContract
{
/**
* #return bool
*/
public function hasEntries(): bool;
//function type declaration available only after PHP 7.0
}
class ModelA implements HasEntriesContract {
use EntriesRelationTrait;
use ComputeByEntriesTrait;
public function hasEntries(): bool {
return $this->entries()->where([ /* Some condition */ ])->exists();
}
}
class ModelB implements HasEntriesContract {
use ComputeByEntriesTrait;
public function hasEntries(): bool {
return false;
}
}
The contract ensures that the created function follows a standard, in this case, a boolean response from the hasEntries method.

Laravel collection - filter() method setting custom property

I made a custom collection class which extends laravel collection class. This custom collection has some data specific handeling methods and a property to make my life easier.
use Illuminate\Support\Collection;
class CustomCollection extends Collection
{
public bool $myProperty = true;
public function filterBasedOnCustomStuff()
{
return $this->filter(function ($row)
{
// Some custom sorcerry
return ($var === "whatever");
});
}
}
And my question is. When I call filterBasedOnCustomStuff() method it returns a new CustomCollection object. Which is what I want of course. But I would also like to set the value of the $myProperty parametr of the new collection instance. Is it possible or I have to do that on the instance afterwards? Somethink like that:
$newCollection = $oldCollection->filterBasedOnCustomStuff();
$newCollection->myProperty = $oldCollection->myProperty;
I would like to avoid this aproach of setting it afterwards if possible.
Thank you in advance.
I would simply use a variable to hold the filtered instance:
<?php
use Illuminate\Support\Collection;
class CustomCollection extends Collection
{
public bool $myProperty = true;
public function filterBasedOnCustomStuff()
{
$filtered = $this->filter(function ($row)
{
// Some custom sorcerry
return ($var === "whatever");
});
$filtered->myProperty = $this->myProperty;
return $filtered;
}
}

Cast To Inherited Class via Eloquent Realtionship

Thought I'd ask this as Laravel is the most elegant Framework I've come across and wondered if there was a "prettier way" of doing this.
I have a system which records books such that:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
}
In the system there are number of other models which extend from "Book" such as "Novel", "Biography" etc. Is there a way for Eloquent to provide me with a correctly cast object given the right info (i.e. a namespaced class)? Currently, I am obtaining the book and the casting it using the function at https://gist.github.com/borzilleri/960035 which works but doesn't feel very "tidy".
I can see a few different options here. One would be to write your class like this:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
public function biography()
{
return $this->belongsTo('\App\Biography')->where('type', 'biography');
}
public function novel()
{
return $this->belongsTo('\App\Novel')->where('type', 'novel');
}
}
You'd then need to know ahead of time which type of book it is though. Another would be to do something like this:
class Chapter extends Model
{
protected function parent_book()
{
return $this->belongsTo('\App\Book');
}
public function getBookAttribute()
{
$book = $this->parent_book;
if (!$book) return $book; // No related book.
if ($book->type == 'novel') return (Novel)$book;
if ($book->type == 'biography') return (Biography)$book;
return $book;
}
}
You still have to do all of the casting yourself, but at least it's all in one place and transparent to the rest of the app, as it can still just reference $chapter->book For this second solution, if you ever set $chapter->book = new Book(), you'd also need to make sure to make a setBookAttribute() function.
One more complicated possibility would be to create your own custom relationship type by extending the BelongsTo class and overriding getResults() to to the casting before returning the result. This would be pretty transparent from the outside and would let you still call $chapter->book() and treat it as a relationship.
This should be attributed to Joshua Dwire as he set me on the path to this solution. I was intrigued by his reference to extending the standard BelongsTo class and make it work for me. Ideally I want to be able to call a custom relationship:
$this->belongsToBook('\App\Book');
And for that function to return a correctly cast object.
Routing through the code I found that it was the trait HasRelationship used by Model which was responsible for returning the relationship. By changing that relationship we can change the implementation and therefore the returned object.
I also wanted to replicate the same methodology that Laravel employs so have mimiced it in my own app.
With all that in mind the first step is to create a new trait HasBookRelationship which can be used in a model to handle the call to $this->belongsToBook('\App\Book'):
trait HasBookRelationship
{
public function belongsToBook($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}
$instance = $this->newRelatedInstance($related);
if (is_null($foreignKey)) {
$foreignKey = \Str::snake($relation).'_'.$instance->getKeyName();
}
$ownerKey = $ownerKey ?: $instance->getKeyName();
//We change the return relationship here
**return new BelongsToBook(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);**
}
}
This is simply copied from the existing belongsTo method in the HasRelationships trait. The key thing here is that we are going to return a custom relationship BelongsToBook and use that to override what is returned. The last line of the method is changed to return our desired relationship class.
The class we use is extended from BelongsTo but we change the get method to cast the object before returning it.
class BelongsToBook extends BelongsTo
{
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
{
parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName);
}
public function get($columns = ['*'])
{
$objs = $this->query->get($columns);
//iterate over the collated objects...
$objs->transform(function($item)
{
//..and return a cast object with whatever method you want
return castTheCorrectObject($item);
});
return $objs;
}
}
castTheCorrectObject can be any casting function you like perhaps set up as a helper or another method in the relationship.
Once these are set up, we can empoy it in our own Model:
class Author extends Model
{
use HasBookRelationship;
public function books()
{
return $this->belongsToBook('\App\Book');
}
}
This will return a collection of correctly cast objects and maintains the relationship.
One thing did puzzle me though. The method I overrode in my BelongsToBook class was get() and not getResults() as suggested by Joshua. get() is defined in Relation and is inherited by BelongsTo where as getResults() is defined in BelongsTo. I'm not sure what the difference between getResults() and get() is nor why I had to override get() rather than getResults(). If anyone can shed any light , it would be appreciated.

Laravel get related models of related models

I have a RepairRequest model, which is associated with a Vehicle.
class RepairRequest extends \Eloquent {
public function vehicle() {
return $this->belongsTo('Vehicle');
}
}
class Vehicle extends \Eloquent {
public function requests() {
return $this->hasMany('RepairRequest');
}
}
I would like to get all RepairRequests for the vehicle associated with a given RepairRequest, so I do
return RepairRequests::find($id)->vehicle->requests;
This works fine.
However, RepairRequests have RepairItems:
// RepairRequest class
public function repairItems() {
return $this->hasMany('RepairItem', 'request_id');
}
// RepairItem class
public function request() {
return $this->belongsTo('RepairRequest', 'request_id');
}
which I would like to return too, so I do
return RepairRequests::find($id)->vehicle->requests->with('repairItems');
but I get the following exception:
Call to undefined method Illuminate\Database\Eloquent\Collection::with()
How can I write this so that the returned json includes the RepairItems in the RepairRequest json?
Load related models using load method on the Collection:
return RepairRequests::find($id)->vehicle->requests->load('repairItems');
which is basically the same as:
$repairRequest = RepairRequests::with('vehicle.requests.repairItems')->find($id);
return $repairRequest->vehicle->requests;
I'd suggest eager loading everything.
return RepairRequests::with('vehicle.requests.repaireItems')->find($id);

Laravel: Pass Parameter to Relationship Function?

Is it possible to pass, somehow, a parameter to a relationship function?
I have currently the following:
public function achievements()
{
return $this->belongsToMany('Achievable', 'user_achievements')->withPivot('value', 'unlocked_at')->orderBy('pivot_unlocked_at', 'desc');
}
The problem is that, in some cases, it does not fetch the unlocked_at column and it returns an error.
I have tried to do something like:
public function achievements($orderBy = true)
{
$result = $this->belongsToMany (...)
if($orderBy) return $result->orderBy(...)
return $result;
}
And call it as:
$member->achievements(false)->(...)
But this does not work. Is there a way to pass parameters into that function or any way to check if the pivot_unlocked_at is being used?
Well what I've did was just adding new attribute to my model and then add the my condition to that attirbute,simply did this.
Class Foo extends Eloquent {
protected $strSlug;
public function Relations(){
return $this->belongsTo('Relation','relation_id')->whereSlug($this->strSlug);
}
}
Class FooController extends BaseController {
private $objFoo;
public function __construct(Foo $foo){
$this->objFoo = $foo
}
public function getPage($strSlug){
$this->objFoo->strSlug = $strSlug;
$arrData = Foo::with('Relations')->get();
//some other stuff,page render,etc....
}
}
You can simply create a scope and then when necessary add it to a builder instance.
Example:
User.php
public function achievements()
{
return $this->hasMany(Achievement::class);
}
Achievement.php
public function scopeOrdered(Builder $builder)
{
return $builder->orderBy(conditions);
}
then when using:
//returns unordered collection
$user->achievements()->get();
//returns ordered collection
$user->achievements()->ordered()->get();
You can read more about scopes at Eloquent documentation.
You can do more simple, and secure:
When you call the relation function with the parentesis Laravel will return just the query, you will need to add the get() or first() to retrieve the results
public function achievements($orderBy = true)
{
if($orderBy)
$this->belongsToMany(...)->orderBy(...)->get();
else
return $this->belongsToMany(...)->get();
}
And then you can call it like:
$member->achievements(false);
Works for the latest version of Laravel.
Had to solve this another was as on Laravel 5.3 none of the other solutions worked for me. Here goes:
Instantiate a model:
$foo = new Foo();
Set the new attribute
$foo->setAttribute('orderBy',true);
Then use the setModel method when querying the data
Foo::setModel($foo)->where(...)
This will all you to access the attribute from the relations method
public function achievements()
{
if($this->orderBy)
$this->belongsToMany(...)->orderBy(...)->get();
else
return $this->belongsToMany(...)->get();
}

Categories