How to get or create Context in HttpCacheHitEvent subscriber? - symfony

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

Related

Get navigationId from HttpCacheHitEvent in Shopware 6

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'];
//...
}
}
}

How to create ObjectManager service with Symfony2?

At beginning, I used repository.
But, after some code reviews on github, I'm interesting to use ObjectManager (to alleviate controllers, and also by curiosity ^^).
The problem is I didn't see some good tutorial about it. Even tutorials I saw was to initialize a service by an object manager but not to create one.
In the FriendsOfSymfony github, we could see an example for that but I don't really understand how to initialize the service. I have this error "Cannot instantiate interface Doctrine\Common\Persistence\ObjectManager" when I initialize my manager service like this:
<?xml version="1.0" ?>
http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="md_mechanical.entity.enginemanager.class">MD\MechanicalBundle\Entity\EngineManager</parameter>
</parameters>
<services>
<service id="md_mechanical.enginemanager.default" class="%md_mechanical.entity.enginemanager.class%">
<argument type="service" id="md_mechanical.object_manager" />
<argument>%md_engine.engine.class%</argument>
</service>
<!-- The factory is configured in the DI extension class to support more Symfony versions -->
<service id="md_mechanical.object_manager" class="Doctrine\Common\Persistence\ObjectManager">
<argument>%fos_user.model_manager_name%</argument>
</service>
</services>
thanks in advance for your help
Cannot instantiate interface Doctrine\Common\Persistence\ObjectManager means you are trying to instantiate an interface, which is not possible.
You have to create an object which implements this interface, and define all functions
use Doctrine\Common\Persistence\ObjectManager as ObjectManager;
class MyObjectManager implements ObjectManager
{
public function __construct(/* some params here */)
{
// Construct your manager here
}
public function find($className, $id)
{
// Do stuff
}
public function persist($object)
{
// Do stuff
}
public function remove($object)
{
// Do stuff
}
public function merge($object)
{
// Do stuff
}
public function clear($objectName = null)
{
// Do stuff
}
public function detach($object)
{
// Do stuff
}
public function refresh($object)
{
// Do stuff
}
public function flush()
{
// Do stuff
}
public function getRepository($className)
{
// Do stuff
}
public function getClassMetadata($className)
{
// Do stuff
}
public function getMetadataFactory()
{
// Do stuff
}
public function initializeObject($obj)
{
// Do stuff
}
public function contains($object)
{
// Do stuff
}
}
Then declare it as a service
<services>
<service id="myObjectManager" class="%myObjectManager.class%">
<argument>...</argument>
</service>
# Use your brand new object manager
<service id="md_mechanical.enginemanager.default" class="%md_mechanical.entity.enginemanager.class%">
<argument type="service" id="myObjectManager" />
<argument>%md_engine.engine.class%</argument>
</service>
</services>
You should have a look at Doctrine\ORM\EntityManager and Doctrine\ORM\EntityManagerInterface, it may help you.

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>

Error: You cannot create a service ("templating.helper.assets") of an inactive scope ("request")

I am getting below error,
[Twig_Error_Runtime]
An exception has been thrown during the rendering of a template ("You cannot create a service ("templating.helper.assets") of an inactive scope ("request").") in "AcmeMessagingBundle:Comment:email.html.twig".
I am rendering twig template from symfony 2 custom console command
Below is my service class which is the event subscriber,I am triggering onCommentAddEmail event by symfony console command to send email,
class NotificationSubscriber implements EventSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
'comment.add' => array('onCommentAddEmail', 0),
);
}
public function onCommentAddEmail(CommentAddEvent $event)
{
...................
$body = $this->container->get('templating')->render(
'AcmeMessagingBundle:Comment:email.html.twig',
array('template' => $template)
);
.......
}
}
$body is passed to swiftmailer to send an email.
This is my service defination,
Acme\MessagingBundle\Subscriber\NotificationSubscriber
<services>
<service id="notification_subscriber" class="%notification_subscriber.class%">
<argument type="service" id="service_container" />
<tag name="kernel.event_subscriber" />
</service>
</services>
Below post says the problem is fixed in symfony 2.1, but I am still getting error,
https://github.com/symfony/symfony/issues/4514
I have already refereed to http://symfony.com/doc/current/cookbook/service_container/scopes.html, I have passed entire container to my service.
Not sure if it is the best way but adding this worked for me,
$this->container->enterScope('request');
$this->container->set('request', new Request(), 'request');
As quoted by Stof :
if you use the request-based asset helper (getting the base url from
the request object), it cannot be used from the CLI indeed, as you
don't have a request there
Translation, you can't use the asset function inside your template if you are intending to use it from the CLI.
The problem arises because you use asset() in your template, which depends on Request, and there is no Request in command line app.
Quick fix:
framework:
templating:
assets_base_urls: { http: ["http://yoursite.com"], ssl: ["http://yoursite.com"] }
More here: https://stackoverflow.com/a/24382994/1851915

