Multisites with only one authentication point - symfony

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.

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

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>

Override a FOSUserBundle RegistrationController

In FOSUserBundle, i need to override FOSUserBundle RegistrationController because i need to add this:
if($user->getType()=="Student") {
$user->addRole("ROLE_Student");
}
else {
$user->addRole("ROLE_TEACHER");
}
It works when i add it in vendor--->...---->registrationcontroller. That's why i need to override registration controller, but how?
Do not override the controller. You should use the event system! Create an event handler which subscribe to FOSUserEvents::REGISTRATION_COMPLETE and then perform the role addition.
Documentation:
Symfony: EventDispatcher component
Symfony: Using events
FOSUserBundle: Hooking into the controllers
The listener:
class RegistrationListener implements EventSubscriberInterface
{
public function __construct(/* ... */)
{
// ...
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_COMPLETE => 'addRole',
);
}
public function addRole(FilterUserResponseEvent $event)
{
$user = $event->getUser();
// Add the role here
// ...
}
}
The service definition:
<service id="my_app.event.registration" class="MyApp\Event\RegistrationListener">
<tag name="kernel.event_subscriber" />
<!-- ... -->
</service>

Resources