Convert local scope to global scope or query by parent attribute - sql

I want to retrieve all comments which belong to active posts.
I have a local scope on my Posts model looking like this.
public function scopePublic($query) {
return $query->whereHas('post', function ($q) {
$q->where('is_public', true);
});
}
Which works fine, but breaks with PHP message: PHP Fatal error: Allowed memory size of X bytes exhausted as soon as I want to convert it to a global scope like this:
static::addGlobalScope('is_public', function (Builder $builder) {
return $builder->whereHas('post', function ($q) {
$q->where('is_public', true);
});
});
My end goal is for all comment queries only to show public comments, unless I specifically ask not to.
I've been through quite a few solutions. I've tried joining the post on the comments, and I tried adding a sub-select to no luck.
$builder->addSelect(['is_public' => Post::select('is_private')
->whereColumn('id', 'comment.post_id')->limit(1)
]);
$builder->join('posts','posts.id','=','comments.post_id')
->where('comments.is_private', false);

Make a new class PublicScope
use Illuminate\Database\Eloquent\Scope;
class CommentPublicScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->whereHas('post', function ($q) {
$q->where('is_public', true);
});
}
}
Then you can add the global scope
Class Comment extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new CommentPublicScope);
}
}

Related

Override the condition from ActiveQuery->init()

On Yii2 i have this code in ProjectQuery
class ProjectQuery extends \yii\db\ActiveQuery
{
public function init()
{
$this->andOnCondition(['deleted' => 0]);
parent::init();
}
Obviously the deleted condition must always apply, but there could be cases where this isn't true (for example an option for the user to see his deleted projects). How can i override this condition? Do i have to use something different from init() ?
(note, i want to apply this condition to all kind of queries normally, that's why i used init() on ProjectQuery and not the ProjectSearch class)
You can still use init() but to override the 0 you should bind a parameter.
public function init()
{
$this->andOnCondition('deleted = :deleted', [':deleted' => 0]);
parent::init();
}
So to create a query that only shows the deleted projects write something like this:
$query = Project::find()->addParams([':deleted' => 1]);
To show all projects, deleted and not deleted, you could add a function to the ProjectQuery object to modify it accordingly.
public function includeDeleted() {
$this->orOnCondition(['deleted' => 1]);
return $this;
}
And then write your query like:
$query = Project::find()->includeDeleted();
You can use onCondition() to override existing on conditions:
public function init() {
$this->andOnCondition('deleted = :deleted', [':deleted' => 0]);
parent::init();
}
public function includeDeleted() {
$this->onCondition(null);
// remove unused param from old ON condition
unset($this->params[':deleted']);
return $this;
}
You can use where() in the same way if you want to override conditions added by where(), andWhere and orWhere().
Assuming that you have a class Project where you have overwritten the find() method to return a ProjectQuery instance, the following might be another option. I also assume that you regularely query for undeleted items, and not so often but explicitly for all/deleted items.
Another option could be to add another method to the Project class and remove the default initialization in the ProjectQuery class.
class ProjectQuery extends \yii\db\ActiveQuery
{
public function init()
{
parent::init();
}
...
}
And:
class Project extends \yii\db\ActiveRecord {
...
public static function find()
{
return (new ProjectQuery(get_called_class()))
->andOnCondition(['deleted' => 0]);
}
public static function findAllProjects() // or find any better name for this
{
return new ProjectQuery(get_called_class());
}
}
Now, whenever you want to query explicitly all projects you would need to use this extra method Project::findAllProjects(). So in normal circumstances you won't have to remember that you have to modify the query in some way. No danger, that this could be forgotten.
It is still not 100% satisfying, since one could use find() and add ->andOnCondition(['deleted' => 1]) which would mean no records can be found. However, regarding security this is not so critical and the problem is found easily, I guess.

Recursive relationship with scope

A user has a sponsor:
public function sponsor()
{
return $this->belongsTo(User::class, 'sponsor_id');
}
A user has referrals:
public function referrals()
{
return $this->hasMany(User::class, 'sponsor_id');
}
A user is considered capped when they have 2 or more referrals:
public function activeReferrals()
{
return $this->referrals()->whereActive(true);
}
public function isCapped()
{
return $this->activeReferrals()->count() >= 2;
}
A user can give points. By default, the sponsor will receive them, but if the sponsor is capped, I want the points to go to a sponsor's referral that is NOT capped. If all the referrals are capped, then it does the same thing with the level below (the referral's referrals).
If I go user by user making database calls for each one, it's gonna take a long time. How can I write a scope that makes recursive calls until it finds the first active referral in the tree that's not capped?
This is what I'm trying to do:
Please give this a try... I believe this will work for you :)
public function scopeNotCappedActiveReferrals($query, $count) {
return $query->withCount(['referrals' => function($q) {
$q->where('active', true);
}])->where('referrals_count', '<', $count);
}
For the second part...
// Finally you can call it with
public function allReferrals() {
$users = User::notCappedActiveReferrals(2)->get();
$allUsers = $this->findNotCappedActiveReferralsRecurrsively($users);
}
// Do not place this function in the model,
// place it in your Controller or Service or Repo or blahblah...
// Also, not tested... but should work :)
protected function findNotCappedActiveReferralsRecurrsively($users) {
if(!count($user)) {
return $users;
}
foreach($users as $user) {
$moreUsers = $user->notCappedActiveReferrals(2)->get();
return $users->merge($this->findNotCappedActiveReferralsRecurrsively($moreUsers));
}
}
Hope this is what you need :)

