Skip to content

No Copy-Paste

Intro

Copy-pasting code is a bad habit. Once you copy-paste some code, you (should) assume the responsibility of constantly checking the source of copy-paste and apply the updates to where you pasted the code. This is especially troublesome when a project is updated. The core files are updated, but the copy->pasted code is not.

This page lists some easy ways of avoiding copy->paste.

JavaScript code

Assuming you are extending a JS file from core, e.g. wfed/dir/file, you should import the core file and then extend it. The core file can still be identified by prefixing the file alias with _: _wfed/dir/file in our example.

Backbone classes

Backbone views/collections/modules can be extended like this:

javascript
define(['_wfed/dir/file'], function (BaseFile) {
    return BaseFile.extend({
        functionFromCore: function() {
            const ret = BaseFile.prototype.functionFromCore.apply(this, arguments);

            // your code here

            return ret;
        }
    });
});

Note: Backbone classes have an extend method. The code above is passing to it an object containing the new methods.

Note: The new method calls BaseFile.prototype.__NAME_OF_THE_METHOD__.apply(this, arguments) to call the code from core.

Objects of functions

Some files in core are just objects of functions.

Example:

javascript
define([], function() {
    return {
        function1: function() {
            // code here
        },
        function2() {
            // code here
        }
    }
})

These can be extended like this:

javascript
define(['_wfed/dir/file'], function(BaseFile) {
    return Object.assign({}, BaseFile, {
        function2: function() {
            const ret = BaseFile.prototype.function2.apply(this, arguments);
            
            // your code here
            
            return ret;
        }
    });
});

Note: The Object.assign method is used to merge the core object with the new methods into a newly created object.

Note: The new method calls BaseFile.prototype.__NAME_OF_THE_METHOD__.apply(this, arguments) to call the code from core. (Oh, the irony! This line in documentation was copy->pasted from the Backbone part 😄 Once you're in the code of the function, the method for calling code from core is the same.)

PHP code

Symfony Forms

v5+ Reordering core fields

One common reason for copy->pasting is the need to add a form field in a specific position. For this, one would usually copy->paste the entire form configuration from core and then add the new field in the desired position.

For Symfony forms, the wf_cms_admin.form.field_mover service (alias Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover if using autowire) can be injected.

It provides a moveField method that can be used to reorder fields in a form.

php
<?php

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover;

class FormExtension extends AbstractTypeExtension 
{
    private $fieldMover;

    public function __construct(FormFieldMover $fieldMover)
    {
        $this->fieldMover = $fieldMover;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('fieldToMove');
        $builder->add('afterField');
        
        $this->fieldMover->moveField($builder, 'fieldToMove', FormFieldMover::POSITION_AFTER, 'afterField');
        
        // or
        
        $this->fieldMover->moveField($builder, 'afterField', FormFieldMover::POSITION_FIRST);
        
        // or
        
        $this->fieldMover->moveField($builder, 'fieldToMove', FormFieldMover::POSITION_LAST);
    }

    public function __construct(
}

SonataAdmin

v5+ Reordering SonataAdmin form fields

Since v5.0, XalokNext ships with WfSonataAdminTrait, a PHP Trait that helps with reordering fields in SonataAdminBundle forms. All admin classes defined in the core of XalokNext use this trait.

The trait provides a method moveFormField that can be used to reorder fields in a form.

php
<?php

use Sonata\AdminBundle\Form\FormMapper;
use Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover;

class MyAdmin extends Admin
{
    use WfSonataAdminTrait;

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper->add('fieldToMove');
        $formMapper->add('afterField');
        
        $this->moveFormField($formMapper, 'fieldToMove', FormFieldMover::POSITION_AFTER, 'afterField');
        
        // or
        
        $this->moveFormField($formMapper, 'afterField', FormFieldMover::POSITION_FIRST);
        
        // or
        
        $this->moveFormField($formMapper, 'fieldToMove', FormFieldMover::POSITION_LAST);
    }
}

v5+ Reordering list (datagrid) fields

The trait also provides a method moveListField that can be used to reorder fields in a list (datagrid).

php
<?php

use Sonata\AdminBundle\Datagrid\ListMapper;
use Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover;

class MyAdmin extends Admin
{
    use WfSonataAdminTrait;

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('fieldToMove');
        $listMapper->add('afterField');
        
        $this->moveListField($listMapper, 'fieldToMove', FormFieldMover::POSITION_AFTER, 'afterField');
        
        // or
        
        $this->moveListField($listMapper, 'afterField', FormFieldMover::POSITION_FIRST);
        
        // or
        
        $this->moveListField($listMapper, 'fieldToMove', FormFieldMover::POSITION_LAST);
    }
}

