Is it possible to enable the automatic loading of an association for products? - associations

I'm looking for a way to automatically load all streams from a product when loading a product (product/category page) but can't seem to find a way to add this. I could add it as an extended attribute, but I would rather make use of the $product->getStreams() function.
I've tried the following but I couldn't figure out how to add an association through this method:
public static function getSubscribedEvents(): array
{
return [
ProductEvents::PRODUCT_LOADED_EVENT => 'onLoad'
];
}
public function onLoad(EntityLoadedEvent $event): void
{
$product = $event->getEntities();
}

You could decorate the product.repository and alter the criteria to add the association for streams before calling search of the decorated repository. You can find an example here where the media.repository used to be decorated to alter the search criteria before fetching the data.
Service definition:
<service id="MyPlugin\Core\Content\Product\ProductRepositoryDecorator" decorates="product.repository">
<argument type="service" id="MyPlugin\Core\Content\Product\ProductRepositoryDecorator.inner"/>
</service>
In the Decorator:
public function search(Criteria $criteria, Context $context): EntitySearchResult
{
$criteria->addAssociation('streams');
return $this->decorated->search($criteria, $context);
}

Create your own product repository
final class SalesChannelProductRepository implements SalesChannelRepositoryInterface
{
/**
* #var SalesChannelRepositoryInterface
*/
private $repository;
public function __construct(
SalesChannelRepositoryInterface $repository
) {
$this->repository = $repository;
}
public function search(Criteria $criteria, SalesChannelContext $salesChannelContext): EntitySearchResult
{
$this->processCriteria($criteria);
return $this->repository->search($criteria, $salesChannelContext);
}
private function processCriteria(Criteria $criteria): void
{
$criteria->addAssociation('streams');
}
}
define service decorate default product repository
<service id="my_example.sales_channel.product.repository"
class="MyExample\Core\Content\Product\SalesChannel\SalesChannelProductRepository"
decorates="sales_channel.product.repository"
public="false">
<argument type="service" id="my_example.sales_channel.product.repository.inner"/>
</service>

Thanks to the people who reacted tho this question I've gotten to the decorated below. I'm still running into some problems related to using properties as filter in the Dynamic Product group rules. I've created a new thread for that here: Getting streams from product returns zero results with some rules
Decorator class:
class ProductRepositoryDecorator implements SalesChannelRepositoryInterface
{
private SalesChannelRepositoryInterface $decorated;
public function __construct(
SalesChannelRepositoryInterface $decorated
) {
$this->decorated = $decorated;
}
public function search(Criteria $criteria, SalesChannelContext $salesChannelContext): EntitySearchResult
{
$this->processCriteria($criteria);
return $this->decorated->search($criteria, $salesChannelContext);
}
private function processCriteria(Criteria $criteria): void
{
$criteria->addAssociation('streams');
}
public function aggregate(Criteria $criteria, SalesChannelContext $salesChannelContext): AggregationResultCollection
{
return $this->decorated->aggregate($criteria, $salesChannelContext);
}
public function searchIds(Criteria $criteria, SalesChannelContext $salesChannelContext): IdSearchResult
{
return $this->decorated->searchIds($criteria, $salesChannelContext);
}
}
services.xml:
<service id="Vendor\Base\Core\Content\Product\ProductRepositoryDecorator" decorates="sales_channel.product.repository">
<argument type="service" id="Vendor\Base\Core\Content\Product\ProductRepositoryDecorator.inner"/>
</service>

Related

Too few arguments to function ReadingData::__construct(), 1 passed in ... KernelDevDebugContainer.php on and exactly 2 expected

