Best practice for making a custom query in a model - php

I have a model named "Rate" and a related controller named "RateController".
if want to make a custom query in Rate model, we could use local scope or static function like these:
using static function in the Rate model:
public static function avgerage($type, $id)
{
return static::where('rateble_type', $type)
->where('rateble_id', $id)
->avg('star');
}
using local scope in Rate model:
public function scopeAverageRate($type, $id, $query)
{
return $query->where('rateble_type', $type)
->where('rateble_id', $id)
->avg('star');
}
and finally, we call one of the above methods in the controller.
The question is, which one is the best practice?
using a local scope or defining a static function in the model?
Thank you.

To elaborate further, if you instantiate a Model and are using a method on it and don't care about the state of that particular instance, you should use static methods. The number of times this situation is actually applicable is pretty rare. Most of the time it will be non-static, so most of the time you'll be using local scope.
When creating a query, ask yourself does it relate to the behavior of the Model? If so, then use local scope. If it relates to the concept of the Model, then use static.

Don't use static methods if you need the query builder.
Static methods are great for making complete queries and returning the results.
More information about Static methods on Eloquent model It will help.

Since controller should be only responsible for handling the request and sending the response, my suggestion is to avoid making them "fat" with query implementations (this is model layer responsibility).
I would do the following:
class MyModel
{
public function average($type, $id)
{
// query implementation
}
}
class MyController
{
public function index($type, $id)
{
$model = new MyModel();
$average = $model->average($type, $id);
// return response
}
}

Related

Why is it better to mock repository class than Eloquent models?

I know what dependency injection is and why to use it (it's better to inject class than to use new keyword because it's kind of makes impossible to test it later). I'm going to talk about Laravel's ways of how to write controllers.
Way 1)
Let's say I have the following function in controller and I want to test it.
public function index() {
$allActors = \App\Actor::all();
foreach($allActors as $actor){
$actor->name="gio";
}
return $allActors;
}
As you see, controller and model is tightly coupled. My question is why I can't test this method? I looked through Laravel's mockery and I think it could be tested. What I'd do is before making a call to index method, I'd mock the \App\Actor and what it should return. Why shouldn't I follow this idea?
Way 2)
protected $actor;
public function __construct(\App\Actor $actor){
$this->actor = $actor;
}
public function index() {
$allActors = $this->actor->all();
foreach($allActors as $actor){
$actor->name="gio";
}
return $allActors;
}
Now you think this could be tested? how? One thing I can think of is before creating new controller, I'd create new class that extends Actor model and override its functions, in this case all() function and get the results from file.
The main question is why can't I test it in the first example and in the second example? Which one is better for DI? It seems like the second option is DI, but I looked through Laravel's docs and I think the first example can be mocked still. But I also read that Eloquent models can't be mocked. I am seeking a whole idea about all of this.

Using a query scope in a collection laravel

My Association model looks like this (irrelevant code redacted):
class Association extends Model
{
public function members() {
return $this->hasMany('App\Member');
}
}
My Member model looks like this:
class Member extends Model
{
public function scopeActive($query) {
return $query->where('membership_ended_at', Null);
}
public function scopeInactive($query) {
return $query->whereNotNull('membership_ended_at');
}
}
This is what I want to be able to do:
$association = Association::find(49);
$association->members->active()->count();
Now, I'm aware there's a difference between a Query and a Collection. But what I'm basically asking is if there's some kind of similar scope for collections. Of course, the optimal solution would be to not have to write TWO active methods, but use one for both purposes.
(question already answered in the comments, but might as well write a proper answer)
It is not possible to use a query scope in a Colletion, since query scope is a concept used in Eloquent to add constraints to a database query while Collections are just a collection of things (data, objects, etc).
In your case, what you need to do is to change this line:
$association->members->active()->count();
to:
$association->members()->active()->count();
This works because when we call members as a method, we are getting a QueryBuilder instance, and with that we can start chaining scopes to the query before calling the count method.

Why use static method in PHP's laravel model class?

