Customizing Zurmo

The goal of this article is to provide a best practice approach to customizing the Zurmo application.  Oftentimes, there will be use cases that require custom code including new modules, logic, etc.  I have set up a fork of Zurmo called Zurmo Zoo.  This is an example of customization for a fictitious company.  The Zurmo application will be used to manage the zoo including the animals, feedings, cages, shows, etc.

To start I have created a custom module called Animals with an Animal Model.

You can find the repository here:
https://bitbucket.org/zurmo/zurmozoo/

The revision that was used to make the Animal module is here:
https://bitbucket.org/zurmo/zurmozoo/changeset/8223ab5a9591

As I get more time, i will explain in more detail various customizations and how best to approach them. My hope is that the links above, especially the changeset, can provide a baseline for how to customize Zurmo in an upgrade safe way.

With customizations you have to always consider upgrades. This approach allows you to do your code changes in a completely upgrade safe manner.

Creating The Module

All extended classes will be defined in his own module; in this example the module will be called animals.

.

The module will have the following files

.

The following table details the role of each one of the files.

File

Description

AnimalsModule.php Main module definition.Holds the functions to create the menus, handle the translations and security roles.
DefaultController.php Default controller for the module.Handles the actions required to list, edit, create and delete the class.
AnimalsDefaultDataMaker.php Class to make default data that needs to be created upon an installation
AnimalsModuleForm.php Form used to view  the class, just extends from ModuleForm.
AnimalsSearchForm.php Form used to search  the class, just extends from OwnedSearchForm.
Animal.php Defines the main class of your module.
AnimalEditAndDetailsView.php View that defines the Edit and details view. Extends SecuredEditAndDetailsView
AnimalsConfigurationView.php View that defines the Configuration view. Extends ConfigurationView.
AnimalsListView.php View that defines the List view. Extends SecuredListView.
AnimalsModalListView View that defines the Modal view. Extends ModalListView.
AnimalsModalSearchAndListView.php View that defines the Modal Search and List view. Extends GridView.
AnimalsModalSearchView.php View that defines the Modal Search view. Extends SearchView.
AnimalsModalEditView.php View that defines the Modules  Edit view. Extends ModuleEditView.
AnimalsPageView.php View that defines the Page  view. Extends ZurmoPageView.
AnimalsSearchView.php View that defines the Search view. Extends ZurmoSearchView.
AnimalsMassEditView.php View that defines the Mass edit view. Extends MassEditView.
.
In order to integrate and install the module in the Zurmo application two other files need to be created
.

.
perInstanceConfig : Custom configuration for the Zurmo Zoo project.
 <?php
/**
 * Custom configuration for the Zurmo Zoo project.
 */

$instanceConfig   = array(
    'modules' => array(
        'animals',
    ),
);
$instanceConfig['components']['custom']['class'] =
    'application.extensions.zurmozoo.components.ZurmoZooCustomManagement';
?>
 

ZurmoZooInstallUtil: Helper class for Zurmo Zoo customizations.

<?php
/**
 * Helper class for Zurmo Zoo customizations.
 */
class ZurmoZooInstallUtil
{
    public static function resolveCustomMetadataAndLoad()
    {
        $shouldSaveZurmoModuleMetadata = false;
        $metadata                      = ZurmoModule::getMetadata();
        if(!in_array('animals', $metadata['global']['tabMenuItemsModuleOrdering']))
        {
            $metadata['global']['tabMenuItemsModuleOrdering'][] = 'animals';
            $shouldSaveZurmoModuleMetadata = true;
        }
        if($shouldSaveZurmoModuleMetadata)
        {
            ZurmoModule::setMetadata($metadata);
            GeneralCache::forgetAll();
        }
        $metadata = Activity::getMetadata();
        if(!in_array('Animal', $metadata['Activity']['activityItemsModelClassNames']))
        {
            $metadata['Activity']['activityItemsModelClassNames'][] = 'Animal';
            Activity::setMetadata($metadata);
            GeneralCache::forgetAll();
        }
        Yii::import('application.extensions.zurmoinc.framework.data.*');
        Yii::import('application.modules.animals.data.*');
        $defaultDataMaker = new AnimalsDefaultDataMaker();
        $defaultDataMaker->make();
    }
}
?>
Adding Basic Fields To Your Class

In this section we will explain how to add new basic fields of type integer, float or string to your recently created class, in this example we will add a new description field to the Animal class.

The first thing to do is define the new field in the class. Open the file protected->modules->animals->models->Animal.php and add the definition of the field description as follows.