I created a Service to read data from the database.
In order to achieve that, I want to make a Controller and throw this controller I want to call first the ReadingDataService.
Error Message:
Too few arguments to function TryPlugin\Service\ReadingData::__construct(), 1 passed in /var/www/html/var/cache/dev_he0523cc28be2f689acaab5c325675d68/ContainerFt0wDoq/Shopware_Production_KernelDevDebugContainer.php on line 25455 and exactly 2 expected
Code:
ReadingData.php
class ReadingData
{
private EntityRepositoryInterface $productRepository;
private Context $con;
public function __construct(EntityRepositoryInterface $productRepository, Context $con)
{
$this->productRepository = $productRepository;
$this->con = $con;
}
public function readData(): void
{
$criteria1 = new Criteria();
$products = $this->productRepository->search($criteria1, $this->con)->getEntities();
}
}
PageController.php
/**
* #RouteScope (scopes={"storefront"})
*/
class PageController extends StorefrontController
{
/**
* #Route("/examples", name="examples", methods={"GET"})
*/
public function showExample(ReadingData $ReadingDatan): Response
{
$meinData = $ReadingDatan->readData();
return $this->renderStorefront('#Storefront/storefront/page/content/index.html.twig', [
'products' => $meinData,
]);
}
}
Service.xml:
<service id="TryPlugin\Service\ReadingData">
<argument type="service" id="product.repository"/>
</service>
<!--ReadingDate From Controller-->
<service id="TryPlugin\Storefront\Controller\PageController" public="true">
<call method="setContainer">
<argument type="service" id="service_container"/>
</call>
<tag name="controller.service_arguments"/>
</service>
You need to pass the 2nd argument in service.xml
Your class requires two arguments:
public function __construct(EntityRepositoryInterface $productRepository, Context $con) { //...
but only provides one in service.xml:
<service id="TryPlugin\Service\ReadingData">
<argument type="service" id="product.repository"/>
<!-- Need argument for `Context $con` -->
</service>
Looking at the documentation, Context does not appear to be autowired by default.
Therefore, you must inject the service yourself in service.xml.
If you grow tired of all ways specifying the arguments in service.xml, look into enabling and configuring autowire for ShopWare.
This is unecessary in the first place.
Do the following in your controller and pass the context down:
namespace TryPlugin\Storefront\Controller;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\Routing\Annotation\LoginRequired;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use TryPlugin\Service\ReadingData;
/**
* #RouteScope (scopes={"storefront"})
*/
class PageController extends StorefrontController
{
/**
* #Route("/examples", name="examples", methods={"GET"})
*/
public function showExample(ReadingData $ReadingDatan, Context $context): Response
{
$meinData = $ReadingDatan->readData($context);
return $this->renderStorefront('#Storefront/storefront/page/content/index.html.twig', [
'products' => $meinData,
]);
}
}
This will work since there is a parameter resolver for controllers.

Symfony2 FOSUser custom validation not working

i'm using Symfony 2.8 and FOSUser bundle 2.0 for my user management.
I'm trying to create custom validator for phone number field without success.
This is my code:
UserBundle/Resources/config/validation.xml
<class name="Acme\UserBundle\Entity\User">
<property name="phoneNumber">
<constraint name="Acme\UserBundle\Validator\Constraint\IsConfirmedPhonenumber">
<option name="groups">
<value>Acme</value>
</option>
</constraint>
</property>
</class>
Service definition
<service class="Acme\UserBundle\Validator\Constraint\IsConfirmedPhonenumberValidator" id="acme_user.validator.is_confirmed_phonenumber_validator">
<argument id="doctrine.orm.default_entity_manager" type="service"/>
<tag name="acme.constraint_phonenumber"/>
</service>
On my form type
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\User',
'intention' => 'profile',
));
}
My custom constraint
class IsConfirmedPhonenumber extends Constraint
{
public $message = 'The phonenumber "%phonenumber%" is not verified.';
public function validatedBy()
{
return "acme.constraint_phonenumber";
}
}
And validator
class IsConfirmedPhonenumberValidator extends ConstraintValidator
{
/**
* #var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* Checks if the passed value is valid.
*
* #param mixed $value The value that should be validated
* #param Constraint $constraint The constraint for the validation
*/
public function validate($value, Constraint $constraint)
{
$phoneVerificationRepo = $this->em->getRepository('AcmeCoreBundle:PhoneVerification');
$phoneVerificationRepo->findBy(array('phoneNumber' => $value, 'verified' => true));
if(!$phoneVerificationRepo)
{
$this->context->buildViolation($constraint->message)
->setParameter('%phonenumber%', $value)
->addViolation();
}
return;
}
}
This configuration doesn't work for me since validate method in my custom validator never gets called.
In your service definition, your tag name is wrong, it should be validator.constraint_validator to be registered as a Validator.
<service class="Acme\UserBundle\Validator\Constraint\IsConfirmedPhonenumberValidator" id="acme_user.validator.is_confirmed_phonenumber_validator">
<argument id="doctrine.orm.default_entity_manager" type="service"/>
<tag name="validator.constraint_validator" />
</service>
And your constraint should be configured using the id of your validator service, therefor:
class IsConfirmedPhonenumber extends Constraint
{
public $message = 'The phonenumber "%phonenumber%" is not verified.';
public function validatedBy()
{
return "acme_user.validator.is_confirmed_phonenumber_validator";
}
}
I managed to fix the issue by specifying validation groups in createForm factory method in overridden editAction method of ProfileController.
$form = $formFactory->createForm(['validation_groups' => array('AcmeProfile', 'Profile')]);
Setting validation groups in config or custom FormType didn't help either since i'm overriding FOS factory and ProfileController editAction.

