I want to inject ParameterBagInterface and EntityManagerInterface inside my unit tests (WebTestCase and KernelTestCase), but i couldn't find a method which returns their namespace and name correctly (Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface). is there a way for doing that?
what i have tried is:
$this->parameterBag = self::$container->get(ParameterBagInterface::class);
returns Symfony\Component\DependencyInjection\ParameterBag\ContainerBag
$this->parameterBag = $this->prophesize(ParameterBagInterface::class)->reveal();
returns Double\ParameterBagInterface\P1
$this->parameterBag = $this->createMock(ParameterBagInterface::class);
returns Mock_ParameterBagInterface_fccf09f9
All my classes are using ParameterBagInterface and are type-hinted as such.
Here is example test class:
/**
*
* #package App\Tests\Entity
*/
class LogCollectTest extends WebTestCase
{
use CronManagerCron;
/**
* #var EntityManager
*/
private $em;
/**
* {#inheritDoc}
*/
protected function setUp()
{
self::bootKernel();
$this->parameterBag = self::$container->get(ParameterBagInterface::class);
}
/**
* Test saving click
*/
public function testSavingClick()
{
// truncate the log collect table to be sure to get the right click
$this->truncateLogCollectTable();
$userAgents = [...];
foreach ($userAgents as $agent => $expectedResult) {
// we make fake client requests and record them in database (test enviropment)
$clientStatus = $this->sendClientData($agent);
// the controller is resulting properly
$this->assertEquals(200, $clientStatus);
/**
* #var LogCollect $logCollectEntry
*/
$logCollectEntry = $this->em->getRepository(LogCollect::class)->getLast(); <--
...
// later we process this client requests with cron and later assert the data
$logCollectorCron = new LogCollectorCron(
$this->container,
$this->em,
$this->parameterBag,
'test'
);
$logCollectorCron->run();
...
}
Any suggestions?
You're not gonna get any interface since an interface cannot be instanciated ever, by nature, so to be clear : a ParameterBagInterface object can't exist.
When you ask the container to give you ParameterBagInterface, the container gives you a service that implements this interface.
Related
I have a command which take a long time to run (it generates a big file).
I would like to use a controller to start it in background and don't wait for the end of its execution to render a view.
Is it possible? If yes, how?
I though the Process class would be useful but the documentation says:
If a Response is sent before a child process had a chance to complete, the server process will be killed (depending on your OS). It means that your task will be stopped right away. Running an asynchronous process is not the same as running a process that survives its parent process.
I solved my problem using the Messenger component as #msg suggested in comments.
To do so, I had to:
install the Messenger component by doing composer require symfony/messenger
create a custom log entity to track the file generation
create a custom Message and a custom MessageHandler for my file generation
dispatch the Message in my controller view
move my command code to a service method
call the service method in my MessageHandler
run bin/console messenger:consume -vv to handle the messages
Here is my code:
Custom log entity
I use it to show in my views if a file is being generated and to let the user download the file if its generation is complete
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\MyLogForTheBigFileRepository")
*/
class MyLogForTheBigFile
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $generationDateStart;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $generationDateEnd;
/**
* #ORM\Column(type="string", length=200, nullable=true)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $generator;
public function __construct() { }
// getters and setters for the attributes
// ...
// ...
}
Controller
I get the form submission and dispatch a message which will run the file generation
/**
* #return views
* #param Request $request The request.
* #Route("/generate/big-file", name="generate_big_file")
*/
public function generateBigFileAction(
Request $request,
MessageBusInterface $messageBus,
MyFileService $myFileService
)
{
// Entity manager
$em = $this->getDoctrine()->getManager();
// Creating an empty Form Data Object
$myFormOptionsFDO = new MyFormOptionsFDO();
// Form creation
$myForm = $this->createForm(
MyFormType::class,
$myFormOptionsFDO
);
$myForm->handleRequest($request);
// Submit
if ($myForm->isSubmitted() && $myForm->isValid())
{
$myOption = $myFormOptionsFDO->getOption();
// Creating the database log using a custom entity
$myFileGenerationDate = new \DateTime();
$myLogForTheBigFile = new MyLogForTheBigFile();
$myLogForTheBigFile->setGenerationDateStart($myFileGenerationDate);
$myLogForTheBigFile->setGenerator($this->getUser());
$myLogForTheBigFile->setOption($myOption);
// Save that the file is being generated using the custom entity
$em->persist($myLogForTheBigFile);
$em->flush();
$messageBus->dispatch(
new GenerateBigFileMessage(
$myLogForTheBigFile->getId(),
$this->getUser()->getId()
));
$this->addFlash(
'success', 'Big file generation started...'
);
return $this->redirectToRoute('bigfiles_list');
}
return $this->render('Files/generate-big-file.html.twig', [
'form' => $myForm->createView(),
]);
}
Message
Used to pass data to the service
namespace App\Message;
class GenerateBigFileMessage
{
private $myLogForTheBigFileId;
private $userId;
public function __construct(int $myLogForTheBigFileId, int $userId)
{
$this->myLogForTheBigFileId = $myLogForTheBigFileId;
$this->userId = $userId;
}
public function getMyLogForTheBigFileId(): int
{
return $this->myLogForTheBigFileId;
}
public function getUserId(): int
{
return $this->userId;
}
}
Message handler
Handle the message and run the service
namespace App\MessageHandler;
use App\Service\MyFileService;
use App\Message\GenerateBigFileMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class GenerateBigFileMessageHandler implements MessageHandlerInterface
{
private $myFileService;
public function __construct(MyFileService $myFileService)
{
$this->myFileService = $myFileService;
}
public function __invoke(GenerateBigFileMessage $generateBigFileMessage)
{
$myLogForTheBigFileId = $generateBigFileMessage->getMyLogForTheBigFileId();
$userId = $generateBigFileMessage->getUserId();
$this->myFileService->generateBigFile($myLogForTheBigFileId, $userId);
}
}
Service
Generate the big file and update the logger
public function generateBigFile($myLogForTheBigFileId, $userId)
{
// Get the user asking for the generation
$user = $this->em->getRepository(User::class)->find($userId);
// Get the log object corresponding to this generation
$myLogForTheBigFile = $this->em->getRepository(MyLogForTheBigFile::class)->find($myLogForTheBigFileId);
$myOption = $myLogForTheBigFile->getOption();
// Generate the file
$fullFilename = 'my_file.pdf';
// ...
// ...
// Update the log
$myLogForTheBigFile->setGenerationDateEnd(new \DateTime());
$myLogForTheBigFile->setFilename($fullFilename);
$this->em->persist($myLogForTheBigFile);
$this->em->flush();
}
Today I started upgrading my application from symfony 3 to 4 (and so the related libraries) and I couldn't understand why I couldn't make certain routes work (I had a 401 error but they were supposed to be public routes so no security checks were made there), then I ended up finding this question: #Security annotation on controller class being overridden by action method
A recent comment on the question says that while in a previous version of symfony framework extra bundle, if you put the security annotation on both a class and a method inside that class, the method annotation would override the class annotation, now they stack instead.
This can also be seen (altough it's not very clear since you could already put a #Security annotation on both class and method) on the SensioFramework changelog https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/master/CHANGELOG.md for version 4.0
allowed using multiple #Security annotations (class and method)
This is a very big change for me since a lot of routes in my application relied on that behavior (which was similar to Symfony 1 where you could set a default security behavior and then a more specific one for each action)
/**
* #Route("my-route")
* #Security("is_granted('IS_AUTHENTICATED_FULLY')")
*/
class MyController extends Controller {
/**
* In Symfony 3.x this would've removed security checks for the route,
* now it checks both the class and the method Security expressions
* #Security(true)
*/
public function myAction(Request $request) {
}
}
Is there some way other than "don't upgrade to symfony 4" or "reorganize your code" (which is my "plan B") to have this behavior back? Something like a configuration option or similar...
I can't seem to find anything about this
I had forgot about this question but I did solve this issue by making my own annotation and EventListener.
Disclaimers:
1) My code uses the Dependency Injection bundle to inject and declare services using annotations
2) I'm sharing the code AS IS, with no warranty it'd work for you too, but i hope you can get the gist of it
I created 2 annotations (#IsGrantedDefault and #SecurityDefault) that work exactly like #IsGranted and #Security (they actually extend the original annotations) except they can be applied only to classes, then i created 2 event listeners, one for each annotation. The event listeners also extend the original event listeners, but they just check if a method already has a Security or IsGranted annotation, in which case they do nothing.
IsGrantedDefault.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Annotation;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
/**
* #Annotation
* #Target("CLASS")
*/
class IsGrantedDefault extends IsGranted {
public function getAliasName() {
return 'is_granted_default';
}
public function allowArray() {
return false;
}
}
SecurityDefault.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Annotation;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Annotation
* #Target("CLASS")
*/
class SecurityDefault extends Security {
public function getAliasName() {
return 'security_default';
}
public function allowArray() {
return false;
}
}
DefaultListenerTrait.php (Values::DEFAULT_LISTENER_PREFIX is just a string with an underscore "_")
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event\Traits;
use App\Project\AppBundle\Utils\Values;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
Trait DefaultListenerTrait {
/**
* #var string
*/
private $defaultAttribute;
/**
* #var string
*/
private $otherAttributes = [];
/**
* #var string
*/
private $attribute;
/**
* Sets the class attributes
* #param [type] $defaultAnnotation
* #param string|null $modifyAttr
* #return void
*/
protected function setAttributes($defaultAnnotation, ?string $modifyAttr) {
//Get the attirbutes names
$this->attribute = $modifyAttr;
$this->defaultAttribute = Values::DEFAULT_LISTENER_PREFIX . $defaultAnnotation->getAliasName();
$annotations = [new IsGranted([]), new Security([])];
foreach($annotations as $annotation) {
$this->otherAttributes[] = Values::DEFAULT_LISTENER_PREFIX . $annotation->getAliasName();
}
}
/**
* Checks wheter or not the request needs to be handled by the annotation. If it does adds the correct attribute to the request
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return boolean
*/
protected function updateDefaultListener(FilterControllerArgumentsEvent $event) {
$request = $event->getRequest();
$default = $request->attributes->get($this->defaultAttribute);
//If there's already an "IsGranted" annotation or there's no "IsGrantedDefault" annotation
if (!$default) {
return false;
}
foreach($this->otherAttributes as $attr) {
if ($request->attributes->get($attr) || !$default) {
return false;
}
}
//We set IsGranted from the default and then call the parent eventListener so that it can handle the security
$request->attributes->set($this->attribute, [$default]);
return true;
}
/**
* Calls the event listener for the class if the request is handled by the class
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return void
*/
protected function callEventListener(FilterControllerArgumentsEvent $event) {
if($this->updateDefaultListener($event)) {
parent::onKernelControllerArguments($event);
}
}
}
IsGrantedDefaultListener.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event;
use App\Project\AppBundle\Annotation\IsGrantedDefault;
use App\Project\AppBundle\Event\Traits\DefaultListenerTrait;
use App\Project\AppBundle\Utils\Values;
use RS\DiExtraBundle\Annotation as DI;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\IsGrantedListener;
use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* #DI\Service(autowire = true)
* #DI\Tag("kernel.event_subscriber")
*/
class IsGrantedDefaultListener extends IsGrantedListener {
use DefaultListenerTrait;
/**
* #param \Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter $argumentNameConverter
* #param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authChecker
* #DI\InjectParams({
* "argumentNameConverter" = #DI\Inject("framework_extra_bundle.argument_name_convertor"),
* "authChecker" = #DI\Inject("security.authorization_checker")
* })
*/
public function __construct(ArgumentNameConverter $argumentNameConverter, AuthorizationCheckerInterface $authChecker = null) {
parent::__construct($argumentNameConverter, $authChecker);
$modifyAttr = new IsGranted([]);
$this->setAttributes(new IsGrantedDefault([]), Values::DEFAULT_LISTENER_PREFIX . $modifyAttr->getAliasName());
}
/**
* #param \Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent $event
* #return void
*/
public function onKernelControllerArguments(FilterControllerArgumentsEvent $event) {
$this->callEventListener($event);
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments'];
}
}
SecurityDefaultListener.php
<?php
/*
* #author valepu
*/
namespace App\Project\AppBundle\Event;
use App\Project\AppBundle\Annotation\SecurityDefault;
use App\Project\AppBundle\Event\Traits\DefaultListenerTrait;
use App\Project\AppBundle\Utils\Values;
use Psr\Log\LoggerInterface;
use RS\DiExtraBundle\Annotation as DI;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener;
use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter;
use Sensio\Bundle\FrameworkExtraBundle\Security\ExpressionLanguage;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
/**
* #DI\Service(autowire = true)
* #DI\Tag("kernel.event_subscriber")
*/
class SecurityDefaultListener extends SecurityListener {
use DefaultListenerTrait;
/**
* #param \Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter $argumentNameConverter
* #param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authChecker
* #DI\InjectParams({
* "argumentNameConverter" = #DI\Inject("framework_extra_bundle.argument_name_convertor"),
* "language" = #DI\Inject("sensio_framework_extra.security.expression_language.default"),
* "trustResolver" = #DI\Inject("security.authentication.trust_resolver"),
* "roleHierarchy" = #DI\Inject("security.role_hierarchy"),
* "tokenStorage" = #DI\Inject("security.token_storage"),
* "authChecker" = #DI\Inject("security.authorization_checker"),
* "logger" = #DI\Inject("logger")
* })
*
*/
public function __construct(ArgumentNameConverter $argumentNameConverter, ExpressionLanguage $language = null, AuthenticationTrustResolverInterface $trustResolver = null, RoleHierarchyInterface $roleHierarchy = null, TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authChecker = null, LoggerInterface $logger = null) {
parent::__construct($argumentNameConverter, $language, $trustResolver, $roleHierarchy, $tokenStorage, $authChecker, $logger);
$modifyAttr = new Security([]);
$this->setAttributes(new SecurityDefault([]), Values::DEFAULT_LISTENER_PREFIX . $modifyAttr->getAliasName());
}
public function onKernelControllerArguments(FilterControllerArgumentsEvent $event) {
$this->callEventListener($event);
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments'];
}
}
You can delete the class annotation and declare them on all methods
I have a symfony entity that has a not mapped calculated field
namespace AppBundle\Entity;
class Page
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Page count. Non-mapped
*
* #var integer
*/
protected $pageCount;
}
The $pageCount value is obtainable by consuming a remote service that will provide the value for use in the application.
I figured the best way is to use the postLoad event to handle this.
class PageListener
{
/**
* #ORM\PostLoad
*/
public function postLoad(LifecycleEventArgs $eventArgs)
{
// ...
}
}
I need to retrieve this value when loading values.
public function indexAction()
{
// I want to fetch the pageHits here
$pagesListing = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
// I don't want to fetch the pageHits here
$pagesListing2 = $this->getDoctrine()
->getRepository('AppBundle:Pages')
->findAll();
}
However, this will ALWAYS result in a call to a remote service.
There may be cases where I do not want the service to be invoked, so that it reduced a performance load on the application.
How can I fetch the remote values automatically, but only when I want to.
Your "problem" is pretty common and one of the reasons I never use Doctrine repositories directly.
Solution I would recommend
Always make custom repository services and inject Doctrine into them.
That way, if you want to merge some data from some other data source (eg. Redis, filesystem, some remote API), you have complete control over it and process is encapsulated.
Example:
class PageRepository
{
private $em;
private $api;
public function __construct(EntityManagerInterface $em, MyAwesomeApi $api)
{
$this->em = $em;
$this->api = $api;
}
public function find($id)
{
return $em->getRepository(Page::class)->find($id);
}
public function findAll()
{
return $em->getRepository(Page::class)->findAll();
}
public function findWithCount($id)
{
$page = $this->find($id);
$count = $this->myAwesomeApi->getPageCount($id);
return new PageWithCount($page, $count);
}
}
Solution I wouldn't recommend, but works :)
If you don't want to change your code structure and want to keep it as it is, you could make a really simple change that will make your pageCount be loaded only when it is necessary:
Move code from Page::postLoad method into Page::getPageCount()
Example:
public function getPageCount()
{
if (null === $this->pageCount) {
$this->pageCount = MyAwesomeApi::getPageCount($this->id);
}
return $this->pageCount;
}
This way, pageCount will only be loaded if something tries to access it.
I'm writing a functional test for an Action entity having a relationship with the User entity:
<?php
namespace Acme\AppBundle\Entity;
/**
* Class Action
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Acme\AppBundle\Repository\ActionRepository")
*/
class Action
{
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Acme\AppBundle\Entity\User
*
* #ORM\ManyToOne(targetEntity="\Acme\AppBundle\Entity\User", inversedBy="actions")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $createdBy;
}
User:
namespace Acme\AppBundle\Entity;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Action", mappedBy="createdBy")
*/
private $actions;
}
And the user is setted in the controller with the following snippet:
<?php
namespace Acme\ApiBundle\Controller;
/**
*
* #Route("/actions")
*/
class ActionController extends FOSRestController
{
public function postAction(Request $request)
{
$action = new Action();
$action->setCreatedBy($this->getUser());
return $this->processForm($action, $request->request->all(), Request::METHOD_POST);
}
}
When calling the action with a REST client for example, everything works fine, the relationship between Action and User is persisted correctly.
Now, when testing the action with a functional test, the relationship is not working because of the following error:
A new entity was found through the relationship 'Acme\AppBundle\Entity\Action#createdBy' that was not configured to cascade persist operations for entity: test. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
For my functional test I need to inject a JWT and a session token because my routes are secured by a JWT and I need to have a user in session.
Here is how I inject that:
<?php
namespace Acme\ApiBundle\Tests;
class ApiWebTestCase extends WebTestCase
{
/**
* #var ReferenceRepository
*/
protected $fixturesRepo;
/**
* #var Client
*/
protected $authClient;
/**
* #var array
*/
private $fixtures = [];
protected function setUp()
{
$fixtures = array_merge([
'Acme\AppBundle\DataFixtures\ORM\LoadUserData'
], $this->fixtures);
$this->fixturesRepo = $this->loadFixtures($fixtures)->getReferenceRepository();
$this->authClient = $this->createAuthenticatedClient();
}
/**
* Create a client with a default Authorization header.
*
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->fixturesRepo->getReference('user-1');
$jwtManager = $this->getContainer()->get('lexik_jwt_authentication.jwt_manager');
$token = $jwtManager->create($user);
$this->loginAs($user, 'api');
$client = static::makeClient([], [
'AUTHENTICATION' => 'Bearer ' . $token,
'CONTENT_TYPE' => 'application/json'
]);
$client->disableReboot();
return $client;
}
}
Now, the issue is that the injected UsernamePasswordToken contains a User instance which is detached from the current EntityManager, thus resulting in the Doctrine error above.
I could merge the $user object in the postAction method into the EntityManager but I don't want to do that because it means I modify my working code to make a test passes.
I've also tried directling merging the $user object in my test into the EntityManager like this:
$em = $client->getContainer()->get('doctrine')->getManager();
$em->merge($user);
But it's not working either.
So now, I'm stuck, I really don't know what to do except that I need to attach the user in session back to the current EntityManager.
The error message you are getting indicates that the EntityManager contained in the test client's container doesn't know about your User entity. This leads me to believe that the way you are retrieving the User in your createAuthenticatedClient method is using a different EntityManager.
I suggest you try to use the test kernel's EntityManager to retrieve the User entity instead. You can get it from the test client's container, for example.
Thanks to your tweet, I come to complete the given answer and (try to) propose a solution,
The problem is that your user is not managed by the EntityManager, and more simply, because it's not a real existing user that is registered in database.
To get around this problem, you need to have a real (managed) user that doctrine could use for the association that your action is trying to create.
So, you can either create this user at each execution of your functional test case (and delete it when finished), or create it only once when execute the test case for the first time on a new environment.
Something like this should do the trick:
/** #var EntityManager */
private $em;
/**
*/
public function setUp()
{
$client = static::createClient();
$this->em = $client->getKernel()
->getContainer()
->get('doctrine');
$this->authClient = $this->createAuthenticatedClient();
}
/**
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->em
->getRepository('Acme\AppBundle\Entity\User')
->findOneBy([], ['id' => DESC]; // Fetch the last created
// ...
return $client;
}
That's a pity for your fixtures (that are so much sexier), but I don't see any way to attach your fixture as a real entry, as you can't interact more with the tested controller.
Another way would be to create a request to your login endpoint, but it would be even more ugly.
File based translations don't work for me because clients need to change the texts.
So I am thinking about implementing this interface to fetch data from the database and cache the results in an APC cache.
Is this a good solution?
This could be what you are looking for:
Use a database as a translation provider in Symfony 2
Introduction
This article explain how to use a database as translation storage in Symfony 2. Using a database to provide translations is quite easy to do in Symfony 2, but unfortunately it’s actually not explained in Symfony 2 website.
Creating language entities
At first, we have to create database entities for language management. In my case, I’ve created three entities : the Language entity contain every available languages (like french, english, german).
The second entity is named LanguageToken. It represent every available language tokens. The token entity represent the source tag of the xliff files. Every translatable text available is a token. For example, I use home_page as a token and it’s translated as Page principale in french and as Home page in english.
The last entity is the LanguageTranslation entity : it contain the translation of a token in a specific language. In the example below, the Page principale is a LanguageTranslation entity for the language french and the token home_page.
It’s quite inefficient, but the translations are cached in a file by Symfony 2, finally it’s used only one time at Symfony 2 first execution (except if you delete Symfony 2’s cache files).
The code of the Language entity is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageRepository")
*/
class Language {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200) */
private $locale;
/** #ORM\column(type="string", length=200) */
private $name;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getLocale() {
return $this->locale;
}
public function setLocale($locale) {
$this->locale = $locale;
}
public function getName() {
return $this->name;
}
public function setName($name) {
$this->name = $name;
}
}
The code of the LanguageToken entity is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageTokenRepository")
*/
class LanguageToken {
/**
* #ORM\Id #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200, unique=true) */
private $token;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getToken() {
return $this->token;
}
public function setToken($token) {
$this->token = $token;
}
}
And the LanguageTranslation entity’s code is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageTranslationRepository")
*/
class LanguageTranslation {
/**
* #ORM\Id #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200) */
private $catalogue;
/** #ORM\column(type="text") */
private $translation;
/**
* #ORM\ManyToOne(targetEntity="YourApp\YourBundle\Entity\Language", fetch="EAGER")
*/
private $language;
/**
* #ORM\ManyToOne(targetEntity="YourApp\YourBundle\Entity\LanguageToken", fetch="EAGER")
*/
private $languageToken;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getCatalogue() {
return $this->catalogue;
}
public function setCatalogue($catalogue) {
$this->catalogue = $catalogue;
}
public function getTranslation() {
return $this->translation;
}
public function setTranslation($translation) {
$this->translation = $translation;
}
public function getLanguage() {
return $this->language;
}
public function setLanguage($language) {
$this->language = $language;
}
public function getLanguageToken() {
return $this->languageToken;
}
public function setLanguageToken($languageToken) {
$this->languageToken = $languageToken;
}
}
Implementing a LoaderInterface
The second step is to create a class implementing the Symfony\Component\Translation\Loader\LoaderInterface. The corresponding class is shown here :
class DBLoader implements LoaderInterface{
private $transaltionRepository;
private $languageRepository;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager){
$this->transaltionRepository = $entityManager->getRepository("AppCommonBundle:LanguageTranslation");
$this->languageRepository = $entityManager->getRepository("AppCommonBundle:Language");
}
function load($resource, $locale, $domain = 'messages'){
//Load on the db for the specified local
$language = $this->languageRepository->getLanguage($locale);
$translations = $this->transaltionRepository->getTranslations($language, $domain);
$catalogue = new MessageCatalogue($locale);
/**#var $translation Frtrains\CommonbBundle\Entity\LanguageTranslation */
foreach($translations as $translation){
$catalogue->set($translation->getLanguageToken()->getToken(), $translation->getTranslation(), $domain);
}
return $catalogue;
}
}
The DBLoader class need to have every translations from the LanguageTranslationRepository (the translationRepository member). The getTranslations($language, $domain) method of the translationRepository object is visible here :
class LanguageTranslationRepository extends EntityRepository {
/**
* Return all translations for specified token
* #param type $token
* #param type $domain
*/
public function getTranslations($language, $catalogue = "messages"){
$query = $this->getEntityManager()->createQuery("SELECT t FROM AppCommonBundle:LanguageTranslation t WHERE t.language = :language AND t.catalogue = :catalogue");
$query->setParameter("language", $language);
$query->setParameter("catalogue", $catalogue);
return $query->getResult();
}
...
}
The DBLoader class will be created by Symfony as a service, receiving an EntityManager as constructor argument. All arguments of the load method let you customize the way the translation loader interface work.
Create a Symfony service with DBLoader
The third step is to create a service using the previously created class. The code to add to the config.yml file is here :
services:
translation.loader.db:
class: MyApp\CommonBundle\Services\DBLoader
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: translation.loader, alias: db}
The transation.loader tag indicate to Symfony to use this translation loader for the db alias.
Create fake translation files
The last step is to create an app/Resources/translations/messages.xx.db file for every translation (with xx = en, fr, de, …).
I didn’t found the way to notify Symfony to use DBLoader as default translation loader. The only quick hack I’ve found is to create a app/Resources/translations/messages.en.db file. The db extension correspond to the db alias used in the service declaration. A corresponding file is created for every language available on the website, like messages.fr.db for french or messages.de.db for german.
When Symfony find the messages.xx.db file he load the translation.loader.db to manage this unknown extension and then the DBLoader use database content to provide translation.
I’ve also didn’t found the way to clean properly the translations cache on database modification (the cache have to be cleaned to force Symfony to recreate it). The code I actually use is visible here :
/**
* Remove language in every cache directories
*/
private function clearLanguageCache(){
$cacheDir = __DIR__ . "/../../../../app/cache";
$finder = new \Symfony\Component\Finder\Finder();
//TODO quick hack...
$finder->in(array($cacheDir . "/dev/translations", $cacheDir . "/prod/translations"))->files();
foreach($finder as $file){
unlink($file->getRealpath());
}
}
This solution isn’t the pretiest one (I will update this post if I find better solution) but it’s working ^^
Be Sociable, Share!
Take a look at the Translatable behavior extension for Doctrine 2. StofDoctrineExtensionsBundle integrates it with Symfony.
You may want to take a look into this Loader + Resource using PDO connection: https://gist.github.com/3315472
You then only need to make it cache aware, like adding a memcache, apc, .. in between.
If so, you can then disable the filecaching of the Translator itself.