<?php
….
public static function getDefaultMetadata()
{
    $metadata = parent::getDefaultMetadata();
    $metadata[__CLASS__] = array(
        'members' => array(
            'name',
            'description',
        ),
        'relations' => array(
            'type'         => array(RedBeanModel::HAS_ONE,   'OwnedCustomField', RedBeanModel::OWNED),
        ),
        'rules' => array(
            array('name',             'required'),
            array('name',             'type',    'type' => 'string'),
            array('name',             'length',  'max' => 100),
            array('description',   'type',    'type' => 'string'),
        ),
        'elements' => array(
             'description'     => 'TextArea',
        ),
        'customFields' => array(
            'type'  => 'AnimalType',
        ),
        'defaultSortAttribute' => 'name',
        'noAudit' => array(
            'description'
        ),
    );
    return $metadata;
}
…..
?>

Now we need to position the field in the screen. Open the file protected->modules->animals->views->AnimalEditAndDetailsView.php and position the new created field as follows.

<?php
class AnimalEditAndDetailsView extends SecuredEditAndDetailsView
    {
        public static function getDefaultMetadata()
        {
            $metadata = array(
                'global' => array(
                    'toolbar' => array(
                        'elements' => array(
                            array('type' => 'CancelLink', 'renderType' => 'Edit'),
                            array('type' => 'SaveButton', 'renderType' => 'Edit'),
                            array('type' => 'ListLink',
                                'renderType' => 'Details',
                                'label' => "eval:Yii::t('Default', 'Return to List')"
                            ),
                            array('type' => 'EditLink', 'renderType' => 'Details'),
                            array('type' => 'AuditEventsModalListLink', 'renderType' => 'Details'),
                        ),
                    ),
                    'derivedAttributeTypes' => array(
                        'DateTimeCreatedUser',
                        'DateTimeModifiedUser',
                    ),
                    'panelsDisplayType' => FormLayout::PANELS_DISPLAY_TYPE_ALL,
                    'panels' => array(
                        array(
                            'rows' => array(
                                array('cells' =>
                                    array(
                                        array(
                                            'elements' => array(
                                                array('attributeName' => 'name', 'type' => 'Text'),
                                            ),
                                        ),
                                    )
                                ),
                                array('cells' =>
                                    array(
                                        array(
                                            'elements' => array(
                                                array('attributeName' => 'type', 'type' => 'DropDown'),
                                            ),
                                        ),
                                    )
                                ),
                                array('cells' =>
                                    array(
                                        array(
                                            'detailViewOnly' => true,
                                            'elements' => array(
                                                array('attributeName' => 'null', 'type' => 'DateTimeCreatedUser'),
                                            ),
                                        ),
                                    )
                                ),
                                array('cells' =>
                                    array(
                                        array(
                                            'detailViewOnly' => true,
                                            'elements' => array(
                                                array('attributeName' => 'null', 'type' => 'DateTimeModifiedUser'),
                                            ),
                                        ),
                                    )
                                ),
                            ),
                        ),
                    ),
                ),
            );
            return $metadata;
        }

        protected function getNewModelTitleLabel()
        {
            return Yii::t('Default', 'Create AnimalsModuleSingularLabel',
                                     LabelUtil::getTranslationParamsForAllModules());
        }
    }
?>
?>

As you can see we added the to position the field

array('cells' =>
    array(
        array(
            'elements' => array(
                array('attributeName' => 'description', 'type' => 'TextArea'),
            ),
        ),
    )
),

But also the following link

 array('type' => 'AnimalDeleteLink', 'renderType' => 'Details'), 

This is not mandatory but will allow you to delete animals if required. For it to work you have to add the file AnimalDeleteLinkActionElement.php under protected->modules->animals->elements->actions and complete it with the following code.

<?php
class AnimalDeleteLinkActionElement extends DeleteLinkActionElement
{
    protected function resolveConfirmAlertInHtmlOptions($htmlOptions)
    {
        $htmlOptions['confirm'] = Yii::t('Default',
                                         'Are you sure you want to remove this AnimalsModuleSingularLowerCaseLabel?',
                                         LabelUtil::getTranslationParamsForAllModules());
        return $htmlOptions;
    }
} ?>

As of today Zurmo support the following type of fields:

Check Box A check box
Currency A currency field
Date A date field
Date Time A date/time field
Decimal A decimal field
Pick List A pick list with specific values to select from
Integer An integer field
Phone A phone field
Radio Pick List A radio button pick list
Text A text field
Text Area A description box
URL A field that contains a URL

.

The following source code shows how to integrate them in your code.

models->Animals.php

First you need to define all the fields in the model

