Search

A new extension, "Schema Migration" is now available for download. Comments and feedback can be left here but if you discover any issues, please post it on the issue tracker.

Thanks, Rainer. I'm really looking forward to trying this out!

It looks like you've been travelling through time to release this extension ;-)

Released: 2 October 2011

@bauhouse Glad you liked. Back to the future!

@rainerborene: Any idea why it might be saving nothing at all, neither sections nor pages? The "sections" folder has been successfully created upon installation.

@michael-e Try to save sections and pages again.

I did. Several times. Still nothing gets saved.

Looking forward to playing with this! Cheers :-)

Does this allow two developers to work independently and then merge their two builds?

@michael-e, I think I did the same thing as you did. I installed the extension and started creating sections and pages. Nothing was being saved to the pages and sections directories, until I took a cursory look at the extension driver and realized it was looking for a configuration setting: a checkbox with the following label:

Enable tracking of section and page changes

Go to your preferences page to enable Schema Migrations. :-)

I've been trying to test the extension, and I'm finding that the pages and section XML files are being saved:

test.xml

<?xml version="1.0" encoding="UTF-8"?>
<section id="1">
  <meta>
    <name>Test</name>
    <handle>test</handle>
    <sortorder>1</sortorder>
    <entry_order></entry_order>
    <entry_order_direction>asc</entry_order_direction>
    <hidden>no</hidden>
    <navigation_group>Content</navigation_group>
  </meta>
  <fields>
    <entry>
      <required>yes</required>
      <show_column>yes</show_column>
      <id>1</id>
      <label>Title</label>
      <element_name>title</element_name>
      <type>input</type>
      <parent_section>1</parent_section>
      <sortorder>0</sortorder>
      <location>main</location>
      <field_id>1</field_id>
      <validator></validator>
    </entry>
    <entry>
      <show_column>no</show_column>
      <required>yes</required>
      <id>2</id>
      <label>Description</label>
      <element_name>description</element_name>
      <type>textarea</type>
      <parent_section>1</parent_section>
      <sortorder>1</sortorder>
      <location>main</location>
      <field_id>2</field_id>
      <formatter>markdown_extra_with_smartypants</formatter>
      <size>15</size>
    </entry>
  </fields>
</section>

_pages.xml

<?xml version="1.0" encoding="UTF-8"?>
<pages>
  <entries>
    <entry>
      <id>1</id>
      <parent></parent>
      <title>Home</title>
      <handle>home</handle>
      <path></path>
      <params></params>
      <data_sources></data_sources>
      <events></events>
      <sortorder>1</sortorder>
    </entry>
  </entries>
  <types>
    <type>
      <page_id>1</page_id>
      <type>index</type>
    </type>
  </types>
</pages>

However, when I click on the Migrate button, the page does not get created. The Test section does get created, but without any fields.

Thanks, Stephen. You are right. We should have been told that it has to be enabled on the prefs page...

Like you, I can now "migrate" a section, but no fields are created.

Hey guys,

I'm going to do some further tests this week.

@nickdunn Not yet. Any idea how we can achieve this?

@bauhouse Pulled your fix to my repository on GitHub. Thanks!

Thanks, Rainer. That appears to fix the creation of pages during migration. But, I'm still working on how to create section fields.

I figured out where the Synchronizer class is failing when migrating section fields. Everything works great until the final commit() step. But because the field ID is being passed, the first condition of the if statement passes and the commit() attempts to edit the specified field ID rather than create a new field.

If I hack the commit() method in the Field class, I can get the Synchronizer class to create new section fields, but this will create new fields every time the migration is run.

public function commit(){
    $fields = array();

    $fields['element_name'] = Lang::createHandle($this->get('label'));
    if(is_numeric($fields['element_name']{0})) $fields['element_name'] = 'field-' . $fields['element_name'];

    $fields['label'] = $this->get('label');
    $fields['parent_section'] = $this->get('parent_section');
    $fields['location'] = $this->get('location');
    $fields['required'] = $this->get('required');
    $fields['type'] = $this->_handle;
    $fields['show_column'] = $this->get('show_column');
    $fields['sortorder'] = (string)$this->get('sortorder');

    /* if($id = $this->get('id')){
        return FieldManager::edit($id, $fields);
    }
    else if($id = FieldManager::add($fields)){
        $this->set('id', $id);
        $this->createTable();
        return true;
    } */

    $id = FieldManager::add($fields);
    $this->set('id', $id);
    $this->createTable();
    return true;

    return false;
}

I need to find a way to prevent a field from being created if it already exists within the section. So, that means modifying the original conditional to something like this untested, shot in the dark code:

    if($label = $this->get('label')){
        $id = FieldManager::add($fields);
        $this->set('id', $id);
        $this->createTable();
        return true;
    }
    else if($id = $this->get('id')){
        return FieldManager::edit($id, $fields);
    }

