Blog Posts

Updating old Symfony2 CMF projects

We released RC1 of the Symfony CMF last week. Now it is time now to upgrade older installations to the latest and greatest. I decided to keep a record of what i had to do and write it down for others to follow the steps. There are notes in the CHANGELOG.md files of each CMF bundle, but a common blogpost is more convenient. The whole update took me a bit less than a day of work. Now that we are in release candidate state with the project, further upgrades should need no more changes, or only small ones.

There where some code and configuration changes, and quite some changes to the Doctrine models. While small updates can be done with the doctrine phpcr commands as noted in the various CHANGELOG.md files, I decided to write a custom command in my project to change what i needed to change in PHPCR. Using a dump of the repository (app/console doctrine:phpcr:workspace:export -p /cms dump.xml), i can rerun the whole data migration repeatedly to test, with code along those lines:

app/console --force doctrine:phpcr:node:remove /cms \
&& app/console doctrine:phpcr:workspace:import dump.xml \
&& app/console migration:update

I am happy for feedback and additions. I plan to update this blogpost when people point out additional things that could be needed.

Doctrine & Default model classes

General tip: running app/console --env=prod cache:clear will load all document classes and validate the mapping information. This is a convenient way to see if anything is wrong.

  • The bundles took one step to become storage agnostic. You need to configure the persistence to be used. You can do this centrally in the CmfCoreBundle by setting "persistence.phpcr.enabled: true". The PHPCR base paths are configured based on the core bundle option and all moved into persistence.phpcr.*
  • The PHPCR documents moved from Document to Doctrine\Phpcr. Look for references to Document in any Cmf namespace (a regexp like this should do: Symfony\\Cmf\\.*Bundle\\Document\\) and change Document to Doctrine\Phpcr (btw, it is totally okay to keep your project documents in the Document namespace. the separation is only relevant for reusable bundles that want to support more than one persistence layer).

This is the code section migrating the classes and adjusting some properties that had to be renamed.

    $qm = $session->getWorkspace()->getQueryManager();
    $query = $qm->createQuery('select * from [nt:unstructured] 
                               where [phpcr:class] LIKE "Symfony\Cmf\Bundle%"',
                              QueryInterface::JCR_SQL2);
    /** @var $row RowInterface */
    foreach ($query->execute() as $row) {
        $node = $row->getNode();
        $class = $node->getPropertyValue('phpcr:class');
        $class = str_replace('\\Document\\', '\\Doctrine\\Phpcr\\', $class);
        $class = str_replace('\\Multilang', '\\', $class);

        // menu documents saw some refactoring
        if ('Symfony\\Cmf\\Bundle\\MenuBundle\\Doctrine\\Phpcr\\MenuNode' == $class) {
            $node->setProperty('display', true);
            $node->setProperty('displayChildren', true);
            if ($node->hasProperty('weakContent') || $node->hasProperty('strongContent')) {
                $weak =
                $prop = $node->getProperty('weak')->getValue() 
                    ? $node->getProperty('weakContent') 
                    : $node->getProperty('hardContent');
                $node->setProperty('menuContent', $prop->getString(), PropertyType::WEAKREFERENCE);
                $prop->remove();
                $node->getProperty('weak')->remove();
            }
        }

        $node->setProperty('phpcr:class', $class);

        if ($node->hasProperty('isPublishable')) {
            $node->setProperty('publishable', $node->getPropertyValue('isPublishable'));
            $node->getProperty('isPublishable')->remove();
        }
        // many documents now implement publishable, defaulting the publishable property to true
        // we need to make them published if the property is not stored in PHPCR yet.
        // php < 5.3.9 does not support interfaces in is_subclass_of
        $dummy = new $class;
        if ($dummy instanceof PublishableInterface && !$node->hasProperty('publishable')) {
            $node->setProperty('publishable', true);
        }
    }

Images

  • When you are using images, do not forget to instantiate the recently added CmfMediaBundle in your AppKernel.
  • If you used to render images loaded from PHPCR with the DoctrinePHPCRLoader found in LiipImagineBundle, you should switch to use the new cmf_media_doctrine_phpcr data loader.
  • Note that the CmfMediaBundle image does not provide its id as __toString method and thus you may not pass Image objects to imagine filters, but only the id. Something like {{ image.id|imagine_filter('my_filter') }}

Image specific code was removed from both Doctrine PHPCR-ODM and CmfCreateBundle.

Converting Doctrine Image documents

The doctrine Image had a child that was a file. The CmfMedieBundle Image extends File and thus has no file child. We want to make the CmfMediaBundle File class an nt:file nodetype but currently its of the nt:unstructured type.

    $query = $qm->createQuery('select * from [nt:unstructured] 
                               where [phpcr:class] = "Doctrine\ODM\PHPCR\Document\Image"', 
                              QueryInterface::JCR_SQL2);
    foreach ($query->execute() as $row) {
        $node = $row->getNode();

        $fileChild = $node->getNode('file');
        $node->setProperty('phpcr:class', 'Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image');

        // CmfMediaBundle image *is* the file, does not have a file child
        $resourceChild = $fileChild->getNode('jcr:content');
        $node->setProperty('jcr:createdBy', $fileChild->getPropertyValue('jcr:createdBy'));
        $node->setProperty('jcr:lastModifiedBy', $resourceChild->getPropertyValue('jcr:lastModifiedBy'));
        $node->setProperty('jcr:created', $fileChild->getPropertyValue('jcr:created'));
        $node->setProperty('jcr:lastModified', $resourceChild->getPropertyValue('jcr:lastModified'));
        $node->setProperty('contentType', $resourceChild->getPropertyValue('jcr:mimeType'));
        $session->move($fileChild->getPath() . '/jcr:content', $node->getPath() . '/jcr:content');
        $fileChild->remove();
    }