In PHP laravel, we have codes like
$user = User::find(1);
var_dump($user->name);
I am not concerning how to use the find method, I am concerning why laravel use a static method? Shouldn't the use of static method make the method hard to test?
Will it be better if they designed using singleton?
e.g.
$user = User::getInstance()->find(1);
var_dump($user->name);
In fact, your example is very similar to what Laravel does behind the scene. When you do User::find(), you are actually asking for a new instance, either an instance of Collection or a QueryBuilder.
Illuminate\Database\Eloquent\Model (reference):
public static function find($id, $columns = array('*'))
{
if (is_array($id) && empty($id)) return new Collection;
$instance = new static;
return $instance->newQuery()->find($id, $columns);
}
As a side note, you'll also see another way of using static methods in Laravel e.g. Input::get(). These are called Facades.
Facades provide a "static" interface to classes that are available in the application's IoC container ... Laravel "facades" serve as "static proxies" to underlying classes in the IoC container, providing the benefit of a terse, expressive syntax while maintaining more testability and flexibility than traditional static methods.
When a user references any static method on the ... facade, Laravel resolves the cache binding from the IoC container and runs the requested method (in this case, get) against that object.
You can read more about Larave's Facades at: http://laravel.com/docs/facades
Unnawut has a very good answer, however I felt it necessary to add in further explanation.
In your example
$user = User::find(1);
var_dump($user->name);
Laravel isn't using a static method, you are. Another way to do this which you are probably looking for is to use dependency injection, which Laravel makes very easy because it can be done automatically. So in whatever class you are using your User model in, you should be setting up something like this in the constructor...
public function __construct(User $user)
{
$this->user = $user;
}
And then you can modify your code to not use the static bindings.
$user = $this->user->find(1);
var_dump($user->name);
This would restrict the system from only having one User. Whilst the find method may be static, the User class will have other methods and properties that aren't, a likely example is in your example: $user->name
A method that does not rely upon any instance variables, I.e variables who's value is specific to the particular object instance, but instead provides generic functionality that applies to all instances, can, and probably should, be static. This is why the $this operator is illegal within static methods, as it can make no reference to a particular object instance.
#unnawut reference link to the source code of Model.php is no longer using static find function as the class has been refactored. The function that get called eventually is in Builder.php (source code),
The logic still same
- magic function `__callStatic` of Model class get called
- a new instance of Builder is created and return
- call the find method of the Builder instance
Model.php
public function newEloquentBuilder($query)
{
return new Builder($query);
}
Builder.php
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
return $this->whereKey($id)->first($columns);
}

Why I'm getting 'Non-static method should not be called statically' when invoking a method in a Eloquent model?

Im trying to load my model in my controller and tried this:
return Post::getAll();
got the error Non-static method Post::getAll() should not be called statically, assuming $this from incompatible context
The function in the model looks like this:
public function getAll()
{
return $posts = $this->all()->take(2)->get();
}
What's the correct way to load the model in a controller and then return it's contents?
You defined your method as non-static and you are trying to invoke it as static. That said...
1.if you want to invoke a static method, you should use the :: and define your method as static.
// Defining a static method in a Foo class.
public static function getAll() { /* code */ }
// Invoking that static method
Foo::getAll();
2.otherwise, if you want to invoke an instance method you should instance your class, use ->.
// Defining a non-static method in a Foo class.
public function getAll() { /* code */ }
// Invoking that non-static method.
$foo = new Foo();
$foo->getAll();
Note: In Laravel, almost all Eloquent methods return an instance of your model, allowing you to chain methods as shown below:
$foos = Foo::all()->take(10)->get();
In that code we are statically calling the all method via Facade. After that, all other methods are being called as instance methods.
Why not try adding Scope? Scope is a very good feature of Eloquent.
class User extends Eloquent {
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
public function scopeWomen($query)
{
return $query->whereGender('W');
}
}
$users = User::popular()->women()->orderBy('created_at')->get();
Eloquent #scopes in Laravel Docs
TL;DR. You can get around this by expressing your queries as MyModel::query()->find(10); instead of MyModel::find(10);.
To the best of my knowledge, starting PhpStorm 2017.2 code inspection fails for methods such as MyModel::where(), MyModel::find(), etc (check this thread), and this could get quite annoying.
One (elegant) way to get around this is to explicitly call ::query() wherever it makes sense to. This will let you benefit from free auto-completion and a nice formatting/indentating for your queries.
Examples
BAD
Snippet where inspection complains about static method calls
// static call complaint
$myModel = MyModel::find(10);
// another poorly formatted query with code inspection complaints
$myFilteredModels = MyModel::where('is_foo', true)
->where('is_bar', false)
->get();
GOOD
Well formatted code with no complaints
// no complaint
$myModel = MyModel::query()->find(10);
// a nicely formatted and indented query with no complaints
$myFilteredModels = MyModel::query()
->where('is_foo', true)
->where('is_bar', false)
->get();
Just in case this helps someone, I was getting this error because I completely missed the stated fact that the scope prefix must not be used when calling a local scope. So if you defined a local scope in your model like this:
public function scopeRecentFirst($query)
{
return $query->orderBy('updated_at', 'desc');
}
You should call it like:
$CurrentUsers = \App\Models\Users::recentFirst()->get();
Note that the prefix scope is not present in the call.
Solution to the original question
You called a non-static method statically. To make a public function static in the model, would look like this:
public static function {
}
In General:
Post::get()
In this particular instance:
Post::take(2)->get()
One thing to be careful of, when defining relationships and scope, that I had an issue with that caused a 'non-static method should not be called statically' error is when they are named the same, for example:
public function category(){
return $this->belongsTo('App\Category');
}
public function scopeCategory(){
return $query->where('category', 1);
}
When I do the following, I get the non-static error:
Event::category()->get();
The issue, is that Laravel is using my relationship method called category, rather than my category scope (scopeCategory). This can be resolved by renaming the scope or the relationship. I chose to rename the relationship:
public function cat(){
return $this->belongsTo('App\Category', 'category_id');
}
Please observe that I defined the foreign key (category_id) because otherwise Laravel would have looked for cat_id instead, and it wouldn't have found it, as I had defined it as category_id in the database.
You can give like this
public static function getAll()
{
return $posts = $this->all()->take(2)->get();
}
And when you call statically inside your controller function also..
I've literally just arrived at the answer in my case.
I'm creating a system that has implemented a create method, so I was getting this actual error because I was accessing the overridden version not the one from Eloquent.
Hope that help?
Check if you do not have declared the method getAll() in the model. That causes the controller to think that you are calling a non-static method.
For use the syntax like return Post::getAll(); you should have a magic function __callStatic in your class where handle all static calls:
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}

