Adding methods to Eloquent Model in Laravel - 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.

Related

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);
}

How to achieve this on laravel 5 eloquent

How can i achieve something like this?
public function getInformation($model) {
$result = $model::with(['province', 'city']);
if($model == 'App\Models\Business') {
$result->with(['businessProvince', 'businessCity']);
}
$result->get();
}
// call the function
$information->getInformation(\App\Models\Business::class);
i'm getting error
Object of class Illuminate\Database\Eloquent\Builder could not be
converted to string
on the sample code above. Any suggestion is really appreciated.
After taking a fourth look $model should be a string, and $result is an Eloquent Builder instance and never an instance of the model class (since a query was started when with was called).
So the $model == 'App\Models\Business' I would change to $model === \App\Models\Business::class but that should not change the outcome.
Are you sure this error comes from this part of the application? Which line specifically?
Original wrong answer.
You are trying to compare the model instance with a string (since $model::with() created a instance of the model class you passed in the $model argument).
You can use the instanceof keyword for comparing an instance with a class name (http://php.net/manual/en/language.operators.type.php).
if($model instanceof \App\Models\Business) {
$result->with(['businessProvince', 'businessCity']);
}
This solved my problem, thank you guys.
public function getInformation($model) {
$result = $model::with(['province', 'city']);
if($model == 'App\Models\Business') {
// my mistake
//$result->with(['businessProvince', 'businessCity']);
$result = $result->with(['businessProvince', 'businessCity']);
}
$result->get();
}

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.

laravel controller action structure

After watching many laracasts, one statement is everywhere: keep the controller as light as possible.
Ok, I am trying to familiarize myself with laravel concepts and philosophy, with the Repository and the separation of concerns patterns and I have some questions that bother me, let's assume the following:
Route::resource('/item', 'ItemController');
class Item extends \Eloquent {}
the repo
class EloquentItemRepo implements ItemRepo {
public function all()
{
return Item::all();
}
public function find($id)
{
return Item::where('id', '=', $id);
}
}
and the controller:
class ItemController extends BaseController {
protected $item;
public function __construct(ItemRepo $item)
{
$this->item = $item;
}
public function index()
{
$items = $this->item->all();
return Response::json(compact('items'))
}
}
For now, everything is simple and clean (assume that the repo is loaded by providers etc.) the controller is really simple and does nothing except loading and returning the data (I used json but anything will do).
Please assume that I am using an auth filter that checks that the user
is logged in and exists, or return an error if it doesn't, so I don't
have to do any further check in the controller.
Now, what if I need to do more checks, for instance:
response_* methods are helpers that format a Json response
public function destroy($id)
{
try {
if ($this->item->destroy($id)) {
return Response::json(['success' => true]);
}
return response_failure(
Lang::get('errors.api.orders.delete'),
Config::get('status.error.forbidden')
);
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
In this case I have to test many things:
The desctuction worked? (return true)
The destruction failed? (return false)
There was an error during deletion ? (ex.: the item wasn't found with firstOrFail)
I have methods where many more tests are done, and my impression is that the controller is growing bigger and bigger so I can handle any possible errors.
Is it the right way to manage this ? The controller should be full of checks or the tests should be moved elsewhere ?
In the provider I often use item->firstOrFail() and let the exception bubble up to the controller, is it good ?
If someone could point me to the right direction as all the laracasts or other tutorials always use the simpler case, where not many controls are needed.
Edits: Practical case
Ok so here a practical case of my questioning:
controller
/**
* Update an order.
* #param int $id Order id.
* #return \Illuminate\Http\JsonResponse
*/
public function update($id)
{
try {
$orderItem = $this->order->update($id, Input::all());
if (false === $orderItem) {
return response_failure(
Lang::get('errors.api.orders.update'),
Config::get('status.error.forbidden')
);
}
return response_success();
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
repo
public function update($id, $input)
{
$itemId = $input['itemId'];
$quantity = $input['quantity'] ?: 1;
// cannot update without item id
if (!$itemId) {
return false;
}
$catalogItem = CatalogItem::where('hash', '=', $itemId)->firstOrFail();
$orderItem = OrderItem::fromCatalogItem($catalogItem);
// update quantity
$orderItem->quantity = $quantity;
return Order::findOrFail($id)->items()->save($orderItem);
}
In this case thare are 3 possible problems:
order not found
catalogItem not found
itemId not set in post data
In the way I have organized that, the problem is that the top level error message won't be clear, as it will alway state: "order not found" even if it's the catalog item that couldn't be found.
The only possibility that I see is to catch multiple exceptions codes in the controller and raise a different error message, but won't this overload the controller ?

Problems updating using Eloquent

Right now I'm working on my first update function using Eloquent ORM. Trying to follow the docs, I have this in my model:
public function updateAvailability()
{
$this->active = Input::get('available');
$this->activeDetails = Input::get('availableStatus');
$this->save();
}
which returns:
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
and all of this is being called to in my controller as:
public function updateProfile($id)
{
if(Input::get('type')=='availability'){
$availability = User::find($id)->updateAvailability;
}
$name = str_replace(' ', '', Input::get('name'));
return Redirect::to('people/'.$name);
}
Are there some gaps in my understanding of updating in Eloquent? (I'm sure there are). I would love to use ajax to handle it, but I can't seem to find the right resources to get that working.
SOLVED: $availability = User::find($id)->updateAvailability; needed to be changed to $availability = User::find($id)->updateAvailability();. That's all.

Resources