Converting CmfCreateBundle Image documents

I put this code in the above code to convert document class names right after determining the new class name. Pay attention where you put this code to check $class for the correct value.

    if ('Symfony\Cmf\Bundle\CreateBundle\Doctrine\Phpcr\Image' == $class) {
        $class = 'Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image';
        $node->addMixin('mix:created');
        $content = $node->addNode('jcr:content', 'nt:resource');
        $content->setProperty('jcr:mimeType', $node->getPropertyValue('mimeType'));
        $node->setProperty('contentType', $node->getPropertyValue('mimeType'));
        $node->getProperty('mimeType')->remove();
        $content->setProperty('jcr:data', $node->getPropertyValue('content'));
        $node->setProperty('description', $node->getPropertyValue('caption'));
        $node->getProperty('caption')->remove();
    }

Multilanguage

All default provided documents now support multilanguage. We removed the Multilang from the names. If the CmfCoreBundle has no multilang.locales configured, it will remove the doctrine multilanguage mapping, making the documents non-translated. There is a new TranslatableInterface used for this. You can remove locales configuration from the other cmf bundles as the core bundle prepends the information to all bundles.

In your own code, you can access the available locales as %cmf_core.multilang.locales%.

Publish Workflow

  • Many models now implement the publish workflow interfaces. If you do not completely disable the publish workflow, you need to set the published property to true on all of your existing documents, or they will become hidden.
  • PublishWorkflowInterface was split into PublishableInterface and PublishTimePeriodInterface to allow adding other criteria at a later point. Simply implement those two interfaces instead of the removed one.
  • There is a sonata admin extension that can be enabled to show the publish workflow fields.

Dependency injection

If you overwrite some of the CMF classes in your configuration, check for any parameters still called ..._class. The parameters that define service classes now all end on .class. Previously, some where ending on _class.

Routing and the RouteAwareInterface => RouteReferrersInterface

  • RouteAwareInterface was renamed to RouteReferrersInterface. The addRoute and removeRoute methods in the interface have no typehint.
  • The content of the route is set with Route::setContent and no longer Route::setRouteContent for consistency.
  • if you mapped referring routes, you not only need to change the referringDocument to say Doctrine\Phpcr instead of Document, but also adjust referencedBy to point to "content" as we cleaned up the model (the PHPCR name is still routeContent). The new mapping will look like:
    @PHPCRODM\Referrers(referringDocument="Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route", referencedBy="content", cascade="persist")
  • There is a sonata admin extension for route referrers that adds the necessary form elements to manage routes for a RouteReferrersInterface document. You need to enable the extension to use it.

Sonata

  • remove the tree routing entry @SonataDoctrinePHPCRAdminBundle/Resources/config/routing/phpcrodmbrowser.xml from your global routing file, it is no longer needed as in symfony 2.3 we can render the tree without an explicit route.
  • setBlockRoot / setContentRoot in DI => setRootPath provided by base admin class. This was used a lot with the admins for BlockBundle
  • as "multilang" was removed from document class names, the labels changed as well. i.e. dashboard.label_multilang_simple_block is now dashboard.label_simple_block
  • If multilanguage is configured, there is a sonata admin extension that puts the locale chooser into admin forms.

Related Entries:
- Semantic web meets frontend-awesomeness
- What Liip did after winning the IKS semantics UX contest
- Symfony CMF hackday october 22nd in Cologne
- A frontend editor for Symfony2 CMF with the help of VIE
- Progress on PHPCR with a hackday

About the author


Find more about him on Twitter, Google+ and his personal site.

Comments [4]

Nicolas, 18.09.2013 09:31 CET

Thanks David for this post, it will be useful.

I updated my CMF bundles yesterday and I just had an issue with the tree browser which was rendering nothing.

To fix it I had to add a public property "children" to my Menu and MenuNode models (which extends the CMF ones) and implement __toString() method.

And documentManager->move method doesn't work on beta5 tag.
Thanks to Lukas I updated phpcr-odm to master and it works now but it woud be great to have a new tag on this.

david, 18.09.2013 17:48 CET

hi nicolas,
thanks for the feedback. i hit the same issue with the menus and found that sonata admin was using reflection which seemed to produce some issue with the doctrine proxies not loading data. this PR fixed things:
https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/pull/173
i think the __toString is already present, at least i did not need to do anything to see useful names.

for phpcr-odm as discussed in the issue we need to finish the querybuilder refactoring and sync all places that use querybuilder before we can tag.

Saud, 03.10.2013 20:47 CET

Hey thanks for this post. Under Sonata section above, removing the folowing:

@SonataDoctrinePHPCRAdminBundle/Resources/config/routing/phpcrodmbrowser.xml

helped me get past the following error:

[Symfony\Component\Config\Exception\FileLoaderLoadException]
Cannot import resource "@SonataDoctrinePHPCRAdminBundle/Resources/config/routing/phpcrodmbrowser.xml" from "/var/www/cm/app/config
/routing.yml". Make sure the "SonataDoctrinePHPCRAdminBundle" bundle is correctly registered and loaded in the application kernel
class.

david, 31.10.2013 14:22 CET

hi saud,
sorry, seems i did not get a notification for this comment. you can remove that reference, it is not needed anymore. the routes are registered programmatically which is more fault tolerant.
david

Add a comment

Your email adress will never be published. Comment spam will be deleted!