Dynamically generate translatable columns using metadata - php

I would like to make a Translatable behavior for my entities using metadata.
I have a class Article
class Article implements TranslatableInterface {
/**
* #HeidanTranslatable
*/
private $title;
/**
* #HeidanLocale
*/
private $locale;
public function getTitle() {
return $this->title;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getLocale() {
return $this->locale;
}
public function setLocale($locale) {
$this->locale = $locale;
return $this;
}
I would like to have kind of Gedmo Doctrine Extension behavior which will create in database columns depending on property and allowed locales.
For example, with the entity article, I would like that two columns are created : title_fr, title_en.
I'd like this stuff is bridged to Doctrine behavior and I made a loadClassMetadataListener
class LoadClassMetadataListener {
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$metadata
->mapField(array('fieldName' => 'title_fr', 'type' => 'text'))
;
}
When I run a doctrine:schema:update --force I have the following error :
[ReflectionException]
Property Heidan\CoreBundle\Entity\Article::$title_fr does not exist
So I guess they said that the property title_fr does not exist, and that's right.
I do not want to set manually properties (private $title_fr, private $title_en, private $content_fr, private $content_en) for all my entities.
Is there any way to achieve this behavior so far ?
Thanks a lot for your help.

Related

How to display a message in the default locale when the message is not available in the current locale ? Prezent Translations

I'm working on a multilingual website, using the Prezent bundle to translate my entities.
Actually, the input in all the locales works, but I have some issues to display messages when they are not defined in the current locale.
Here is an extract of my Category entity (the field "name" is translated) :
/**
* #ORM\Entity(repositoryClass="App\Repository\CategoryRepository")
*/
class Category extends TranslatableEntity
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="CategoryTranslation", mappedBy="translatable", cascade={"persist", "remove"}, indexBy="locale")
*/
protected $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
$this->translationEntity = 'CategoryTranslation';
}
public function getId(){
return $this->id;
}
public function setId($id){
$this->id = $id;
}
public function getName()
{
return $this->translate()->getName();
}
public function setName($name){
$this->translate()->setName($name);
return $this;
}
}
The translate method is in TranslatableEntity, here is the code :
abstract class TranslatableEntity extends AbstractTranslatable
{
/**
* #Prezent\CurrentLocale
*/
protected $currentLocale;
protected $translationEntity;
/**
* Cache current translation. Useful in Doctrine 2.4+
*/
protected $currentTranslation;
public function getCurrentLocale()
{
return $this->currentLocale;
}
public function setCurrentLocale($locale)
{
$this->currentLocale = $locale;
return $this;
}
/**
* Translation helper method
*/
public function translate($locale = null)
{
if (null === $locale) {
$locale = $this->currentLocale;
}
if (!$locale) {
throw new \RuntimeException('No locale has been set and currentLocale is empty');
}
if ($this->currentTranslation && $this->currentTranslation->getLocale() === $locale) {
return $this->currentTranslation;
}
if (!$translation = $this->translations->get($locale)) {
$className=$this->translationEntity;
$translation = new $className;
$translation->setLocale($locale);
$this->addTranslation($translation);
}
$this->currentTranslation = $translation;
return $translation;
}
}
I use this way to display the translated names in Twig :
{{ cat.translations.get(app.request.locale).name }}
This works but I'm pretty sure that it is not the right way to do it. Moreover, the method throws an error when I try to display a name which not defined in the current locale.
I think that ...
{{ cat.translations.get(app.request.locale).name is defined ? cat.translations.get(app.request.locale).name : cat.translations.get(default_locale).name }}
.. would solve but I'm also pretty sure that the case of "not available for this locale" is supported by the bundle.
Do you have any idea of what I am doing wrong ?

PhpStorm metadata file for repository classes