Edit: The above doesn't work. The idea is that the section should be tested to verify that the label of the field does not already exist in the section. If it doesn't already exist, the field is created. If it does exist, edit the existing field.

Aha! I've discovered an easy way to force the Synchronizer class to add section fields (without hacking the core Field class). If I unset the ID of the field in the updateSections() method, the fields are created:

if (is_array($fields) && !empty($fields)){
    foreach($fields as $data){
        unset($data['id']);
        $field = $fieldManager->create($data['type']);
        $field->setFromPOST($data);
        $field->commit();
    }
}

But this will add fields regardless of whether they already exist or not. So there needs to be some sort of test to determine whether the field already exists in the section. But how should it determine which fields to update, add or remove?

The previous line appears to remove fields with IDs that don't match those that already exist in the section:

self::__removeMissingFields($fields, $fieldManager);

Should field IDs be identical between Symphony installs? I'm not sure whether that makes sense, though. If fields are being removed based on ID, they should be added with the specified IDs. However, the FieldManager class only adds fields to a section when the ID is not specified. So the __removeMissingFields() method should test for something other than ID when updating and label is the only other unique variable.

Yeah. This is a problem.

The only way that migrations can work is if the IDs are identical. But the FieldManager class cannot guarantee identical IDs for fields when creating, because it only creates when no ID is passed.

I figured out the logic to be able to create section fields if the same label does not already exist in the section:

if (is_array($fields) && !empty($fields)){
    foreach($fields as $data){
        $field_label = $data['label'];
        $field_exists = Symphony::Database()->fetchCol('id', "SELECT `id` FROM `sym_fields` WHERE `parent_section` = '$section_id' AND `label` = '$field_label'");
        if(!$field_exists) {
            unset($data['id']);
        }
        $field = $fieldManager->create($data['type']);
        $field->setFromPOST($data);
        $field->commit();
    }
}

But that's not the best solution, since this does not allow the migration of existing fields where the label has changed. So, it would be best to test for the ID:

if (is_array($fields) && !empty($fields)){
    foreach($fields as $data){
        $field_id = $data['id'];
        $field_exists = Symphony::Database()->fetchCol('id', "SELECT `id` FROM `sym_fields` WHERE `parent_section` = '$section_id' AND `id` = '$field_id'");
        if(!$field_exists) {
            unset($data['id']);
        }
        $field = $fieldManager->create($data['type']);
        $field->setFromPOST($data);
        $field->commit();
    }
}

Fields are successfully created if they don't already exist, but IDs of the newly created fields won't necessarily match the IDs of the original Symphony install. So, migrating section schemas will no longer work for those fields that have different IDs.

Can anyone think of a way to overcome this problem, other than modifying the core behaviour?

At any rate, I've got the extension to a point where a one-time migration of pages and section fields will work, which is still quite a handy thing! I'm sure I will be using this a lot.

Good observation. I think Symphony 3 uses some sort of guid hash to match fields and sections. This might be the best approach to use.

I've been wanting this sort of feature in Symphony for a long time. So, sorry that I've been a little impatient to get this working. ;-)

I've been doing some testing and wanted to be able to quick create a section:

