Imagine you have the following resources for example: Users, Posts and Comments (With typical relationship setup in laravel).
When fetching a single Post, you will have the following endpoint
GET /api/posts/1
// With route model binding
public function show(Post $post)
{
return $post;
}
This is fine if I only want the Post object, but in some pages in my application, I also need to load the User and the Comments associated with the Post.
How do you guys handle that kind of scenario?
1. Should I load everything in that endpoint like:
return $post->load(['user', 'comments.user']);
and call it a day? (nope)
2. Should I accept an additional parameter that will tell my controller to load the relationship based on that value?
// With route model binding
public function show(Request $request, Post $post)
{
// rel for "relationship"
if ($request->has('rel')) {
$post->load($request->input('rel'));
}
return $post;
}
with this approach I could do something like this:
GET /api/posts/1?rel=user
returns Post with User
or I could build an array of parameter with jquery's $.param(['user', 'comments.user'])
GET /api/posts/1?rel%5B%5D=user&rel%5B%5D=comments.user
returns Post with User + Comments.User
but anyone can easily mess with the 'rel' parameter so I also need to check that
¯\(°_o)/¯
3. Just create a new endpoint for every specific requirements. (what should your endpoint look like for the example above?).
I'm building a SPA with Angular + Laravel (just a self-consumed API) for my Internal Project when I encounter this pitfall. The second approach is what I currently using for basic fetching and I use the third approach for more complex requirements.
Any inputs are appreciated.
Related
I'm using Redis to cache different parts of my app. My goal is to not make a database query when the user is not logged in, as the app's content don't get updated regularly.
I cache the archive queries in my controller, however when I type hint a model in the controller, the model is retrieved from the database and then passed to the controller:
// My route
Route::get('page/{page:id}', [ PageController::class, 'show' ] );
// My controller
public function show ( Page $page ) {
// Here, the $page will be the actual page model.
// It's already been queried from the database.
}
What I'm trying to do is to try and resolve the page from the cache first, and then if the cache does not contain this item, query the database. If I drop the Page type-hint, I get the desired result ( only the id is passed to controller ) but then I will lose the benefit of IoC, automatic ModelNotFoundException, and more.
I've come across ideas such as binding the page model to a callback and then parsing the request(), but seems like a bad idea.
Is there any way to properly achieve this? I noticed that Laravel eloquent does not have a fetching event, which would be perfect for this purpose.
You can override the default model binding logic:
Models\Page.php
public function resolveRouteBinding($value, $field = null)
{
return \Cache::get(...) ?? $this->findOrFail($value);
}
Read more here https://laravel.com/docs/8.x/routing#customizing-the-resolution-logic
In order to check for existence of the data in Redis, you shouldn't type-hint the model into the controller's action. Do it like this:
public function show($pageId) {
if(/* check if cached */) {
// Read page from cache
} else {
Page::where('id', $pageId)->first();
}
}
I'm adding caching to my Laravel app routes. I have a function that renders a blog post on my site:
public function show(Post $post)
{
SEO::setTitle($post->title);
SEO::setDescription($post->subtitle);
SEO::setCanonical('https://employbl.com/blog/' . $post->slug);
SEO::opengraph()->setUrl('https://employbl.com/blog/' . $post->slug);
SEO::opengraph()->addProperty('type', 'article');
SEO::opengraph()->addImage($post->featured_image);
SEO::twitter()->setSite('#Employbl_Jobs');
$markdown = Markdown::parse($post->body);
return view('blog.post', compact('post', 'markdown'));
}
This is the route that calls the method: Route::get('/blog/{post}', 'PostController#show')->name('posts.show'); so that my blog renders a URL with a slug like: https://employbl.com/blog/laravel-vue-tailwindcss-single-page-application-spa
What is the best way to implement caching on this route so the page loads faster for users?
Would it be something like:
$post = Cache::rememberForever('blog-post' . $post->id, function(){
return $post;
});
Or is caching even necessary with route model binding? Does the cache key need to be unique or can I just use "blog-post" as cache key? Would it be better to cache the $markdown variable instead of the $post variable? Both?
You've got a few questions in here, so I'll try to answer each. The answers may not be letter perfect as I am going from memory without any way to reference or confirm them myself at the moment.
If you're trying to cache the final output of your view, you can effectively do it be replacing your final view call with:
return Cache::rememberForever('blog-post' . $post->id, function() use ($post) {
// Do your SEO and markdown stuff here
return view('blog.post', compact('post', 'markdown'))->render();
});
The cache key needs to be unique for the post. The model routing system knows nothing about the cache system, it's just a way of passing a value to a controller which makes some assumptions about the incoming data based on the URI. So what you are doing currently is fine.
The problem your question about should I cache the post, the markdown or both? is that it probably won't make a difference
1) You're calling a model GET route. This has the effect of loading the Post from the DB each time, making the caching of the Post itself irrelevant. This is true even with the caching of the render view itself.
2) Your view call requires the Post itself as a parameter [of compact()]. You'll need to load it from somewhere, so that means a database call again to retrieve the post.
3) You're using Cache::rememberForever which means the cache will never expire. So loading the Post after the first time will be pointless, since it will never be used again (the results are cached forever!). Future edits (if any) won't work unless you invalidate the cache (which makes rememberForever kind of pointless).
So, I recommend, for this case, that you move away from the model route and instead try a traditional id based Route
public function show(Request $request, $id)
{
return Cache::remember('blog-post'.$id, ttl, function() use($id) {
$post = Post::find($id);
// Do SEO and markdown stuff
return view('blog.post', compact('post', 'markdown'))->render();
});
}
where ttl is the time for the cache to expire.
I was looking to solve a similar issue with caching models that were bound using Route Model Binding and found the following solution.
// On the Model class add the following method.
public function resolveRouteBinding($value, $field = null): ?Model
{
return Cache::remember('my.custom.key'.$value, 3600, function () use ($value) {
return $this->where('slug', $value)->firstOrFail();
});
}
The method details can be found here: Customizing Resolution Logic
It's worth noting that there's a very possible chance that you'd rather use this without the Cache::remember() method so that you're not caching something that returns null. It may be better to do this in the following way instead:
// On the Model class add the following method.
public function resolveRouteBinding($value, $field = null): ?Model
{
$cacheName = "my.custom.key.{$value}";
if (Cache::has($cacheName)) {
return Cache::get($cacheName);
}
$result = $this->query('slug', $value)->firstOrFail();
Cache::put($cacheName, $result, 3600);
return $result;
}
I have a controller for ProductController. I have 4 standard methods bound to respective HTTP methodslike
public function index() // GET
public function create() // POST
public function update() // PUT
public function destroy() //DELETE
So far so good, but i have to make few other functions like getProductsByCategory, getProductsAttributes() etc etc. After implementing this, Will my API still be called REST ? If not than how can i handle these requirements
Thanks
Resource URI for getProductsByCategory(...):
GET /products?category=books HTTP/1.1
Host: service.org
Resource URI for getProductsAttributes():
GET /products/bmw-528i/attributes HTTP/1.1
Host: service.org
How you implement handling of these request URIs is a implementation detail. If you are using some framework, you can do something like this:
Routes::add("/products/{product-id}/attributes", function($request, $response) {
// do something here
});
But it is a detail that can not affect RESTfullness of your service.
First off, REST is not a strict standard. The methods you posted comply the REST conventions but a REST service must have several other properties. The two most important ones are:
statelessness: no session, no cookies, authorization on a per-request basis
GET requeste never change any resource
There are other ones, feel free to edit or add in the comments.
The way i see such operations on resources implemented most of the time is:
/<resource-name>/<product-id>/<operation>
For example:
GET /product/<product-id>
GET /product/<product-id>/related
POST /product/<product-id>/purchase
GET /categories/tools
I would like to pass the id of an object to a controller in laravel. Basically I've a page that displays the information relating to a book. The user has the option to add themes to that book. The line below is the link they click in order to open the create theme form:
Add Theme
and the ThemeController method:
public function create(){
$books = Book::all();
return View::make('theme.create', compact('books'));
}
Currently this brings up a Form with a list of all of my books in a dropdown. I would rather just pass the id of the book the user was on prior to being brought to this form and use that instead. Is there any way I can do that? I've also included my routes.php file:
Route::model('book', 'Book');
//Show pages
Route::get('/books', 'BookController#index');
Route::get('book/create', 'BookController#create');
Route::get('book/edit/{book}', 'BookController#edit');
Route::get('book/delete/{book}', 'BookController#delete');
Route::get('book/view/{book}', 'BookController#view');
////Handle form submissions
Route::post('book/create', 'BookController#handleCreate');
Route::post('book/edit', 'BookController#handleEdit');
Route::post('book/delete', 'BookController#handleDelete');
//Themes
Route::model('theme', 'Theme');
Route::get('/themes', 'ThemeController#index');
Route::get('theme/create', 'ThemeController#create');
Route::get('theme/edit/{book}', 'ThemeController#edit');
Route::get('theme/delete/{book}', 'ThemeController#delete');
Route::get('theme/view/{book}', 'ThemeController#view');
Route::post('theme/create', 'ThemeController#handleCreate');
Route::post('theme/edit', 'ThemeController#handleEdit');
Route::post('theme/delete', 'ThemeController#handleDelete');
You can still use normal $_GET methodology.
On your route:
book/create
You could simply request.
book/create?book_id=xxyy
Then in your controller create function
public function create(){
$bookId = Input::get('book_id');
$books = Book::all()->where('book_id' => $bookId);
return View::make('theme.create', compact('books'));
}
PLEASE NOTE:
I haven't used Laravel in a while, don't trust that elequont query :P.. I was just putting a theoretical "where" in there.
And here http://laravel.com/docs/requests#basic-input, you will note this:
"You do not need to worry about the HTTP verb used for the request, as
input is accessed in the same way for all verbs."
So whether you're using get, put, post or delete, whatever you wrapped in your header, you will be able to find using Input Class.
EDIT
Just needed to add, that you won't need to modify your routes at all. This is just a Simple GET parameter in a GET request. Just like in any other application.
I'm writing a control panel for my image site. I have a controller called category which looks like this:
class category extends ci_controller
{
function index(){}// the default and when it called it returns all categories
function edit(){}
function delete(){}
function get_posts($id)//to get all the posts associated with submitted category name
{
}
}
What I need is when I call http://mysite/category/category_name I get all the posts without having to call the get_posts() method having to call it from the url.
I want to do it without using the .haccess file or route.
Is there a way to create a method on the fly in CodeIgniter?
function index(){
$category = $this->uri->segment(2);
if($category)
{
get_posts($category); // you need to get id in there or before.
}
// handle view stuff here
}
The way I read your request is that you want index to handle everything based on whether or not there is a category in a uri segment. You COULD do it that way but really, why would you?
It is illogical to insist on NOT using a normal feature of a framework without explaining exactly why you don't want to. If you have access to this controller, you have access to routes. So why don't you want to use them?
EDIT
$route['category/:any'] = "category/get_posts";
That WOULD send edit and delete to get_posts, but you could also just define those above the category route
$route['category/edit/:num'] = "category/edit";
$route['category/delete/:num'] = "category/delete";
$route['category/:any'] = "category/get_posts";
That would resolve for the edit and delete before the category fetch. Since you only have 2 methods that conflict then this shouldn't really be that much of a concern.
To create method on the fly yii is the best among PHP framework.Quite simple and powerful with Gii & CRUD
http://www.yiiframework.com/doc/guide/1.1/en/quickstart.first-app
But I am a big CI fan not Yii. yii is also cool though.
but Codeigniter has an alternative , web solution.
http://formigniter.org/ here.