Laravel cache with route model binding? - php

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

Related

Properly cache a type-hinted model in Laravel

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

How to handle eager loading on REST API

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.

Laravel Policies - How to Pass Multiple Arguments to function

I'm trying to authorize a users character to delete/update post. I was using policies to do so, but I could only pass one parameter to the policy function. If I pass more than the user and another variable, the variable isn't passed into the function.
Models: User has many characters, a character can post multiple posts. So for authorization purposes, I would have to compare the post's character_id with the current character's id...-
Per the docs, you can pass more multiples to the Gate Facade:
Gate::define('delete-comment', function ($user, $post, $comment) {
//
});
But I couldn't find anyway to do so with policies. What I had to do was to inject the Request object to get the object needed for authorization. Basically I wouldn't even need the User Object.
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
Using the Request object works, but it feels very hacky. Is there a nicer way to achieve this?
edit:
In the CharacterLocationController I have a method show and I want to authorize the action before showing the resource.
public function show(Request $request, Character $character, Location $location)
{
$this->authorize([$location, $character]);
...
}
The policy is registered like this: 'App\Location' => 'App\Policies\LocationPolicy' in the AuthServiceProvider
I dumped the array passed to the policy function, and it only outputs the $location.
public function show(User $user, $data) {
dd($data); // expecting location and character
return !$location->private || $location->authorized->contains($this->character);
}
I think there is possibly some confusion here on what functions are doing what.
When you use
Gate::define('delete-comment', function ($user, $post, $comment) {
//
});
Or in the CommentPolicy
public function delete(User $user, Post $post, Comment $comment)
{
return $user->id === $post->user_id;
}
All you are doing is defining the rules. At this point, we aren't worried about passing anything, only that the objects we received can or should be able to interact with each other. The only difference between these two is when using policies, it's just an easy way to abstract all your rules into one simple and easy to read class. If you have an app with potentially hundreds of tables and models, it will get confusing fast if you have these rules littered all over your app so policies would help to keep them all organized.
It's when you are actually checking if someone has permission to do something when you should be passing these items along. For example, when you do the following,
if (Gate::allows('delete-comment', [$post, $comment])) {
//
}
Or if in the CommentController
$this->authorize('delete', [$post, $comment]);
That is what controls which parameters are going to be passed to the policy or the Gate::define method. According to the docs, the $user parameter is already added for you so in this case, you only need to worry about passing the correct $post and $comment being modified.

Non-public accessible function in CakePHP

