Laravel 5: How to create a router model binding on multiple parameters - laravel

So far I know how to create a router model binding on single parameters like so:
// RouteServiceProvider.php
$router->model('subject_slug', 'App\Subject', function($slug) {
return Subject::where('slug', $slug)->firstOrFail();
});
The above can then be used like this:
// routes.php
Route::get('/{subject_slug}', 'MenuController#showSubject');
And in the controller:
public function showSubject(Subject $subject) {
....
}
But sometimes I need to specify multiple parameters in order to get the right model.
For example consider the following route:
Route::get('/{subject_slug}/{topic_slug}/', 'MenuController#showTopic');
and the corresponding controller:
public function showTopic(Subject $subject, Topic $topic) {
....
}
However to get the correct model for Topic I need to know the Subject. For example:
// !!! Invalid laravel code !!!
$router->model('topic_slug', 'App\Topic, function($subject_slug, $topic_slug) {
// ERROR: $subject_slug is obviously not defined!
return Topic::where([
'subject_slug' => $subject_slug,
'slug' => $topic_slug,
])->firstOrFail();
});
How can I make a router model binding for Topic bearing in mind I need to know the Subject parameter before it in order to fetch the correct Topic.
Is there an alternative better way of doing this?
UPDATE
Currently my showTopic method in my controller is like this:
public function showTopic(Subject $subject, $topic_slug) {
$topic = Topic::where([
'subject_slug' => $subject_slug,
'slug' => $topic_slug,
])->firstOrFail();
// ...
}
and I have no router model binding for topic_slug.
This works as expected, but I would like to take advantage of router model bindings!

It turns out the way I was doing it was a bit flawed. I was unnessarily using model bindings when instead it would be better to have used a normal binding like so:
$router->bind('topic_slug', function($slug, Route $route) {
$subject = $route->parameter('subject_slug');
return Topic::where([
'subject_slug' => $subject->slug,
'slug' => $slug,
])->firstOrFail();
});
Also I was using model bindings completely wrong before as the 3rd function should be the "not found behaviour" (not for additional logic)!

Related

Laravel 8: Storing custom route attributes

What is the most simple way to store custom attributes such as page titles or other key value pairs that may be attached to a route?
For example, say I want to add my own metadata data to:
Route::get('/themetest', [MyController::class, 'list'])->name('themetest');
I thought I could add a route macro to save metadata to be retrieved later using an addMetadata method like
Route::get('/themetest', [MyController::class, 'list'])->name('themetest')->addMetadata('title' => 'Page Title');
Is that possible? Doesn't seem like it is.
Is there a standard way to store this type of info? Or, any practical way? I thought maybe I could store them using default(), but that could change the default parameters for a controller function.
You could use the 'action' array of the Route to store this information if you had to:
// In a Service Provider # boot
Illuminate\Routing\Route::macro('addMetaData', function ($key, $value) {
$this->action['meta'][$key] = $value;
return $this;
});
Illuminate\Routing\Route::macro('getMetaData', function ($key = null) {
return is_null($key)
? $this->getAction('meta')
: $this->getAction('meta.'. $key);
});
// Route definition
Route::get('/themetest', [MyController::class, 'list'])
->name('themetest')
->addMetaData('title', 'Page Title');
// Controller method (Route action)
public function list(Request $request)
{
dump($request->route()->getMetaData('title'));
}

Simplify API boilerplate in Laravel's controllers?