DataTransformer get error "undefined call method getName"

I'm a weird problem that I do not know how to solve.
I created a datatransformer, which among other things in my other projects, works perfectly, but when I start the page I get this error:
FatalErrorException: Error: Call to undefined method
Acme\CoreBundle\Transformer\HiddenToIdTransformer::getName() in
/var/www/Acme/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/DependencyInjection/DependencyInjectionExtension.php line 49
Here the code:
transformer
class HiddenToIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* #var string
*/
protected $class;
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function transform($entity)
{
if (null === $entity) {
return;
}
return $entity->getId();
}
public function reverseTransform($name)
{
if (!$name) {
return null;
}
$entity = $this->objectManager
->getRepository('AcmeCoreBundle:Locality')
->findOneByLocality($name);
if (null === $entity) {
throw new TransformationFailedException();
}
return $entity;
}
}
type
class EntityHiddenType extends AbstractType
{
/**
* #var ObjectManager
*/
protected $objectManager;
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new HiddenToIdTransformer($this->objectManager);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'class' => null,
'invalid_message' => 'The entity does not exist.',
));
}
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'entity_hidden';
}
}
services
<service id="datatransformer.entity_hidden" class="Acme\CoreBundle\Transformer\HiddenToIdTransformer">
<argument type="service" id="doctrine.orm.entity_manager" />
<tag name="form.type" alias="entity_hidden" />
</service>
And in this way recall the transformer:
->add('locality', 'entity_hidden')
I do not understand what is wrong, I repeat that in my other projects, the exact same code works fine!
Maybe it's a bug in 2.3.7 incurred?
You have configured the class of the Transformer as a service tagged with form.type instead of the form-type's class:
<service
id="datatransformer.entity_hidden"
class="Acme\CoreBundle\Transformer\HiddenToIdTransformer" <!-- <= HERE -->
>
The form-type's service definition should be like this:
<service id="form.type.entity_hidden" class="Acme\CoreBundle\Form\Type\EntityHiddenType">
<argument type="service" id="doctrine.orm.entity_manager" />
<tag name="form.type" alias="entity_hidden" />
</service>
If you want your data-transformer to be a service aswell ... don't tag it with form.type otherwise symfony will try to call it's getName() method which doesn't exist.
Remove the tag ...
<tag name="form.type" alias="entity_hidden" />
... and the exception will disappear.

Multiple entity manager for FOSUserBundle

