Get navigationId from HttpCacheHitEvent in Shopware 6 - symfony

I have a subscriber that is listening to HttpCacheHitEvent and I would like to find the navigationId for the requested page.
For storefront events I use $event->getRequest()->getRequestUri(). But for this event i get URLs like /navigation/5943fc.... I currently use the basename() function to get the navigationIds of those URLs but that does not seem to be the clean way to do it.
Is there an alternative way to retrieve the navigationId from a HttpCacheHitEvent?

When you subscribe to this event you can't access the _route and other parameter attributes as usual, as the cached response will be returned before they are usually set.
$request = $event->getRequest();
var_dump($request->attributes->get('_route'));
// null
To solve that issue, you may inject the router service when registering your listener.
<service id="Foo\MyPlugin\CacheHitListener">
<argument type="service" id="router"/>
<tag name="kernel.event_subscriber"/>
</service>
In your listener you then retrieve your route parameters using the service and the request object from the event, so you can determine which route is being requested. Depending on the route, you can then go ahead and use parameters of the specific route.
class CacheHitListener implements EventSubscriberInterface
{
private $matcher;
/**
* #param UrlMatcherInterface|RequestMatcherInterface $matcher
*/
public function __construct($matcher)
{
$this->matcher = $matcher;
}
public static function getSubscribedEvents(): array
{
return [HttpCacheHitEvent::class => 'onCacheHit'];
}
public function onCacheHit(HttpCacheHitEvent $event): void
{
if ($this->matcher instanceof RequestMatcherInterface) {
$parameters = $this->matcher->matchRequest($event->getRequest());
} else {
$parameters = $this->matcher->match($event->getRequest()->getPathInfo());
}
if ($parameters['_route'] === 'frontend.navigation.page') {
$navigationId = $parameters['navigationId'];
//...
}
}
}

Related

How to get or create Context in HttpCacheHitEvent subscriber?

I have a subscriber class subscribing to HttpCacheHitEvent. I want to extract context specific data from other objects. I have been using Context::createDefaultContext() before, but that does not account different parameters of the request such as the locale.
Whats the best way to get or create a context from HttpCacheHitEvent?
You have to start the session, grab the token of it and resolve than the Context using SalesChannelContextFactory.
I cannot recommend doing this as it will make the HttpCache much slower. Maybe you can set cookies to the browser and use them there?
You can inject the service SalesChannelRequestContextResolver and use it in combination with the router service to assemble a SalesChannelContext (and subsequently the Context) from the resolved Request.
<service id="Foo\MyPlugin\CacheHitListener">
<argument type="service" id="router"/>
<argument type="service" id="Shopware\Core\Framework\Routing\SalesChannelRequestContextResolver"/>
<tag name="kernel.event_subscriber"/>
</service>
class CacheHitListener implements EventSubscriberInterface
{
private $matcher;
private RequestContextResolverInterface $contextResolver;
/**
* #param UrlMatcherInterface|RequestMatcherInterface $matcher
*/
public function __construct($matcher, RequestContextResolverInterface $contextResolver)
{
$this->matcher = $matcher;
$this->contextResolver = $contextResolver;
}
public static function getSubscribedEvents(): array
{
return [HttpCacheHitEvent::class => 'onCacheHit'];
}
public function onCacheHit(HttpCacheHitEvent $event): void
{
if ($this->matcher instanceof RequestMatcherInterface) {
$parameters = $this->matcher->matchRequest($event->getRequest());
} else {
$parameters = $this->matcher->match($event->getRequest()->getPathInfo());
}
$event->getRequest()->attributes->add($parameters);
$this->contextResolver->resolve($event->getRequest());
$context = $event->getRequest()->attributes->get(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT);
// ...
}
}
You can also get the SalesChannelContext like this:
$request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);

Doctrine filter not working on itemOperations (Api-Platform)

