Laravel Specifying a condition for a morphMany collection - laravel

I have the following Eloquent relationship
class Broker extends Eloquent{
public function configurable(){
return $this->morphTo();
}
}
class ProductConfiguration extends Eloquent{
public function productConfigurations()
{
return $this->morphMany('Excel\Products\ProductConfiguration','configurable');
}
}
I can very easily find all the ProductConfigurations that belong to a Broker by doing this:
$broker = Broker::find($id);
$productConfigurations = $broker->productConfigurations;
What I am unclear about though is how to specify conditions for the ProductConfigurations, so if my ProductConfiguration has a type field, something like:
$broker = Broker::find($id);
$productConfigurations = $broker->productConfigurations->where('type' = 'reg');
Checking the documentation I can't exactly find how to do that.

Ok, must just have had a temporary brain freeze or something, it was as easy as this:
$broker = Broker::find($id);
$configurations = $broker->productConfigurations()
->where('type',$type)
->get();

You can do it another way,
Note that
morphMany(),hasOn(),hasMany()
and other relation methods are all returning an instance of
\Illuminate\Database\Eloquent\Relations\
That means you can use this code,
public function productConfigurations()
{
$relation= $this->morphMany('Excel\Products\ProductConfiguration','configurable');
**return $relation->->where('type',$type);**
}

Related

I want make array for eloquent relationship laravel