When I write APIs with Laravel, I often use the same method as GitHub API v3. I add URLs to ease the navigation during development and also for the users that will develop using that API.
In this example, I manually add the URLs on every field then add a count for the pagers on the frontend. I sometime have more complicated stuff if I want to add the necessary to filter the results (if used with Vuetify or Kendo Grid).
class UserController extends Controller
{
function index() {
$users = User::all()->each(function ($item, $key) {
$item['activities'] = url("/api/users/{$item['id']}/activities");
$item['classrooms'] = url("/api/users/{$item['id']}/classrooms");
});
return [
'count' => count($users),
'courses' => $users,
];
}
}
Is there a way to make this code less boilerplate? Is there a package that does everything out of the box?
I'm a big fan of fractal especially spaties fractal package. This enables you to transform objects into responses.
There are two concepts in fractal serializers and transformers. Serializers is about the whole request, meta information data etc. Transformer is how you transform each model or object. Normally you would not make custom serializers, but in your case in can solve most of your trouble.
use League\Fractal\Serializer\ArraySerializer;
class YourSerializer extends ArraySerializer {
public function collection($resourceKey, array $data)
{
return [
$resourceKey => $data,
'count' => count($data),
];
}
}
Create your transformer for your user. The other big thing you gain for using this, is you have one plays to be responsible for transformation. Instead of each controller having to have the logic, which will be spread out and you have to remember to include it.
use League\Fractal\TransformerAbstract;
class AccountBalanceTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'id' => $user->id,
'name' => $user->id,
// other user fields
'activities' => url("/api/users/{$user->id}/activities"),
'classrooms' => url("/api/users/{$user->id}/classrooms"),
];
}
}
You have to assign the serializer in your fractal.php config.
'default_serializer' => YourSerializer::class,
Now you should be able to transform you responses like so.
return fractal($users, new UserTransformer())
->withResourceName('courses')
->respond(Response::HTTP_OK);
For making it easier to use and avoid repeating your self, i usually do a method on a parent controller like so. While setting the transformer on the object.
public function respond($data, $resourceKey, $status = Response::HTTP_OK) {
return fractal($data, $this->transformer)
->withResourceName($resourceKey)
->respond($status);
}
This will produce a response similar to what was specified in the question.

Laravel - Change URL parameters using GET

I have RESTful API built on Laravel.
Now I'm passing parameter like
http://www.compute.com/api/GetAPI/1/1
but I want to pass parameter like
http://www.compute.com/api/GetAPI?id=1&page_no=1
Is there a way to change Laravel routes/functions to support this?
you can use link_to_route() and link_to_action() methods too.
(source)
link_to_route take three parameters (name, title and parameters). you can use it like following:
link_to_route('api.GetAPI', 'get api', [
'page_no' => $page_no,
'id' => $id
]);
If you want to use an action, link_to_action() is very similar but it uses action name instead of route.
link_to_action('ApiController#getApi', 'get api', [
'page_no' => $page_no,
'id' => $id
]);
href text
with these methods anything after the expected number of parameters is exceeded, the remaining arguments will be added as a query string.
Or you can use traditional concatination like following:
create a route in routes.php
Route::get('api/GetAPI', [
'as' => 'get_api', 'uses' => 'ApiController#getApi'
]);
while using it append query string like this. you can use route method to get url for required method in controller. I prefer action method.
$url = action('ApiController#getApi'). '?id=1&page_no=1';
and in your controller access these variables by following methods.
public function getApi(Request $request) {
if($request->has('page_no')){
$page = $request->input('page_no');
}
// ...your stuff
}
Or by Input Class
public function getApi() {
if(Input::get('page_no')){
$page = Input::get('page_no');
}
// ...your stuff
}
Yes you can use those parameters, then in your controllers you can get their values using the Request object.
public function index(Request $request) {
if($request->has('page_no')){
$page = $request->input('page_no');
}
// ...
}

Laravel resource: How define the name of the key parameter?

When you define a resource with Route::resource('recipe', 'RecipeController');, among others, the following route is defined: /photo/{photo}/edit, and once you define all your resources you have something like this:
/recipes/{recipes}/edit
/allergens/{allergens}/edit
/ingredients/{ingredients}/edit
Because all my records use id as primary key (MongoDB), I'd like to have {id} instead, like so:
/recipes/{id}/edit
/allergens/{id}/edit
/ingredients/{id}/edit
I dug in the Router class but I don't see how to specify this.
More over when I create a form with Form::model($record) I get actions like /recipes/{recipes} because recipes is a property of $record.
How can I define the name of the key parameter to id instead of recipes, allergens, ingredients?
In order to change the param name for Route::resource, you need custom ResourceRegistrar implementation.
Here's how you can achieve that in a shortest possible way:
// AppServiceProvider (or anywhere you like)
public function register()
{
$this->app->bind('Illuminate\Routing\ResourceRegistrar', function ($app) {
// *php7* anonymous class for brevity,
// feel free to create ordinary `ResourceRegistrar` class instead
return new class($app['router']) extends \Illuminate\Routing\ResourceRegistrar
{
public function register($name, $controller, array $options = [])
{
if (str_contains($name, '/')) {
return parent::register($name, $controller, $options);
}
// ---------------------------------
// this is the part that we override
$base = array_get($options, 'param', $this->getResourceWildcard(last(explode('.', $name))));
// ---------------------------------
$defaults = $this->resourceDefaults;
foreach ($this->getResourceMethods($defaults, $options) as $m) {
$this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
}
}
};
});
}
Now your routes will look like:
Route::resource('users', 'UsersController', ['param' => 'some_param'])
/users/{some_param}
// default as fallback
Route::resource('users', 'UsersController')
/users/{users}
Mind that this way can't work for nested resources and thus they will be a mix of default and custom behaviour, like this:
Route::resource('users.posts', 'SomeController', ['param' => 'id'])
/users/{users}/posts/{id}
I know this is 4 year old question but for anyone who is googling; you can pass a third argument to override key naming:
Route::resource('ingredients', 'IngredientController', ['parameters' => ['ingredients' => 'id']]);
Or
Route::resource('ingredients', 'IngredientController')->parameters(['ingredients' => 'id']);
You can pass your ids to the routes you don't necessary need to change the parameters {recipes} to {id} since the parameters are just a placeholder.
So
public function edit($recipes){
// code goes hr
}
is the same as this
public function edit($id){
// code goes hr
}
for this route /recipes/{recipes}/edit