API Platform version(s) affected: 2.6.8
Description
In a project which uses PostgreSQL and API-Platform, I need to filter all records by a locale string. A doctrine filter is my preferred choice to do so.
This is the filter:
class LocaleFilter extends SQLFilter
{
public const LOCALE_FILTER_NAME = 'locale_filter';
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (!$targetEntity->reflClass->implementsInterface(LocalizedEntityInterface::class)) {
return '';
}
return $targetTableAlias . '.locale = ' . $this->getParameter('locale');
}
}
The parameter locale will be set on each onKernelRequest event, the locale is the value of the header X-Locale:
public function onKernelRequest(RequestEvent $event): void
{
$locale = $event->getRequest()->headers->get('X-Locale');
$this->setFilterLocale($locale);
}
private function setFilterLocale(string $locale): void
{
if (!$this->entityManager->hasFilters()) {
return;
}
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
Now, when I send a request to a collectionOperations endpoint, such as http://example.com/products with the X-Locale header value de_DE, the filter is working and I get a response which contains only the according data in de_DE. When I send a request with locale fr_FR, I get a response with data in fr_FR.
But, when I send a request with the same X-Locale header to a itemOperations endpoint like http://example.com/products/<a-existing-id> I'm getting the error message The parameter "locale" is not set which comes from doctrine.
After investigating that issue, I can say that it works when I override the default ItemDataProvider from API-platform:
<?php
namespace App\DataProvider;
[...]
class ItemDataProvider implements ItemDataProviderInterface
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly RequestStack $requestStack,
) {
}
public function getItem(string $resourceClass, $id, ?string $operationName = null, array $context = []): object
{
$locale = $this->requestStack->getMainRequest()->headers->get('X-Locale');
if ($this->entityManager->hasFilters()) {
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
$query = $this->entityManager->getRepository($resourceClass)
->createQueryBuilder('x')
->where('x.publicId = :pubid')
->setParameter('pubid', $id)
->getQuery();
return $query->getOneOrNullResult();
}
}
But is still required to set the filter value again in "my own" ItemDataProvider. If I delete the first 7 lines of the method getItem of the ItemDataProvider, I get the same error from above.
That doesn't make sense like that, does it? It seems like Api-Platform overrides the Doctrine filters in the default ItemDataProvider and make them useless. Howewer, I didn't found the reason for that issue.
Overriding the ItemDataProvider is a working workaround, but I don't think it's a good one, since the cause is more likely a bug and that way some features of Api-Platform are no longer present in the whole project.

Symfony: internal forwarding vs render controller

I have simple action in simple controller:
public function _targetAction(RequestStack $requestStack)
{
$request = $requestStack->getMasterRequest();
// ...
}
And two ways to call it. First:
// The same or other controller
public function topAction(Request $request)
{
// forward to SimpleController:_target
return $this->forward('AppBundle:Simple:_target');
}
Second from twig (subrequest):
// SimpleController
public function topAction(Request $request)
{
// render
return $this->render('AppBundle:Simple:top.html.twig');
}
// top.html.twig
{{ render(controller('AppBundle:Simple:_target')) }}
How can i idenitfy which way i get to the SimpleController::_targetAction in this method:
public function _targetAction(RequestStack $requestStack)
{
// what can i do here to uniquely identify current way
// Note: $requestStack->getParentRequest() is not null in both cases
}
In my opinion, if you need to execute different code depending on the call type, you should considere create separate routes for each action.
In case you really want to use the same, my best shot is to add a parameter on the route to identify the request.
/**
*
* #Route("/target/{from}", name="_target")
*/
public function _targetAction($from)
{
if($from == 'view'){
// execute code for view call
} else {
// execute code for controller call
}
}
And then, when you call it, pass a different parameter depending on caller type:
TWIG
{{ render(controller('AppBundle:Simple:_target', { 'from': 'view' })) }}
CONTROLLER
return $this->forward('_target', array('from' => 'controller'));

Where to transform raw data?