in Course model this relation are include
public function course_modules()
{
return $this->hasMany(CourseModule::class, 'course_id');
}
public function course_lessons()
{
return $this->hasMany(CourseLesson::class, 'course_id');
}
public function course_contents()
{
return $this->hasMany(CourseContent::class, 'course_id');
}
i want to make a array for hasMany relation like
$hasMany=[
CourseModule::class,
CourseLesson::class
]
I wanted to do this for fun, turned out pretty difficult, but here you go, there are some requirements you need to make sure of, but it gets the job done, I will be using a mix of PHP & Laravel to accomplish this.
Step 1: Make sure your main class has proper return method types. So in your case.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Course extends Model
{
public function course_modules() : HasMany
{
return $this->hasMany(CourseModule::class, 'course_id');
}
public function course_lessons() : HasMany
{
return $this->hasMany(CourseLesson::class, 'course_id');
}
public function course_contents() : HasMany
{
return $this->hasMany(CourseContent::class, 'course_id');
}
}
Step 2: In your controller, you need to use ReflectionClass, would love if someone actually can improve this for learning purposes.
<?php
namespace App\Http\Controllers;
use ReflectionClass;
class CourseController extends Controller
{
public function test(){
//We will build a hasMany array
$hasMany = [];
//here we will use ReflectionClass on our primary class that we want to use.
$reflection = new ReflectionClass(new \App\Models\Course);
//Lets loop thru the methods available (300+ i don't like this part)
foreach($reflection->getMethods() as $method){
//if the method return type is HasMany
if($method->getReturnType() != null && $method->getReturnType()->getName() == 'Illuminate\Database\Eloquent\Relations\HasMany'){
//we grab the method name
$methodName = $method->getName();
//then we finally check for the relatedClass name and add to the array
array_push($hasMany, get_class(($instance = new Course)->$methodName()->getRelated()));
}
}
//lets dump to see the results
dd($hasMany);
}
Results: an array of the classes :D
array:2 [▼
0 => "App\Models\ProgramTest",
1 => "App\Models\ProgramAnotherTest"
]
According to syntax, we are not able do this in Laravel. However, you can use an model mutors to solve this issue.
public function getCourseDetailsAttribute(){
$arr=[
"course_modules"=>$this->course_modules(),
"course_lessions"=>$this->course_lessons(),
];
return $arr;
}
In the Controller you can write like this,
$course=Course::find(1)->course_details;
For more details t;
https://laravel.com/docs/5.7/eloquent-mutators

Parent Controller class to call child overridden methods (Laravel)

I know this might seem anti pattern, and a lot will throw stones at me, but please hear me out.
I want to create a generic Controller to support many reference tables (mostly id, label). So I did something like this:
class GenericController extends Controller
{
public function index($modelName)
{
$x = '\\App\\Models\\'.$modelName;
$data = $model->all();
return view('generic.list', ['model'=>$model, 'data'=>$data]);
}
}
And this way my routes in web.php will be reduced to the minimum like this:
//List
Route::get('/{model}', function ($model) {
return App::call('\App\Http\Controllers\GenericController#index', ['modelName' => $model]);
});
It's working very well with simple CRUD actions like store, update, etc.. However I know I am over simplifying the design because sometimes I need to return a field from a joined table in the index list for example. That's where I am heading into a dead end, sort of.
My first thought was to create a controller for each model that inherits from the GenericController like this:
class CategoryController extends GenericController
{
}
And whenever I need to override the GenericController method, I would simply add it to the child class. However how can I do this from inside the GenericController (call a method in a sub class from parent class)? Because otherwise I will have to create routes for every single model which is against my wish.
So basically I am looking for something like this:
class GenericController extends Controller
{
public function index($modelName)
{
$x = '\\App\\Models\\'.$modelName;
//this thing I'm looking for is something like this:
//Check if we have CategoryController and it has a definition for index
//if yes do something like $data = CategoryController->index();
//otherwise just call $data = $model->all();
return view('generic.list', ['model'=>$model, 'data'=>$data]);
}
}
So I know this seems weird and anti-pattern, but other wise how can I create my generic routes and controller actions?
You are right, this is not really what is called "best practice". However, from a POO standpoint, it is an interesting question.
This what you can do:
class GenericController extends Controller
{
protected function getData(string $model)
{
return $model::all();
}
public function index($modelName)
{
$model = '\\App\\Models\\'.$modelName;
$data = $this->getData($model);
return view('generic.list', ['model'=>$model, 'data'=>$data]);
}
}
By default, the data will be retrieved "the simple way", using $data = $this->getData($model);.
However, if you make a CategoryController:
class CategoryController extends GenericController
{
protected function getData(string $model)
{
return Category::query()->with('something')->where('hello','world')->get();
}
}
You will just have to override the getData method inside your CategoryController.
This is the way to go if you want something clean. Of course, your categories routes will have to use this CategoryController instead of the GenericController.

Laravel hasMany with multiple constraints

I have query that I run on my ServiceController
return Service::with('contacts', 'locations.datetimes')->find($service->id);
This works great perfectly, but I need to change it. The relationships are defined as follows:
class Service extends Model
{
public function locations()
{
return $this->belongsToMany('App\Models\Service_location', 'service_location_service');
}
}
class Service_location extends Model
{
public function datetimes()
{
return $this->hasMany('App\Models\Service_detail');
}
}
I need a second constraint on the datetimes where I need the relationship to be along the lines of
return $this->hasMany('App\Models\Service_detail')->where('service_id', $service->id);
The problem is I can't find a way to pass through the $service_id. How do you handle two constraints on a hasMany relationship?
Let try this. Change Service_location to this
class Service_location extends Model
{
public function datetimes()
{
return $this->hasMany('App\Models\Service_detail');
}
public function scopeServiceId($query)
{
return $query->where('service_id', $service->id);
}
}
Now your query will be
Service::with(['contacts', 'locations.datetimes' =>function($q) use($serviceId){
$q->serviceId($serviceId);
}])->find($service->id);
There is no way to pass argument to a relationship function. If you do its highly likely that you will run into the N+1 query problem.

Limit Eloquent Model to specific columns

I'm pulling from a rather large database and for security reasons, my database user can only select a limited number of columns from the student table: name, graduation_date, and gender. But there are dozens of other columns returned in a select * statement.
In regular SQL, if I run something like:
SELECT * FROM students
will return an error on that table. Same if I run the eloquent model
Students::all();
will return an error as well.
I know in Eloquent, you can limit your selects when defining a relationship similar to:
class Students extends Eloquent {
protected $table = 'student_info';
public function classes() {
return $this->hasMany('classes')->select(array('room', 'time'));
}
}
So, My question is, can the select limits be done on the main model, similar to limiting it on the classes table. So, when I run Student::all(); it only selects the columns I need.
The main problem is every time I run a student Query, I'm having to do a specific select command each time instead of just saying "Student::all()". Same thing for Student::find(1); will also return an error, because it still runs a SELECT * FROM student_info WHERE id = 1.
I tried setting $visible variable, but it still renders sql equivalent to SELECT * FROM ...
Anyone have a solution?
UPDATE:
Please note that I'm looking or a solution on the model level, not the controller level. I can select from the controller side, but that defeats the purpose of a Model concept and have to declare the columns to select at every query.
Thanks!
Troy
You can create an intermediate class and overload the all() function. Let's call this class Elegant
Elegant.php
abstract class Elegant extends Model
{
public static $returnable = [];
public function all()
{
return $this->get(static::$returnable)->all();
}
}
Then you extend this class, and define your returnable columns to it.
Student.php
<?php
class Student extends Elegant
{
public static $returnable = ['room', 'time'];
}
Now use it as you wanted: Student::all() in your controller. If you leave returnable as an empty array, then you will get everything.
Expanding on Jarek's suggestion of using Global Scope you could do it like this, I'm not 100% sure the remove part is right though, will need testing.
SelectLimitTrait.php
trait SelectLimitTrait {
public static function bootSelectLimitTrait()
{
static::addGlobalScope(new SelectLimitScope);
}
public function getQueryable()
{
if(! $this->queryable ) return array('*');
return $this->queryable;
}
}
SelectLimitScope.php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ScopeInterface;
class SelectLimitScope implements ScopeInterface {
public function apply(Builder $builder)
{
$query = $builder->getQuery();
$queryable = $builder->getModel()->getQueryable();
$query->columns = $queryable;
}
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$query->columns = null;
}
}
And then in your Eloquent model put this:
Students.php
class Students extends \Eloquent {
use SelectLimitTrait;
protected $queryable = array('name','graduation_date', 'gender');
}
Now Students::all() and Students::find(1) etc. are limited to querying name, graduation_date and gender
You may use something like this:
public function newQuery()
{
return parent::newQuery()->select('room', 'time');
}
Put the newQuery method in your Students model and use the Student model normally you would use. It's a hacky way but easiest one. Just override the parent::query(). In this way you'll always get the selected fields.

