How to create ObjectManager service with Symfony2? - symfony

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.

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

Symfony2 service container: Inject array of services parameter as an argument to another service using XML

I have a parameter which should represent array of services in my services.xml file:
<parameters>
<parameter key="decorators.all" type="collection">
<parameter type="service" id="decorator1" />
<parameter type="service" id="decorator2" />
<parameter type="service" id="decorator3" />
</parameter>
</parameters>
<services>
<service id="decorator1" class="\FirstDecorator" />
<service id="decorator2" class="\SecondDecorator" />
<service id="decorator3" class="\ThirdDecorator" />
</services>
Now I want to inject this collection to another service as an array of services:
<services>
<service id="notifications_decorator" class="\NotificationsDecorator">
<argument>%decorators.all%</argument>
</service>
</services>
But it doesn't work. Can't understand why. What am I missing?
So, you inject array of parameters no array of services. You can inject service by service via:
<services>
<service id="notifications_decorator" class="\NotificationsDecorator">
<argument type="service" id="decorator1"/>
<argument type="service" id="decorator2"/>
<argument type="service" id="decorator3"/>
</service>
</services>
Or (in my opinion better way) tag decorators services and inject them to notifications_decorator during compilation passes.
UPDATE: Working with Tagged Services
In your case you have to modify your services like this:
<services>
<service id="decorator1" class="\FirstDecorator">
<tag name="acme_decorator" />
</service>
<service id="decorator2" class="\SecondDecorator">
<tag name="acme_decorator" />
</service>
<service id="decorator3" class="\ThirdDecorator">
<tag name="acme_decorator" />
</service>
</services>
Additionaly you should remove decorators.all parameter from <parameters> section. Next, you have to add sth like addDectorator function for \NotificationsDecorator:
class NotificationsDecorator
{
private $decorators = array();
public function addDecorator($decorator)
{
$this->decorators[] = $decorator;
}
// more code
}
It would be great if you write some interface for decorator's and add this as type of $decorator for addDecorator function.
Next, you have to write own compiler pass and ask them about tagged services and add this services to another one (simillarly to doc):
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class DecoratorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('notifications_decorator')) {
return;
}
$definition = $container->getDefinition('notifications_decorator');
$taggedServices = $container->findTaggedServiceIds('acme_decorator');
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addDecorator',
array(new Reference($id))
);
}
}
}
Finally, you should add your DecoratorCompilerPass to Compiler in your bundle class like:
class AcmeDemoBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new DecoratorCompilerPass());
}
}
Good luck!
Little bit different approach with tagged services (or whatever you need) and CompilerPassInterface using array of services instead of method calls.
Here are the differences from #NHG answer:
<!-- Service definition (factory in my case) -->
<service id="example.factory" class="My\Example\SelectorFactory">
<argument type="collection" /> <!-- list of services to be inserted by compiler pass -->
</service>
CompilerPass:
/*
* Used to build up factory with array of tagged services definition
*/
class ExampleCompilerPass implements CompilerPassInterface
{
const SELECTOR_TAG = 'tagged_service';
public function process(ContainerBuilder $container)
{
$selectorFactory = $container->getDefinition('example.factory');
$selectors = [];
foreach ($container->findTaggedServiceIds(self::SELECTOR_TAG) as $selectorId => $tags) {
$selectors[] = $container->getDefinition($selectorId);
}
$selectorFactory->replaceArgument(0, $selectors);
}
}
in yaml you can do:
app.example_conditions:
class: AppBundle\Example\Conditions
arguments:
[[ "#app.example_condition_1", "#app.example_condition_2", "#app.example_condition_3", "#app.example_condition_4" ]]
and in AppBundle\Example\Conditions you receive the array...
Symfony 5.3+ and php 8.0+
class NotificationsDecorator
{
private $decorators;
public function __construct(
#[TaggedIterator('app.notifications_decorators')] iterable $decorators
) {
$this->decorators = $decorators;
}
}
#[Autoconfigure(tags: ['app.notifications_decorators'])]
interface DecoratorInterface
{
}
class FirstDecorator implements DecoratorInterface
{
}

Symfony 2 registering a doctrine listener not working

Services.xml file:
<?xml version="1.0" ?>
<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="task.task_history_insertion" class="Acme\Bundle\EventListener\TaskHistoryInsertion">
<argument type="service" id="service_container" />
<tag name="doctrine.event_listener" event="postPersist" method="postPersist"/>
</service>
</services>
</container>
TaskHistoryInsertion.php
class TaskHistoryInsertion implements EventSubscriber
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getSubscribedEvents()
{
return array(
Event::postPersist
);
}
public function postPersist(LifecycleEventArgs $args)
{
//not being called
}
}
Any ideas on why postPersist isn't being called after persisting?
Make sure you use the right tag for your service. You need to use doctrine.event_subscriber :
<service id="task.task_history_insertion" class="Acme\Bundle\EventListener\TaskHistoryInsertion">
<argument type="service" id="service_container" />
<tag name="doctrine.event_subscriber"/>
</service>
You are mixing event subscriber and event listener!
I would go for a event listener:
Remove
implements EventSubscriber
and
public function getSubscribedEvents()
{
return array(
Event::postPersist
);
}
Make sure you use
use Doctrine\ORM\Event\LifecycleEventArgs;
and that the services.xml gets loaded in src/Acme/Bundle/DependencyInjection/AcmeExtension.php.
Clear the cache and it should work.
The official documentation can be found at
http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html
When you want to implement eventListener - you should name the method in your listener class exactly like an event. In your example - you should have public method called postPersist. And the listener class shouldn't implement EventSubscriber.
There is a link that can give you much clearer picture about this topic http://docs.doctrine-project.org/en/latest/reference/events.html

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