I'm starting a new bundle. Its goal is to display some statistics arrays and charts. The problem is I don' t know where to transform raw data into usable data in view's arrays and charts. I read lot of articles about keeping the controllers as thin as possible. And as far as I know, repositories are meant to extract data, not transform them.
Where am I supposed to transform my raw data, according to Symfony2 best practices?
it depends on your application but based on what you described looks like you need to define a Service and write all your logic there so your controller would look something like this
$customService = $this->get('my_custom_service');
$data = $customService->loadMyData();
read more about Services in Symfony: http://symfony.com/doc/current/book/service_container.html
Simply create your own, custom service that uses some repository/ies to extract the data and transform it into usable form.
Sample:
// repository
interface MyRepository {
public function findBySomething($something);
}
class MyRepositoryImpl extends EntityRepository implements MyRepository {
public function findBySomething($something) {
return $this->createQueryBuilder('a')
->where('a.sth = :sth')
->setParameter('std', $something)
->getQuery()
->getResult();
}
}
// service
interface MyService {
public function fetchSomeData();
}
class MyServiceImpl implements MyService {
/** #var MyRespostiory */
private $repo;
public function __construct(MyRepository $repo) {
$this->repo = $repo;
}
public function fetchSomeData() {
$rawData = $this->repo->findBySomething(123);
$data = [];
// do sth
return $data;
}
}
// final usage, eg. within a constructor
class MyConstructor extends Controller {
/** #var MyService */
private $myService;
public function __construct(MyService $myService) {
$this->myService = $myService;
}
public function someAction() {
// you could also get access to the service using $this->get('...')
$data = $this->myService->fetchSomeData();
return $this->render('SomeTemplate', [
'data' => $data
]);
}
}
// service declaration
<service id="myService" class="MyServiceImpl">
<argument type="service" id="doctrine.repository.my_repository" />
</service>

Symfony2 common session strategy

I have a symfony2 web project consisting of ten pages which are rendered via 5 controllers. User can land a site through any of the pages above (via shared link for example). And I need to show a welcome popup (just div with position:absolute) to users who opens the page for the first time during the current session.
I've already placed my popup in the common twig template which is used by all the pages needed. Now I have to determine whether to show popup or not. I'm going to show popup based on boolean value from controller.
I have to work with session and cookies, but I have to do that on each page and I don't want to write the same code (check and set cookies, output a boolean to show popup in template) in every method of each controller. Is there a way to this according to DRY concepts?
You could make a wrapper class which handles checking, setting, and getting the current session values and make it a service.
<?php
namespace My\Bundle\My\Namespace;
use Symfony\Component\HttpFoundation\Session\Session;
class SessionManager /* or whatever you want to call it */
{
public function __construct(Session $session)
{
$this->session = $session;
}
public function getMyValue()
{
return $this->session->get('my.value.key',null);
}
public function setMyValue($value)
{
$this->session->set('my.value.key',$value);
return $this;
}
public function hasMyValue()
{
return $this->session->has('my.value.key');
}
}
And in your bundle services.yml
<service id="mytag.session_manager" class="My\Bundle\My\Namespace\SesionManager">
<argument type="service" id="session" />
</service>
And in your controllers
public function someAction()
{
$sessionManager = $this->get('mytag.session_manager');
if($sessionManager->hasMyValue())
{
// do something
}
}
Thanks to the Sgoettschkes answer here Where to place common business logic for all pages in symfony2
I tried this method
http://symfony.com/doc/current/book/templating.html#embedding-controllers
And it looks awesome:
My Popup is included in the main template like this
{{ render(controller('MalyutkaMainBundle:WelcomePopup:index')) }}
Than inside the controller I manipulate session vars
class WelcomePopupController extends Controller {
public function indexAction(Request $request) {
$session = $this->get('session');
$showWelcomePopup = 0;
if ($session->has("have_seen_welcome_popup_on")) {
// tbd compare to the date of publishing of the new popup screen
} else {
$showWelcomePopup = 1;
$session->set("have_seen_welcome_popup_on", new \DateTime());
}
$params = array (
'show_welcome_popup' => $showWelcomePopup
);
return $this->render('MalyutkaMainBundle:WelcomePopup:welcome_popup.html.twig', $params);
}
}
And nothing is to be added in other controllers - just what I wanted to do.
But it is impossible to change cookies that way, so I store my data in the session.

Resources