……
public static function getDefaultMetadata()
{
    $metadata = parent::getDefaultMetadata();
    $metadata[__CLASS__] = array(
        'members' => array(
            'name',
            'description',
            'cust_checkbox',
            'cust_date',
            'cust_datetime',
            'cust_decimal',
            'cust_integer',
            'cust_phone',
            'cust_text',
            'cust_textarea',
            'cust_url',
        ),
        'relations' => array(
            'type'          => array(RedBeanModel::HAS_ONE,   'OwnedCustomField', RedBeanModel::OWNED),
            'cust_currency' => array(RedBeanModel::HAS_ONE,   'CurrencyValue', RedBeanModel::OWNED),
            'cust_picklist' => array(RedBeanModel::HAS_ONE,   'OwnedCustomField', RedBeanModel::OWNED),
            'cust_radiopicklist' => array(RedBeanModel::HAS_ONE,   'OwnedCustomField', RedBeanModel::OWNED),
        ),
        'rules' => array(
            array('name',           'required'),
            array('name',           'type',           'type'  => 'string'),
            array('name',           'length',         'max'   => 100),
            array('description',    'type',           'type'  => 'string'),
            array('cust_checkbox',  'type',           'type'  => 'boolean'),
            array('cust_checkbox',  'default',        'value' => 1),
            array('cust_date',      'type',           'type'  => 'date'),
            array('cust_date',      'dateTimeDefault','value' => 2),
            array('cust_datetime',  'type',           'type'  => 'datetime'),
            array('cust_datetime',  'dateTimeDefault','value' => 2),
            array('cust_decimal',   'default',        'value' => 1),
            array('cust_decimal',   'length',         'max'   => 18),
            array('cust_decimal',   'numerical',      'precision' => 2),
            array('cust_decimal',   'type',           'type'   => 'float'),
            array('cust_integer',   'length',         'max'    => 11),
            array('cust_integer',   'numerical',      'max'    => 9999, 'min' => 0 ),
            array('cust_integer',   'type',           'type'   => 'integer'),
            array('cust_picklist',  'default',        'value'  => 'Value one'),
            array('cust_phone',     'length',         'max'    => 20),
            array('cust_phone',     'type',           'type'   => 'string'),
            array('cust_text',      'length',         'max'    => 255),
            array('cust_text',      'type',           'type'   => 'string'),
            array('cust_textarea',  'type',           'type'   => 'string'),
            array('cust_url',       'length',         'max'    => 255),
            array('cust_url',       'url'),
        ),
        'elements' => array(
            'description'     => 'TextArea',
            'cust_checkbox'   => 'CheckBox',
            'cust_currency'   => 'CurrencyValue',
            'cust_date'       => 'Date',
            'cust_datetime'   => 'DateTime',
            'cust_decimal'    => 'Decimal',
            'cust_integer'    => 'Integer',
            'cust_picklist'   => 'DropDown',
            'cust_phone'      => 'Phone',
            'cust_radiopicklist'     => 'RadioDropDown',
            'cust_text'       => 'Text',
            'cust_textarea'   => 'TextArea',
            'cust_url'        => 'Url',
        ),
        'customFields' => array(
            'type'               => 'AnimalType',
            'cust_picklist'      => 'Cust_picklist',
            'cust_radiopicklist' => 'Cust_radiopicklist',
        ),
        'defaultSortAttribute' => 'name',
        'noAudit' => array(
            'description',
            'cust_date',
            'cust_datetime',
            'cust_decimal',
            'cust_integer',
            'cust_picklist',
            'cust_phone',
            'cust_radiopicklist',
            'cust_text',
            'cust_textarea',
            'cust_url'
        ),
        'labels' => array(
            'cust_checkbox'  => array('en' => 'Check Box'),
            'cust_currency'  => array('en' => 'Currency'),
            'cust_date'  => array('en' => 'Date'),
            'cust_datetime'  => array('en' => 'Date Time'),
            'cust_decimal'  => array('en' => 'Decimal'),
            'cust_integer'  => array('en' => 'Integer'),
            'cust_picklist'  => array('en' => 'Pick List'),
            'cust_phone'  => array('en' => 'Phone'),
            'cust_radiopicklist'  => array('en' => 'Radio Pick List'),
            'cust_text'  => array('en' => 'Text'),
            'cust_textarea'  => array('en' => 'Text Area'),
            'cust_url'  => array('en' => 'URL'),
        ),
    );
    return $metadata;
}
….

views-> AnimalEditAndDetailsView.php

Second position the fields in the view