Multiple routes to same Laravel resource controller action

I like to use resource controllers in Laravel, as it makes me think when it comes to data modelling. Up to now I’ve got by, but I’m now working on a website that has a public front-end and a protected back-end (administration area).
I’ve created a route group which adds an “admin” prefix, like so:
Route::group(array('before' => 'auth', 'prefix' => 'admin'), function()
{
Route::resource('article', 'ArticleController');
Route::resource('event', 'EventController');
Route::resource('user', 'UserController');
});
And I can access the methods using the default URL structure, i.e. http://example.com/admin/article/1/edit.
However, I wish to use a different URL structure on the front-end, that doesn’t fit into what resource controllers expect.
For example, to access an article, I’d like to use a URL like: http://example.com/news/2014/06/17/some-article-slug. If this article has an ID of 1, it should (under the hood) go to /article/1/show.
How can I achieve this in Laravel? In there some sort of pre-processing I can do on routes to match dates and slugs to an article ID, and then pass that as a parameter to my resource controller’s show() method?
Re-visiting this, I solved it by using route–model binding and a pattern:
$year = '[12][0-9]{3}';
$month = '0[1-9]|1[012]';
$day = '0[1-9]|[12][0-9]|3[01]';
$slug = '[a-z0-9\-]+';
// Pattern to match date and slug, including spaces
$date_slug = sprintf('(%04d)\/(%02d)\/(%02d)\/(%s)', $year, $month, $day, $slug);
Route::pattern('article_slug', $date_slug);
// Perform the route–model binding
Route::bind('article_slug', function ($slug) {
return Article::findByDateAndSlug($date_slug);
});
// The actual route
Route::get('news/{article_slug}', 'ArticleController#show');
This then injects an Article model instance into my controller action as desired.
One simple solution would be to create one more route for your requirement and do the processing there to link it to the main route. So, for example:
//routes.php
Route::get('/arical/{date}/indentifier/{slug}', array (
'uses' => 'ArticleController#findArticle'
));
//ArticleContoller
public function findArticle($date,$slug){
$article = Article::where('slug','=','something')->first(); //maybe some more processing;
$article_id = $article->id;
/*
Redirect to a new route or load the view accordingly
*/
}
Hope this is useful.
It seems like if Laravel 4 supports (:all) in routing, you would be able to do it with ease, but unfortunately (:all) is not supported in Laravel 4.
However, Laravel 4 allows detecting routes by regular expression, so we can use ->where('slug', '.*').
routes.php: (bottom of the file)
Route::get('{slug}', 'ArticleController#showBySlug')->where('slug', '.*');
Since Laravel will try to match the top most route in routes.php first, we can safely put our wildcard route at the bottom of routes.php so that it is checked only after all other criteria are already evaluated.
ArticleController.php:
class ArticleController extends BaseController
{
public function showBySlug($slug)
{
// Slug lookup. I'm assuming the slug is an attribute in the model.
$article_id = Article::where('slug', '=', $slug)->pluck('id');
// This is the last route, throw standard 404 if slug is not found.
if (!$article_id) {
App::abort(404);
}
// Call the controller's show() method with the found id.
return $this->show($article_id);
}
public function show($id)
{
// Your resource controller's show() code goes here.
}
}
The code above assumes that you store the whole URI as the slug. Of course, you can always tailor showBySlug() to support a more advanced slug checking.
Extra:
You could also do:
Route::get('{category}/{year}/{slug}', 'ArticleController#showBySlug')->where('slug', '.*');
And your showBySlug() would just have additional parameters:
public function showBySlug($category, $year, $slug)
{
// code
}
Obviously you can extend to month and day, or other adaptations.

Resources