In our application, we use repositories for models that are fetched from the database. So, we have an abstract repository that knows about the database, has a loadById method to load a database record and an abstract getEntity method that creates an object for that specific repository. Example code:
abstract class EntityRepository {
/**
* #param int $id
* #return AbstractEntity
*/
public function loadById($id) {
$record = $this->db->loadById($id);
$entity = $this->getEntity();
return $this->inflate($record, $entity);
}
/**
* #return AbstractEntity
*/
protected abstract function getEntity();
}
class PeopleRepository extends EntityRepository {
protected function getEntity() {
return new PeopleEntity();
}
}
abstract class AbstractEntity {
private $id;
/**
* #return int
*/
public function getId() {
return $this->id;
}
/**
* #param int $id;
*/
public function setId($id) {
$this->id = $id;
}
}
class PeopleEntity extends AbstractEntity {
private $name;
/**
* #return string
*/
public function getName() {
return $this->name;
}
/**
* #param string $name;
*/
public function setName($name) {
$this->name= $name;
}
}
When using an instance of PeopleRepository and fetching a model through loadById, PhpStorm is not able to resolve the returned model to a concrete type, but provides only code completion for the functions of AbstractEntity. Is there any simple way to make it work?
In https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata, I've only found ways to make it work for concrete classes and their functions. So, enumerating all repository classes and all their ways of creating an entity might work. But I'd love to see an abstract way of defining like "All instances of EntityRepository will return an entity of that type defined in getEntity() when loading an entity"
I doubt there's a blanket way of doing this. Even using PHPStorm meta you have to be explicit for each case. Perhaps the way of doing this is by doing something like adding a repository facade e.g.
class RepositoryFacade {
public static function __callStatic($method, $args) {
if ($args[0] == People::class) {
array_shift($args);
return new PeopleRepository()->{$method}(...$args);
}
}
}
Then you might be able to typehint this using:
override(RepositoryFacade::loadById(0), type(0));
Of course the facade is not really the best pattern to be using in general so I can see how this might not be ideal.

Getting the child-type of an object from a method inherited from the father