I have built a simple Notification system in my Cake app that I want to have a function that will create a new notification when I call a certain method. Because this is not something the user would actually access directly and is only database logic I have put it in the Notification model like so:
class Notification extends AppModel
{
public $name = 'Notification';
public function createNotification($userId, $content, $url)
{
$this->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
}
And then whenever I want to create a notification within my app I just do:
$this->Notification->createNotification($userId,'Test','Test');
However this doesn't work! The controller is talking to the model fine, but it doesn't create the row in the database... I'm not sure why... but it would seem I'm doing this wrong by just doing all the code in the model and then calling it across the app.
Edit: Based on answers and comments below, I have tried the following the code to create a protected method in my notifications controller:
protected function _createNotification($userId, $content, $url)
{
$this->Notification->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
Now the thing that is stumping me still (apologies if this is quite simple to others, but I have not used protected methods in CakePHP before) is how do I then call this from another controller? So for example If have a method in my PostsController and want to create a notification on successful save, how would I do this?
I thought about in my PostsController add method:
if($this->save($this->request-data){
$this->Notification->_createNotification($userId,'Test','Test');
}
But being protected I wouldn't be able to access the method from outside of the NotificationsController. Also I'm using the same syntax as if I was calling a function from a model so again it doesn't feel right.
Hopefully someone can help me out and get me back on track as this is a new area to me.
the controller should pass all data to the model
$this->createNotification($this->request->data);
the model then can use the data:
public function createNotification(array $data) {
$key = $data[$this->alias]['key'];
$data[...] = ...;
$this->create();
return $this->save($data);
}
you never ever try to access the controller (and/or its request object) from within a model.
you can also invoke the method from other models, of course:
public function otherModelsMethod() {
$this->Notification = ClassRegistry::init('Notification');
$data = array(
'Notification' => array(...)
);
$this->Notification->createNotification($data);
}
and you can make your methods verbose, but that usually makes it harder to read/understand/maintain with more and more arguments:
public function createNotification($userId, $content, $url) {
$data = array();
// assign the vars to $data
$data['user_id'] = $userId;
...
$this->create();
return $this->save($data);
}
so this is often not the cake way..
Methods in a model are not "publicly accessible" by definition. A user cannot call or invoke a method in a model. A user can only cause a controller action to be initiated, never anything in the model. If you don't call your model method from any controller, it's never going to be invoked. So forget about the "non-public" part of the question.
Your problem is that you're working in the model as if you were in a controller. There is no request object in a model. You just pass a data array into the model method and save that array. No need for $this->request. Just make a regular array(), put the data that was passed by the controller in there and save it.
The whole approach is totally wrong in the MVC context IMO and screams for the use of the CakePHP event system. Because what you want is in fact trigger some kind of event. Read http://book.cakephp.org/2.0/en/core-libraries/events.html
Trigger an Event and attach a global event listener that will listen for this kind of events and execute whatever it should do (save something to db) when an event happens. It's clean, flexible and extendible.
If you did a proper MVC stack for your app most, if not all, events aka notifications should be fired from within a model like when a post was saved successfully for example.
This is what I have ended up doing. While it certainly isn't glamorous. It works for what I want it to do and is a nice quick win as the notifications are only used in a few methods so I'm not creating a large amount of code that needs improving in the future.
First to create a notification I do the following:
$notificationContent = '<strong>'.$user['User']['username'].'</strong> has requested to be friends with you.';
$notificationUrl = Router::url(array('controller'=>'friends','action'=>'requests'));
$this->Notification->createNotification($friendId,$notificationContent,$notificationUrl);
Here I pass the content I want and the URL where the user can do something, in this case see the friend request they have been notified about. The url can be null if it's an information only notification.
The createNotification function is in the model only and looks like:
public function createNotification($userId, $content, $url = null)
{
$this->saveField('user_id',$userId);
$this->saveField('content',$content);
$this->saveField('url',$url);
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
This creates a new record in the table with the passed content, sets its status to 0 (which means unread) and the date it was created. The notification is then set as read when a user visits the notifications page.
Again this is most probably not an ideal solution to the problem outlined in this question... but it works and is easy to work with And may prove useful to others who are learning CakePHP who want to run functions from models when building prototype apps.
Remember nothing to stop you improving things in the future!
First of all, you can improve your last solution to do one save() (instead of 5) the following way:
public function createNotification($userId, $content, $url = null){
$data = array(
'user_id' => $userId,
'content' => $content,
'url' => $url,
'datetime' => date('Y-m-d H:i:s'),
'status' => 0
);
$this->create();
$this->save($data);
}
When I began programming CakePHP(1.3) more than a year ago I also had this problem.
(I wanted to use a function of a controller in any other controller.)
Because I didn't know/researched where to place code like this I've done it wrong for over a year in a very big project. Because the project is really really big I decided to leave it that way. This is what i do:
I add a function (without a view, underscored) to the app_controller.php:
class AppController extends Controller {
//........begin of controller..... skipped here
function _doSomething(){
//don't forget to load the used model
$this->loadModel('Notification');
//do ur magic (save or delete or find ;) )
$tadaaa = $this->Notification->find('first');
//return something
return $tadaaa;
}
}
This way you can access the function from your Notification controller and your Posts controller with:
$this->_doSomething();
I use this kind of functions to do things that have nothing to do with data submittance or reading, so i decided to keep them in the app_controller. In my project these functions are used to submit e-mails to users for example.. or post user actions to facebook from different controllers.
Hope I could make someone happy with this ;) but if you're planning to make a lot of these functions, it would be much better to place them in the model!

Zend cache frontend configuration

Zend_Cache can be configured to cache several types of output, including
the results of function calls, the results of object and static method calls,
entire pages, and configuration data.
Given this controller and related views how would you go with caching?From what some of you suggested here (see #marcin) I understood that clearing the whole cache for just a single comment or a post-update would be too much.How should I go for them to be cached separately?
Basically I have a blog page where I'm loading all of the posts with relative users comments.
-Index controller (home-page):
class IndexController extends Zend_Controller_Action
{
public function indexAction()
{
//Post is a db_table model
$post_manager=new Post();
$allPosts=$post_manager->getAllPosts();
$this->view->posts=$allPosts;
}
}
-index.phtml:
echo $this->partialLoop('_posts.phtml',$this->posts);
-_posts.phtml:
echo $this->object->title;
echo $this->object->text;
echo $this->partialLoop('_comments.phtml',$this->object->getComments());
-_comments.phtml:
echo $this->object->text;
Please post practical examples
thanks again
Sorry I did not replay earlier. Quickly, I'll would like to present one way of catching this for your consideration. For simplicity, I'll just concentrate on caching your outputs using Output frontend.
In you application.ini you can setup your catching as follows:
resources.cachemanager.myviewcache.frontend.name = Output
resources.cachemanager.myviewcache.frontend.customFrontendNaming = false
resources.cachemanager.myviewcache.frontend.options.lifetime = 7200
resources.cachemanager.myviewcache.frontend.options.caching = true
resources.cachemanager.myviewcache.frontend.options.automatic_serialization = true
resources.cachemanager.myviewcache.backend.name = Apc
Note, that I use Apc as a backend. You may use file backend if you don't have or don't want Apc.
With this, I would cache your posts and comments separately. For example, in _posts.phtml you could do something similar to the following:
// first cache an output related to the body of your post with key being associated
// with your post.
if (!($this->viewCache()->start('post_' . (string) $this->object->post_id))) {
echo $this->object->title;
echo $this->object->text;
}
// now I cache an output of a comments associated with a give post
if (!($this->viewCache()->start('post_comments_' . (string) $this->object->post_id))) {
echo $this->partialLoop('_comments.phtml',$this->object->getComments());
}
In this example, viewCache() view helper is as follows:
class My_View_Helper_ViewCache extends Zend_View_Helper_Abstract {
/**
*
* #return Zend_Cache_Frontend_Output
*/
public function viewCache() {
return Zend_Registry::get('outputCache');
}
}
Whereas I set outputCache into registry in the Bootstrap.php:
protected function _initPutChachesIntoRegistry() {
$this->bootstrap('cachemanager');
$cacheManager = $this->getResource('cachemanager');
Zend_Registry::set('outputCache', $cacheManager->getCache('myviewcache'));
}
Notice that caches are associated with keys which in turn relate to a given post and its comments. With this, when you get a new comment, you just reset a cache related to the given post. For example, in an action, you can remove comment cache for a post with $post_id=5 as follows:
$this->view->viewCache()->remove('post_comments_' . $post_id);
Hope that this will help you or at least give you some ideas how to make it.
I don`t know how to do better, but your can do like this:
Write plugin, that will be cache response of controller using postDispatch, and restore it if it in cache using preDispatch

Categories