Invalidate Shopware 6 page cache on entity update via API - caching

We created a custom CMS element which displays entries which are managed via API.
Now when entries are updated and Shopware 6 runs ins production mode, the changes are not reflected on the page. I believe this is due to the page cache. (APP_ENV=prod)
What do we have to do to invalidate the cache automatically?
I checked the docs, but did not find the necessary information.
For the product box it works: When I place a product CMS element on the main page and change the product, the page is updated when I reload in the browser.
I was expecting to find some hint in \Shopware\Core\Content\Product\Cms\ProductBoxCmsElementResolver but there are no cache tags or something like this added there.
EDIT: Actually I was a bit inaccurate. The page I have lists the entities, and it is a category page.
So I believe I have too hook into CategoryRouteCacheTagsEvent.
For testing I hard-coded into:
\Shopware\Core\Content\Category\SalesChannel\CachedCategoryRoute::extractProductIds
$slots = $page->getElementsOfType('jobs');
/** #var CmsSlotEntity $slot */
foreach ($slots as $slot) {
$box = $slot->getData();
$ids = array_merge($ids, $box['jobs']->getIds());
}
But this does not yet work.
PS: Also I noticed some code duplication in the core in \Shopware\Core\Content\Category\SalesChannel\CachedCategoryRoute::extractProductIds and \Shopware\Core\Content\LandingPage\SalesChannel\CachedLandingPageRoute::extractIds

The Shopware\Core\Framework\Adapter\Cache\CacheInvalidationSubscriber listens to a lot of events, including indexer and entity-written events. This in turn uses the CacheInvalidator to invalidate cached data based on tags/cache keys.
You should be able to add invalidation based on your own entity in a similar fashion.
For this to work with a custom entity, you will probably have to tag any cache entries with something you can generate on invalidation. For CMS pages, I would probably start with the CachedLandingPageRoute as a reference.

I suggest you should have a look at the CacheInvalidationSubscriber and its service definition. You can see that there are already a bunch of events that are dispatched when write operations to certain entities occur. When you then look at the respective handler you can see how it invalidates the cache for whatever kind of routes it should affect.
When you speak of entries I assume you implemented your own custom entities for use in your CMS element? If that is the case just replicate the listener for your own entities. Otherwise you'll have to look for another event that is dispatched once you save your changes and then invalidate the cache likewise.

Based on the answers of dneustadt and Uwe, as for the job listings I solved it like with this two subscribes. I do not need any single ID here, because the full listing page has to be invalidated in case a job is deleted or added. This is why I went with the any-jobs tag:
use Shopware\Core\Content\Category\Event\CategoryRouteCacheTagsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CacheKeyEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
CategoryRouteCacheTagsEvent::class => 'generateTags'
];
}
public function generateTags(CategoryRouteCacheTagsEvent $event): void
{
$page = $event->getResponse()->getCategory()->getCmsPage();
$slots = $page->getElementsOfType('jobs');
if (!empty($slots)) {
$event->setTags(
array_merge($event->getTags(), ['any-jobs'])
);
}
}
}
and
class CacheInvalidationSubscriber implements EventSubscriberInterface
{
private CacheInvalidator $cacheInvalidator;
public static function getSubscribedEvents(): array
{
return [
EntityWrittenContainerEvent::class => 'invalidateJobs'
];
}
public function __construct(CacheInvalidator $cacheInvalidator)
{
$this->cacheInvalidator = $cacheInvalidator;
}
public function invalidateJobs(EntityWrittenContainerEvent $event): void
{
if (!empty($event->getPrimaryKeys(\ApplicationManagement\Core\Content\JobAd\JobAdDefinition::ENTITY_NAME))) {
$this->cacheInvalidator->invalidate(
['any-jobs']
);
}
}
}

Related

Updating a session and using it within my view

