symfony-cmf/core-bundle

A way to convert non-translated documents to translated

jrmyio opened this issue · 4 comments

After inserting the first documents into Phpcr I decided that I wanted translation support. I couldn't find a way to upgrade the existing field data to translatable fields data. All documents look empty as soon as you make a field translatable because it tries to load the default locale.

I created the following Migration script to convert documents by using the current field data (before translatable was added) data to the translatable default locale.

Warning: Since I have little to no experience with Phpcr I doubt my method is the best way!

However, bits and pieces of this script could be used to support this in Cmf/CoreBundle. Let me know what to improve /change.

<?php

namespace Symfony\Cmf\Bundle\CoreBundle\Migrator\Phpcr;

use Symfony\Component\Console\Output\OutputInterface;
use PHPCR\SessionInterface;
use Doctrine\Bundle\PHPCRBundle\Migrator\MigratorInterface;
use Doctrine\ODM\PHPCR\DocumentManager;
use Doctrine\Bundle\PHPCRBundle\ManagerRegistry;

class Translatable implements MigratorInterface{

    /**
     * @var SessionInterface
     */
    protected $session;

    /*
    * @var OutputInterface
    */
    protected $output;

    /**
     * @var DocumentManager
     */
    protected $dm;

    /**
     * @var string
     */
    protected $toLocale;

    /**
     * @param ManagerRegistry $registry
     */
    public function __construct(ManagerRegistry $registry)
    {
        $this->dm = $registry->getManager();
        $this->session = $registry->getConnection();
    }

    /**
     * @param SessionInterface $session
     * @param OutputInterface $output
     */
    public function init(SessionInterface $session, OutputInterface $output)
    {
        $this->session = $session;
        $this->output = $output;
    }

    /**
     * @param string $path
     * @param int $depth
     * @return int
     */
    public function migrate($path = "/", $depth = -1)
    {
        // Get the node from the path
        $node = $this->dm->find(null, $path);

        $this->migrateNode($node);
        return 0;
    }

    /**
     * @param $node
     */
    protected function migrateNode($node){

        // Save the id and class name of the node
        $nodeId = $node->getId();
        $nodeClass = get_class($node);

        // Check if the node is translatable
        if ($this->dm->isDocumentTranslatable($node)){

            // Find the current translator value on the object
            $metaData = $this->dm->getClassMetadata($nodeClass);

            // If a translator is defined in the mapping
            if (($translator = $metaData->translator) !== null){

                // Clear the unit of work
                $this->dm->getUnitOfWork()->clear();

                // Temporary set the translator to null
                $metaData->translator = null;

                // Find the node without translation
                $node = $this->dm->find(null, $nodeId);

                // Get data for translation
                $data = [];

                // Get the data from the translatable fields
                foreach ($metaData->translatableFields as $field){

                    // Get the non translated value
                    $data[$field] = call_user_func(array($node, "get".ucfirst($field)));

                    // Reference to the field
                    $fieldReference = $nodeId."/".$field;

                    $this->output->writeLn("Removing: ".$fieldReference);

                    // Remove the field if it exists
                    if ($this->session->itemExists($fieldReference))
                        $this->session->removeItem($fieldReference);
                }

                // Save the changes
                $this->dm->flush($node);

                // Clear the unit of work
                $this->dm->getUnitOfWork()->clear();

                // Reset the translator value to its original value
                $metaData->translator = $translator;

                // Find the node with translation
                $node = $this->dm->find(null, $nodeId);

                // Set the new data if the current value is null
                foreach ($data as $field => $value){

                    // Get the translated value, if any
                    $current = call_user_func(array($node, "get".ucfirst($field)));

                    // If the translated value is not there yet
                    if (is_null($current)){

                        // Set the translated value
                        call_user_func_array(array($node, "set".ucfirst($field)), [$value]);
                    }
                }

                // Save the changes
                $this->dm->flush($node);

                $this->output->writeLn("Migrated: ".$nodeId);
            }

        }else{
            $this->output->writeLn("Skipped: ".$nodeId);
        }

        // Clear the unit of work and retrieve un untouched node
        $this->dm->getUnitOfWork()->clear();
        $node = $this->dm->find(null, $nodeId);

        // Check if the node has childeren
        if (method_exists($node, "getChildren")){

            // Migrate the children
            $children = $node->getChildren();
            if (!is_null($children)){
                foreach ($children as $childNode){
                    $this->migrateNode($childNode);
                }
            }
        }

    }

} 
dbu commented

cool idea! i would probably not operate on the document level but really on the underlying phpcr level. (what you call node in this class is actually a document). using the PHPCR\NodeInterface instead would allow to avoid the ucfirst calls to getters which will not work if the property metadata was already changed.

you could read the metadata for the document and then go to the node for that document to read the non-translated properties and then use the translation strategy to store the translations.

something like (i did not test this):

public function migrate($document) 
{
    $meta = $this->dm->getClassMetadata(get_class($document));
    if (!$meta->translator) {
        return;
    }
    $node = $dm->getNodeForDocument($document);
    $translations = array();
    foreach ($meta->translatableFields as $propertyName) {
        $translations[$propertyName] = $node->getPropertyValue($propertyName);
    }
    $dm->getTranslationStrategy($meta->translator)->saveTranslation($translations, $node, $meta, $defaultLocale);
}

@lsmith77 is a migrator the best thing for this or should it go elsewhere?

I think it would be awesome to have some out of the box migrators to add/remove translations. I do not have time to review this right now but I am marking this for 1.3

dbu commented

i had to make some fields that was previously not translated to translated fields. i did a phpcr query with the phpcr-shell:

UPDATE [nt:unstructured] AS a 
  INNER JOIN [nt:unstructured] AS t ON ISCHILDNODE(t, a) 
SET 
    t.originalLinkCaption = expr('row.getNode("a").getPropertyValueWithDefault("originalLinkCaption", null)'), 
    t.role = expr('row.getNode("a").getPropertyValueWithDefault("role", null)'), 
    t.authorInfo = expr('row.getNode("a").getPropertyValueWithDefault("authorInfo", null)') 
WHERE a.[phpcr:class] = 'AppBundle\Document\Article';

i don't think this query could create the translation child node however. for attribute translations, it would be enough however.