Laravel 5.7 : Cleanest way to generate nested urls in views - php

I currently have nested routes.
Let's say I have
Route::resource('a', 'AController');
Route::resource('a.b', 'BController');
Route::resource('a.b.c', 'CController');
In my views, is there a better way to generate URL than doing :
route('a.b.c.show', ['a'=>$aId, 'b'=>$bId, 'c'=>$cId])
It's getting a little annoying for nested urls. Why can't we just pass an Eloquent model instance? Like route('a.b.c.show', $cInstance)
Thank you

I often pass the responsibility of generating the url to the model, e.g. with getters:
$cInstance->url
public function getUrlAttribute() {
return route('a.b.c.show', $this);
// or:
// return action([CController::class, 'show'], ['a' => $this]);
}
But it depends on your use case. Normal methods work just as well, like if you want to add more parameters than just what the model knows about already. For example:
public function url($b, $c) {
return route('a.b.c', ['a' => $this, 'b' => $b, 'c' => $c]);
}
However, if you're talking about just getting the ID from a model before using it, like in this case:
$aId = $a->id;
return route('a', ['a' => $aId]);
Then Laravel actually does support that already: route('a.b.c.show', compact('a', 'b', 'c')) should work fine. Eloquent models implement the Illuminate\Contracts\Routing\UrlRoutable interface, which has a getRouteKey() method that the router understands how to use.

Related

How can I add function name as route in Laravel 5.7?