I have a wish list, that is throughout the shopping pages. I need to know if this makes sense/the proper way of structuring.
Store the wish list as a session, when a user adds/deletes a new item it updates the session by an ajax call that just returns true/false if successful. On the partial view of the wish list component, I check for the session and cast it to my viewModel (which the session is based on) or serialize it for my knockout.
Let me know if this makes sense, otherwise I can post some code samples
It's hard to say without having a look at your basic structure, and not knowing you exact needs.
I don't know if you know this, but you can actually access the Session directly in Views:
#{
var wishlist = (WishList)HttpContext.Current.Session["Wishlist"];
}
It's fine to use Ajax to update it server side; and then you can return a partial view from the controller, to use however you like in the Ajax success call.
I hope this makes sense.
To begin with, if the wishlist is only supposed to exist for the duration of their visit then storing it in a session would be the best thing to do. However if the wishlist is supposed to live longer than a single visit and should be available to the user upon their return then I would suggest storing it in the database against the user's credentials/account (this is presuming they have an account).
As for the session itself, whilst you can access session data from a view I would not suggest it as you start to have a dependency on the session and before long you'll have code such as this scattered throughout your views.
var wishlist = (WishList)HttpContext.Current.Session["Wishlist"];
What happens when you want to change the way the wishlist works and instead have it database driven as you'd now like to persist the wishlist? You'll have to go through all of your views updating the references to the session.
Instead I would opt for registering your session with your IoC container of choice and injecting it using dependency injection, here is a simple example of how to register the session with StructureMap:
public class WebsiteRegistry : Registry
{
public WebsiteRegistry()
{
this.For<IUserWishlist>().HybridHttpOrThreadLocalScoped().Use(() => GetUserWishlistFromSession());
}
public static IUserWishlist GetUserWishlistFromSession()
{
var session = HttpContext.Current.Session;
if (session["WishList"] != null)
{
return session["WishList"] as IUserWishlist;
}
/* Create new empty session object */
session["WishList"] = new UserWishlist();
return session["WishList"] as IUserWishlist;
}
}
Now you're able to inject your wishlist into your controller and pass the data to your view via a view model. And as you're now programming against an interface instead of an implementation you could easily change how the wishlist is persisted without needing to change any code that references the wishlist.
public class WishlistController : Controller {
private readonly IUserWishlist userWishlist;
public void WishlistController(IUserWishlist userWishlist) {
this.userWishlist= userWishlist;
}
public ActionResult ViewProfile()
{
...
var viewModel = new UserWishlistViewModel {
wishlist = this.userWishlist.GetWishList()
}
...
}
}
I've written a more detailed example up in a blog post that might be of interest which can be read here. I hope this helps!

silverstripe static publisher - pages affected by DataObject Changes

is there a possibility to trigger an update of the cache if a DataObject is edited?
for example updating a News DataObject should update the cache of pages, that are displaying these NewsObjects.
many thanx,
Florian
Here is what I could do using the StaticPublishQueue module. In your NewsDataObject.php:
function onAfterWrite() {
parent::onAfterWrite();
$url = array();
$pages = $this->Pages(); //has_many link to pages that include this DataObject
foreach($pages as $page) {
$pagesAffected = $page->pagesAffected();
if ($pagesAffected && count($pagesAffected) > 0) {
$urls = array_merge((array)$urls, (array)$pagesAffected);
}
}
URLArrayObject::add_urls($urls);
}
This takes each of the pages that references your DataObject, asks it for all it's URL and the URL of any related pages (e.g. Virtual Pages that reference that page), compiles all the URLs into a big array, then adds that array to the static publishing queue. The queue will gradually process until all the affected pages are republished.
The event system allows you to add a layer of abstraction between the republishing and the triggers for republishing, but for something simple you don't necessarily need to use it. Instead, you can add pages to the queue directly. (You might also like to read this blog post describing the StaticPublishQueue module)
The StaticPublisherQueue module will handle that for you.
In case anyone else comes across this, and doesn't wish to use the StaticPublishQueue module instead of StaticPublisher, it does appear to be possible in StaticPublisher, the following works for me:
function onAfterWrite() {
parent::onAfterWrite();
$urls = array();
$pages = Page::get();
foreach($pages as $page) {
$urls[] = $page->Link();
}
$sp = new FilesystemPublisher();
$sp->publishPages($urls);
}
Note the last 2 lines, and use the Page::get to specify the exact pages that need to be updated.

How to override save() method in a component

Where and how I am overriding the save method in Joomla 3.0 custom component ?
Current situation:
Custom administrator component.
I have a list view that displays all people stored in table.
Clicking on one entry I get to the detailed view where a form is loaded and it's fields can be edited.
On save, the values are stored in the database. This all works fine.However, ....
When hitting save I wish to modify a field before storing it into the database. How do I override the save function and where? I have been searching this forum and googled quiet a bit to find ways to implement this. Anyone who give me a simple example or point me into the right direction ?
Thanks.
Just adding this for anyone who wants to know the answer to the question itself - this works if you explicitly wish to override the save function. However, look at the actual solution of how to manipulate values!
You override it in the controller, like this:
/**
* save a record (and redirect to main page)
* #return void
*/
function save()
{
$model = $this->getModel('hello');
if ($model->store()) {
$msg = JText::_( 'Greeting Saved!' );
} else {
$msg = JText::_( 'Error Saving Greeting' );
}
// Check the table in so it can be edited.... we are done with it anyway
$link = 'index.php?option=com_hello';
$this->setRedirect($link, $msg);
}
More details here: Joomla Docs - Adding Backend Actions
The prepareTable in the model (as mentioned above) is intended for that (prepare and sanitise the table prior to saving). In case you want to us the ID, though, you should consider using the postSaveHook in the controller:
protected function postSaveHook($model, $validData) {
$item = $model->getItem();
$itemid = $item->get('id');
}
The postSaveHook is called after save is done, thus allowing for newly inserted ID's to be used.
You can use the prepareTable function in the model file (administrator/components/yourComponent/models/yourComponent.php)
protected function prepareTable($table)
{
$table->fieldname = newvalue;
}

Hole punching Mage_Catalog_Block_Product_Price in Magento EE FPC