Laravel Testing Error

I just started with learning how to test within Laravel. I came across some problems though..
I'm testing my controller and want to check if a View has a variable assigned.
My controller code:
class PagesController extends \BaseController {
protected $post;
public function __construct(Post $post) {
$this->post = $post;
}
public function index() {
$posts = $this->post->all();
return View::make('hello', ['posts' => $posts]);
}
}
And my view contains a foreach loop to display all posts:
#foreach ($posts as $post)
{{post->id}}
#endforeach
Last but not least my test file:
class PostControllerTest extends TestCase {
public function __construct()
{
// We have no interest in testing Eloquent
$this->mock = Mockery::mock('Eloquent', 'Post');
}
public function tearDown()
{
Mockery::close();
}
public function testIndex() {
$this->mock->shouldReceive('all')->once()->andReturn('foo');
$this->app->instance('Post', $this->mock);
$this->call('GET', '/');
$this->assertViewHas('posts');
}
}
Now comes the problem, when I run "phpunit" the following error appears:
ErrorException: Invalid argument supplied for foreach()
Any ideas why phpunit returns this error?
Your problem is here:
$this->mock->shouldReceive('all')->once()->andReturn('foo');
$this->post->all() (which is what you're mocking) should return an array, and that's what your view expects. You're returning a string.
$this->mock->shouldReceive('all')->once()->andReturn(array('foo'));
should take care of the error you have, though you'll then get an error of the "Getting property of non-object" type.
You could do this:
$mockPost = new stdClass();
$mockPost->id = 1;
$this->mock->shouldReceive('all')->once()->andReturn(array($mockpost));
You should mock the view as well:
public function testIndex() {
$this->mock->shouldReceive('all')->once()->andReturn('foo');
$this->app->instance('Post', $this->mock);
View::shouldReceive('make')->with('hello', array('posts', 'foo'))->once();
$this->call('GET', '/');
}

How to extend Illuminate\Database\Query\Builder

I'm planning to have a function that will store the sql statement on the Cache using the given second parameter on remember() as the key and whenever the sql statement changes it will run against the database again and overwrite the stored sql, also the cached result, and if not it will take the default cached result by the remember() function.
So I am planning to have something like this on Illuminate\Database\Query\Builder
/**
* Execute the query based on the cached query
*
* #param array $columns
* #return array|static[]
*/
public function getCacheByQuery($columns = array('*'))
{
if ( ! is_null($this->cacheMinutes))
{
list($key, $minutes) = $this->getCacheInfo();
// if the stored sql is the same with the new one then get the cached
// if not, remove the cached query before calling the getCached
$oldSql = self::flag($key);
$newSql = $this->toSql().implode(',', $this->bindings);
if ($newSql!==$oldSql)
{
// remove the cache
\Cache::forget($key);
// update the stored sql
self::updateFlag($key, $newSql);
}
return $this->getCached($columns);
}
return $this->getFresh($columns);
}
public static function updateFlag($flag, $value)
{
$flags = \Cache::get(t().'databaseFlags', []);
$flags[$flag] = $value;
\Cache::put(t().'databaseFlags', $flags, USER_SESSION_EXPIRATION);
}
public static function flag($flag)
{
$flags = \Cache::get(t().'databaseFlags', []);
return #$flags[$flag] ?: false;
}
But the thing is, I don't want to put this directly on Illuminate\Database\Query\Builder since it is just my need for the current application I am working. I'm trying to extend Illuminate\Database\Query\Builder, but the problem is it does not detect the my extension class.
Call to undefined method Illuminate\Database\Query\Builder::getCachedByQuery()
My Extension Class
<?php namespace Lukaserat\Traits;
class QueryBuilder extends \Illuminate\Database\Query\Builder {
/**
* Execute the query based on the caced query
*
* #param array $columns
* #return array|static[]
*/
public function getCachedByQuery($columns = array('*'))
{
if ( ! is_null($this->cacheMinutes))
{
list($key, $minutes) = $this->getCacheInfo();
// if the stored sql is the same with the new one then get the cached
// if not, remove the cached query before calling the getCached
$oldSql = self::flag($key);
$newSql = $this->toSql().implode(',', $this->bindings);
if ($newSql!==$oldSql)
{
// remove the cache
\Cache::forget($key);
// update the stored sql
self::updateFlag($key, $newSql);
}
return $this->getCached($columns);
}
return $this->getFresh($columns);
}
public static function updateFlag($flag, $value)
{
$flags = \Cache::get(t().'databaseFlags', []);
$flags[$flag] = $value;
\Cache::put(t().'databaseFlags', $flags, USER_SESSION_EXPIRATION);
}
public static function flag($flag)
{
$flags = \Cache::get(t().'databaseFlags', []);
return #$flags[$flag] ?: false;
}
}
Implementing on..
<?php
use LaravelBook\Ardent\Ardent;
use Lukaserat\Traits\DataTable;
use Lukaserat\Traits\QueryBuilder as QueryBuilder;
use Illuminate\Support\MessageBag as MessageBag;
class ArdentBase extends Ardent implements InterfaceArdentBase{
use DataTable;
Am I missing something?
Is it correct that I overwrite the get() method on the Illuminate\Database\Query\Builder by renaming the function I made in my extension class from getCachedByQuery to get since I just extending the routine of the get.
I changed
public function getCachedByQuery($columns = array('*'))
to
public function get()
on my Lukaserat\Traits\QueryBuilder
and it is now working as I expected..

Yii Framework - from url to route

I searched, but couldnt find something.
So, I have route rules:
...
'/reg' => '/user/user/registration',
...
in
Yii::app()->request
I couldn find any route information.
So, how can I get in module init function and having only url, route lile
/reg -> user/user/registration
UPD
The route is only available from the running controller. By the time when a module is initialized the controller is not yet available, thus you can't find out the route there. (You can follow CWebApplication::processRequest to see what happens when a request is resolved up to the point where the controller is run.)
It depends on what you try to achieve, but you could override WebModule::beforeControllerAction to do something before the module controller is run.
Today (next day after my question), I could solve this.
I will try to explain:
As Michael wrote, we cant know in module in which controller we are.
But I net get just reversed route, so, its quite esay.
Yii::app()->getUrlManager()->parseUrl('/reg');
This will return my reversed route
user/user/registration
parseUrl
Solution for Yii 1.1.15 workes for me.
class HttpRequest extends CHttpRequest {
protected $_requestUri;
protected $_pathInfo;
public function setUri($uri){
$this->_requestUri = $uri;
}
public function setPathInfo($route){
$this->_pathInfo = $route;
}
public function getPathInfo(){
/* copy from parent */
}
public function getRequestUri(){
/* copy from parent */
}
}
The usage:
$uri_path = 'my/project-alias/wall';
/** #var HttpRequest $request */
$request = clone Yii::app()->getRequest();
$request->setUri($uri_path);
$request->setPathInfo(null);
$route = Yii::app()->getUrlManager()->parseUrl($request);
//$route equals 'project/profile/wall' etc here (like in route rules);
I'm using a slightly different sub-class of CHttpRequest:
class CustomHttpRequest extends \CHttpRequest
{
/**
* #var string
*/
var $pathInfo;
/**
* #var string
*/
private $method;
public function __construct($pathInfo, $method)
{
$this->pathInfo = $pathInfo;
$this->method = $method;
}
public function getPathInfo()
{
return $this->pathInfo; // Return our path info rather than the default
}
public function getRequestType()
{
return $this->method;
}
}
Then to call it (to create a controller, which is what I want):
$request = new CustomHttpRequest($uri, $method); // e.g. 'my/project-alias/wall' and 'GET'
$route = \Yii::app()->getUrlManager()->parseUrl($request);
list($jcontroller, $actionName) = \Yii::app()->createController($route);