Eloquent Relationship with a conditional scope - php

I'm working on a Fantasy sports app. The models I am working with are FantasyPlayer, PlayerGame, TeamGame
FantasyPlayer can have many PlayerGame and can have many TeamGame
public function PlayerGame()
{
return $this->hasMany('App\Models\PlayerGame','player_id','player_id');
}
public function TeamGame()
{
return $this->hasMany('App\Models\FantasyData\TeamGame','team','fantasy_player_key');
}
When I load the data I use eager loading currently:
FantasyPlayer::with(['PlayerGame', 'TeamGame'])->take(1)->get();
It is becoming tedious to load both relationships and then which are loaded. Ideally, I want to have the model handle this logic for. So I would be able to do something like this:
FantasyPlayer::with(['FantasyGame'])->take(1)->get();
Then my FantasyGame scope would contain either the PlayerGame or the TeamGame record I need based on an a FantasyPlayer value for the position. Something like this is what I want... but it doesn't work for me:
public function scopeFantasyGame($query)
{
if($this->position == "DEF"){
return $this->TeamGame();
}
else{
return $this->PlayerGame();
}
}
Does anyone know way I could use eager loading and have the FantasyGame return the correct relationship based on a FantasyPlayer position attribute?:
FantasyPlayer::with(['FantasyGame'])->take(1)->get();

#1
You can't eager load relationships conditionally based on the result elements, this because the eager loading happens before you retrieve the records, that's why this won't work:
# FantasyPlayer.php
public function scopeFantasyGame($query)
{
if($this->position == "DEF") // <--- Laravel doens't retrieve the records yet,
{ // so this won't work
//
}
else
{
//
}
}
#2
Local Query scopes are used to constraint queries, in your case you want to load relationships with this scope, not its general purpose but sure you could do it:
# FantasyPlayer.php
public function scopeFantasyGame($query)
{
return $query->with(['PlayerGame', 'TeamGame']);
}
Then use it like this:
# YourController.php
public function myFunction($query)
{
$fantasyPlayers = FantasyPlayer::fantasyGame()->get();
}
#3
But then, if you want to always eager load the relationships, why use a query scope and not just tell laravel to load your desired relationships by default? You could do specify it in your model (check the Eager Loading By Default of this section of the docs):
# FantasyPlayer.php
protected $with = ['PlayerGame', 'TeamGame'];
Update
If you want to retrieve elements that always have a given relationship, you have two paths. For the first one, you could use a query scope to only load those elements:
# FantasyPlayer.php
public function scopeHasFantasyGame($query)
{
return $query
->has('PlayerGame')
->has('TeamGame');
}
Then:
# YourController.php
public function myFunction($query)
{
$fantasyPlayers = FantasyPlayer::hasFantasyGame()->get();
}
The second option would be to retrieve the elements, then filter the collection based on the existence of the relationship (using the Map() function):
# YourController.php
public function myFunction($query)
{
$fantasyPlayers = FantasyPlayer::all()
->map(function ($fantasyPlayer) {
return $fantasyPlayer->PlayerGame()->exists()
&& $fantasyPlayer->TeamGame()->exists();
});
}

Related

Dynamic model filter in Laravel's Eloquent

I'm looking for a way to make a dynamic & global model filter in Laravel.
I'm imagining a function like the following in my User.php model:
public function filter() {
return ($someVariable === true);
}
Whenever I do a query using Eloquent's query builder, I only want users to show up in the collection when the filter above returns true. I would have thought a feature like that existed, but a quick look at the documentation suggests otherwise. Or did I miss it?
I believe what you're looking for is Query Scopes.
They are methods that may be defined in a global or local context, that mutate the current query for a given model.
https://laravel.com/docs/5.5/eloquent#query-scopes
For example:
Lets say I have a database table called "Teams" and it has a column on it called "Wins." If I wanted to retrieve all Teams that had a number of Wins between Aand B I could write the following Local scope method on the teams model:
public function scopeWinsBetween($query, int $min, int $max)
{
return $query->whereBetween('wins', $min, $max);
}
And it could be invoked as such:
$teams = Teams::winsBetween(50, 100)->get();
I think you could use Collection macro but you will need to suffix all your eloquent get(); to get()->userDynamicFilter();
Collection::macro('userDynamicFilter', function () {
//$expected = ...
return $this->filter(function ($value) use($expected) {
return $value == $expected;
});
});
Thanks. For now I've simply added a post filter option to the models using the following code:
// Apply a post filter on the model collection
$data = $data->filter(function($modelObject) {
return (method_exists($modelObject, 'postFilter')) ? $modelObject->postFilter($modelObject) : true;
});
in Illuminate/Database/Eloquent/Builder.php's get() function, after creating the collection. This allows me to add a function postFilter($model) into my model which returns either true or false.
Probably not the cleanest solution but a working one for now.

