How to query outside model scope in Laravel? - laravel

In Laravel we know the scope functions as documented here: https://laravel.com/docs/8.x/eloquent#local-scopes.
In my project I want to query all active documents, so I created this scope:
class Document extends Model {
//...
public function scopeActive($query)
{
return $query->whereNotIn('status', ['done', 'expired'])->orWhere(function($query) {
$query->whereIn('reviewed', ['pending', 'todo'])->whereNotNull('reviewed');
});
}
Utilizing this scope you can do something like this: $activeDocuments = Document::active()->get(). All documents not within this scope are considered archived.
Is it possible to query all documents that not within this scope? E.g. $notActiveDocuments = Document::notActive()->get(); My query is relatively complex, I do not like to write this query in 'reverse' considering future maintenance.
If not, what is the most solid and maintainable approach to accomplish an 'outside scope' query?

You should move these arrays to model as constants. This way it will work for both scopes
class Document extends Model {
const ACTIVE_SCOPE_EXCLUDED_DOCUMENT_STATUSES = ['done', 'expired'];
const ACTIVE_SCOPE_INCLUDED_DOCUMENT_REVIEW_STATUSES = ['pending', 'todo'];
public function scopeActive($query)
{
return $query->whereNotIn('status', self::ACTIVE_SCOPE_EXCLUDED_DOCUMENT_STATUSES)
->orWhere(function($query) {
$query->whereIn('reviewed', self::ACTIVE_SCOPE_INCLUDED_DOCUMENT_REVIEW_STATUSES)
->whereNotNull('reviewed'); // I think this is unnecessary
});
}
// reverse scope should look something like this
public function scopeNotActive($query)
{
return $query->whereIn('status', self::ACTIVE_SCOPE_EXCLUDED_DOCUMENT_STATUSES)
->where(function($query) {
$query->whereNotIn('reviewed', self::ACTIVE_SCOPE_INCLUDED_DOCUMENT_REVIEW_STATUSES)
->orWhereNull('reviewed'); // I think this is unnecessary
});
}

I think you can do it in the following way, if the query is always the same for both cases:
public function scopeActiveOrNotActive($query, $status, $reviewed)
{
return $query->whereNotIn('status', $status)
->orWhere(function($query) use ($reviewed) {
$query->whereIn('reviewed', $reviewed)
->whereNotNull('reviewed');
});
}
Knowing that these two variables will always be an array, you just do it this:
Document::activeOrNotActive(['foo', 'bar'], ['otherFoo', 'otherBar'])->get();
And if i think a bit, I think you can do this:
public function scopeActive($query, $status, $reviewed)
{
return $query->whereNotIn('status', $status)
->orWhere(function($query) use ($reviewed) {
$query->whereIn('reviewed', $reviewed)
->whereNotNull('reviewed');
});
}
public function scopeNotActive($query, $status, $reviewed)
{
return $query->active($status, $reviewed);
}
And after just do it:
Document::active(['foo', 'bar'], ['foo1', 'bar2'])->get();
Document::notActive([! 'foo', ! 'bar'], [! 'foo1', ! 'bar2'])->get();
But i'm not so sure it works, it just came to my mind.
I hope i helped you.

Related

Passing a variable from Eloquent to Controller

Is it possible to pass a variable from Eloquent to Controller?
I have here my code for Eloquent where i get the result of my filtered query and i wanna use that same variable to my Controller so that i can use it for my Exporting to excel.
Here's my code for my eloquent model:
$result = $query
->orderBy('id', 'desc')
->paginate($perPage);
if ($search) {
$result->appends(['search' => $search]);
}
if ($emp_status) {
$result->appends(['emp_status' => $emp_status]);
}
if ($company) {
$result->appends(['company' => $company]);
}
if ($age) {
$result->appends(['age' => $age]);
}
if ($tenure) {
$result->appends(['tenure' => $tenure]);
}
if ($benefit) {
$result->appends(['benefit' => $benefit]);
}
if ($level) {
$result->appends(['level' => $level]);
}
if ($gender) {
$result->appends(['gender' => $gender]);
}
if ($civil_status) {
$result->appends(['civil_status' => $civil_status]);
}
if ($birthmonth) {
$result->appends(['birthmonth' => $birthmonth]);
}
return $result;
How can I get "$result" and use it in UsersController?
Sounds like you need to use something like a query scope in your model
So something like this in your model:
public function scopeWhatever($query)
{
return $query->
...
your conditions
...
}
Then in your controller use:
YourModel::whatever()->get();
I would suggest you add the function to the controller, and call it there. But if for some reason, you need it in the Eloquent class, I can suggest one of these two:
make a new instance of your model without saving it, and call the method with it.
(new Model())->myFunction();
this will work for any instance of the model.
myInstanceOfTheModel->myFunction();
the better solution(in my opinion), is using a trait. write a trait with that function and add it to both your model and controller.
`
trait QueryFunction(){
theQuery(){
//your function comes here
}
}
class SomeController extends Controller{
use QueryFunction;
}
class YourModel extends Elloquent{
use QueryFunction;
}
`
Try this:-
Model::query()->where($yourConditions)->get();

Laravel dynamic scope only works first time

I'm missing something with how the global scopes work in Laravel 5.5.
In my controller, index , I am passing filters into a getter:
public function index(SaleFilters $filters)
{
return new SaleCollection($this->getSales($filters));
}
getSales:
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}
protected function range($range)
{
$dates = explode(" ", $range);
if (count($dates) == 2) {
$this->builder = Sale::with(['office', 'staff'])
->where('sale_date', '>=', $dates[0])
->where('sale_date', '<', $dates[1])
->orderBy('sale_date', 'desc');
return $this->builder;
}
return false;
}
I have a scope setup in the sale model as such, which I would have thought would apply to the above filter automatically ? If not, do I have to reapply the same scope, duplicating the scope code in the filter ?
protected static function boot()
{
parent::boot();
$user = Auth::user();
if (($user) && ($user['office_id'])) {
return Sale::ofOffice($user['office_id'])->get();
}
}
public function scopeOfOffice($query, $office)
{
return $query->where('office_id', $office);
}
So basically, IF the user has an office_id applied, it should apply the ofOffice scope, therefore it should only ever return the sales that apply to that office_id.
Basically it works on page load via axios GET request
Route::get('/sales', 'SalesController#index')->middleware('auth:api');
axios
.get('api/sales/?range=" + this.rangeFilter)
rangeFilter is basically a start and end date passed into the above filter query.
Can anyone shed some light on how the scopes really work or if anything is obvious as to why its not always working? As I said, it works on page load where I provide default values for the rangeFilter, however when I change those days and it refetches via the same axios call, it seems to not be applying the scope, and I get ALL results instead of where office_id = 'x'
As far as i'm concerned, the range filter above would be executing on the first page load as well, so not sure why it would apply there, and not afterwards.
You should not mix the use of dynamic scope with global one. Also, static boot function does not expect a return. In order to use dynamic scope, you need to call it every time you need it. Hence, the name is dynamic. Query applied is not always executed by default. There so,
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice($anyOfficeHere)->with('office')->filter($filters);
return $sales->paginate(50);
}
To suit your existing code, you may want to add an if statement in your model. Then call the scope function without argument.
public function scopeOfOffice($q)
{
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
}
// Your controller
protected function getSales(SaleFilters $filters)
{
$sales = Sale::ofOffice()->with('office')->filter($filters);
return $sales->paginate(50);
}
If you feel so much cumbersome to type ofOffice repeatedly. A global scope is the way to go. Within your model static boot function, you can also apply anonymous function if you feel creating a separated class kinda bloat your apps.
protected static function boot()
{
parent::boot();
static::addGlobalScope('officeOrWhatNot', function ($q) {
if (($user = \Auth::user()) && ($office = $user->office_id)) {
$q->where('office_id', $office);
}
});
}
// Your controller. No more `ofOffice`, it's automatically applied.
protected function getSales(SaleFilters $filters)
{
$sales = Sale::with('office')->filter($filters);
return $sales->paginate(50);
}

Modify all attributes of a Laravel model

Accessors will do their job on a single attribute perfectly, but I need a way to have a method to do an Accessor/Getter job on all attributes and automatically.
The purpose is that I want to replace some characters/numbers on getting attributes and then printing them out. I can do it from within controller and manually but I think it would be great to have it from model side and automatically.
Like overriding getAttributes() method:
public function getAttributes()
{
foreach ($this->attributes as $key => $value) {
$this->attributes[$key] = str_replace([...], [...], $value);
}
return $this->attributes;
}
But I have to call it every time on model $model->getAttributes();
Any way to do it automatically and DRY?
Try something like:
public function getAttribute($key)
{
if (array_key_exists($key, $this->attributes) || $this->hasGetMutator($key)) {
if($key === 'name') return 'modify this value';
return $this->getAttributeValue($key);
}
return $this->getRelationValue($key);
}
It's fully overriding the default method so be a bit careful.
EDIT
Also check out: http://laravel.com/docs/5.1/eloquent-mutators
I would go with following approach and override the models __get method:
public function __get($key)
{
$excluded = [
// here you should add primary or foreign keys and other values,
// that should not be touched.
// $alternatively define an $included array to whitelist values
'foreignkey',
];
// if mutator is defined for an attribute it has precedence.
if(array_key_exists($key, $this->attributes)
&& ! $this->hasGetMutator($key) && ! in_array($key, $excluded)) {
return "modified string";
}
// let everything else handle the Model class itself
return parent::__get($key);
}
}
How about running it with each Creating and Updating events. So you can do something like that:
public function boot()
{
Model::creating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
Model::updating(function ($model)
return $model->getAttributes(); //or $this->getAttributes()
});
}