To use different Entity Manager / Connection based on URL in Symfony if fairly easy. With the following routing configuration
connection:
pattern: /a/{connection}
defaults: { _controller: AcmeTestBundle:User:index }
and from the following Cookbook;
How to work with Multiple Entity Managers and Connections
My controller would look something like this;
class UserController extends Controller
{
public function indexAction($connection)
{
$products = $this->get('doctrine')
->getRepository('AcmeStoreBundle:Product', $connection)
->findAll()
;
..................
and I'll be able to fetch product information from different em/connection/database.
Now, if I add something like this to my routing;
login:
pattern: /a/{connection}/login
defaults: { _controller: FOSUserBundle:Security:login }
How can I easily make the login to use connection as defined in the connection variable?
This setup assume each database has their own user login information (the fos_user table).
Edit: Updated routing information
Edit2:
I'm still new with PHP/Symfony/Doctrine though, so please forgive me if I'm completely wrong here. I tried to manually set the connection at FOS\UserBundle\Doctrine\UserManager. The following is the constructor of the class
//
use Doctrine\Common\Persistence\ObjectManager;
//
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer, CanonicalizerInterface $emailCanonicalizer, ObjectManager $om, $class)
{
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer);
$this->objectManager = $om;
$this->repository = $om->getRepository($class);
$metadata = $om->getClassMetadata($class);
$this->class = $metadata->getName();
}
In a controller, we can use the following method to change the em to 'testing'
$em = $this->get('doctrine')->getManager('testing');
$repository = $this->get('doctrine')->getRepository($class, 'testing')
For that I changed the code to the following to use EntityManager instead of ObjectManager.
//
//use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
//
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer, CanonicalizerInterface $emailCanonicalizer, EntityManager $om, $class)
{
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer);
$this->objectManager = $om;
$this->repository = $om->getRepository($class);
$metadata = $om->getClassMetadata($class);
$this->class = $metadata->getName();
}
My app works fine with no error.
From the way it works with the controller, I tried changing the connection by adding a parameter to this line then, but it's still using the default connection.
$this->repository = $om->getRepository($class, 'testing');
What else could I be missing here?
As you can see, FOSUserBundle can have only one EntityManager. You can see it from the settings orm.xml
<service id="fos_user.entity_manager" factory-service="doctrine" factory-method="getManager" class="Doctrine\ORM\EntityManager" public="false">
<argument>%fos_user.model_manager_name%</argument>
</service>
Parameter %fos_user.model_manager_name% specified in settings as model_manager_name
fos_user:
db_driver: ~ # Required
user_class: ~ # Required
firewall_name: ~ # Required
model_manager_name: ~
So into the constructor comes the instance of EntityManager, which does not accept the second parameter in the getRepository. Therefore, the standard FOSUserBundle can only work with one database.
But this is not the end of story, it's Symfony :)
We can write out UserManager, that can use different db connections. In the setting see that fos_user.user_manager is a fos_user.user_manager.default. We find it in orm.xml
<service id="fos_user.user_manager.default" class="FOS\UserBundle\Doctrine\UserManager" public="false">
<argument type="service" id="security.encoder_factory" />
<argument type="service" id="fos_user.util.username_canonicalizer" />
<argument type="service" id="fos_user.util.email_canonicalizer" />
<argument type="service" id="fos_user.entity_manager" />
<argument>%fos_user.model.user.class%</argument>
</service>
We can override this class to add an additional parameter that will determine what kind of connection you want to use. Further by ManagerFactory you can get the desired ObjectManager. I wrote simple example for the two databeses (if you need more databases you can write your factory for this service)
define your services in services.yml
services:
acme.user_manager.conn1:
class: Acme\DemoBundle\Service\UserManager
public: true
arguments:
- #security.encoder_factory
- #fos_user.util.username_canonicalizer
- #fos_user.util.email_canonicalizer
- #doctrine
- 'conn1_manager'
- %fos_user.model.user.class%
acme.user_manager.conn2:
class: Acme\DemoBundle\Service\UserManager
public: true
arguments:
- #security.encoder_factory
- #fos_user.util.username_canonicalizer
- #fos_user.util.email_canonicalizer
- #doctrine
- 'conn2_manager'
- %fos_user.model.user.class%
Your manager
/**
* Constructor.
*
* #param EncoderFactoryInterface $encoderFactory
* #param CanonicalizerInterface $usernameCanonicalizer
* #param CanonicalizerInterface $emailCanonicalizer
* #param RegistryInterface $doctrine
* #param string $connName
* #param string $class
*/
public function __construct(EncoderFactoryInterface $encoderFactory, CanonicalizerInterface $usernameCanonicalizer,
CanonicalizerInterface $emailCanonicalizer, RegistryInterface $doctrine, $connName, $class)
{
$om = $doctrine->getEntityManager($connName);
parent::__construct($encoderFactory, $usernameCanonicalizer, $emailCanonicalizer, $om, $class);
}
/**
* Just for test
* #return EntityManager
*/
public function getOM()
{
return $this->objectManager;
}
and simple test
/**
* phpunit -c app/ src/Acme/DemoBundle/Tests/FOSUser/FOSUserMultiConnection.php
*/
class FOSUserMultiConnection extends WebTestCase
{
public function test1()
{
$client = static::createClient();
/** #var $user_manager_conn1 UserManager */
$user_manager_conn1 = $client->getContainer()->get('acme.user_manager.conn1');
/** #var $user_manager_conn2 UserManager */
$user_manager_conn2 = $client->getContainer()->get('acme.user_manager.conn2');
/** #var $om1 EntityManager */
$om1 = $user_manager_conn1->getOM();
/** #var $om2 EntityManager */
$om2 = $user_manager_conn2->getOM();
$this->assertNotEquals($om1->getConnection()->getDatabase(), $om2->getConnection()->getDatabase());
}
}
I'm sorry that the answer was so big. If something is not clear to the end, I put the code on github
FosUserBundle is not able to have more than one entity manager.
The easiest way I found to use 2 databases, is to override the 'checkLoginAction' of the SecurityController.
<?php
//in myuserBunle/Controller/SecurityController.php
class SecurityController extends BaseController
{
/**
* check the user information
*/
public function checkLoginAction(Request $request){
$username = \trim($request->request->get("_username"));
$user = $this->container->get('fos_user.user_manager')->findUserByUsername($username);
$userDB2 = .....
$password = \trim($request->request->get('_password'));
if ($user) {
// Get the encoder for the users password
$encoder = $this->container->get('security.encoder_factory')->getEncoder($user);
$encoded_pass = $encoder->encodePassword($password, $user->getSalt());
if (($user->getPassword() == $encoded_pass) || $this->checkSecondEM()) {
$this->logUser($request, $user);
return new RedirectResponse($this->container->get('router')->generate($this->container->get('session')->get('route'), $request->query->all() ));
} else {
// Password bad
return parent::loginAction($request);
}
} else {
// Username bad
return parent::loginAction($request);
}
}
}