Laravel Interface to multiple models

I'm writing a system around an existing database structure using Laravel 4.1. The current system is based around two websites which use their own table, a and b, both of which are identical. This is an unavoidable problem until we rewrite the other system.
I need to be able to query both tables at the same time using Eloquents Query Builder, so I may need to get a list of rows from both tables, or INSERT or UPDATE from either at any time.
Currently we have a model for both tables, but no way to link between them and implement the missing methods such as all or find.
Our thought is to have an Interface which will bind these results together, however we're not sure how to go about this at all.
<?php
interface HotelInterface {
public function all();
public function find();
}
use Illuminate\Database\Model;
class Hotel implements HotelInterface {
}
?>
Is all we have so far.
I asked on the Laravel forums and got the answer I was looking for! I'm reposting here incase.
What we're actually after is a Repository which would look like this:
<?php
class HotelRepository {
public $A;
public $B;
public function __construct(A $A, B $B) {
$this->A = $A;
$this->B = $B;
}
public function find($iso = NULL, $hotelid = NULL) {
$A = $B = NULL;
if($iso !== NULL) {
$A = $this->A->where('country', $iso);
$B = $this->B->where('country', $iso);
if($hotelid !== NULL) {
$A = $A->where('id', $hotelid);
$B = $B->where('id', $hotelid);
}
}
if($hotelid !== NULL) {
if($A->first()) {
return $A->first();
}
if($B->first()) {
return $B->first();
}
}else{
return $A->get()->merge($B->get());
}
}
public function all() {
$aCollection = $this->A->all();
$bCollection = $this->B->all();
return $aCollection->merge($bCollection);
}
}
Now in the controller where I want to call this, I just add:
<?php
class HomeController extends BaseController {
public function __construct(HotelRepository $hotels) {
$this->hotels = $hotels;
}
}
And I can now use $this->hotels to access the find and all method that I created.
If the tables are identical you should only need one model, the connection is the only thing that needs to change. Have a read here: http://fideloper.com/laravel-multiple-database-connections.
There's a Eloquent method on() for specifying the connection, here's an example:
The Eloquent example looks like what you need:
$results = Model::on('mysql')->find(1);
Add both connections to your database config and then change the on() part depending on which DB you need to query.
Update: misunderstood the question
If you only need to change the table and not the database, you can use setTable()
$model = new Model
$model->setTable('b');
$model->find(1);
Although that may get confusing.
Instead, you could also define a base model and then extend it with the only difference being the table protected $table = 'b';
You can always change the default database for the one you need. I use this Config::set('database.default', 'chronos');, where chronos is one of my databases. When I need to change to the "other", I just change de database name. You can call it wherever you want. I think that what you're looking for is just switch between the databases.
You need to have two different models, one for each table on each database, though.
Let me know if I got it wrong.

Laravel 4: How to apply a WHERE condition to all queries of an Eloquent class?

I'm trying to implement an "approved' state for a table I have, it's pretty straightforward, basically, if the row's approve column equals 1; that row should be retrieved, otherwise it shouldn't.
The problem is, now I have to go through the whole codebase and add a WHERE statement(i.e., function call) which is not only time consuming but also inefficient(if I ever want to remove that feature, etc.)
How can I do that? Is it as easy as adding $this->where(..) inside the Eloquent child class' constructor? Wouldn't that affect other CRUD operations? such as not updating an unapproved row?
The answer was given when there was no query scope feature available.
You can override the main query, only for the Post model, like
class Post extends Eloquent
{
protected static $_allowUnapprovedPosts = false;
public function newQuery()
{
$query = parent::newQuery();
if (!static::$_allowUnapprovedPosts) {
$query->where('approved', '=', 1);
} else {
static::$_allowUnapprovedPosts = false;
}
return $query;
}
// call this if you need unapproved posts as well
public static function allowUnapprovedPosts()
{
static::$_allowUnapprovedPosts = true;
return new static;
}
}
Now, simply use anything, but unapproved users won't appear in the result.
$approvedPosts = Post::where('title', 'like', '%Hello%');
Now, if you need to retrieve all posts even unapproved ones then you can use
$approvedPosts = Post::allowUnapprovedPosts()->where('title', 'like', '%Hello%');
Update (Using the query scope):
Since, Laravel now provides Global Query Scopes, leverage that instead of this hacky solution, notice the date of this answer, it's too old and so much things changed by now.
// Using a local query scope
class Post extends Eloquent
{
public function scopeApproved($query)
{
return $query->where('approved', 1);
}
}
You can use it like:
$approvedPosts = Post::approved()->get();
The closest thing I found is Eloquent query scope.
Even though it requires a minor change in my code(prefixing queries) it still gives me what I'm looking with great flexibility.
Here's an example:
Create a function within the Eloquent child class:
class Post extends Eloquent {
public function scopeApproved($query)
{
return $query->where('approved', '=', 1/*true*/);
}
}
Then simply use it like this:
$approvedPosts = Post::approved()-><whatever_queries_you_have_here>;
Works perfectly. No ugly repeated WHERE function calls. easy to modify. Much easier to read(approved() makes much more sense than where('approved', '=', 1) )
You can use global scope for your need, docs for that are here : https://laravel.com/docs/5.6/eloquent#query-scopes
Good example is SoftDeletingScope which is applied to all queries by default on models which use SoftDeletes trait.

