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.
Related
Context
In a simple Symfony project, I've created two entities, Product and Category, which are related by a #ManyToOne and a #OneToMany relationship with Doctrine Annotations. One category can have multiple products and one product relates to one category. I've manually inserted data in the Category table.
When I fetch data using Category entity repository and I display it with a var_dump(...), an infinite recursion happens. When I return a JSON response with these data, it is just empty. It should retrieve exactly the data I inserted manually.
Do you have any idea of how to avoid this error without removing the inverse side relationship in the Category entity?
What I've tried
Adding the Doctrine Annotation fetch="LAZY" in one side, the other side and both side of the relationship.
Inserting Category object in the database using Doctrine to see if the database connection is working. Yes it is.
Removing the inverse side of the relationship. It worked but it's not what I want.
Code snippet
Controller
dummy/src/Controller/DefaultController.php
...
$entityManager = $this->getDoctrine()->getManager();
$repository = $entityManager->getRepository(Category::class);
// ===== PROBLEM HERE =====
//var_dump($repository->findOneByName('house'));
//return $this->json($repository->findOneByName('house'));
...
Entities
dummy/src/Entity/Category.php
<?php
namespace App\Entity;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=CategoryRepository::class)
*/
class Category
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="category", fetch="LAZY")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* #return Collection|Product[]
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setCategory($this);
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
// set the owning side to null (unless already changed)
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}
dummy/src/Entity/Product.php
<?php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity=Category::class, inversedBy="products", fetch="LAZY")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
I assume you use var_dump for debugging purposes. For debugging purposes use dump or dd which is from symfony/debug and should already be enabled on dev by default. Both dump and dd should abort the infinite recursion in time. (Lots of symfony/doctrine objects/services have circular references or just a lot of referenced objects.) dump adds the given php var(s) to either the profiler (target mark symbol in the profiler bar) or to the output. dd adds the given var(s) like dump but also ends the process (so dump and die). - On production never use dump/dd/var_dump, but properly serialize your data.
Secondly, $this->json is essentially a shortcut for packing json_encode into a JsonResponse object (or use the symfony/serializer instead). json_encode on the other hand serializes public properties of the object(s) given unless the object(s) implement JsonSerializable (see below). Since almost all entities usually have all their properties private, the result is usually an empty object(s) serialization.
There are a multitude of options to choose from, but essentially you need to solve the problem of infinite recursion. The imho standard options are:
using the symfony serializer which can handle circular references (which cause the infinite recursion/loop) and thus turning the object into a safe array. However, the results may still not be to your liking...
implementing JsonSerializable on your entity and carefully avoid recursively adding the child-objects.
building a safe array yourself from the object, to pass to $this->json ("the manual approach").
A safe array in this context is one, that contains only strings, numbers and (nested) arrays of strings and numbers, which essentially means, losing all actual objects.
There are probably other options, but I find these the most convenient ones. I usually prefer the JsonSerializable option, but it's a matter of taste. One example for this would be:
class Category implements \JsonSerializable { // <-- new implements!
// ... your entity stuff
public function jsonSerialize() {
return [
'id' => $this->id,
'name' => $this->name,
'products' => $this->products->map(function(Product $product) {
return [
'id' => $product->getId(),
'name' => $product->getName(),
// purposefully excluding category here!
];
})->toArray(),
];
}
}
After adding this your code should just work. For dev, you always should use dump as mentioned and all $this->json will just work. That's why I usually prefer this option. However, the caveat: You only can have one json serialization scheme for categories this way. For any additional ways, you would have to use other options then ... which is almost always true anyway.
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.
Hiho,
what i want to achieve: i have Projects, and within Project-edit, there is a form where i can drop images on (with dropzone.js) and those images are saved and assigned to the given Project.
Image upload works, the Image entities are being saved to the images table and they have the right project_id. But if i access the Project Enity, "images" is "null" in the project array. not an Collection of Image Entites.
It looks like a simple private Variable with no default value.
I guess my OneToMany and ManyToOne associations don't seem to work.
Some Code:
Project.php
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Image", mappedBy="project", cascade={"persist, remove"})
*/
private $images;
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getImages()
{
return $this->images;
}
public function addImage(Image $image)
{
$this->images[] = $image;
}
public function removeImage(Image $image) {
$this->images->removeElement($image);
}
Image.php
/**
* #var \AppBundle\Entity\Project
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Project", inversedBy="images")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="project_id", referencedColumnName="id")
* })
*/
private $project;
/**
* Set project
*
* #param \AppBundle\Entity\Project $project
*
* #return Image
*/
public function setProject(\AppBundle\Entity\Project $project = null)
{
$this->project = $project;
return $this;
}
/**
* Get project
*
* #return \AppBundle\Entity\Project
*/
public function getProject()
{
return $this->project;
}
Everything is saved to DB
but images is "null" (not an ArrayCollection :( )
Perhaps, there is something missing. But i dont see it somehow (although its soooo basic stuff)
Cheers
Adrian
Do the following in Project.php
In the constructor, add the following:
public function __construct()
{
// ...
$this->images = new ArrayCollection();
}
*Change addImage function to :
public function addImage(Image $image)
{
if(!$this-images->contains($image))
$this-images->add(image);
}
And then in Image.php, change setProject to:
public function setProject(\AppBundle\Entity\Project $project = null)
{
$this->project = $project;
if($project != null)
$project->addImage($this);
return $this;
}
Persist a few images and check if "Images" is not null.
I agree with Medard about the constructor and you also can try to set the fetch parameter to lazy on the oneToMany annotation.
Maybe a miss with the paramConverter
Oh Boy...
as i generated the Entities from DB (with doctrine:generate) there were Mapping files in the middle of the generating process (see http://symfony.com/doc/current/doctrine/reverse_engineering.html)
as soon as i deleted the src/AppBundle/Resources/config/doctrine folder (with orm.config.xml files in it) the Images were showing as persistent ArrayCollection.
but still the results where empty.
so i had to additionally put fetch="EAGER" into the OneToMany Mapping, as Lazyloading didnt seem to work properly (while dumping in twig, the result project.images was not initialized)
So thanks alot for your help. in the end, my images are showing up properly.
it would be nice, if symfony would write mapping errors in the dev.log, so i wouldnt have to search for 2 days.
Cya
Adrian
I have an entity Users
class Users
{
//...
/**
* #ORM\Column(name="firstName", type="string", length=50, nullable=true)
*/
private $firstname;
public function getFirstname()
{
return $this->firstname;
}
/*
*#ORM\OneToMany(targetEntity='Worksamples', mappedBy='user')
*/
private $worksample;
public function __constraction()
{
$this->worksample = new ArrayCollection();
}
public function getWorksample()
{
$this->worksample;
}
}
and another one entity called Worksamples
class Worksamples
{
//...
/**
* #ORM\Column(name="sampleSource", type="string", length=255, nullable=false)
*/
private $samplesource;
public function getSamplesource()
{
return $this->samplesource;
}
/**
* #ORM\Column(name="UserId", type="integer", nullable=false)
*/
private $userid;
public function getUserid()
{
return $this->userid;
}
/*
*#ORM\ManyToOne(targetEntity="Users", inversedBy="worksample")
*#ORM\JoinColumn(name="UserId", referencedColumnName="id")
*/
private $user;
public function getUser()
{
return $this->user;
}
}
in my controller i have this action
public function indexAction($id)
{
$user = $this->getDoctrine()
->getRepository('AcmeWellcomeBundle:Users')
->find($id);
$sample = $user->getWorksample()->getSamplesource();
return $this->render('AcmeWellcomeBundle:Default:index.html.twig', array('sample' => $sample));
}
and I have this error
FatalErrorException: Error: Call to a member function getSamplesource() on a non-object in ....
it supposed a User has many Worksamples and a Worksample has only one User.
any help?
Before using your code
Did you run your classes through the console of your app? If not execute this in your terminal or a console through SSH on your Server:
php app/console doctrine:generate:entities [YourVendorName]
Afterwards update your database using:
php app/console doctrine:schema:update --force
And clear caches:
app/console cache:clear
Those steps will make sure that all annotations are actually used and your database is set up correctly.
Then: You don't need private $userid; in Worksamples and you don't need $this->worksample = new ArrayCollection(); in Users. Doctrine will handle all this for your. Also it would be good to rename $worksample to $worksamples as it will always return an arrayCollection and never a single object.
When all is set up correctly, you can simply use $workspace->getUser() which will return an object of class User attached to this specific object.
In your controller
First of all get the list of worksamples. Then check whether any worksamples are attached to the object. Then for example get the vlue from the first object in the list:
$samples = $user->getWorksamples();
if ($samples) {
$sample = $samples[0]->getSamplesource();
}
Some notes:
The constructor of a class is called __construct() and not __constraction().
As a single object represents a user or a workspace, your classes should also be named User and Wokrspace in singular.
I get this exeption when I submit my form:
Found the public method "addRemote", but did not find a public "removeRemote" on class App\CoreBundle\Entity\Scene
The weired think is that the remove method exist ...
But i wrote it myself (When I did php app/console doctrine:generate:entities) doctrine didn't generated it. Did I make something wrong ?
/**
* #var array $remote
*
* #ORM\Column(name="remote", type="array", nullable=true)
*/
private $remote;
/**
* Set remote
*
* #param array $remote
* #return Scene
*/
public function addRemote($value, $key=null) {
if($key!=null){
$this->remote[$key] = $value;
}else{
$this->remote[] = $value;
}
return $this;
}
/**
* Remove remote
*/
public function removeRemote(){
unset($this->remote);
}
I allso tried:
/**
* Remove remote
*/
public function removeRemote($key=null){
if($key!=null && array_key_exists($key, $this->remote)){
unset($this->remote[$key]);
}
unset($this->remote);
return $this;
}
You have bigger problem than this; you are abusing your forms :)
Add.. and Remove... methods should be used for relations, not columns as per your code. Also, both add and remove methods must accept parameter that will be either added or removed.
If you still need an array, than getRemotes() method should return key=>value array. Adder and remover will later get that key, based on what user have picked in choice form type.