<?xml version="1.0" encoding="UTF-8"?>
<section id="1">
  <meta>
    <name>Members</name>
    <handle>members</handle>
    <sortorder>1</sortorder>
    <entry_order></entry_order>
    <entry_order_direction>asc</entry_order_direction>
    <hidden>no</hidden>
    <navigation_group>Forum</navigation_group>
  </meta>
  <fields>
    <entry>
      <required>yes</required>
      <show_column>yes</show_column>
      <id>1</id>
      <label>Name</label>
      <element_name>name</element_name>
      <type>input</type>
      <parent_section>1</parent_section>
      <sortorder>0</sortorder>
      <location>main</location>
      <field_id>1</field_id>
      <validator></validator>
    </entry>
    <entry>
      <required>yes</required>
      <show_column>yes</show_column>
      <id>2</id>
      <label>Username</label>
      <element_name>username</element_name>
      <type>memberusername</type>
      <parent_section>1</parent_section>
      <sortorder>1</sortorder>
      <location>main</location>
      <field_id>2</field_id>
      <validator></validator>
    </entry>
    <entry>
      <required>yes</required>
      <length>6</length>
      <strength>good</strength>
      <show_column>yes</show_column>
      <id>3</id>
      <label>Password</label>
      <element_name>password</element_name>
      <type>memberpassword</type>
      <parent_section>1</parent_section>
      <sortorder>2</sortorder>
      <location>main</location>
      <field_id>3</field_id>
      <salt>SaltyLikeTheOCEAN!</salt>
    </entry>
    <entry>
      <required>yes</required>
      <show_column>yes</show_column>
      <id>4</id>
      <label>Email</label>
      <element_name>email</element_name>
      <type>memberemail</type>
      <parent_section>1</parent_section>
      <sortorder>3</sortorder>
      <location>main</location>
      <field_id>4</field_id>
    </entry>
    <entry>
      <required>no</required>
      <show_column>no</show_column>
      <id>5</id>
      <label>Website</label>
      <element_name>website</element_name>
      <type>input</type>
      <parent_section>1</parent_section>
      <sortorder>4</sortorder>
      <location>main</location>
      <field_id>5</field_id>
      <validator>/^[^s:/?#]+:(?:/{2,3})?[^s./?#]+(?:.[^s./?#]+)*(?:/[^s?#]*??[^s?#]*(#[^s#]*)?)?$/</validator>
    </entry>
    <entry>
      <show_column>yes</show_column>
      <location>sidebar</location>
      <required>yes</required>
      <id>6</id>
      <label>Role</label>
      <element_name>role</element_name>
      <type>memberrole</type>
      <parent_section>1</parent_section>
      <sortorder>5</sortorder>
      <field_id>6</field_id>
      <default_role>1</default_role>
    </entry>
    <entry>
      <show_column>yes</show_column>
      <id>7</id>
      <label>Active</label>
      <element_name>active</element_name>
      <type>memberactivation</type>
      <parent_section>1</parent_section>
      <required>no</required>
      <sortorder>6</sortorder>
      <location>sidebar</location>
      <field_id>7</field_id>
      <code_expiry>1 hour</code_expiry>
      <activation_role_id>0</activation_role_id>
    </entry>
    <entry>
      <required>no</required>
      <show_column>yes</show_column>
      <id>8</id>
      <label>Location</label>
      <element_name>location</element_name>
      <type>input</type>
      <parent_section>1</parent_section>
      <sortorder>7</sortorder>
      <location>sidebar</location>
      <field_id>8</field_id>
      <validator></validator>
    </entry>
    <entry>
      <required>no</required>
      <show_column>no</show_column>
      <id>9</id>
      <label>Country</label>
      <element_name>country</element_name>
      <type>input</type>
      <parent_section>1</parent_section>
      <sortorder>8</sortorder>
      <location>sidebar</location>
      <field_id>9</field_id>
      <validator></validator>
    </entry>
    <entry>
      <show_column>no</show_column>
      <location>sidebar</location>
      <required>no</required>
      <id>10</id>
      <label>Timezone Offset</label>
      <element_name>timezone-offset</element_name>
      <type>membertimezone</type>
      <parent_section>1</parent_section>
      <sortorder>9</sortorder>
      <field_id>10</field_id>
      <available_zones>AFRICA,AMERICA,ANTARCTICA,ARCTIC,ASIA,ATLANTIC,AUSTRALIA,EUROPE,INDIAN,PACIFIC</available_zones>
    </entry>
    <entry>
      <location>sidebar</location>
      <show_column>yes</show_column>
      <id>11</id>
      <label>Email Opt-in</label>
      <element_name>email-opt-in</element_name>
      <type>checkbox</type>
      <parent_section>1</parent_section>
      <required>no</required>
      <sortorder>10</sortorder>
      <field_id>11</field_id>
      <default_state>off</default_state>
      <description>Send me email when there is important news.</description>
    </entry>
  </fields>
</section>

So far, this is working great! Well, except for Symphony choking on the Timezone Offset field. I'm getting this error:

implode() [function.implode]: Invalid arguments passed
An error occurred in /Users/stephen/Sites/tmp/migrate/extensions/members/fields/field.membertimezone.php around line 105

So, sorry that I've been a little impatient to get this working.

Don't worry :)

So far, this is working great! Well, except for Symphony choking on the Timezone Offset field.

I didn't find field.membertimezone.php file in the current branch of members extension. Can you share it?

Brendan's latest work is in the integration branch.

See my latest commits on GitHub. I've started implementing the guid hash concept for sections, fields and pages.

Another problem: fields that do references to sections should be changed manually only as a precaution.

Create an account or sign in to comment.

Symphony • Open Source XSLT CMS

Server Requirements

  • PHP 5.3-5.6 or 7.0-7.3
  • PHP's LibXML module, with the XSLT extension enabled (--with-xsl)
  • MySQL 5.5 or above
  • An Apache or Litespeed webserver
  • Apache's mod_rewrite module or equivalent

Compatible Hosts

Sign in

Login details