v7.1+Reordering SonataAdmin filter fields

Starting with v7.1, the WfSonataAdminTrait provides a method moveDatagridField:

php
<?php

use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover;

class MyAdmin extends WfCmsBaseAdmin
{
    use WfSonataAdminTrait;

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        parent::configureDatagridFilters($formMapper);
        
        $datagridMapper->add('fieldToMove');
        $datagridMapper->add('afterField');
        
        $this->moveDatagridField($listMapper, 'fieldToMove', FormFieldMover::POSITION_AFTER, 'afterField');
        
        // or
        
        $this->moveDatagridField($listMapper, 'afterField', FormFieldMover::POSITION_FIRST);
        
        // or
        
        $this->moveDatagridField($listMapper, 'fieldToMove', FormFieldMover::POSITION_LAST);
        
    }
}

v7.1+Adding options to SonataAdmin form fields

Starting with v7.1, the WfSonataAdminTrait provides a method mergeFormFieldOptions:

php
<?php

use Sonata\AdminBundle\Form\FormMapper;
use Wf\Bundle\CmsBaseAdminBundle\Form\FormFieldMover;

class MyAdmin extends WfCmsBaseAdmin
{
    use WfSonataAdminTrait;

    protected function configureFormFields(FormMapper $formMapper)
    {
        parent::configureFormFields($formMapper);
        
        $this->mergeFormFieldOptions($formMapper, 'fieldToChange', [
            'row_attr' => [
                'class' => 'wf-col-md-6'
            ]
        ]);
    }
}

Symfony services

A common source of copy->pasting occurs when overwriting services defined either in wfcms/standard or in third party bundles, when desiring to add an extra dependency and this is done through the __construct method. It's likely that the service to be extended already has a list of dependencies in its constructor and this is copy->pasted to add the project's dependencies at the end.

The issue is that if the extended service needs a new dependency, its constructor signature will change, so the code in the project will fail. For this reason, prefer using setter injection instead of constructor injection.

If using Symfony's autowiring, you can use the @required annotation to inject the service:

php
<?php

class MyService
{
    private $x;

    /**
     * @required
     */
    public function setX(X $x)
    {
        $this->x = $x;
    }
    
}

If the dependency cannot be autowired, use the compiler pass to call this method:

php
<?php

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class MyCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $container->findDefinition('my_service')
            ->setClass(MyService::class)
            ->addMethodCall('setX', [new Reference('x')]);
    }
}

Bundles configurations

Most of the bundles handle merging different configurations, so you don't need to copy->paste an existing configuration from wfcms/standard (or soemewhere else) in order to add to it in the project.

For example, fos_elastica indexes have some configuration in wfcms/standard.

yaml
# vendor/wfcms/standard/Wf/Bundle/CmsBaseBundle/Resources/config/third_party/fos_elastica.yml
wf_fos_multilanguage:
    indexes:
        "%fos_index_name%_image":
            # ...
            properties:
                title: ~
                tags:
                    property_path: tagsArray
                tagsIds: ~
                description: ~
                createdAt:
                    type: date
                source: ~

To add a custom property to the image index, there's no need to copy->paste the entire definition in the project, one should only add the desired properties:

yaml
# app/config/misc/fos_elastica.yml
wf_fos_multilanguage:
    indexes:
        "%fos_index_name%_image":
            properties:
                custom: ~

Notable exceptions

security.firewalls and security.access_control are not allowed to be merged. If one needs to add a new firewall or a new access_control, they need to copy->paste the configuration from wfcms/standard.

Note: security.role_hierarchy (and, generally, other security related configurations with the exception of the two above) CAN be merged, so one should not copy->paste them, but add to them or overwrite wfcms/standard roles.