Laravel 4 - Model properties' names different than database columns

I have one question, that seems to be logical, but I can't find answer for it.
Let's say I have Model Task:
class Task extends Eloquent {
protected $fillable = array('is_done');
}
So, I have one property is_done, but when working on frontend and backend part of application, I would like to have isDone as model property.
Is there a way to say it to framework, to somehow repack it for me? So that I am able to use isDone, throughout application, and that Model takes care of converting it to is_done, when it comes to saving/updating.
This would help me, so I don't have to think about names specified in database (like when using alias in traditional SQL clauses).
Is this possible at all? Does it make sense?
To prevent writing a getter/setter methods for every single attribute of the model, you can override the magic methods from the Eloquent class to access them in camelCase style:
class Model extends Eloquent {
public function __get($key)
{
$snake_key = snake_case($key);
return parent::__get($snake_key);
}
public function __set($key, $value)
{
$snake_key = snake_case($key);
parent::__set($snake_key, $value);
}
public function __isset($key)
{
$snake_key = snake_case($key);
return parent::__isset($snake_key);
}
public function __unset($key)
{
$snake_key = snake_case($key);
parent::__unset($snake_key);
}
}
Would a getter method for your attribute help you? If yes:
<?php
class Task extends Eloquent {
public function isDone()
{
return $this->getAttribute('is_done');
}
}
If not, and you really need to access $Task->isDone: try to overwrite the $key in magic _get() method for $key == 'isDone' (and maybe other attributes) and return the parent::_get() with $key:
<?php
class Task extends Eloquent {
public function __get($key)
{
if($key == 'isDone')
$key = 'is_done';
return parent::__get($key);
}
}
And perhaps, your Eloquent needs an attribute mapper for the attribute magic methods ;)

Resources