I have a controller which returns enums for respective fields. e.g.
// Expected route - /api/getFamilyTypes - only GET method is allowed
public function getFamilyTypes()
{
return [
'Nuclear Family',
'Joint Family'
];
}
I've around 20 functions like this. How can I add this without manually adding an entry per function in routes file?
Thanks in advance.
In your routes file, add something like this,
Route::get('/something/{func}', 'SomeController#functionRoute');
Where something is whatever path you're wanting to use and SomeController is the controller with the 20 functions you're using and functionRoute is the action that we're about to make.
Then in your controller, make a function like this,
public function functionRoute($func)
{
return $this->$func();
}
This will make it so that whenever someone browses to /something/* on your website, it'll execute the function name at the end. So if you navigate to /something/getFamilyTypes it'll run your getFamilyTypes function.
This isn't particularly secure. If you do this, the user will be able to run any of the controller's methods. You could set up a blacklist like this.
public function functionRoute($func)
{
$blacklist = [
'secret',
'stuff',
];
return in_array($func, $blacklist) ? redirect('/') : $this->$func();
}
Or you could set up a whitelist like this,
public function functionRoute($func)
{
$whitelist = [
'getFamilyTypes',
'otherUserFriendlyStuff',
];
return in_array($func, $whitelist) ? $this->$func() : redirect('/');
}
If the responses are always from hard-coded arrays (as opposed to being from a database) then one way might be to have a variable in your route:
Route::get('/api/enum/{field}', 'EnumController#getField');
And then in your controller method, use the variable to get the correct data from a keyed array:
public function getField($field)
{
$fields = [
'family' => [
'Nuclear Family',
'Joint Family'
],
// ...
];
return $fields[$field];
}
If you want to continue using different methods for every field then Michael's answer is the easiest option, with one caveat. Allowing users to call any method by name on your controller is a security risk. To protect yourself, you should validate the method name against a whitelist.

Laravel 5.6: Invoking eloquent relationships change the collection data

Is there a way to invoke eloquent relationship methods without changing the original eloquent collection that the method runs on? Currently I have to employ a temporary collection to run the method immutable and to prevent adding entire related record to the response return:
$result = Item::find($id);
$array = array_values($result->toArray());
$temp = Item::find($id);
$title = $temp->article->title;
dd($temp); //This prints entire article record added to the temp collection data.
array_push($array, $title);
return response()->json($array);
You are not dealing with collections here but with models. Item::find($id) will get you an object of class Item (or null if not found).
As far as I know, there is no way to load a relation without storing it in the relation accessor. But you can always unset the accessor again to delete the loaded relation (from memory).
For your example, this process yields:
$result = Item::find($id);
$title = $result->article->title;
unset($result->article);
return response()->json(array_merge($result->toArray(), [$title]));
The above works but is no very nice code. Instead, you could do one of the following three things:
Use attributesToArray() instead of toArray() (which merges attributes and relations):
$result = Item::find($id);
return response()->json(array_merge($result->attributesToArray(), [$result->article->title]));
Add your own getter method on the Item class that will return all the data you want. Then use it in the controller:
class Item
{
public function getMyData(): array
{
return array_merge($this->attributesToArray(), [$this->article->title]);
}
}
Controller:
$result = Item::find($id);
return response()->json($result->getMyData());
Create your own response resource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'title' => $this->article->title,
'author' => $this->article->author,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Which can then be used like this:
return new ItemResource(Item::find($id));
The cleanest approach is option 3. Of course you could also use $this->attributesToArray() instead of enumerating the fields, but enumerating them will yield you security in future considering you might extend the model and do not want to expose the new fields.
I see two ways you can achieve that.
First, you can use an eloquent Resource. Basically it'll allow you to return exactly what you want from the model, so in your case, you'll be able to exclude the article. You can find the documentation here.
The second way is pretty new and is still undocumented (as fas i know), but it actually works well. You can use the unsetRelation method. So in your case, you just have to do:
$article = $result->article; // The article is loaded
$result->unsetRelation('article'); // It is unloaded and will not appear in the response
You can find the unsetRelation documentation here
There is not as far as I know. When dealing with Model outputs, I usually construct them manually like this:
$item = Item::find($id);
$result = $item->only('id', 'name', 'description', ...);
$result['title'] = $item->article->title;
return $result;
Should you need more power or a reusable solution, Resources are your best bet.
https://laravel.com/docs/5.6/eloquent-resources#concept-overview

Laravel validate data coming from my own application and database

In a function in my controller I call this:
$item = Item::where('i_id', $Id)->where('type', 1)->first();
$firebaseData = app('firebase')->getDatabase()->getReference('items/'.$Id)->getSnapshot()->getValue();
Then I do a lot of "validation" between the data from the two sources above like:
if ($item->time_expires < strtotime(Carbon::now()) && $firebaseData['active'] == 1) {
return response()->json(['errors' => [trans('api.pleaserenew')]], 422);
}
And since this is not data coming from a user/request I cant use Laravels validate method
I dont want to keep this kind of logic inside my controller but where should I put it? Since part of my data is coming from Firebase I cant setup a Eloquent model to handle it either.
I recommend to receive the firebase data via a method within the model:
public function getFirebaseData()
{
app('firebase')->getDatabase()->getReference('items'/ . $this->i_id)->getSnapshot()->getValue();
}
That way you have the logic to receive the data decoupled from controller logic and moved it to where it makes more sense. Adding a validation method could work similarily within the model then:
public function validateData()
{
$combined = array_merge($this->toArray(), $this->getFirebaseData());
Validator::make($combined, [
'active' => 'in:1',
'time_expires' => 'before:' . Carbon::now(),
]);
}
The caveat with this is that the validation error will be thrown within the model instead of the controller, but that shouldn't really be an issue I don't think.
For any data you have in your application you can use Laravel validation.
You can merge your data and process it using Validator facade like this:
$combinedData = array_merge($item->toArray(), $firebaseData);
Validator::make($combinedData, [
'active' => 'required|in:1',
'time_expires' => 'required|before:' . Carbon::now()->toDateTimeString()
], $customMessageArray);
I think the best place for this code is some kind of service class you will inject to controller or other service class using Laravel dependency injection.

Laravel Custom Model Methods

Whenever I add additional logic to Eloquent models, I end up having to make it a static method (i.e. less than ideal) in order to call it from the model's facade. I've tried searching a lot on how to do this the proper way and pretty much all results talk about creating methods that return portions of a Query Builder interface. I'm trying to figure out how to add methods that can return anything and be called using the model's facade.
For example, lets say I have a model called Car and want to get them all:
$cars = Car::all();
Great, except for now, let's say I want to sort the result into a multidimensional array by make so my result may look like this:
$cars = array(
'Ford' => array(
'F-150' => '...',
'Escape' => '...',
),
'Honda' => array(
'Accord' => '...',
'Civic' => '...',
),
);
Taking that theoretical example, I am tempted to create a method that can be called like:
$cars = Car::getAllSortedByMake();
For a moment, lets forget the terrible method name and the fact that it is tightly coupled to the data structure. If I make a method like this in the model:
public function getAllSortedByMake()
{
// Process and return resulting array
return array('...');
}
And finally call it in my controller, I will get this Exception thrown:
Non-static method Car::getAllSortedByMake() should not be called statically, assuming $this from incompatible context
TL;DR: How can I add custom functionality that makes sense to be in the model without making it a static method and call it using the model's facade?
Edit:
This is a theoretical example. Perhaps a rephrase of the question would make more sense. Why are certain non-static methods such as all() or which() available on the facade of an Eloquent model, but not additional methods added into the model? This means that the __call magic method is being used, but how can I make it recognize my own functions in the model?
Probably a better example over the "sorting" is if I needed to run an calculation or algorithm on a piece of data:
$validSPG = Chemical::isValidSpecificGravity(-1.43);
To me, it makes sense for something like that to be in the model as it is domain specific.
My question is at more of a fundamental level such as why is all()
accessible via the facade?
If you look at the Laravel Core - all() is actually a static function
public static function all($columns = array('*'))
You have two options:
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
or
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Both will allow you to do
Car::getAllSortedByMake();
Actually you can extend Eloquent Builder and put custom methods there.
Steps to extend builder :
1.Create custom builder
<?php
namespace App;
class CustomBuilder extends \Illuminate\Database\Eloquent\Builder
{
public function test()
{
$this->where(['id' => 1]);
return $this;
}
}
2.Add this method to your base model :
public function newEloquentBuilder($query)
{
return new CustomBuilder($query);
}
3.Run query with methods inside your custom builder :
User::where('first_name', 'like', 'a')
->test()
->get();
for above code generated mysql query will be :
select * from `users` where `first_name` like ? and (`id` = ?) and `users`.`deleted_at` is null
PS:
First Laurence example is code more suitable for you repository not for model, but also you can't pipe more methods with this approach :
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
Second Laurence example is event worst.
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Many people suggest using scopes for extend laravel builder but that is actually bad solution because scopes are isolated by eloquent builder and you won't get the same query with same commands inside vs outside scope. I proposed PR for change whether scopes should be isolated but Taylor ignored me.
More explanation :
For example if you have scopes like this one :
public function scopeWhereTest($builder, $column, $operator = null, $value = null, $boolean = 'and')
{
$builder->where($column, $operator, $value, $boolean);
}
and two eloquent queries :
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->where('first_name', 'like', 'b');
})->get();
vs
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->whereTest('first_name', 'like', 'b');
})->get();
Generated queries would be :
select * from `users` where (`first_name` like ? and `first_name` like ?) and `users`.`deleted_at` is null
vs
select * from `users` where (`first_name` like ? and (`id` = ?)) and `users`.`deleted_at` is null
on first sight queries look the same but there are not. For this simple query maybe it does not matter but for complicated queries it does, so please don't use scopes for extending builder :)
for better dynamic code, rather than using Model class name "Car",
just use "static" or "self"
public static function getAllSortedByMake()
{
//to return "Illuminate\Database\Query\Builder" class object you can add another where as you want
return static::where('...');
//or return already as collection object
return static::where('...')->get();
}
Laravel model custom methods -> best way is using traits
Step #1: Create a trait
Step #2: Add the trait to model
Step #3: Use the method
User::first()->confirmEmailNow()
app/Model/User.php
use App\Traits\EmailConfirmation;
class User extends Authenticatable
{
use EmailConfirmation;
//...
}
app/Traits/EmailConfirmation.php
<?php
namespace App\Traits;
trait EmailConfirmation
{
/**
* Set email_verified_at to now and save.
*
*/
public function confirmEmailNow()
{
$this->email_verified_at = now();
$this->save();
return $this;
}
}

Passing Object Manager in view

Using ZF2 with Doctrine 2.
For the needs of the application i passed objectmanager as a variable ($om) in return new ViewModel
so i can use it in my view:
$om->getRepository('\Schema\Entity\Category')->findBy(.....
The thing that confuses me is:
1) Do I break MVC rules? (Because some say that is a bad practice and some others not)
2) Am I going to face any problems with the development of the application using this tactic?
yes you do
violation of coding standards always has negative side-effects
The Job of the Controller is to pass the Data you need into the View. For this Purpose the Controller interacts with the Model.
public function fooAction()
{
return new ViewModel([
'entities' => $this->getObjectManager()->findBy([
'foo' => 'bar'
])
]);
}
That's pretty much how you will do it. Next question for you should be: What does getObjectManager() do or how do i properly inject the ObjectManager into the Controller?
The answer is as simple: You have to use Controller-Factories
//module.config.php
'controllers' => [
'factories' => [
'YourNamespace\Controller\FooController' => function($cpm) {
$ctrl = new FooController();
$ctrl->setObjectManager(
$cpm->getServiceLocator()->get('Doctrine\ORM\EntityManager')
);
return $ctrl;
}
]
]
And that's all there is to it. Now you have a very clean setup of what you want to achieve ;)

Categories