Constructor in Symfony2 Controller

How can I define a constructor in Symfony2 controller. I want to get the the logged in user data available in all the methods of my controller, Currently I do something like this in every action of my controller to get the logged in user.
$em = $this->getDoctrine()->getEntityManager("pp_userdata");
$user = $this->get("security.context")->getToken()->getUser();
I want to do it once in a constructor and make this logged in user available on all my actions
For a general solution for executing code before every controller action you can attach an event listener to the kernel.controller event like so:
<service id="your_app.listener.before_controller" class="App\CoreBundle\EventListener\BeforeControllerListener" scope="request">
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
<argument type="service" id="security.context"/>
</service>
Then in your BeforeControllerListener you will check the controller to see if it implements an interface, if it does, you will call a method from the interface and pass in the security context.
<?php
namespace App\CoreBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\SecurityContextInterface;
use App\CoreBundle\Model\InitializableControllerInterface;
/**
* #author Matt Drollette <matt#drollette.com>
*/
class BeforeControllerListener
{
protected $security_context;
public function __construct(SecurityContextInterface $security_context)
{
$this->security_context = $security_context;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
// not a object but a different kind of callable. Do nothing
return;
}
$controllerObject = $controller[0];
// skip initializing for exceptions
if ($controllerObject instanceof ExceptionController) {
return;
}
if ($controllerObject instanceof InitializableControllerInterface) {
// this method is the one that is part of the interface.
$controllerObject->initialize($event->getRequest(), $this->security_context);
}
}
}
Then, any controllers that you want to have the user always available you will just implement that interface and set the user like so:
use App\CoreBundle\Model\InitializableControllerInterface;
class DefaultController implements InitializableControllerInterface
{
/**
* Current user.
*
* #var User
*/
private $user;
/**
* {#inheritdoc}
*/
public function initialize(Request $request, SecurityContextInterface $security_context)
{
$this->user = $security_context->getToken()->getUser();
}
// ....
}
The interface is nothing more than
namespace App\CoreBundle\Model;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;
interface InitializableControllerInterface
{
public function initialize(Request $request, SecurityContextInterface $security_context);
}
I'm runnig a bit late, but in a controller you can just access the user:
$this->getUser();
Should be working since 2.1
My approach to this was:
Make an empty Interface InitializableControllerInterface
Make event Listener for
namespace ACMEBundle\Event;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class ControllerConstructor
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
// not a object but a different kind of callable. Do nothing
return;
}
$controllerObject = $controller[0];
if ($controllerObject instanceof InitializableControllerInterface) {
$controllerObject->__init($event->getRequest());
}
}
}
In your controller add:
class ProfileController extends Controller implements
InitializableControllerInterface
{
public function __init()
{
$this->user = $security_context->getToken()->getUser();
}
And you will be able to get the $this->user in each action.
Regards

Resources