Calling same eloquent statement in several controllers

I have an eloquent statement like this:
$constraint = function ($query) {
$query->where('session', Session::getId());
};
$selectedImages = ImageSession::with(['folder' => $constraint])
->whereHas('folder', $constraint)
->where('type', 'single')
->get();
Which I need to call in several controllers.
How is the best way to do it without putting this code every time?
Should I put this code in the Model? but how I put the ImageSession::with if it is inside the same model that has ImageSession class?
In the controller do I have to write...
$imageSession_table = new ImageSession;
$selectedImages = $imageSession_table->getSelectedImages();
Well there are several solutions to this, but one rule that I have learned is whenever you are doing copy paste in the same file it means you need to create a function to encapsulate that code.
The same applies when you are copying and pasting the same code over classes/controllers it means you need to create a class that will have a method, that will encapsulate that code.
Now you could in fact change your model and this depends on your application and what kind of level of abstraction you have.
Some people tend to leave the models as pure as possible and then use transformers, repositories, classes whatever you want to call it. So the flow of communication is something like this:
Models -> (transformers, repositories, classes) -> Controllers or other classes
If that's the case just create a ImageSessionRepository and in there have your method to get the selected images:
<?php namespace Your\Namespace;
use ImageSession;
use Session;
class ImageSessionRepository
{
protected $imageSession;
public function __construct(ImageSession $imageSession)
{
$this->imageSession = $imageSession;
}
public function getSelectedImages($sessionId = false){
if(!$sessionId){
$sessionId = Session::getId()
}
$constraint = function ($query) use ($sessionId){
$query->where('session', $sessionId);
};
$selectedImages = ImageSession::with(['folder' => $constraint])
->whereHas('folder', $constraint)
->where('type', 'single')
->get();
return $selectedImages;
}
}
Then on your controller you just inject it:
<?php namespace APP\Http\Controllers;
use Your\Namespace\ImageSessionRepository;
class YourController extends Controller
{
/**
* #var ImageSessionRepository
*/
protected $imageSessionRepository;
public function __construct(ImageSessionRepository $imageSessionRepository)
{
$this->imageSessionRepository = $imageSessionRepository;
}
public function getImages()
{
$selectedImages = $this->imageSessionRepository->getSelectedImages();
//or if you want to pass a Session id
$selectedImages = $this->imageSessionRepository->getSelectedImages($sessionID = 1234);
//return the selected images as json
return response()->json($selectedImages);
}
}
Another option is adding that code directly into your Model, using scopes, more info here
So on your ImageSession Model just add this function:
public function scopeSessionFolder($query, $session)
{
$constraint = function ($constraintQuery) use ($sessionId){
$query->where('session', $sessionId);
};
return $query->with(['folder' => $constraint])
->whereHas('folder', $constraint);
}
And on your controller just do this:
$selectedImages = ImageSession::sessionFolder(Session::getId())
->where('type', 'single')
->get();
Or you can include everything in your scope if that's your case
public function scopeSessionFolder($query, $session)
{
$constraint = function ($constraintQuery) use ($sessionId){
$query->where('session', $sessionId);
};
return $query->with(['folder' => $constraint])
->whereHas('folder', $constraint);
->where('type', 'single');
}
And then again on your controller you will have something like this:
$selectedImages = ImageSession::sessionFolder(Session::getId())
->get();
Just a side note I haven't tested this code, so if you just copy and paste it it's possible that you find some errors.