Class methods and instance methods in codeigniter, how can I use them?

I am very new to codeigniter but understand OOP and MVC as I do a lot of Rails development. One thing I haven't figured out yet is how to write a class level method in codeigniter and access it in a controller. For example, I have
<?php
class User_model extends Model {
function user_model()
{
parent::Model();
}
public static function get_total_users_count(){
$results = $this->db->query("SELECT * FROM bhr_users GROUP BY userid");
if($results){
return $results->num_rows();
}
return 0;
}
}
?>
I think what I have done here is established a class level method for my model that I should be able to call with User_model::get_total_users_count() Now in my controller which a previous programmer called "Welcome" I have something like:
<?php
class Welcome extends Controller {
function Welcome()
{
parent::Controller();
$this->load->model('bhr_model');
$this->load->model('user_model');
}
function index()
{
$invite = $this->uri->segment(3);
if($invite == 'invitefriends') {
$pagedata['invitefriends'] = $invite;
} else {
$pagedata['invitefriends'] = '';
}
$pagedata['numberofpeople'] = User_model::get_total_users_count();
$this->load->view('default_page', $pagedata);
}
}
The above method call to get_total_users_count does not work because it says because I am using the db method on a class level function in get_total_users_count. In other words $this has no db method when I reference a class.
So now my question is a bit more theoretical. I always thought that instance methods should only be used when a method is acting on a specific instance of an class. Makes sense, right? However, get_total_users_count is acting on all "users" and counting them. It just seems like that should be a class level method. Do you agree? If do, do you know how I can access the database from withing the framework inside a class level function?
Thanks!
Since you are not instantiating User_model, you must get the CI instance, then use that for your db queries.
Inside get_total_users_count():
$ci_ins =& get_instance();
$ci_ins->db->query();
You can make your class as a helper so it will not be load as a instance. Only the code will be included so you can just call it as:
$sample = class_name::method();
CodeIgnighter works is by instantiating your models as you load them. What Thorpe Obazee said is the correct codeIgnighter way to use your Model.
What you are asking is if you can use a static method as you'd expect in most circumstances, which just isn't how CI works.
To accomplish what you're after, mives points out get_instance() which is the correct way to get at the main CI object. I use that way myself to do what you're doing.
get_total_user_count is more of a function for a user table gateway.
User model should have things like getUsername and getLastLogin.
User Table Gateway should have things like findUserById, createNewUser, and getTotalUserCount
$pagedata['numberofpeople'] = $this->user_model->get_total_users_count();
That's the CI way.

Categories