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
Related
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);
}
i'm trying to load translations from database in Symfony 4. The Translator instance doesn't call the custom loader i wrote using this tutorial (https://medium.com/#andrew72ru/store-translation-messages-in-database-in-symfony-3f12e579df74).
I created dummy files in the /translation folder (messages.it.db) to trigger the loader but it doesn't get called.
services.yaml
parameters:
locales: ['it','en']
db_i18n.entity: App\Entity\Translation
services:
translation.loader.db:
class: App\Loader\DbLoader
arguments:
- '#service_container'
- '#doctrine.orm.entity_manager'
tags:
- { name: translation.loader, alias: db}
DbLoader.php
namespace App\Loader;
use Creative\DbI18nBundle\Interfaces\EntityInterface;
use Creative\DbI18nBundle\Interfaces\TranslationRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
class DbLoader implements LoaderInterface
{
/**
* #var EntityManagerInterface
*/
private $doctrine;
/**
* #var string
*/
private $entityClass;
public function __construct(ContainerInterface $container, EntityManagerInterface $doctrine)
{
$this->doctrine = $doctrine;
$this->entityClass = $container->getParameter('db_i18n.entity');
}
public function load($resource, $locale, $domain = 'messages')
{
$messages = $this->getRepository()->findByDomainAndLocale($domain, $locale);
$values = array_map(static function (EntityInterface $entity) {
return $entity->getTranslation();
}, $messages);
$catalogue = new MessageCatalogue($locale, [
$domain => $values
]);
return $catalogue;
}
public function getRepository(): TranslationRepositoryInterface
{
return $this->doctrine->getRepository($this->entityClass);
}
}
Here's my translation table
Here is the test code i'm using to call the Translator
TestController.php
class TestController extends AbstractController
{
/**
* #Route("/test", name="test")
*/
public function index(TranslatorInterface $translator)
{
$translator->trans('prova', [], 'messages', 'it');
return new Response();
}
}
The result is supposed to be "prova it" but I get "prova" instead, which is the key of the translation. I tried to put a dd() on the DbLoader constructor and it's never been called.
I also have in my project Api Platform, but i don't think it's causing this problem.
I resolved my issue.
By using dd() on my Translator instance i discovered that Symfony wasn't loading my translation files correctly. Looking through the properties i noticed the path of my translation files were not correct.
I placed them in src/Resources/translations instead and then it worked!
I need to run the controller method every 2 hours. I read somewhere that you need to create a command and run this command by using CRON. It is correct?
MY COMMAND:
namespace AppBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Annotation\Route;
class RunCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'app:run';
protected function configure()
{
// ...
}
protected function execute(InputInterface $input, OutputInterface $output)
{
echo 'BEGIN';
$controller = new \AppBundle\Controller\DefaultController();
$controller->storeAction();
echo 'END';
}
}
MY CONTROLLER:
/**
* #Route("/to-db", name="to-db")
*/
public function storeAction()
{
$entityManager = $this->getDoctrine()->getManager();
$data = new Skuska();
$data->setName('Keyboard');
$entityManager->persist($data);
$entityManager->flush();
// die();
}
My error: In ControllerTrait.php line 424: Call to a member function has() on null
Is my code correct? How do I run a method using cron?
I don't want to use another bundle. I want to program it myself
As mentioned in the comments, you should move the logic out of the controller and into a service, and use that service both in the command and in the controller.
With the default service autoloading configuration, you don't even have to care about your service declarations. Your command will automatically be a service, and you can inject other services into it.
https://symfony.com/doc/current/console/commands_as_services.html
For controllers, you don't even need to use a specific constructor.
https://symfony.com/doc/current/controller.html#fetching-services
<?php
// AppBundle/Service/StoreService.php
use AppBundle\Entity\Skuska;
use Doctrine\ORM\EntityManager;
class StoreService
{
/** #var EntityManager */
private $entityManager;
/**
* StoreService constructor.
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function store()
{
$data = new Skuska();
$data->setName('Keyboard');
$this->entityManager->persist($data);
$this->entityManager->flush();
}
}
<?php
// AppBundle/Controller/StoreController.php
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Service\StoreService;
class StoreController extends Controller
{
/**
* #Route("/to-db", name="to-db")
* #param StoreService $storeService
* #return Response
*/
// Hinting to you service like this should be enough for autoloading.
// No need for a specific constructor here.
public function storeAction(StoreService $storeService)
{
$storeService->store();
return new Response(
// Return something in you response.
);
}
}
<?php
// AppBundle/Command/RunCommand.php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use AppBundle\Service\StoreService;
class RunCommand extends Command
{
protected static $defaultName = 'app:run';
/** #var StoreService */
protected $storeService;
/**
* RunCommand constructor.
* #param StoreService $storeService
*/
public function __construct(StoreService $storeService)
{
$this->storeService = $storeService;
parent::__construct();
}
protected function configure()
{
// ...
}
protected function execute(InputInterface $input, OutputInterface $output)
{
echo 'BEGIN';
$this->storeService->store();
echo 'END';
}
}
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.
in my services constructor
public function __construct(
EntityManager $entityManager,
SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
$this->entityManager = $entityManager;
I pass entityManager and securityContext as argument.
also my services.xml is here
<service id="acme.memberbundle.calendar_listener" class="Acme\MemberBundle\EventListener\CalendarEventListener">
<argument type="service" id="doctrine.orm.entity_manager" />
<argument type="service" id="security.context" />
but now,I want to use container in services such as
$this->container->get('router')->generate('fos_user_profile_edit')
how can I pass the container to services?
It's easy, if service extends ContainerAware
use \Symfony\Component\DependencyInjection\ContainerAware;
class YouService extends ContainerAware
{
public function someMethod()
{
$this->container->get('router')->generate('fos_user_profile_edit')
...
}
}
service.yml
your.service:
class: App\...\YouService
calls:
- [ setContainer,[ #service_container ] ]
Add:
<argument type="service" id="service_container" />
And in your listener class:
use Symfony\Component\DependencyInjection\ContainerInterface;
//...
public function __construct(ContainerInterface $container, ...) {
It's 2016, you can use trait which will help you extend same class with multiple libraries.
<?php
namespace iBasit\ToolsBundle\Utils\Lib;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Component\DependencyInjection\ContainerInterface;
trait Container
{
private $container;
public function setContainer (ContainerInterface $container)
{
$this->container = $container;
}
/**
* Shortcut to return the Doctrine Registry service.
*
* #return Registry
*
* #throws \LogicException If DoctrineBundle is not available
*/
protected function getDoctrine()
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application.');
}
return $this->container->get('doctrine');
}
/**
* Get a user from the Security Token Storage.
*
* #return mixed
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
return $user;
}
/**
* Returns true if the service id is defined.
*
* #param string $id The service id
*
* #return bool true if the service id is defined, false otherwise
*/
protected function has ($id)
{
return $this->container->has($id);
}
/**
* Gets a container service by its id.
*
* #param string $id The service id
*
* #return object The service
*/
protected function get ($id)
{
if ('request' === $id)
{
#trigger_error('The "request" service is deprecated and will be removed in 3.0. Add a typehint for Symfony\\Component\\HttpFoundation\\Request to your controller parameters to retrieve the request instead.', E_USER_DEPRECATED);
}
return $this->container->get($id);
}
/**
* Gets a container configuration parameter by its name.
*
* #param string $name The parameter name
*
* #return mixed
*/
protected function getParameter ($name)
{
return $this->container->getParameter($name);
}
}
Your object, which will be service.
namespace AppBundle\Utils;
use iBasit\ToolsBundle\Utils\Lib\Container;
class myObject
{
use Container;
}
Your service settings
myObject:
class: AppBundle\Utils\myObject
calls:
- [setContainer, ["#service_container"]]
Call your service in controller
$myObject = $this->get('myObject');
If all your services are ContainerAware, I suggest to create a BaseService class that will contain all common code with your other services.
1) Create the Base\BaseService.php class:
<?php
namespace Fuz\GenyBundle\Base;
use Symfony\Component\DependencyInjection\ContainerAware;
abstract class BaseService extends ContainerAware
{
}
2) Register this service as abstract in your services.yml
parameters:
// ...
geny.base.class: Fuz\GenyBundle\Base\BaseService
services:
// ...
geny.base:
class: %geny.base.class%
abstract: true
calls:
- [setContainer, [#service_container]]
3) Now, in your other services, extends your BaseService class instead of ContainerAware:
<?php
namespace Fuz\GenyBundle\Services;
use Fuz\GenyBundle\Base\BaseService;
class Loader extends BaseService
{
// ...
}
4) Finally, you can use the parent option in your services declaration.
geny.loader:
class: %geny.loader.class%
parent: geny.base
I prefer this way for several reasons:
there is consistency between the code and the config
this avoids duplicating too much config for each service
you have a base class for each services, very helpful for common code