Adding methods to Eloquent Model in Laravel

I'm a bit confused how I am to add methods to Eloquent models. Here is the code in my controller:
public function show($id)
{
$limit = Input::get('limit', false);
try {
if ($this->isExpand('posts')) {
$user = User::with(['posts' => function($query) {
$query->active()->ordered();
}])->findByIdOrUsernameOrFail($id);
} else {
$user = User::findByIdOrUsernameOrFail($id);
}
$userTransformed = $this->userTransformer->transform($user);
} catch (ModelNotFoundException $e) {
return $this->respondNotFound('User does not exist');
}
return $this->respond([
'item' => $userTransformed
]);
}
And the code in the User model:
public static function findByIdOrUsernameOrFail($id, $columns = array('*')) {
if (is_int($id)) return static::findOrFail($id, $columns);
if ( ! is_null($user = static::whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
So essentially I'm trying to allow the user to be retrieved by either user_id or username. I want to preserve the power of findOrFail() by creating my own method which checks the $id for an int or string.
When I am retrieving the User alone, it works with no problem. When I expand the posts then I get the error:
Call to undefined method
Illuminate\Database\Query\Builder::findByIdOrUsernameOrFail()
I'm not sure how I would go about approaching this problem.
You are trying to call your method in a static and a non-static context, which won't work. To accomplish what you want without duplicating code, you can make use of Query Scopes.
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
if ( ! is_null($user = $query->whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
You can use it exactly in the way you are trying to now.
Also, you can use firstOrFail:
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
return $query->whereUsername($id)->firstOrFail($columns);
}
Your method is fine, but you're trying to use it in two conflicting ways. The one that works as you intended is the one in the else clause, like you realised.
The reason the first mention doesn't work is because of two things:
You wrote the method as a static method, meaning that you don't call it on an instantiated object. In other words: User::someStaticMethod() works, but $user->someStaticMethod() doesn't.
The code User::with(...) returns an Eloquent query Builder object. This object can't call your static method.
Unfortunately, you'll either have to duplicate the functionality or circumvent it someway. Personally, I'd probably create a user repository with a non-static method to chain from. Another option is to create a static method on the User model that starts the chaining and calls the static method from there.
Edit: Lukas's suggestion of using a scope is of course by far the best option. I did not consider that it would work in this situation.

Resources