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 DoctrinePhpcr. Look for references to Document in any Cmf namespace (a regexp like this should do: Symfony\Cmf.*Bundle\Document) and change Document to DoctrinePhpcr (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 "SymfonyCmfBundle%"',
                              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] = "DoctrineODMPHPCRDocumentImage"',
                              QueryInterface::JCR_SQL2);
    foreach ($query->execute() as $row) {
        $node = $row->getNode();

        $fileChild = $node->getNode('file');
        $node->setProperty('phpcr:class', 'SymfonyCmfBundleMediaBundleDoctrinePhpcrImage');

        // 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 ('SymfonyCmfBundleCreateBundleDoctrinePhpcrImage' == $class) {
        $class = 'SymfonyCmfBundleMediaBundleDoctrinePhpcrImage';
        $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 DoctrinePhpcr 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:

@PHPCRODMReferrers(referringDocument="SymfonyCmfBundleRoutingBundleDoctrinePhpcrRoute", 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.