my problem is getting the right type of object from a method, which is returning a "mixed" type due to inhreitance.
I've got a generic list class
class List
{
/**
* #var Item[]
*/
protected $items;
public function __construct()
{
$this->items = array();
}
/**
* #return Item[]
*/
public function getAll()
{
return $this->items;
}
/**
* #return Item
*/
public function getOne($index)
{
if (isset($this->items[$index])) {
return $this->items[$index];
}
return null;
}
}
containing element of type Item, which is a generic class either
class Item
{
/**
* #var string
*/
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
Such generic classes are extended by N different lists. Just an example
class UserList extends List
{
/* user-specific implementation */
}
class User extends Item
{
/* user-specific implementation */
}
In the client code
$user_list = new UserList();
foreach ($user_list->getAll() as $user) {
echo $user->getEmailAddr();
}
Inside the foreach I don't have code completion, because my getAll method (inherited from the father) is returning Item[], or mixed[], not a User[]. Same problem with getOne method.
I wouldn't like to have to override such methods.
Is there a more clever and elegant solution?
Thank you
I don't think there's any way for the IDE to infer the type automatically. Use a phpdoc type annotation:
foreach ($user_list->getAll() as $user) {
/** #var User $user */
echo $user->getEmailAddr();
}
See the related question PHPDoc type hinting for array of objects?

Doctrine 2 postPersist then save/update entity

I'm in the process of creating a trait that I want to insert into a number of my Doctrine entity classes. The trait basically allows for a slug property to be created using the Hashids PHP library based on the entities id (primary key).
I've included the required properties & getters/setters along with the postPersist() method on the trait, but I'm now wondering how I go about re-saving / updating / persisting that change from within the postPersist() method?
Any help or direction would be great.
SlugTrait
trait Slug
{
/**
* #ORM\Column(type="string")
*/
private $slug;
/**
* #ORM\PostPersist
*/
public function postPersist()
{
$this->slug = (new SlugCreator())->encode($this->id);
// Save/persist this newly created slug...?
}
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
}
After some trial and error I found out how to persist the update/change. As I'm using Laravel I just resolved the Entity Manager from the IoC container and then used that to persist the updated slug field like so (you could also just new up the Entity Manager manually):
trait Slug
{
/**
* #ORM\Column(type="string")
*/
protected $slug;
/**
* #ORM\PostPersist
*/
public function postPersist()
{
$this->slug = (new SlugCreator())->encode($this->id);
// Save/persist this newly created slug.
// Note: We must add the top level class annotation
// '#ORM\HasLifecycleCallbacks()' to any Entity that
// uses this trait.
$entityManager = App::make('Doctrine\ORM\EntityManagerInterface'); // or new up the em "new EntityManager(...);
$entityManager->persist($this);
$entityManager->flush();
}
public function getSlug()
{
return $this->slug;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
}

Catchable Fatal Error: Argument 1 passed to (...) must be an instance of (...) integer given

I'm making fixtures and when I try to load them I have an error. I need an instance of a Movie object but what I give, and I don't know why, is an integer. For this reason it says me that I have the following error:
[Symfony\Component\Debug\Exception\ContextErrorException]
Catchable Fatal Error: Argument 1 passed to Filmboot\MovieBundle\Document\A
ward::setMovie() must be an instance of Filmboot\MovieBundle\Document\Movie
, integer given, called in C:\Programming\xampp\htdocs\filmboot.web\src\Fil
mboot\MovieBundle\DataFixtures\MongoDB\Awards.php on line 143 and defined i
n C:\Programming\xampp\htdocs\filmboot.web\src\Filmboot\MovieBundle\Documen
t\Award.php line 107
This is my Fixture class:
namespace Filmboot\MovieBundle\DataFixtures\MongoDB;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Filmboot\MovieBundle\Document\Award;
class Awards extends AbstractFixture implements OrderedFixtureInterface {
public function load(ObjectManager $manager) {
$awards = array(
array(
"name" => "Sitges",
"year" => "1992",
"category" => "Best director"
);
foreach ($awards as $award) {
$document = new Award();
$document->setName ($award["name"]);
$document->setYear ($award["year"]);
$document->setCategory($award["category"]);
$manager->persist($document);
$this->addReference("award-" .$i, $award);
}
$manager->flush();
}
public function getOrder() {
return 1;
}
}
And this is the document class:
namespace Filmboot\MovieBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\Common\Collections\ArrayCollection;
use Filmboot\MovieBundle\Util;
/**
* #ODM\Document(db="filmbootdb", collection="awards")
* #ODM\Document(repositoryClass="Filmboot\MovieBundle\Document\AwardRepository")
*/
class Award {
/**
* #ODM\Id
*/
private $id;
/**
* #ODM\String
*/
private $name;
/**
* #ODM\Int
*/
private $year;
/**
* #ODM\String
*/
private $category;
/**
* #ODM\ReferenceOne(targetDocument="Movie", mappedBy="awards", cascade={"persist"})
*/
private $movie;
public function getId() {
return $this->id;
}
public function setName($name) {
return $this->name = $name;
}
public function getName() {
return $this->name;
}
public function setYear($year) {
return $this->year = $year;
}
public function getYear() {
return $this->year;
}
public function setCategory($category) {
return $this->category = $category;
}
public function getCategory() {
return $this->category;
}
public function setMovie(\Filmboot\MovieBundle\Document\Movie $movie) {
$movie->setAward($this);
return $this->movie = $movie;
}
}
As we can see you explicitly give an integer for the movie :
$awards = array(
array(
// ...
"movie" => 1,
),
);
// ...
$document->setMovie ($award["movie"]);
Instead of a movie object, so the script crashes because it requires a Movie object :
public function setMovie(\Filmboot\MovieBundle\Document\Movie $movie) {
return $this->movie = $movie;
}
So the solution is to create the fixtures of the movies and give them a reference:
// When saving inside Movie fixtures
$manager->persist($movie);
$manager->flush();
$this->addReference('movie-'.$i, $movie); // Give the references as movie-1, movie-2...
Then load them first with getOrder() method :
public function getOrder()
{
return 0; // 0, loaded first
}
Control that Awards are loaded after movies :
public function getOrder()
{
return 1; // loaded after 0...
}
and after in your Award fixtures retrieve them like that by reference, it will load the entire object, not just an id (integer) :
$awards = array(
array(
// ...
"movie" => $this->getReference('movie-1'), // Get the Movie object, not just id
),
);
// ...
$document->setMovie ($award["movie"]);
Please note that if you want to use reference and order, your fixture classes need to implement OrderedFixtureInterface :
namespace Acme\HelloBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\HelloBundle\Entity\Group;
class LoadGroupData extends AbstractFixture implements OrderedFixtureInterface
You have to do this with all your other entities like actors, directors...
You can find the documentation for sharing objects (by reference) between your fixtures here.
EDIT :
to make bidirectionnal working, adapt Award setter :
public function setMovie(\Filmboot\MovieBundle\Document\Movie $movie) {
$movie->setAward($this);
return $this->movie = $movie;
}
and adapt persistance with cascade persist :
/**
* #ODM\ReferenceOne(targetDocument="Movie", mappedBy="awards", cascade={"persist"})
*/
private $movie;
As the error message you got is clear enough. Here's the way you may fix this bad arguments mapping issue.
Instead of setting an integer as a movie to the awards array you're using to populate the document instance. Why don't you just set a given Movie entity you already persisted.
To do that, you'll have to load one or many movies (it depends on your needs) and set this/those entit(y/ies) as argument(s) to populate your document instances.
An example,
Take a look at this example (on the question) of user(s) populated by already persisted userGroup(s). You might use here the same idea.

Categories