'panels' => array(
    array(
        'rows' => array(
            array('cells' =>
                array(
                    array(
                        'elements' => array(
                            array('attributeName' => 'name', 'type' => 'Text'),
                        ),
                    ),
                    array(
                        'elements' => array(
                            array('attributeName' => 'type', 'type' => 'DropDown'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'elements' => array(
                            array('attributeName' => 'description', 'type' => 'TextArea'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_checkbox', 'type' => 'CheckBox'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_datetime', 'type' => 'DateTime'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_integer', 'type' => 'integer'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_text', 'type' => 'Text'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_url', 'type' => 'Url'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_currency', 'type' => 'CurrencyValue'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_radiopicklist', 'type' => 'RadioDropDown', 'addBlank' => '1'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_picklist', 'type' => 'DropDown', 'addBlank' => '1'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_decimal', 'type' => 'Decimal'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_phone', 'type' => 'Phone'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_textarea', 'type' => 'TextArea'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => false,
                        'elements' => array(
                            array('attributeName' => 'cust_date', 'type' => 'Date'),
                        ),
                    ),
                )
            ),
            array('cells' =>
                array(
                    array(
                        'detailViewOnly' => true,
                        'elements' => array(
                            array('attributeName' => 'null', 'type' => 'DateTimeCreatedUser'),
                        ),
                    ),
                    array(
                        'detailViewOnly' => true,
                        'elements' => array(
                            array('attributeName' => 'null', 'type' => 'DateTimeModifiedUser'),
                        ),
                    ),
                )
            ),
        ),
    ),
),

data-> AnimalsDefaultDataMaker.php

This class create the list of values for the pick lists

class AnimalsDefaultDataMaker extends DefaultDataMaker {

    public function make() {
        $values = array(
            'Type 1',
            'Type 2',
        );
        static::makeCustomFieldDataByValuesAndDefault('AnimalType', $values);

        $values = array(
            'Value 1',
            'Value 2',
        );
        static::makeCustomFieldDataByValuesAndDefault('Cust_picklist', $values);

        $values = array(
            'Value 3',
            'Value 4',
        );
        static::makeCustomFieldDataByValuesAndDefault('Cust_radiopicklist', $values);
    }
}

Adding Complex Fields To Your Class to come…

Leave a Comment

  • http://www.facebook.com/rpeetoom Ross Peetoom

    Great Article, been looking forward to something like this going up on the site for months so its great to see!

  • http://www.facebook.com/fulfordjim Jim Fulford

    This was a huge help. This is running on an older build so you’ll need to download yii 1.1.8 to run this. Also, index.php was missing from the root so I just copied over from another Zurmo install.

  • Lork Dop

    Hi,

    Is it possible to just pick the required folders (protected/extensions/zurmozoo/, protected/modules/animals/ & protected/config/perInstanceConfig.php ) and drop them in a running zurmo environment ?
    If so, how to enable this new module without having to reinstall all the environment ?

    Thank you :)

    • Ivica Nedeljkovic

      No it is not possible. You must clone zurmozoo repository, and install application again.

      • Lork Dop

        Hi Ivica,
        I don’t understand why I have to clone a complete repo. This is completely against the idea of being modular… How do I plug a new module in an existing zurmo environment ?

        • Ivica Nedeljkovic

          I agree. You can try to copy just those files that are modified, and you will need to upgrade schema(so new tables will be created), clear cache and it should work, but we haven’t test it, and that is why I suggest to clone and install new application from zurmozoo repository.

  • Andrew Bernat

    Remember, that after you have done your own module you need to run console command from command line. The command is: php zurmo update Schema super

    • Ivica Nedeljkovic

      Correct command: zurmoc updateSchema super

      • Andrew Bernat

        Yeah, that is correct. Thanks!

      • tangxi

        -bash: zurmoc: command not found

        • Ivica

          you need first to navigate to app/protected/commands folder, then make zurmoc executive:
          chmod +x zurmoc
          Finally, run command
          ./zurmoc updateSchema super

          Alternative to this command is:
          php zurmo.php updateSchema super

          • tangxi

            THANKS.

  • Химяк Павел

    Dont forget GET parameter to launch custom component ?resolveCustomData=1

  • http://www.bertelsen.ca Brandon Bertelsen

    Highly useful example. Thanks for that.

  • tangxi

    /index.php/users/default/securityDetails?id=2

    Create Animals
    The reference id for this error is 9fb82233fcb84d37b286b80cf87a8f86.

  • JJ

    I got the zurmozoo up and running, but when I go to settings -> groups -> [group name, in this case it was 'Everyone'] -> Module Rights, all I get is a blank screen.

    • JJ

      SOLUTION: If you open the AnimalModule.php, you should find on line 49 (ish):

      RIGHT_CREATE_ACCOUNT
      RIGHT_DELETE_ACCOUNT
      RIGHT_ACCESS_ACCOUNT

      Since this is the Animal Module, you just need to change them to:

      RIGHT_CREATE_ANIMAL
      RIGHT_DELETE_ANIMAL
      RIGHT_ACCESS_ANIMAL

      • Anthony

        The correct Syntax is:

        RIGHT_CREATE_ANIMALS
        RIGHT_DELETE_ANIMALS
        RIGHT_ACCESS_ANIMALS

  • David K

    I am wanting to create a new ‘accounts’ module entitled ‘attorneys’….an exact clone of the ‘accounts’ module just with the name attorney. How can I do this utilizing the above method? I tried just copying the accounts module replacing ‘accounts’ with attorneys in all appropriate module php files and filenames but was unsuccessful. Any help you can offer?

  • Tester

    what if the listing data want with some default criteria to show?
    e.g. IF i want the default list of animal who are only wild. Then how to set default criteria

  • TD

    What if I only want to see the last 4 characters of a name or any attribute? Which part will I edit?

  • Learner

    Thank you for giving such useful example.. I have made all changes as specified in zurmozoo.I have a doubt..
    1)Is there anything to be done with database if I am trying for another module?
    2)Where the module name must be included to get it among one of the tab?

  • binleen

    create the animals modules (for windows):

    use command : zurmoc.bat UpdateSchema Super

    error: PHP Error[2048]: Declaration of Animal::getLabel() should be compatible with tha
    t of RedBeanModel::getLabel()
    in file D:WWWzurmoappprotectedmodulesanimalsmodelsAnimal.php at line
    97
    #0 D:WWWzurmoyiiframeworkYiiBase.php(395): autoload()
    #1 unknown(0): autoload()
    #2 D:WWWzurmoappprotectedcoreutilsRedBeanDatabaseBuilderUtil.php(410): sp
    l_autoload_call()
    #3 D:WWWzurmoappprotectedcoreutilsRedBeanDatabaseBuilderUtil.php(98): aut
    oBuildSampleModel()
    #4 D:WWWzurmoappprotectedmodulesinstallutilsInstallUtil.php(626): autoBu
    ildModels()
    #5 D:WWWzurmoappprotectedmodulesinstallutilsInstallUtil.php(1109): autoB
    uildDatabase()
    #6 D:WWWzurmoappprotectedcommandsUpdateSchemaCommand.php(90): runAutoBuild
    FromUpdateSchemaCommand()
    #7 D:WWWzurmoyiiframeworkconsoleCConsoleCommandRunner.php(67): UpdateSchem
    aCommand->run()
    #8 D:WWWzurmoyiiframeworkconsoleCConsoleApplication.php(91): CConsoleComma
    ndRunner->run()
    #9 D:WWWzurmoyiiframeworkbaseCApplication.php(169): ConsoleApplication->pr
    ocessRequest()
    #10 D:WWWzurmoappprotectedcommandsbootstrap.php(60): ConsoleApplication->r
    un()
    #11 D:WWWzurmoappprotectedcommandszurmoc.php(38): require_once()
    PHP Fatal error: Class ‘RedBeanModelDataProvider’ not found in D:WWWzurmoapp
    protectedmodulesanimalsmodelsAnimal.php on line 97

    Fatal error: Class ‘RedBeanModelDataProvider’ not found in D:WWWzurmoappprot
    ectedmodulesanimalsmodelsAnimal.php on line 97

  • 杨嘉华

    I added the animals module to my zurmo system with following the steps, and then use the command line tool to run updateSchema by doing from the commands folder(app/protected/commands):
    zurmoc updateSchema super,
    and i also cleared cache, by appending query to url ‘?clearCahe=1′ to url.
    After that, I couldn’t see the Animal module in the homepage, but animal table was created in my databases. Why i couldn’t see Animal module in the homepage, please help. Thanks.

    • 杨嘉华

      I’m happly that i can see the “Animal” module now by appending query to url ‘?resolveCustomData=1′ to url, but i don’t know why, anyone can tell me why?
      And there another question where i create an animal example for the “Animal” module, and i found error “Column not found: 1054 Unknown column ‘customfield_id’ in ‘field list’ ” in the file:application.log, and i found the ‘animal’ table miss the ‘customfield_id’ in the database, what’s the problem with this, and i’m not really understand the zurmo mvc flow, particularly the between works of data’s CURD on the view’s data and the DB.
      Please help. Thanks.

  • Rajeev Rajeev K

    is there any way to generate the needed files for zurmo new module

    Thanks!