Decorator case or not?

I have a contract "ArticleStorage" that every storage must be subscribe to be valid for model.
True, this is not the problem, my problem is: pagination ... or "results modification", in this case at fetchAll, i want modify its behavior but without adding parameters, etc
<?php
interface ArticleStorage
{
// public function insert();
// public function update();
// public function delete();
public function fetchAll();
}
class MySQLArticleStorage implements ArticleStorage
{
public function fetchAll()
{
// SELECT * FROM `articles`;
}
}
?>
How my model works.
class ArticlesModel
{
public function __construct(ArticleStorage $storage)
{
}
}
in this case, I expect a "ArticleStorage" but do not know which "Storage" was given, true ... and i want to paginate or apply a results modification, using the Storage.
class MySQLArticleResultsModifier
{
public function __construct(MySQLArticleStorage $storage)
{
}
public function fetchAll()
{
// ...
}
}
In case of a pagination, how i can modify ArticleStorage fetchAll and apply my modified query ?
Is there a case where your model demands that a fetchall on top of another fetchall is possible; I don't think so, infact this is how you decide if you need a decorator or not, by answering this question to yourself
Is the decorator function you are thinking of making works like a decoration{like a real decoration where you can put stars on your christmas tree {decoration1}, and some toys on your tree {decoration2} at the same instance? Otherwise there is no point in making a decorator pattern, The nature of decorator is to decorate the concrete implementations from outside world, and change the output, without being affected by the other decoration being applied to a concrete instance.
Now as to the current implementation, I think #mrhobo is quite right, your fetch function might look like
public function fetch($limit, $order,$sort)
A very smart fetch could also expect the user to send a hashtable of key-value , of the columnname = value of column by using which you can make your own select query on the fly.

Zend framework caching pagination results for different queries

Using Zend Paginator and the paginator cache works fine, but the same cached pages are returned for everything. Ie. I first look at a list of articles, when i go to view the categories the articles list is returned. How can I tell the paginator which result set i am looking for?
Also, how can I clear the paginated results without re-querying the paginator. Ie. I am updated a news article therefore the pagination needs to be cleared.
Thanks
Zend_Paginator uses two methods to define cache ID: _getCacheId and _getCacheInternalId. Second function is calculating cache ID based on two parameters: the number of items per page and special hash of the adapter object. The first function (_getCacheId) is calculating cache ID using result from _getCacheInternalId and current page.
So, if you are using two different paginator objects with 3 same internal parameters: adapter, current page number and the number of items per page, then your cache ID will be the same for these two objects.
So the only way I see is to define you own paginator class inherited from Zend_Paginator and to re-define one of these two internal functions to add a salt to cache ID.
Something like this:
class My_Paginator extends Zend_Paginator {
protected $_cacheSalt = '';
public static function factory($data, $adapter = self::INTERNAL_ADAPTER, array $prefixPaths = null) {
$paginator = parent::factory($data, $adapter, $prefixPaths);
return new self($paginator->getAdapter());
}
public function setCacheSalt($salt) {
$this->_cacheSalt = $salt;
return $this;
}
public function getCacheSalt() {
return $this->_cacheSalt;
}
protected function _getCacheId($page = null) {
$cacheSalt = $this->getCacheSalt();
if ($cacheSalt != '') {
$cacheSalt = '_' . $cacheSalt;
}
return parent::_getCacheId($page) . $cacheSalt;
}
}
$articlesPaginator = My_Paginator::factory($articlesSelect, 'DbSelect');
$articlesPaginator->setCacheSalt('articles');
$categoriesSelect = My_Paginator::factory($categoriesSelect, 'DbSelect');
$articlesPaginator->setCacheSalt('categories');

Categories