laravel controller action structure - laravel

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 ?

Related

Filtering one-to-many connections in entity

I have an entity User which has a one-to-many connection to questions. Questions has a one to many connection to answers. The question and answer entity each has a property called state.
In generally after execute a GET request API Platform returns all users, with all questions including all answers. That works fine!
Now I would like to implement a get request that returns the user with all questions that has a specific state (e.g. "X"). The questions should only include the answers with the same state ("X").
I used the filter function (to filter the whole not necessary data)
Therefore I generated a controller called GetUserObjectAction which the following function
public function __invoke(PaginatorInterface $data, Request $request): PaginatorInterface
{
$repo = $this->managerRegistry->getRepository(Question::class);
foreach ($data as $value) {
$q = $value->getQuestions()->filter(function($q1) {
if($q1->getState() === 'a') {
$q1->values = $q1->values->filter(function($a) {
return $a->getState() === 'a';
});
return true;
} else {
return false;
}
return ;
});
$value->setQuestions($int);
}
return $data;
}
Is there a better way to implement it?
Thanks
You chose the right way because you did not tell us how are your data managed (Doctrine ORM/ODM, custom data providers) we cannot tell you more. But I suggest you are using API Platform defaults, so you can filter your data before fetching them in your QuestionRepository and omit to iterate over data.
Here is example:
QuestionRepository
...
public function findWithAnswersByState(string $state): array
{
$qb = $this->createQueryBuilder('q')
->join('q.answers', 'a')
->andWhere('q.state = :state')
->andWhere('a.state = :state')
->setParameter('state', $state);
return $qb->getQuery()->getResult();
}
...
Controller:
...
public function __invoke(PaginatorInterface $data, Request $request): PaginatorInterface
{
$repo = $this->managerRegistry->getRepository(Question::class);
return $repo->findWithAnswersByState('a');
}
...

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

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.

Route resources set before auth not working in Laravel 4

I added this in routes.php, expected it will check the authentication session for the page, however it is not working.
Route::resource('ticket', 'TicketController', array('before' => 'auth') );
Then I go to the controller, work in another way. It's work.
class TicketController extends BaseController {
public function __construct()
{
$this->beforeFilter('auth');
}
May I know where can get more documentation regarding the Route::resource(), what type of argument it able to accept?
OK... I found the answer.
in
\vendor\laravel\framework\src\Illuminate\Routing\Router.php
public function resource($resource, $controller, array $options = array())
{
// If the resource name contains a slash, we will assume the developer wishes to
// register these resource routes with a prefix so we will set that up out of
// the box so they don't have to mess with it. Otherwise, we will continue.
if (str_contains($resource, '/'))
{
$this->prefixedResource($resource, $controller, $options);
return;
}
// We need to extract the base resource from the resource name. Nested resources
// are supported in the framework, but we need to know what name to use for a
// place-holder on the route wildcards, which should be the base resources.
$base = $this->getBaseResource($resource);
$defaults = $this->resourceDefaults;
foreach ($this->getResourceMethods($defaults, $options) as $method)
{
$this->{'addResource'.ucfirst($method)}($resource, $base, $controller);
}
}
protected function getResourceMethods($defaults, $options)
{
if (isset($options['only']))
{
return array_intersect($defaults, $options['only']);
}
elseif (isset($options['except']))
{
return array_diff($defaults, $options['except']);
}
return $defaults;
}
as you can see, it only accept only and except arguement only.
If you want to archive the same result in route.php, it can be done as below
Route::group(array('before'=>'auth'), function() {
Route::resource('ticket', 'TicketController');
});

Resources