Multisites with only one authentication point

For a future project, I'm looking for a way to manage multisites development with Symfony2. In fact, each site will be on a different subdomain but will works the same way ; only the style will changed a little.
The thing is : the authentication is common to all subsites, and is managed by the main site (www.mydomain.com). Each multisites will then have its own database.
Is it possible to do so with Symfony2 ? I know it's possible to use multidomains, but I don't how about the authentication system. Do you have ideas on how to proceed ?
Thanks !
Actually I've managed to do this in one of projects I'm working on.
It's a bit tricky but once you understand the basic concept behind the symfony's security layer it's extremely easy to integrate into your existing project.
First off, be sure to read this: http://symfony.com/doc/current/book/security.html. I'd also recommend taking a look at the cookbook's security section.
You won't find a straight anwer in the manual but it helps to understand the code I'm going to paste here.
The basic idea is to share the session id across the subdomains.
Note: for the sake of space, I'll be omitting the use and namespace tags in PHP. Don't forget to import and specify appropriate namespaces.
class LoginListener
{
public function onLogin(InteractiveLoginEvent $event)
{
$token = $event->getAuthenticationToken();
//multisite log-in
if ($token->getUser() instanceof User)
{
$_SESSION['_user_id'] = $token->getUser()->getId();
}
}
}
class LogoutListener implements LogoutHandlerInterface
{
public function logout(Request $request, Response $response, TokenInterface $token)
{
if (isset($_SESSION['_user_id']))
{
unset($_SESSION['_user_id']);
}
}
}
class SessionMatcher implements RequestMatcherInterface
{
public function matches(Request $request)
{
$request->getSession()->start();
return isset($_SESSION['_user_id']);
}
}
class CrossLoginUserToken extends AbstractToken
{
private $id;
public function getId()
{
return $this->id;
}
public function __construct($id, array $roles = array())
{
parent::__construct($roles);
$this->id = $id;
parent::setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
}
class CrossLoginProvider implements AuthenticationProviderInterface
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getId());
if ($user)
{
$authenticatedToken = new CrossLoginUserToken($token->getId(),$user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The CrossSite authentication failed.');
}
public function supports(TokenInterface $token)
{
return $token instanceof CrossLoginUserToken;
}
}
class CrossLoginListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
protected $session;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, Session $session)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->session = $session;
}
public function handle(GetResponseEvent $event)
{
$this->session->start();
if (!is_null($this->securityContext->getToken()) && $this->securityContext->getToken()->isAuthenticated())
{
return;
}
if (isset($_SESSION['_user_id']))
{
try
{
$token = $this->authenticationManager->authenticate(new CrossLoginUserToken($_SESSION['_user_id']));
$this->securityContext->setToken($token);
}
catch (AuthenticationException $e)
{
throw $e;
}
}
}
}
class CrossLoginFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.crosslogin.' . $id;
$container
->setDefinition($providerId, new DefinitionDecorator('crosslogin.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.crosslogin.' . $id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('crosslogin.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'crosslogin';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
security_factories.yml:
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="security.authentication.factory.crosslogin" class="MyBundle\Security\Factory\CrossLoginFactory">
<tag name="security.listener.factory" />
</service>
</services>
</container>
config.xml:
<service id="crosslogin.security.authentication.provider" class="MyBundle\Security\Authentication\Provider\CrossLoginProvider">
<argument />
</service>
<service id="crosslogin.security.authentication.listener" class="MyBundle\Security\Firewall\CrossLoginListener">
<argument type="service" id="security.context" />
<argument type="service" id="security.authentication.manager" />
<argument type="service" id="session" />
</service>
<service id="crosslogin.session.matcher" class="MyBundle\Security\Matcher\SessionMatcher">
</service>
<service id="crosslogin.handler.logout" class="MyBundle\Listener\LogoutListener">
<service id="listener.login" class="Backend\CmsBundle\Listener\LoginListener">
<tag name="kernel.event_listener" event="security.interactive_login" method="onLogin" />
</service>
And finally - the security.yml:
firewalls:
...
crosslogin:
crosslogin: true
provider: dao_provider_by_id
request_matcher: crosslogin.session.matcher
logout:
path: /secured/logout
target: /
invalidate_session: true
handlers: [crosslogin.handler.logout]
providers:
...
dao_provider_by_id:
entity: { class: YOUR_SECURITY_CLASS_NAME, property: id }
factories:
CrossLoginFactory: "%kernel.root_dir%/../src/MyBundle/Resources/config/security_factories.xml"
This is the simpliest and as neat as possible thing I could think of.
The only "misused" class here is the SessionMatcher which only checks for the availbility of the session id in the session.
Good luck, and feel free to ask question in the comments section. I know this can be pretty confusing at the beginning.

Resources