I'm having a heck of a time figuring out the code/parameters to hole-punch the Full Page Cache in magento for the Mage_Catalog_Block_Product_Price block. I can get the price to show the first time the page is loaded, but when the cache id is unique, it's not rendering the price properly (it does cache it correctly when it's supposed to be cached) . I know I need to send it parameters, such as product_id etc, but not clear about what (eg 'xx') needs to be sent from getCacheKeyInfo into the cache container for use in $this->_placeholder->getAttribute('xx'). And what needs to be prepared and sent from _renderView() to the price layout/view.
So far I have done the following successfully (they each output testing data)
Created the cache.xml in my module /etc folder
Created the cache container model and verified works (just need settings)
Rewrote/extended the Mage_Catalog_Block_Product_Price into my own model to add the getCacheKeyInfo()
So the problem is that I have tried many variations within the container model's _getCacheId() and _renderBlock() in combination with the getCacheKeyInfo(), like described above. But I am hitting a stumbling block. If anyone can lead me in the right direction, it would be greatly appreciated.
I've been struggling with the Full Page Caching as well.
These are my findings and have been very helpful to me.
Please take a look at: app/code/core/Enterprise/PageCache/Model/Processor/Default.php Line 47
/**
* Check if request can be cached
*
* #param Zend_Controller_Request_Http $request
* #return bool
*/
public function allowCache(Zend_Controller_Request_Http $request)
{
foreach ($this->_noCacheGetParams as $param) {
if (!is_null($request->getParam($param, null))) {
return false;
}
}
if (Mage::getSingleton('core/session')->getNoCacheFlag()) {
return false;
}
return true;
}
Looking at this function there seem to be two ways of avoiding (disabling) the Full Page Cache:
GET Parameter:
You can use the parameters 'store' or 'from_store' prefixed with three underscores to avoid the cache.
Example:
http://magentourl.com/catelog/category/view/id/123?___store
Mage::getUrl('catalog/category/view', array('id' => 123, 'query' => array('___store' => '')))
Session variable:
You could also avoid the Full Page caching by setting a (temporary) session variable:
Mage::getSingleton('core/session')->setNoCacheFlag(true)

mvc3 OutputCache RemoveOutputCacheItem RenderAction

I did my research but haven't found any answers.
I'm using Html.RenderAction in a masterpage ( to render page header with links specific to user permissions ). Action is decorated with OutputCache, returns partial control and gets cached as expected.
When the event happens ( let's say permissions are changed ) I want to programmatically invalidate cached partial control.
I'm trying to use RemoveOutputCacheItem method. It takes a path as a parameter. I'm trying to set the path to the action used in Html.RenderAction. That doesn't invalidate the action.
How can I programmatically invalidate the action?
Thanks
The cache for child actions is stored in the OutputCacheAttribute.ChildActionCache property. The problem is that the API generating ids for child actions and storing them in this object is not public (WHY Microsoft??). So if you try to loop through the objects in this collection you will discover that it will also contain the cached value for your child action but you won't be able to identify it unless you reverse engineer the algorithm being used to generate keys which looks something like this (as seen with Reflector):
internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
StringBuilder builder = new StringBuilder();
builder.Append("_MvcChildActionCache_");
builder.Append(filterContext.ActionDescriptor.UniqueId);
builder.Append(DescriptorUtil.CreateUniqueId(new object[] { this.VaryByCustom }));
if (!string.IsNullOrEmpty(this.VaryByCustom))
{
string varyByCustomString = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, this.VaryByCustom);
builder.Append(varyByCustomString);
}
builder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(this.VaryByParam)));
using (SHA256 sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())));
}
}
So you could perform the following madness:
public ActionResult Invalidate()
{
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
return View();
}
which obviously will invalidate all cached child actions which might not be what you are looking for but I am afraid is the only way other than of course reverse engineering the key generation :-).
#Microsoft, please, I am begging you for ASP.NET MVC 4.0:
introduce the possibility to do donut caching in addition to donut hole caching
introduce the possibility to easily expire the result of a cached controller action (something more MVCish than Response.RemoveOutputCacheItem)
introduce the possibility to easily expire the result of a cached child action
if you do 1. then obviously introduce the possibility to expire the cached donut portion.
You might want to approach this a different way. You could create a custom AuthorizeAttribute -- it would simply allow everyone -- and add override the OnCacheValidation method to incorporate your logic. If the base OnCacheValidation returns HttpValidationStatus.Valid, then make your check to see if the state has changed and if so, return HttpValidationStatus.Invalid instead.
public class PermissionsChangeValidationAttribute : AuthorizeAttribute
{
public override OnAuthorization( AuthorizationContext filterContext )
{
base.OnAuthorization( filterContext );
}
public override HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
{
var status = base.OnCacheAuthorization( httpContext );
if (status == HttpValidationStatus.Valid)
{
... check if the permissions have changed somehow
if (changed)
{
status = HttpValidationStatus.Invalid;
}
}
return status;
}
}
Note that there are ways to pass additional data in the cache validation process if you need to track the previous state, but you'd have to replicate some code in the base class and add your own custom cache validation handler. You can mine some ideas on how to do this from my blog post on creating a custom authorize attribute: http://farm-fresh-code.blogspot.com/2011/03/revisiting-custom-authorization-in.html

Resources