I came across this code snippet on Laravel doc
// Retrieve a model by its primary key...
$flight = App\Flight::find(1);
// Retrieve the first model matching the query constraints...
$flight = App\Flight::where('active', 1)->first();
where and find are builder functions, why App\Flight as a Model can call these function. And what are the differences between Model, Builder and Collection in Laravel?
You're able to call Builder functions on an Eloquent model, because the Model class uses the magic __call method.
As you can see in the method definition below, if the method doesn't exist on the class, or it's not increment or decrement, a new Builder query is created, on which the method is called.
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
try {
return $this->newQuery()->$method(...$parameters);
} catch (BadMethodCallException $e) {
throw new BadMethodCallException(
sprintf('Call to undefined method %s::%s()', get_class($this), $method)
);
}
}
https://github.com/illuminate/database/blob/master/Eloquent/Model.php#L1439
As for the difference between a Model, Builder and Collection:
Model: This follows the pattern where a model is essentially an instance of a database row, allowing you to create, update and delete a single row.
Builder: This is a layer of abstraction between your application and the database. Typically it's used to provide a common API for you to build platform agnostic database queries. E.g. they'll work on MySQL, Postgres, SQL Server, etc.
Collection: This is basically an array on steroids. It provides a chainable API for standard array_* type PHP functions, together with other useful functions for manipulating a collection of data.
Related
My application supports fetching data with filters. My current implementation (which works fine) is
Model::select($fields)->with($relations)->tap(function ($query) use ($filters) {
// A lot of filtering logic here
// $query->where()......
})->get();
However, I would like to move the filtering logic directly into the Model so I could just do
Model::select($fields)
->with($relations)
->applyFilters($filters)
->get();
I have tried to add a filter method to the Model but at that point I'm working with a Builder and it does not recognize my function:
Call to undefined method Illuminate\Database\Eloquent\Builder::applyFilters()
Is there an easier way to do this, other than creating a new builder class and use that?
I figured it out! I just had to add a scopeApplyFilters to my Model class. It injects the Builder as the first parameter automatically, so the logic ends up looking like
public function scopeApplyFilters($query, $filters)
{
// Perform filtering logic with $query->where(...);
return $query;
}
Then I can just call it with Model::applyFilters($filters);
How do I alter the query builder everytime I'm getting a new model?
I've found that overriding the method below works, but I'm not feeling good doing this. Is there a better way to do this?
So I want ->join() to be executed everytime for a specific model.
!! I don't want to use the protected $with = []; property, cause I don't want extra queries to be executed when not necessary.
public function newQueryWithoutScopes()
{
$builder = $this->newEloquentBuilder(
$this->newBaseQueryBuilder()
);
// Once we have the query builders, we will set the model instances so the
// builder can easily access any information it may need from the model
// while it is constructing and executing various queries against it.
return $builder->setModel($this)->join('join statement comes here')->with($this->with);
}
To affect the query every time a model is used, you can use the newQuery method.
public function newQuery($excludeDeleted = true){
return parent::newQuery()->join('orders', 'users.id', '=', 'orders.user_id');
}
But this will probably break Eloquent since your Model isn't representative of your database model anymore; it is now a Frankenmodel of two separate models.
I am trying to validate a model before saving it. Obviously if the model isn't valid, it shouldn't be saved to the database. When validation fails, it throws an exception and doesn't continue to save. This below works:
$question = new Question([...]);
$question->validate();
$question->save();
I have a problem with the answers() hasMany relationship. According to this answer I should be able to call add() on the relation object:
$question = new Question([...]);
$question->answers()->add(new Answer([...]));
$question->validate();
$question->save();
The above fails:
Call to undefined method Illuminate\Database\Query\Builder::add()
I thought the answers() function would return a HasMany relationship object, but it looks like I'm getting a builder instead. Why?
answers() does return a HasMany object. However, because there is no add method on a HasMany object, Laravel resorts to PHP's __call magic method.
public function __call($method, $parameters)
{
$result = call_user_func_array([$this->query, $method], $parameters);
if ($result === $this->query) {
return $this;
}
return $result;
}
The __call method gets the query instance and tries to call the add method on it. There is no add method on the query builder though, which is why you are getting that message.
Finally, the add method is part of Laravel's Eloquent Collection, not a part of HasMany. In order to get the Collection class, you need to drop the parenthesis (as shown in the answer provided in your link) and do this instead:
$question->answers->add(new Answer([...]));
There's no add method. Use the save method:
$question->answers()->save(new Answer([]));
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.
Our current ORM solution uses Data Mappers to represent tables / views in the database which then return a Collection object that can be used to iterate through the retrieved records as Model objects. Between the Data Mapper and Model layers is a Repository layer that handles domain requests to the data mappers and returns the corresponding collections or domain objects.
We are currently looking at refactoring the responsibilities of the Repository and Data Mapper layers so that all application requests to the Data Mapper layer are routed through the Repository and the Data Mapper returns the retrieved data rows to the Repository which then returns the necessary collection to the requesting object.
What I'm wondering is whether it is valid / good practice to pass the entire Repository object into the corresponding Data Mapper so that we can enforce access to the Data Mappers only through the Repository layer.
As an example this is basically how it works now:
class DataMapper {
public function findAll(Criteria $criteria)
{
$select = $criteria->getSelect();
// Build specific select statement
$rows = $this->_fetchAll($select);
return new Collection(array('data' => $rows, 'mapper' => get_class($this)));
}
}
I thinking of doing something like this:
class Repository {
public function findAllByName(Model $model)
{
$this->_criteria->addCondition('name LIKE ?', $model->name);
$rows = $this->_mapper->findAll($this);
return new Collection(array('data' => $rows, 'repository' => get_class($this)));
}
}
class DataMapper {
public function findAll(Repository $repository)
{
$select = $repository->getCriteria()->getSelect();
// Build specific select statement
$rows = $this->_fetchAll($select);
return $rows;
}
}
And then in this version, the Collection object would issue a call to the Repository which could first search through its cached objects and then only issue a call to the database to load the record if its needed.
Chris has a valid suggestion.
It partially depends on the context of the code, but dependency injecting the repository into the DataMapper instances you create, i.e:
$repo = new Repository();
$mapper = new DataMapper($repo);
Will spare you from subsequently having to pass that $repo around whenever you want to use findAll(). IE:
$mapper->findAll();
$mapper->findAllByName();
I find when parameters become a ubiquitous part of every function call I'm making, it makes sense to consider turning them into instance variables (especially when they're identical every time).
If your repository varies from between context/instances then the injection makes more sense. If you find that you're always creating one repo instance and would like to recycle it, a singleton might be appropriate.
The nice thing about Dependency Injection is it does clarify this dependency idea (ironic!). If you want to enforce it, you can do something like throw an exception if the $repo object is null, or not a Repository instance, in your __construct() method.
I would perhaps do this a little bit differently. I would add a setRepository(Repository $repos) method as well as a getRepository() method. Then, in your findAll method, call getRepository(). If setRepository() has not been called yet, then getRepository can return a default repository instance.
I would also perhaps create an interface for the Repository class so that different implementations of Repository can be used within the DataMapper class.
So the get method could look something like
public function getRepository()
{
if (!$this->_repository) {
$this->_repository = new Repository();
}
return $this->_repository;
}
and the set method could look something like
public function setRepository(RepositoryInterface $repos)
{
$this->_repository = $repos;
}