I want to refactoring my code, but now i have error and don't understand what.
Objectif : Don't need to pass parameters when call TokenService, and use autowiring to autowiring EntityManager & Request, and don't set it when controller call service.
Cannot resolve argument $tokenService of App\Controller\TokenController::showTokens()
Cannot autowire service App\Service\TokenService
argument $request of method __construct() references class Symfony\Component\HttpFoundation\Request but no such service exists.
Before :
/src/Controller/TokenController.php
<?php
namespace App\Controller;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/v1")
*/
class TokenController
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function showTokens(Request $request, EntityManagerInterface $em): JsonResponse
{
$tokenService = new TokenService($request, $em);
return $tokenService->getTokens();
}
}
/src/Service/TokenService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class TokenService
* #package App\Service
*/
class TokenService
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function getTokens()
{
return true;
}
}
After :
/config/services.yaml
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
App\Service\TokenService: ~
/src/Controller/TokenController.php
<?php
namespace App\Controller;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/v1")
*/
class TokenController
{
public function showTokens(Request $request, EntityManagerInterface $em, TokenService $tokenService): JsonResponse
{
return $tokenService->getTokens();
}
/src/Service/TokenService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class TokenService
* #package App\Service
*/
class TokenService
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function getTokens()
{
return true;
}
}
Thanks !
I guess it's been awhile since we had a good request stack question. I did a bit of a search and did not find any answer that was directly applicable and provided a decent explanation.
The basic issue is that the Symfony framework supports nested requests. You get these, for example, when using embedded controllers. So there is no actual request service. There actually used to be when Symfony 2.0 was first released but it was a real mess. Supporting nested request services was done at the container level and it was not fun.
So a big hammer known as the request stack was introduced to solve the problem once and for all. You inject the request stack instead of the request and then access the request when you actually need it.
class TokenService
{
private $em;
private $requestStack;
public function __construct(RequestStack $requestStack, EntityManagerInterface $em)
{
$this->requestStack = $requestStack;
$this->em = $em;
}
public function getTokens()
{
$request = $this->requestStack->getMasterRequest(); // or possibly getCurrentRequest depending on where the tokens are
return true;
}
Having said that, I would personally just pass the request from the controller. Doing so gets rid of that 'it depends' comment of mine. I also thinks it reduces the 'magic' involved just a bit. Both approaches will work.
class TokenService
{
public function getTokens(Request $request)
{
return true;
}
...
class TokenController
{
public function showTokens(Request $request, TokenService $tokenService): JsonResponse
{
return $tokenService->getTokens($request);
}
Related
How can I test Services with PHPUnit using symfony? So far, I installed and included test-pack, DAMA Doctrine Bundle, and created Test Database.
Inside .env.test I added Database connection
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
# .env.test.local
DATABASE_URL="mysql://root:root#db:3306/testdb?serverVersion=mariadb-10.4.11&charset=utf8mb4"
I included inside phpunit.xml.dist the DAMA Doctrine bundle
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
Now, what I want to test is my Services (for instance CartService, ProductService etc.)
use App\Entity\Cart;
use App\Entity\CartItem;
use App\Entity\Product;
use App\Entity\User;
use App\Repository\CartItemRepository;
use App\Repository\CartRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Security;
class CartService
{
private CartRepository $cartRepository;
private ManagerRegistry $managerRegistry;
private CartItemRepository $cartItemRepository;
private Security $security;
public function __construct(Security $security, CartItemRepository $cartItemRepository, CartRepository $cartRepository, ManagerRegistry $managerRegistry)
{
$this->cartItemRepository = $cartItemRepository;
$this->cartRepository = $cartRepository;
$this->managerRegistry = $managerRegistry;
$this->security = $security;
}
/**
* Get Cart by ID
*
* #return Cart|null
*/
public function getCartByUserId(): ?Cart
{
$user = $this->security->getUser();
return $this->cartRepository->findOneBy(['customer' => $user]);
}
/**
* Show Cart and Total Price
*
* #return Cart|null
*/
public function showCart(): ?Cart
{
$cart = $this->getCartByUserId();
$this->calculateTotalPrice();
return $cart;
}
When I run phpunit test on CartServiceTest, I get this error:
1) App\Tests\CartServiceTest::testShowCart
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
/var/www/html/Tests/CartServiceTest.php:29
CartServiceTest look like this
<?php
namespace App\Tests;
use App\Entity\Product;
use App\Service\CartService;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CartServiceTest extends KernelTestCase
{
/**
* #var EntityManager
*/
private EntityManager $entityManager;
private CartService $cartService;
public function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testShowCart()
{
$user = 11;
$cart = $this->cartService->getCartByUserId();
dump($cart);
}
protected function tearDown(): void
{
$this->entityManager->close();
}
}
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
Means that you have to already Use the cartService in your application. For exemple if you already inject this service has a dependency injection in one of your controller it's okay.
But you can do better. Just create a service config for your tests "services_test.yaml" and make your service public
Something like:
#servies_test.yaml
services:
App\Service\CartService:
public: true
How to register any service ex: RequestStack to be used in any other services with current state in laravel?
If I inject and push request to request stack in one service, I cannot access it in other service (RequestStack builds anew)
<?php
namespace App\Services;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class CustomClass
{
/**
* #var RequestStack
*/
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function createRequest()
{
$this->requestStack->push(new Request());
}
}
<?php
namespace App\Services;
use Symfony\Component\HttpFoundation\RequestStack;
class CustomClass2
{
/**
* #var RequestStack
*/
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getRequest()
{
// Empty
$request = $this->requestStack->getMasterRequest();
}
}
How to register RequestStack globally, so I can access any modifications by any other service?
In my case I have a class such as:
class Logger
{
/**
* #var EntityManager
*/
protected $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #param Model $model
*/
public function log(Model $model)
{
$logEntity = new LogEntity();
$logEntity->setOrder($model->getOrder());
$logEntity->setType($model->getType());
$logEntity->setScore($model->getScore());
$logEntity->setCreated(new DateTime());
$logEntity->setModified(new DateTime());
$this->entityManager->persist($logEntity);
$this->entityManager->flush();
return $logEntity;
}
Logger class is not testable because in my code 'new' keyword exists, in other hand Logger class and EntityManager class registered singleton in container and can't inject model as dependency.
How to change class for change to testable class?
LoggerModel is a Doctrine entity and use in Laravel framework.
I solve this problem with a sample solution: Factory Pattern.
I need assertion, so when get a new model from factory assert it as mock.
And how?
I create a class with a method that can be a singleton service:
class LogFactory
{
public function makeLogEntity()
{
return new LogEntity();
}
}
In another service, inject factory class:
class Logger
{
/**
* #var EntityManager
*/
protected $entityManager;
/**
* #var LogFactory
*/
protected $logFactory;
public function __construct(EntityManager $entityManager, LogFactory $logFactory)
{
$this->entityManager = $entityManager;
$this->logFactory = $logFactory
}
/**
* #param Model $model
*/
public function log(Model $model)
{
$logEntity = $this->logFactory->makeLogEntity();
$logEntity->setOrder($model->getOrder());
$logEntity->setType($model->getType());
$logEntity->setScore($model->getScore());
$logEntity->setCreated(new DateTime());
$logEntity->setModified(new DateTime());
$this->entityManager->persist($logEntity);
$this->entityManager->flush();
return $logEntity;
}
Now I have a service that is mock able and call $mock->willReturn() function in test.
trying to make an subscriber for Entity actions (CRUD) and cannot figure it out.
I know there is a way, where I can make listener and send him 3 different events, but that's not what I want to reach, I dont even think is good solution.
Event Subscriber
<?php
namespace App\EventListener;
use App\Entity\Log;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
/**
* Part of program created by David Jungman
* #author David Jungman <davidjungman.web#gmail.com>
*/
class EntitySubscriber implements EventSubscriberInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, EntityManagerInterface $em)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public static function getSubscribedEvents()
{
return array(
Events::postPersist,
Events::postUpdate,
Events::postRemove,
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->logEvent($args, "remove");
}
public function postRemove(LifecycleEventArgs $args)
{
$this->logEvent($args, "remove");
}
public function postPersist(LifecycleEventArgs $args)
{
$this->logEvent($args, "create");
}
private function logEvent(LifecycleEventArgs $args, string $method)
{
$entity = $args->getEntity();
if($entity->getShortName() != "Log")
{
$user = $this->tokenStorage->getToken()->getUser();
$log = new Log();
$log
->setUser($user)
->setAffectedTable($entity->getShortName())
->setAffectedItem($entity->getId())
->setAction($method)
->setCreatedAt();
$this->em->persist($log);
$this->em->flush();
}
}
}
and my Service.yaml part
App\EventListener\EntitySubscriber:
tags:
- { name: doctrine.event_subscriber, connection: default }
I have tried:
I've looked into these 2 official tutorials:
-https://symfony.com/doc/current/event_dispatcher.html
-https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html
but neither helped.. when I use shown part of config, my computer freeze.
When I try to debug it, I can see these methods active
( php bin/console debug:event-dispatcher )
but they are listening on "event" event
Doctrine has it's own events handler/subscriber system. However, with the class Symfony\Component\EventDispatcher\EventSubscriberInterface; that you are implementing, that is from the Symfony event system.
<?php
use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber; // **the Doctrine Event subscriber interface**
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
class MyEventSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
Events::postUpdate,
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
// perhaps you only want to act on some "Product" entity
if ($entity instanceof Product) {
// do something with the Product
}
}
}
I have a controller, ApiBundle\Controller\UserController, and it has the following contents:
<?php
namespace ApiBundle\Controller;
use Doctrine\ORM\EntityManager;
use FOS\RestBundle\Controller\Annotations\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\User;
use ApiBundle\Form\RegisterType;
use ApiBundle\Utility\ApiUtility;
class UserController extends ApiController
{
/**
* #var EntityManager $em
*/
private $em;
/**
* UserController constructor.
* #param EntityManager $entityManager
*/
function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #Route("register")
* #Method({"POST"})
* #return \Symfony\Component\HttpFoundation\Response
* #param Request $request
*/
public function registerUser(Request $request)
{
$data = ApiUtility::getContentAsArray($request);
$user = new User();
$form = $this->createForm(new RegisterType(), $user);
$form->submit($data);
if($form->isValid()) {
$user->setUsername($request->get('email'));
$this->em->persist($user);
$this->em->flush();
}else{
return ApiUtility::returnResponse($form->getErrors());
}
}
}
It extends API Controller, seen here:
<?php
namespace ApiBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
class ApiController extends FOSRestController
{
}
And it has the following services:
services:
api.controller.user:
class: ApiBundle\Controller\UserController
public: false
parent: api.controller
arguments: ["#doctrine.orm.entity_manager"]
api.controller:
class: ApiBundle\Controller\ApiController
calls:
- [setContainer, ['#service_container']]
No matter what I do, I can't get the container to inject. Right now, when I just try to get doctrine into the container through the constructor, I get Type error: Too few arguments to function ApiBundle\Controller\UserController::__construct(), 0 passed in app/cache/dev/classes.php on line 2230 and exactly 1 expected, if I switch it back to $this->container->get('doctrine.orm.entity_manager') or getEntityManager, I get container is null. How can I fix this?